aboutsummaryrefslogtreecommitdiffstats
path: root/OpenKeychain/src/test
diff options
context:
space:
mode:
authorVincent Breitmoser <valodim@mugenguild.com>2015-06-11 17:12:07 +0200
committerVincent Breitmoser <valodim@mugenguild.com>2015-06-11 17:12:07 +0200
commitbaea31847022ce88906a0190638ddffed82c8a7c (patch)
treecdd080f224a82a377295a02f64f62fb7534012cb /OpenKeychain/src/test
parentc8790f404aaf76db967a16c8fea79eb6f90c8bce (diff)
parent98ba424576946d2f5ef529c5506113b71b0266c4 (diff)
downloadopen-keychain-baea31847022ce88906a0190638ddffed82c8a7c.tar.gz
open-keychain-baea31847022ce88906a0190638ddffed82c8a7c.tar.bz2
open-keychain-baea31847022ce88906a0190638ddffed82c8a7c.zip
Merge remote-tracking branch 'origin/master' into v/multi-decrypt
Diffstat (limited to 'OpenKeychain/src/test')
-rw-r--r--OpenKeychain/src/test/java/org/sufficientlysecure/keychain/WorkaroundBuildConfig.java14
-rw-r--r--OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/CertifyOperationTest.java252
-rw-r--r--OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/ExportTest.java229
-rw-r--r--OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/PromoteKeyOperationTest.java192
-rw-r--r--OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/KeyRingTest.java57
-rw-r--r--OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/PgpEncryptDecryptTest.java600
-rw-r--r--OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperationTest.java1379
-rw-r--r--OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/UncachedKeyringCanonicalizeTest.java768
-rw-r--r--OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/UncachedKeyringMergeTest.java497
-rw-r--r--OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/UncachedKeyringTest.java161
-rw-r--r--OpenKeychain/src/test/java/org/sufficientlysecure/keychain/provider/ProviderHelperKeyringTest.java104
-rw-r--r--OpenKeychain/src/test/java/org/sufficientlysecure/keychain/provider/ProviderHelperSaveTest.java256
-rw-r--r--OpenKeychain/src/test/java/org/sufficientlysecure/keychain/support/KeyringBuilder.java254
-rw-r--r--OpenKeychain/src/test/java/org/sufficientlysecure/keychain/support/KeyringTestingHelper.java365
-rw-r--r--OpenKeychain/src/test/java/org/sufficientlysecure/keychain/support/ProviderHelperStub.java36
-rw-r--r--OpenKeychain/src/test/java/org/sufficientlysecure/keychain/support/TestDataUtil.java142
-rw-r--r--OpenKeychain/src/test/java/org/sufficientlysecure/keychain/util/Iso7816TLVTest.java103
-rw-r--r--OpenKeychain/src/test/java/org/sufficientlysecure/keychain/util/ParcelableFileCacheTest.java82
-rw-r--r--OpenKeychain/src/test/java/org/sufficientlysecure/keychain/util/TestingUtils.java38
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/COPYING13
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/README26
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000001-006.public_keybin0 -> 171 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000002-013.user_id1
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000003-002.sigbin0 -> 113 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000004-012.ring_trustbin0 -> 4 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000005-002.sigbin0 -> 113 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000006-012.ring_trustbin0 -> 4 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000007-002.sigbin0 -> 220 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000008-012.ring_trustbin0 -> 4 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000009-002.sigbin0 -> 158 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000010-012.ring_trustbin0 -> 4 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000011-002.sigbin0 -> 96 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000012-012.ring_trustbin0 -> 4 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000013-014.public_subkeybin0 -> 171 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000014-002.sigbin0 -> 195 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000015-012.ring_trustbin0 -> 4 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000016-006.public_keybin0 -> 1201 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000017-002.sigbin0 -> 123 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000018-012.ring_trustbin0 -> 4 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000019-013.user_id1
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000020-002.sigbin0 -> 130 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000021-012.ring_trustbin0 -> 4 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000022-002.sigbin0 -> 186 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000023-012.ring_trustbin0 -> 4 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000024-014.public_subkeybin0 -> 608 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000025-002.sigbin0 -> 105 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000026-012.ring_trustbin0 -> 4 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000027-006.public_keybin0 -> 421 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000028-002.sigbin0 -> 99 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000029-012.ring_trustbin0 -> 4 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000030-013.user_id1
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000031-002.sigbin0 -> 132 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000032-012.ring_trustbin0 -> 4 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000033-002.sigbin0 -> 96 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000034-012.ring_trustbin0 -> 4 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000035-006.public_keybin0 -> 143 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000036-013.user_id1
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000037-002.sigbin0 -> 192 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000038-012.ring_trustbin0 -> 4 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000039-002.sigbin0 -> 72 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000040-012.ring_trustbin0 -> 4 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000041-017.attributebin0 -> 1761 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000042-002.sigbin0 -> 192 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000043-012.ring_trustbin0 -> 4 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000044-014.public_subkeybin0 -> 272 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000045-002.sigbin0 -> 161 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000046-012.ring_trustbin0 -> 4 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000047-005.secret_keybin0 -> 610 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000048-013.user_id1
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000049-002.sigbin0 -> 220 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000050-012.ring_trustbin0 -> 4 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000051-007.secret_subkeybin0 -> 611 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000052-002.sigbin0 -> 195 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000053-012.ring_trustbin0 -> 4 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000054-005.secret_keybin0 -> 1275 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000055-002.sigbin0 -> 123 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000056-012.ring_trustbin0 -> 4 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000057-013.user_id1
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000058-002.sigbin0 -> 130 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000059-012.ring_trustbin0 -> 4 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000060-007.secret_subkeybin0 -> 698 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000061-002.sigbin0 -> 104 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000062-012.ring_trustbin0 -> 4 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000063-005.secret_keybin0 -> 484 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000064-002.sigbin0 -> 99 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000065-012.ring_trustbin0 -> 4 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000066-013.user_id1
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000067-002.sigbin0 -> 106 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000068-012.ring_trustbin0 -> 4 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000069-005.secret_keybin0 -> 513 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000070-013.user_id1
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000071-002.sigbin0 -> 192 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000072-012.ring_trustbin0 -> 4 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000073-017.attributebin0 -> 1761 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000074-002.sigbin0 -> 192 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000075-012.ring_trustbin0 -> 4 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000076-007.secret_subkeybin0 -> 961 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000077-002.sigbin0 -> 161 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000078-012.ring_trustbin0 -> 4 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/002182-002.sigbin0 -> 363 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/3F5BBA0B0694BEB6000005-002.sigbin0 -> 1089 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/3F5BBA0B0694BEB6000017-002.sigbin0 -> 1089 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/compressedsig-bzip2.gpgbin0 -> 442 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/compressedsig-zlib.gpgbin0 -> 322 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/compressedsig.gpgbin0 -> 324 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/onepass_sigbin0 -> 15 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/pubring.gpgbin0 -> 179272 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/secring.gpgbin0 -> 9329 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/symmetrically_encryptedbin0 -> 528 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/uncompressed-ops-dsa-sha384.txt.gpgbin0 -> 150 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/uncompressed-ops-dsa.gpgbin0 -> 150 bytes
-rw-r--r--OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/uncompressed-ops-rsa.gpgbin0 -> 236 bytes
-rw-r--r--OpenKeychain/src/test/resources/public-key-canonicalize.blobbin0 -> 1224 bytes
-rw-r--r--OpenKeychain/src/test/resources/public-key-for-sample.blobbin0 -> 35198 bytes
-rw-r--r--OpenKeychain/src/test/resources/sample-altered.txt26
-rw-r--r--OpenKeychain/src/test/resources/sample.txt26
-rw-r--r--OpenKeychain/src/test/resources/test-keys/bad_user_id_encoding.asc60
-rw-r--r--OpenKeychain/src/test/resources/test-keys/broken_cert_version.asc17
-rw-r--r--OpenKeychain/src/test/resources/test-keys/cooperpair/9E669861368BCA0BE42DAF7DDDA252EBB8EBE1AF.asc29
-rw-r--r--OpenKeychain/src/test/resources/test-keys/cooperpair/A55120427374F3F7AA5F1166DDA252EBB8EBE1AF.asc29
-rw-r--r--OpenKeychain/src/test/resources/test-keys/cooperpair/readme3
-rw-r--r--OpenKeychain/src/test/resources/test-keys/divert_to_card_sec.asc44
-rw-r--r--OpenKeychain/src/test/resources/test-keys/mailvelope_07_no_key_flags.asc20
-rw-r--r--OpenKeychain/src/test/resources/test-keys/stripped_flags.asc60
-rw-r--r--OpenKeychain/src/test/resources/test-keys/symantec_public.asc37
-rw-r--r--OpenKeychain/src/test/resources/test-keys/symantec_secret.asc46
126 files changed, 5973 insertions, 0 deletions
diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/WorkaroundBuildConfig.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/WorkaroundBuildConfig.java
new file mode 100644
index 000000000..ab14649f8
--- /dev/null
+++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/WorkaroundBuildConfig.java
@@ -0,0 +1,14 @@
+package org.sufficientlysecure.keychain;
+
+/**
+ * Temporary workaround for https://github.com/robolectric/robolectric/issues/1747
+ */
+public final class WorkaroundBuildConfig {
+ public static final boolean DEBUG = BuildConfig.DEBUG;
+ // Workaround: Use real packageName not applicationId
+ public static final String APPLICATION_ID = "org.sufficientlysecure.keychain";
+ public static final String BUILD_TYPE = BuildConfig.BUILD_TYPE;
+ public static final String FLAVOR = BuildConfig.FLAVOR;
+ public static final int VERSION_CODE = BuildConfig.VERSION_CODE;
+ public static final String VERSION_NAME = BuildConfig.VERSION_NAME;
+}
diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/CertifyOperationTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/CertifyOperationTest.java
new file mode 100644
index 000000000..6984f126e
--- /dev/null
+++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/CertifyOperationTest.java
@@ -0,0 +1,252 @@
+/*
+ * 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 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.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowLog;
+import org.spongycastle.bcpg.sig.KeyFlags;
+import org.spongycastle.jce.provider.BouncyCastleProvider;
+import org.sufficientlysecure.keychain.BuildConfig;
+import org.sufficientlysecure.keychain.WorkaroundBuildConfig;
+import org.sufficientlysecure.keychain.operations.results.CertifyResult;
+import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
+import org.sufficientlysecure.keychain.operations.results.PgpEditKeyResult;
+import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing;
+import org.sufficientlysecure.keychain.pgp.PgpKeyOperation;
+import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
+import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute;
+import org.sufficientlysecure.keychain.provider.KeychainContract.Certs;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.service.CertifyActionsParcel;
+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.util.Passphrase;
+import org.sufficientlysecure.keychain.util.ProgressScaler;
+import org.sufficientlysecure.keychain.util.TestingUtils;
+
+import java.io.PrintStream;
+import java.security.Security;
+import java.util.ArrayList;
+import java.util.Random;
+
+@RunWith(RobolectricGradleTestRunner.class)
+@Config(constants = WorkaroundBuildConfig.class, sdk = 21, manifest = "src/main/AndroidManifest.xml")
+public class CertifyOperationTest {
+
+ static UncachedKeyRing mStaticRing1, mStaticRing2;
+ static Passphrase mKeyPhrase1 = TestingUtils.genPassphrase(true);
+ static Passphrase mKeyPhrase2 = TestingUtils.genPassphrase(true);
+
+ static PrintStream oldShadowStream;
+
+ @BeforeClass
+ public static void setUpOnce() throws Exception {
+ Security.insertProviderAt(new BouncyCastleProvider(), 1);
+ oldShadowStream = ShadowLog.stream;
+ // ShadowLog.stream = System.out;
+
+ Random random = new Random();
+
+ PgpKeyOperation op = new PgpKeyOperation(null);
+
+ {
+ 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.DSA, 1024, null, KeyFlags.SIGN_DATA, 0L));
+ parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
+ Algorithm.ELGAMAL, 1024, null, KeyFlags.ENCRYPT_COMMS, 0L));
+ parcel.mAddUserIds.add("derp");
+ parcel.mNewUnlock = new ChangeUnlockParcel(mKeyPhrase1);
+
+ PgpEditKeyResult result = op.createSecretKeyRing(parcel);
+ Assert.assertTrue("initial test key creation must succeed", result.success());
+ Assert.assertNotNull("initial test key creation must succeed", result.getRing());
+
+ mStaticRing1 = result.getRing();
+ }
+
+ {
+ 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.DSA, 1024, null, KeyFlags.SIGN_DATA, 0L));
+ parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
+ Algorithm.ELGAMAL, 1024, null, KeyFlags.ENCRYPT_COMMS, 0L));
+
+ parcel.mAddUserIds.add("ditz");
+ byte[] uatdata = new byte[random.nextInt(150)+10];
+ random.nextBytes(uatdata);
+ parcel.mAddUserAttribute.add(
+ WrappedUserAttribute.fromSubpacket(random.nextInt(100)+1, uatdata));
+
+ parcel.mNewUnlock = new ChangeUnlockParcel(mKeyPhrase2);
+
+ PgpEditKeyResult result = op.createSecretKeyRing(parcel);
+ Assert.assertTrue("initial test key creation must succeed", result.success());
+ Assert.assertNotNull("initial test key creation must succeed", result.getRing());
+
+ mStaticRing2 = result.getRing();
+ }
+
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ ProviderHelper providerHelper = new ProviderHelper(RuntimeEnvironment.application);
+
+ // don't log verbosely here, we're not here to test imports
+ ShadowLog.stream = oldShadowStream;
+
+ providerHelper.saveSecretKeyRing(mStaticRing1, new ProgressScaler());
+ providerHelper.savePublicKeyRing(mStaticRing2.extractPublicKeyRing(), new ProgressScaler());
+
+ // ok NOW log verbosely!
+ ShadowLog.stream = System.out;
+ }
+
+ @Test
+ public void testSelfCertifyFlag() throws Exception {
+
+ CanonicalizedPublicKeyRing ring = new ProviderHelper(RuntimeEnvironment.application)
+ .getCanonicalizedPublicKeyRing(mStaticRing1.getMasterKeyId());
+ Assert.assertEquals("secret key must be marked self-certified in database",
+ // TODO this should be more correctly be VERIFIED_SELF at some point!
+ Certs.VERIFIED_SECRET, ring.getVerified());
+
+ }
+
+ @Test
+ public void testCertifyId() throws Exception {
+ CertifyOperation op = new CertifyOperation(RuntimeEnvironment.application,
+ new ProviderHelper(RuntimeEnvironment.application), null, null);
+
+ {
+ CanonicalizedPublicKeyRing ring = new ProviderHelper(RuntimeEnvironment.application)
+ .getCanonicalizedPublicKeyRing(mStaticRing2.getMasterKeyId());
+ Assert.assertEquals("public key must not be marked verified prior to certification",
+ Certs.UNVERIFIED, ring.getVerified());
+ }
+
+ CertifyActionsParcel actions = new CertifyActionsParcel(mStaticRing1.getMasterKeyId());
+ actions.add(new CertifyAction(mStaticRing2.getMasterKeyId(),
+ mStaticRing2.getPublicKey().getUnorderedUserIds()));
+ CertifyResult result = op.execute(actions, new CryptoInputParcel(mKeyPhrase1));
+
+ Assert.assertTrue("certification must succeed", result.success());
+
+ {
+ CanonicalizedPublicKeyRing ring = new ProviderHelper(RuntimeEnvironment.application)
+ .getCanonicalizedPublicKeyRing(mStaticRing2.getMasterKeyId());
+ Assert.assertEquals("new key must be verified now",
+ Certs.VERIFIED_SECRET, ring.getVerified());
+ }
+
+ }
+
+ @Test
+ public void testCertifyAttribute() throws Exception {
+ CertifyOperation op = new CertifyOperation(RuntimeEnvironment.application,
+ new ProviderHelper(RuntimeEnvironment.application), null, null);
+
+ {
+ CanonicalizedPublicKeyRing ring = new ProviderHelper(RuntimeEnvironment.application)
+ .getCanonicalizedPublicKeyRing(mStaticRing2.getMasterKeyId());
+ Assert.assertEquals("public key must not be marked verified prior to certification",
+ Certs.UNVERIFIED, ring.getVerified());
+ }
+
+ CertifyActionsParcel actions = new CertifyActionsParcel(mStaticRing1.getMasterKeyId());
+ actions.add(new CertifyAction(mStaticRing2.getMasterKeyId(), null,
+ mStaticRing2.getPublicKey().getUnorderedUserAttributes()));
+ CertifyResult result = op.execute(actions, new CryptoInputParcel(mKeyPhrase1));
+
+ Assert.assertTrue("certification must succeed", result.success());
+
+ {
+ CanonicalizedPublicKeyRing ring = new ProviderHelper(RuntimeEnvironment.application)
+ .getCanonicalizedPublicKeyRing(mStaticRing2.getMasterKeyId());
+ Assert.assertEquals("new key must be verified now",
+ Certs.VERIFIED_SECRET, ring.getVerified());
+ }
+
+ }
+
+
+ @Test
+ public void testCertifySelf() throws Exception {
+ CertifyOperation op = new CertifyOperation(RuntimeEnvironment.application,
+ new ProviderHelper(RuntimeEnvironment.application), null, null);
+
+ CertifyActionsParcel actions = new CertifyActionsParcel(mStaticRing1.getMasterKeyId());
+ actions.add(new CertifyAction(mStaticRing1.getMasterKeyId(),
+ mStaticRing2.getPublicKey().getUnorderedUserIds()));
+
+ CertifyResult result = op.execute(actions, new CryptoInputParcel(mKeyPhrase1));
+
+ Assert.assertFalse("certification with itself must fail!", result.success());
+ Assert.assertTrue("error msg must be about self certification",
+ result.getLog().containsType(LogType.MSG_CRT_ERROR_SELF));
+ }
+
+ @Test
+ public void testCertifyNonexistent() throws Exception {
+
+ CertifyOperation op = new CertifyOperation(RuntimeEnvironment.application,
+ new ProviderHelper(RuntimeEnvironment.application), null, null);
+
+ {
+ CertifyActionsParcel actions = new CertifyActionsParcel(mStaticRing1.getMasterKeyId());
+ ArrayList<String> uids = new ArrayList<String>();
+ uids.add("nonexistent");
+ actions.add(new CertifyAction(1234L, uids));
+
+ CertifyResult result = op.execute(actions, new CryptoInputParcel(mKeyPhrase1));
+
+ Assert.assertFalse("certification of nonexistent key must fail", result.success());
+ Assert.assertTrue("must contain error msg about not found",
+ result.getLog().containsType(LogType.MSG_CRT_WARN_NOT_FOUND));
+ }
+
+ {
+ CertifyActionsParcel actions = new CertifyActionsParcel(1234L);
+ actions.add(new CertifyAction(mStaticRing1.getMasterKeyId(),
+ mStaticRing2.getPublicKey().getUnorderedUserIds()));
+
+ CertifyResult result = op.execute(actions, new CryptoInputParcel(mKeyPhrase1));
+
+ Assert.assertFalse("certification of nonexistent key must fail", result.success());
+ Assert.assertTrue("must contain error msg about not found",
+ result.getLog().containsType(LogType.MSG_CRT_ERROR_MASTER_NOT_FOUND));
+ }
+
+ }
+
+}
diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/ExportTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/ExportTest.java
new file mode 100644
index 000000000..fa1288e08
--- /dev/null
+++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/ExportTest.java
@@ -0,0 +1,229 @@
+/*
+ * 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 org.junit.Assert;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricGradleTestRunner;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowLog;
+import org.spongycastle.bcpg.sig.KeyFlags;
+import org.spongycastle.jce.provider.BouncyCastleProvider;
+import org.sufficientlysecure.keychain.BuildConfig;
+import org.sufficientlysecure.keychain.WorkaroundBuildConfig;
+import org.sufficientlysecure.keychain.operations.results.PgpEditKeyResult;
+import org.sufficientlysecure.keychain.operations.results.ExportResult;
+import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
+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.service.SaveKeyringParcel;
+import org.sufficientlysecure.keychain.service.SaveKeyringParcel.Algorithm;
+import org.sufficientlysecure.keychain.service.SaveKeyringParcel.ChangeUnlockParcel;
+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;
+
+@RunWith(RobolectricGradleTestRunner.class)
+@Config(constants = WorkaroundBuildConfig.class, sdk = 21, manifest = "src/main/AndroidManifest.xml")
+public class ExportTest {
+
+ static Passphrase mPassphrase = TestingUtils.genPassphrase(true);
+
+ static UncachedKeyRing mStaticRing1, mStaticRing2;
+ static Passphrase mKeyPhrase1 = TestingUtils.genPassphrase(true);
+ static Passphrase mKeyPhrase2 = TestingUtils.genPassphrase(true);
+
+ 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 parcel = new SaveKeyringParcel();
+ parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
+ Algorithm.RSA, 1024, null, KeyFlags.CERTIFY_OTHER, 0L));
+ parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
+ Algorithm.DSA, 1024, null, KeyFlags.SIGN_DATA, 0L));
+ parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
+ Algorithm.ELGAMAL, 1024, null, KeyFlags.ENCRYPT_COMMS, 0L));
+ parcel.mAddUserIds.add("snips");
+ parcel.mNewUnlock = new ChangeUnlockParcel(mKeyPhrase1);
+
+ PgpEditKeyResult result = op.createSecretKeyRing(parcel);
+ Assert.assertTrue("initial test key creation must succeed", result.success());
+ Assert.assertNotNull("initial test key creation must succeed", result.getRing());
+
+ mStaticRing1 = result.getRing();
+ }
+
+ {
+ 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.DSA, 1024, null, KeyFlags.SIGN_DATA, 0L));
+ parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
+ Algorithm.ELGAMAL, 1024, null, KeyFlags.ENCRYPT_COMMS, 0L));
+ parcel.mAddUserIds.add("snails");
+ parcel.mNewUnlock = new ChangeUnlockParcel(null, new Passphrase("1234"));
+
+ PgpEditKeyResult result = op.createSecretKeyRing(parcel);
+ Assert.assertTrue("initial test key creation must succeed", result.success());
+ Assert.assertNotNull("initial test key creation must succeed", result.getRing());
+
+ mStaticRing2 = result.getRing();
+ }
+
+ }
+
+ @Before
+ public void setUp() {
+ ProviderHelper providerHelper = new ProviderHelper(RuntimeEnvironment.application);
+
+ // don't log verbosely here, we're not here to test imports
+ ShadowLog.stream = oldShadowStream;
+
+ providerHelper.saveSecretKeyRing(mStaticRing1, new ProgressScaler());
+ providerHelper.saveSecretKeyRing(mStaticRing2, new ProgressScaler());
+
+ // ok NOW log verbosely!
+ ShadowLog.stream = System.out;
+ }
+
+ @Test
+ public void testExportAll() throws Exception {
+ ImportExportOperation op = new ImportExportOperation(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));
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ExportResult result = op.exportKeyRings(new OperationLog(), null, false, out);
+
+ Assert.assertTrue("export must be a success", result.success());
+
+ long masterKeyId1, masterKeyId2;
+ if (mStaticRing1.getMasterKeyId() < mStaticRing2.getMasterKeyId()) {
+ masterKeyId1 = mStaticRing1.getMasterKeyId();
+ masterKeyId2 = mStaticRing2.getMasterKeyId();
+ } else {
+ masterKeyId2 = mStaticRing1.getMasterKeyId();
+ masterKeyId1 = mStaticRing2.getMasterKeyId();
+ }
+
+ IteratorWithIOThrow<UncachedKeyRing> unc =
+ UncachedKeyRing.fromStream(new ByteArrayInputStream(out.toByteArray()));
+
+ {
+ Assert.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",
+ checkForLocal(ring));
+ }
+
+ {
+ Assert.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",
+ checkForLocal(ring));
+ }
+
+ out = new ByteArrayOutputStream();
+ result = op.exportKeyRings(new OperationLog(), null, true, out);
+
+ Assert.assertTrue("export must be a success", result.success());
+
+ unc = UncachedKeyRing.fromStream(new ByteArrayInputStream(out.toByteArray()));
+
+ {
+ Assert.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",
+ checkForLocal(ring));
+
+ Assert.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",
+ checkForLocal(ring));
+ }
+
+ {
+ Assert.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",
+ checkForLocal(ring));
+
+ Assert.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",
+ checkForLocal(ring));
+ }
+
+ }
+
+ /** 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;
+ }
+
+}
diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/PromoteKeyOperationTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/PromoteKeyOperationTest.java
new file mode 100644
index 000000000..4eaee4c48
--- /dev/null
+++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/operations/PromoteKeyOperationTest.java
@@ -0,0 +1,192 @@
+/*
+ * 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 org.junit.Assert;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricGradleTestRunner;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowLog;
+import org.spongycastle.bcpg.sig.KeyFlags;
+import org.spongycastle.jce.provider.BouncyCastleProvider;
+import org.spongycastle.util.encoders.Hex;
+import org.sufficientlysecure.keychain.BuildConfig;
+import org.sufficientlysecure.keychain.WorkaroundBuildConfig;
+import org.sufficientlysecure.keychain.operations.results.PgpEditKeyResult;
+import org.sufficientlysecure.keychain.operations.results.PromoteKeyResult;
+import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey;
+import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType;
+import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing;
+import org.sufficientlysecure.keychain.pgp.PgpKeyOperation;
+import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
+import org.sufficientlysecure.keychain.pgp.UncachedPublicKey;
+import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
+import org.sufficientlysecure.keychain.service.SaveKeyringParcel.Algorithm;
+import org.sufficientlysecure.keychain.service.SaveKeyringParcel.ChangeUnlockParcel;
+import org.sufficientlysecure.keychain.support.KeyringTestingHelper;
+import org.sufficientlysecure.keychain.util.Passphrase;
+import org.sufficientlysecure.keychain.util.ProgressScaler;
+import org.sufficientlysecure.keychain.util.TestingUtils;
+
+import java.io.PrintStream;
+import java.security.Security;
+import java.util.Iterator;
+
+@RunWith(RobolectricGradleTestRunner.class)
+@Config(constants = WorkaroundBuildConfig.class, sdk = 21, manifest = "src/main/AndroidManifest.xml")
+public class PromoteKeyOperationTest {
+
+ static UncachedKeyRing mStaticRing;
+ static Passphrase mKeyPhrase1 = TestingUtils.genPassphrase(true);
+
+ 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 parcel = new SaveKeyringParcel();
+ parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
+ Algorithm.RSA, 1024, null, KeyFlags.CERTIFY_OTHER, 0L));
+ parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
+ Algorithm.DSA, 1024, null, KeyFlags.SIGN_DATA, 0L));
+ parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
+ Algorithm.ELGAMAL, 1024, null, KeyFlags.ENCRYPT_COMMS, 0L));
+ parcel.mAddUserIds.add("derp");
+ parcel.mNewUnlock = new ChangeUnlockParcel(mKeyPhrase1);
+
+ PgpEditKeyResult result = op.createSecretKeyRing(parcel);
+ Assert.assertTrue("initial test key creation must succeed", result.success());
+ Assert.assertNotNull("initial test key creation must succeed", result.getRing());
+
+ mStaticRing = result.getRing();
+ }
+
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ ProviderHelper providerHelper = new ProviderHelper(RuntimeEnvironment.application);
+
+ // don't log verbosely here, we're not here to test imports
+ ShadowLog.stream = oldShadowStream;
+
+ providerHelper.savePublicKeyRing(mStaticRing.extractPublicKeyRing(), new ProgressScaler());
+
+ // ok NOW log verbosely!
+ ShadowLog.stream = System.out;
+ }
+
+ @Test
+ public void testPromote() throws Exception {
+ PromoteKeyOperation op = new PromoteKeyOperation(RuntimeEnvironment.application,
+ new ProviderHelper(RuntimeEnvironment.application), null, null);
+
+ PromoteKeyResult result = op.execute(mStaticRing.getMasterKeyId(), null, null);
+
+ Assert.assertTrue("promotion must succeed", result.success());
+
+ {
+ CachedPublicKeyRing ring = new ProviderHelper(RuntimeEnvironment.application)
+ .getCachedPublicKeyRing(mStaticRing.getMasterKeyId());
+ Assert.assertTrue("key must have a secret now", ring.hasAnySecret());
+
+ Iterator<UncachedPublicKey> it = mStaticRing.getPublicKeys();
+ while (it.hasNext()) {
+ long keyId = it.next().getKeyId();
+ Assert.assertEquals("all subkeys must be gnu dummy",
+ SecretKeyType.GNU_DUMMY, ring.getSecretKeyType(keyId));
+ }
+ }
+
+ }
+
+ @Test
+ public void testPromoteDivert() throws Exception {
+ PromoteKeyOperation op = new PromoteKeyOperation(RuntimeEnvironment.application,
+ new ProviderHelper(RuntimeEnvironment.application), null, null);
+
+ byte[] aid = Hex.decode("D2760001240102000000012345670000");
+
+ PromoteKeyResult result = op.execute(mStaticRing.getMasterKeyId(), aid, null);
+
+ Assert.assertTrue("promotion must succeed", result.success());
+
+ {
+ CanonicalizedSecretKeyRing ring = new ProviderHelper(RuntimeEnvironment.application)
+ .getCanonicalizedSecretKeyRing(mStaticRing.getMasterKeyId());
+
+ for (CanonicalizedSecretKey key : ring.secretKeyIterator()) {
+ Assert.assertEquals("all subkeys must be divert-to-card",
+ SecretKeyType.DIVERT_TO_CARD, key.getSecretKeyType());
+ Assert.assertArrayEquals("all subkeys must have correct iv",
+ aid, key.getIv());
+ }
+
+ }
+ }
+
+ @Test
+ public void testPromoteDivertSpecific() throws Exception {
+ PromoteKeyOperation op = new PromoteKeyOperation(RuntimeEnvironment.application,
+ new ProviderHelper(RuntimeEnvironment.application), null, null);
+
+ byte[] aid = Hex.decode("D2760001240102000000012345670000");
+
+ // only promote the first, rest stays dummy
+ long keyId = KeyringTestingHelper.getSubkeyId(mStaticRing, 1);
+
+ PromoteKeyResult result = op.execute(mStaticRing.getMasterKeyId(), aid, new long[] {
+ keyId
+ });
+
+ Assert.assertTrue("promotion must succeed", result.success());
+
+ {
+ CanonicalizedSecretKeyRing ring = new ProviderHelper(RuntimeEnvironment.application)
+ .getCanonicalizedSecretKeyRing(mStaticRing.getMasterKeyId());
+
+ for (CanonicalizedSecretKey key : ring.secretKeyIterator()) {
+ if (key.getKeyId() == keyId) {
+ Assert.assertEquals("subkey must be divert-to-card",
+ SecretKeyType.DIVERT_TO_CARD, key.getSecretKeyType());
+ Assert.assertArrayEquals("subkey must have correct iv",
+ aid, key.getIv());
+ } else {
+ Assert.assertEquals("some subkeys must be gnu dummy",
+ SecretKeyType.GNU_DUMMY, key.getSecretKeyType());
+ }
+ }
+
+ }
+ }
+
+}
diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/KeyRingTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/KeyRingTest.java
new file mode 100644
index 000000000..64316b2a6
--- /dev/null
+++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/KeyRingTest.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2015 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * 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.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricGradleTestRunner;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.sufficientlysecure.keychain.BuildConfig;
+import org.sufficientlysecure.keychain.WorkaroundBuildConfig;
+
+@RunWith(RobolectricGradleTestRunner.class)
+@Config(constants = WorkaroundBuildConfig.class, sdk = 21, manifest = "src/main/AndroidManifest.xml")
+public class KeyRingTest {
+
+ @Test
+ public void splitCompleteUserIdShouldReturnAll3Components() throws Exception {
+ KeyRing.UserId info = KeyRing.splitUserId("Max Mustermann (this is a comment) <max@example.com>");
+ Assert.assertEquals("Max Mustermann", info.name);
+ Assert.assertEquals("this is a comment", info.comment);
+ Assert.assertEquals("max@example.com", info.email);
+ }
+
+ @Test
+ public void splitUserIdWithAllButCommentShouldReturnNameAndEmail() throws Exception {
+ KeyRing.UserId info = KeyRing.splitUserId("Max Mustermann <max@example.com>");
+ Assert.assertEquals("Max Mustermann", info.name);
+ Assert.assertNull(info.comment);
+ Assert.assertEquals("max@example.com", info.email);
+ }
+
+ @Test
+ public void splitUserIdWithAllButEmailShouldReturnNameAndComment() throws Exception {
+ KeyRing.UserId info = KeyRing.splitUserId("Max Mustermann (this is a comment)");
+ Assert.assertEquals(info.name, "Max Mustermann");
+ Assert.assertEquals(info.comment, "this is a comment");
+ Assert.assertNull(info.email);
+ }
+
+} \ No newline at end of file
diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/PgpEncryptDecryptTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/PgpEncryptDecryptTest.java
new file mode 100644
index 000000000..9c3636d07
--- /dev/null
+++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/PgpEncryptDecryptTest.java
@@ -0,0 +1,600 @@
+/*
+ * 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.openintents.openpgp.OpenPgpMetadata;
+import org.openintents.openpgp.OpenPgpSignatureResult;
+import org.robolectric.*;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowLog;
+import org.spongycastle.bcpg.sig.KeyFlags;
+import org.spongycastle.jce.provider.BouncyCastleProvider;
+import org.spongycastle.openpgp.PGPEncryptedData;
+import org.sufficientlysecure.keychain.BuildConfig;
+import org.sufficientlysecure.keychain.WorkaroundBuildConfig;
+import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
+import org.sufficientlysecure.keychain.operations.results.PgpEditKeyResult;
+import org.sufficientlysecure.keychain.operations.results.PgpSignEncryptResult;
+import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingData;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
+import org.sufficientlysecure.keychain.service.SaveKeyringParcel.Algorithm;
+import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult;
+import org.sufficientlysecure.keychain.service.SaveKeyringParcel.ChangeUnlockParcel;
+import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
+import org.sufficientlysecure.keychain.service.input.RequiredInputParcel.RequiredInputType;
+import org.sufficientlysecure.keychain.support.KeyringTestingHelper;
+import org.sufficientlysecure.keychain.util.InputData;
+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.HashSet;
+
+@RunWith(RobolectricGradleTestRunner.class)
+@Config(constants = WorkaroundBuildConfig.class, sdk = 21, manifest = "src/main/AndroidManifest.xml")
+public class PgpEncryptDecryptTest {
+
+ static Passphrase mPassphrase = TestingUtils.genPassphrase(true);
+
+ static UncachedKeyRing mStaticRing1, mStaticRing2;
+ static Passphrase mKeyPhrase1 = TestingUtils.genPassphrase(true);
+ static Passphrase mKeyPhrase2 = TestingUtils.genPassphrase(true);
+
+ 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 parcel = new SaveKeyringParcel();
+ parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
+ Algorithm.RSA, 1024, null, KeyFlags.CERTIFY_OTHER, 0L));
+ parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
+ Algorithm.DSA, 1024, null, KeyFlags.SIGN_DATA, 0L));
+ parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
+ Algorithm.ELGAMAL, 1024, null, KeyFlags.ENCRYPT_COMMS, 0L));
+ parcel.mAddUserIds.add("bloom");
+ parcel.mNewUnlock = new ChangeUnlockParcel(mKeyPhrase1);
+
+ PgpEditKeyResult result = op.createSecretKeyRing(parcel);
+ Assert.assertTrue("initial test key creation must succeed", result.success());
+ Assert.assertNotNull("initial test key creation must succeed", result.getRing());
+
+ mStaticRing1 = result.getRing();
+ }
+
+ {
+ 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.DSA, 1024, null, KeyFlags.SIGN_DATA, 0L));
+ parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
+ Algorithm.ELGAMAL, 1024, null, KeyFlags.ENCRYPT_COMMS, 0L));
+ parcel.mAddUserIds.add("belle");
+ parcel.mNewUnlock = new ChangeUnlockParcel(mKeyPhrase2);
+
+ PgpEditKeyResult result = op.createSecretKeyRing(parcel);
+ Assert.assertTrue("initial test key creation must succeed", result.success());
+ Assert.assertNotNull("initial test key creation must succeed", result.getRing());
+
+ mStaticRing2 = result.getRing();
+ }
+
+ }
+
+ @Before
+ public void setUp() {
+ ProviderHelper providerHelper = new ProviderHelper(RuntimeEnvironment.application);
+
+ // don't log verbosely here, we're not here to test imports
+ ShadowLog.stream = oldShadowStream;
+
+ providerHelper.saveSecretKeyRing(mStaticRing1, new ProgressScaler());
+ providerHelper.saveSecretKeyRing(mStaticRing2, new ProgressScaler());
+
+ // ok NOW log verbosely!
+ ShadowLog.stream = System.out;
+ }
+
+ @Test
+ public void testSymmetricEncryptDecrypt() {
+
+ String plaintext = "dies ist ein plaintext ☭" + TestingUtils.genPassphrase(true);
+ byte[] ciphertext;
+
+ { // encrypt data with a given passphrase
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ByteArrayInputStream in = new ByteArrayInputStream(plaintext.getBytes());
+
+ PgpSignEncryptOperation op = new PgpSignEncryptOperation(RuntimeEnvironment.application,
+ new ProviderHelper(RuntimeEnvironment.application), null);
+
+ InputData data = new InputData(in, in.available());
+
+ PgpSignEncryptInputParcel b = new PgpSignEncryptInputParcel();
+ b.setSymmetricPassphrase(mPassphrase);
+ b.setSymmetricEncryptionAlgorithm(PGPEncryptedData.AES_128);
+
+ PgpSignEncryptResult result = op.execute(b, new CryptoInputParcel(), data, out);
+
+ Assert.assertTrue("encryption must succeed", result.success());
+
+ ciphertext = out.toByteArray();
+ }
+
+ { // decryption with same passphrase should yield the same result
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ByteArrayInputStream in = new ByteArrayInputStream(ciphertext);
+ InputData data = new InputData(in, in.available());
+
+ PgpDecryptVerify op = new PgpDecryptVerify(RuntimeEnvironment.application,
+ new ProviderHelper(RuntimeEnvironment.application), null);
+ PgpDecryptVerifyInputParcel input = new PgpDecryptVerifyInputParcel();
+ input.setAllowSymmetricDecryption(true);
+ DecryptVerifyResult result = op.execute(
+ input, new CryptoInputParcel(mPassphrase), data, out);
+
+ Assert.assertTrue("decryption must succeed", result.success());
+ Assert.assertArrayEquals("decrypted ciphertext should equal plaintext",
+ out.toByteArray(), plaintext.getBytes());
+ Assert.assertNull("signature should be an error", result.getSignatureResult());
+
+ CryptoInputParcel cryptoInput = result.getCachedCryptoInputParcel();
+ Assert.assertEquals("cached session keys must be empty",
+ 0, cryptoInput.getCryptoData().size());
+
+ OpenPgpMetadata metadata = result.getDecryptMetadata();
+ Assert.assertEquals("filesize must be correct",
+ out.toByteArray().length, metadata.getOriginalSize());
+ }
+
+ { // decryption with a bad passphrase should fail
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ByteArrayInputStream in = new ByteArrayInputStream(ciphertext);
+ InputData data = new InputData(in, in.available());
+
+ PgpDecryptVerify op = new PgpDecryptVerify(RuntimeEnvironment.application,
+ new ProviderHelper(RuntimeEnvironment.application), null);
+ PgpDecryptVerifyInputParcel input = new PgpDecryptVerifyInputParcel();
+ input.setAllowSymmetricDecryption(true);
+ DecryptVerifyResult result = op.execute(input,
+ new CryptoInputParcel(new Passphrase(new String(mPassphrase.getCharArray()) + "x")),
+ data, out);
+
+ Assert.assertFalse("decryption must fail", result.success());
+ Assert.assertEquals("decrypted plaintext should be empty", 0, out.size());
+ Assert.assertNull("signature should be an error", result.getSignatureResult());
+ }
+
+ { // decryption with an unset passphrase should fail
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ByteArrayInputStream in = new ByteArrayInputStream(ciphertext);
+ InputData data = new InputData(in, in.available());
+
+ PgpDecryptVerify op = new PgpDecryptVerify(RuntimeEnvironment.application,
+ new ProviderHelper(RuntimeEnvironment.application), null);
+ PgpDecryptVerifyInputParcel input = new PgpDecryptVerifyInputParcel();
+ input.setAllowSymmetricDecryption(true);
+ DecryptVerifyResult result = op.execute(input,
+ new CryptoInputParcel(), data, out);
+
+ Assert.assertFalse("decryption must fail", result.success());
+ Assert.assertEquals("decrypted plaintext should be empty", 0, out.size());
+ Assert.assertNull("signature should be an error", result.getSignatureResult());
+ }
+
+ { // decryption if symmetric decryption isn't allowed should fail
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ByteArrayInputStream in = new ByteArrayInputStream(ciphertext);
+ InputData data = new InputData(in, in.available());
+
+ PgpDecryptVerify op = new PgpDecryptVerify(RuntimeEnvironment.application,
+ new ProviderHelper(RuntimeEnvironment.application), null);
+ PgpDecryptVerifyInputParcel input = new PgpDecryptVerifyInputParcel();
+ input.setAllowSymmetricDecryption(false);
+ DecryptVerifyResult result = op.execute(input,
+ new CryptoInputParcel(), data, out);
+
+ Assert.assertFalse("decryption must fail", result.success());
+ Assert.assertEquals("decrypted plaintext should be empty", 0, out.size());
+ Assert.assertNull("signature should be an error", result.getSignatureResult());
+ }
+
+ }
+
+ @Test
+ public void testAsymmetricEncryptDecrypt() {
+
+ String plaintext = "dies ist ein plaintext ☭" + TestingUtils.genPassphrase(true);
+ byte[] ciphertext;
+
+ { // encrypt data with key
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ByteArrayInputStream in = new ByteArrayInputStream(plaintext.getBytes());
+
+ PgpSignEncryptOperation op = new PgpSignEncryptOperation(RuntimeEnvironment.application,
+ new ProviderHelper(RuntimeEnvironment.application), null);
+
+ InputData data = new InputData(in, in.available());
+ PgpSignEncryptInputParcel input = new PgpSignEncryptInputParcel();
+
+ input.setEncryptionMasterKeyIds(new long[] { mStaticRing1.getMasterKeyId() });
+ input.setSymmetricEncryptionAlgorithm(PGPEncryptedData.AES_128);
+ PgpSignEncryptResult result = op.execute(input, new CryptoInputParcel(), data, out);
+ Assert.assertTrue("encryption must succeed", result.success());
+
+ ciphertext = out.toByteArray();
+ }
+
+ { // decryption with provided passphrase should yield the same result
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ByteArrayInputStream in = new ByteArrayInputStream(ciphertext);
+ InputData data = new InputData(in, in.available());
+
+ PgpDecryptVerify op = operationWithFakePassphraseCache(null, null, null);
+ PgpDecryptVerifyInputParcel input = new PgpDecryptVerifyInputParcel();
+ DecryptVerifyResult result = op.execute(input, new CryptoInputParcel(mKeyPhrase1), data, out);
+
+ Assert.assertTrue("decryption with provided passphrase must succeed", result.success());
+ Assert.assertArrayEquals("decrypted ciphertext with provided passphrase should equal plaintext",
+ out.toByteArray(), plaintext.getBytes());
+ Assert.assertNull("signature be empty", result.getSignatureResult());
+
+ CryptoInputParcel cryptoInput = result.getCachedCryptoInputParcel();
+ Assert.assertEquals("must have one cached session key",
+ 1, cryptoInput.getCryptoData().size());
+
+ OpenPgpMetadata metadata = result.getDecryptMetadata();
+ Assert.assertEquals("filesize must be correct",
+ out.toByteArray().length, metadata.getOriginalSize());
+
+ }
+
+ { // decryption with passphrase cached should succeed
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ByteArrayInputStream in = new ByteArrayInputStream(ciphertext);
+ InputData data = new InputData(in, in.available());
+
+ PgpDecryptVerify op = operationWithFakePassphraseCache(
+ mKeyPhrase1, mStaticRing1.getMasterKeyId(), null);
+ PgpDecryptVerifyInputParcel input = new PgpDecryptVerifyInputParcel();
+ DecryptVerifyResult result = op.execute(input, new CryptoInputParcel(), data, out);
+
+ CryptoInputParcel cryptoInput = result.getCachedCryptoInputParcel();
+ Assert.assertEquals("must have one cached session key",
+ 1, cryptoInput.getCryptoData().size());
+
+ Assert.assertTrue("decryption with cached passphrase must succeed", result.success());
+ Assert.assertArrayEquals("decrypted ciphertext with cached passphrase should equal plaintext",
+ out.toByteArray(), plaintext.getBytes());
+ Assert.assertNull("signature should be empty", result.getSignatureResult());
+ }
+
+ { // decryption with no passphrase provided should return status pending
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ByteArrayInputStream in = new ByteArrayInputStream(ciphertext);
+ InputData data = new InputData(in, in.available());
+
+ PgpDecryptVerify op = operationWithFakePassphraseCache(
+ null, mStaticRing1.getMasterKeyId(), null);
+ PgpDecryptVerifyInputParcel input = new PgpDecryptVerifyInputParcel();
+ DecryptVerifyResult result = op.execute(input, new CryptoInputParcel(), data, out);
+
+ Assert.assertFalse("decryption with no passphrase must return pending", result.success());
+ Assert.assertTrue("decryption with no passphrase should return pending", result.isPending());
+ Assert.assertEquals("decryption with no passphrase should return pending passphrase",
+ RequiredInputType.PASSPHRASE, result.getRequiredInputParcel().mType);
+ }
+
+ }
+
+ @Test
+ public void testMultiAsymmetricEncryptDecrypt() {
+
+ String plaintext = "dies ist ein plaintext ☭" + TestingUtils.genPassphrase(true);
+ byte[] ciphertext;
+
+ { // encrypt data with a given passphrase
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ByteArrayInputStream in = new ByteArrayInputStream(plaintext.getBytes());
+
+ PgpSignEncryptOperation op = new PgpSignEncryptOperation(RuntimeEnvironment.application,
+ new ProviderHelper(RuntimeEnvironment.application), null);
+
+ InputData data = new InputData(in, in.available());
+
+ PgpSignEncryptInputParcel b = new PgpSignEncryptInputParcel();
+ b.setEncryptionMasterKeyIds(new long[] {
+ mStaticRing1.getMasterKeyId(),
+ mStaticRing2.getMasterKeyId()
+ });
+ b.setSymmetricEncryptionAlgorithm(PGPEncryptedData.AES_128);
+
+ PgpSignEncryptResult result = op.execute(b, new CryptoInputParcel(), data, out);
+ Assert.assertTrue("encryption must succeed", result.success());
+
+ ciphertext = out.toByteArray();
+ }
+
+ { // decryption with passphrase cached should succeed for the first key
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ByteArrayInputStream in = new ByteArrayInputStream(ciphertext);
+ InputData data = new InputData(in, in.available());
+
+ PgpDecryptVerify op = operationWithFakePassphraseCache(
+ mKeyPhrase1, mStaticRing1.getMasterKeyId(), null);
+ PgpDecryptVerifyInputParcel input = new PgpDecryptVerifyInputParcel();
+ DecryptVerifyResult result = op.execute(input, new CryptoInputParcel(), data, out);
+
+ Assert.assertTrue("decryption with cached passphrase must succeed for the first key", result.success());
+ Assert.assertArrayEquals("decrypted ciphertext with cached passphrase should equal plaintext",
+ out.toByteArray(), plaintext.getBytes());
+ Assert.assertNull("signature should be empty", result.getSignatureResult());
+
+ OpenPgpMetadata metadata = result.getDecryptMetadata();
+ Assert.assertEquals("filesize must be correct",
+ out.toByteArray().length, metadata.getOriginalSize());
+ }
+
+ { // decryption should succeed if key is allowed
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ByteArrayInputStream in = new ByteArrayInputStream(ciphertext);
+ InputData data = new InputData(in, in.available());
+
+ // allow only the second to decrypt
+ HashSet<Long> allowed = new HashSet<>();
+ allowed.add(mStaticRing2.getMasterKeyId());
+
+ // provide passphrase for the second, and check that the first is never asked for!
+ PgpDecryptVerify op = operationWithFakePassphraseCache(
+ mKeyPhrase2, mStaticRing2.getMasterKeyId(), null);
+ PgpDecryptVerifyInputParcel input = new PgpDecryptVerifyInputParcel();
+ input.setAllowedKeyIds(allowed);
+ DecryptVerifyResult result = op.execute(input, new CryptoInputParcel(), data, out);
+
+ Assert.assertTrue("decryption with cached passphrase must succeed for allowed key", result.success());
+ Assert.assertArrayEquals("decrypted ciphertext with cached passphrase should equal plaintext",
+ out.toByteArray(), plaintext.getBytes());
+ Assert.assertTrue("other key was skipped", result.getLog().containsType(LogType.MSG_DC_ASKIP_NOT_ALLOWED));
+ Assert.assertNull("signature should be empty", result.getSignatureResult());
+ }
+
+ { // decryption should fail if no key is allowed
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ByteArrayInputStream in = new ByteArrayInputStream(ciphertext);
+ InputData data = new InputData(in, in.available());
+
+ // provide passphrase for the second, and check that the first is never asked for!
+ PgpDecryptVerify op = operationWithFakePassphraseCache(
+ mKeyPhrase2, mStaticRing2.getMasterKeyId(), null);
+ PgpDecryptVerifyInputParcel input = new PgpDecryptVerifyInputParcel();
+ input.setAllowedKeyIds(new HashSet<Long>());
+ DecryptVerifyResult result = op.execute(input, new CryptoInputParcel(), data, out);
+
+ Assert.assertFalse("decryption must fail if no key allowed", result.success());
+ Assert.assertEquals("decryption must fail with key disllowed status",
+ DecryptVerifyResult.RESULT_KEY_DISALLOWED, result.getResult());
+
+ }
+
+ { // decryption with passphrase cached should succeed for the other key if first is gone
+
+ // delete first key from database
+ new ProviderHelper(RuntimeEnvironment.application).getContentResolver().delete(
+ KeyRingData.buildPublicKeyRingUri(mStaticRing1.getMasterKeyId()), null, null
+ );
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ByteArrayInputStream in = new ByteArrayInputStream(ciphertext);
+ InputData data = new InputData(in, in.available());
+
+ PgpDecryptVerify op = operationWithFakePassphraseCache(
+ mKeyPhrase2, mStaticRing2.getMasterKeyId(), null);
+ PgpDecryptVerifyInputParcel input = new PgpDecryptVerifyInputParcel();
+ DecryptVerifyResult result = op.execute(input, new CryptoInputParcel(), data, out);
+
+ Assert.assertTrue("decryption with cached passphrase must succeed", result.success());
+ Assert.assertArrayEquals("decrypted ciphertext with cached passphrase should equal plaintext",
+ out.toByteArray(), plaintext.getBytes());
+ Assert.assertNull("signature should be empty", result.getSignatureResult());
+ }
+
+ }
+
+ @Test
+ public void testMultiAsymmetricSignEncryptDecryptVerify() {
+
+ String plaintext = "dies ist ein plaintext ☭" + TestingUtils.genPassphrase(true);
+ byte[] ciphertext;
+
+ { // encrypt data with a given passphrase
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ByteArrayInputStream in = new ByteArrayInputStream(plaintext.getBytes());
+
+ PgpSignEncryptOperation op = new PgpSignEncryptOperation(RuntimeEnvironment.application,
+ new ProviderHelper(RuntimeEnvironment.application), null);
+
+ InputData data = new InputData(in, in.available());
+ PgpSignEncryptInputParcel b = new PgpSignEncryptInputParcel();
+
+ b.setEncryptionMasterKeyIds(new long[] {
+ mStaticRing1.getMasterKeyId(),
+ mStaticRing2.getMasterKeyId()
+ });
+ b.setSignatureMasterKeyId(mStaticRing1.getMasterKeyId());
+ b.setSignatureSubKeyId(KeyringTestingHelper.getSubkeyId(mStaticRing1, 1));
+ b.setSymmetricEncryptionAlgorithm(PGPEncryptedData.AES_128);
+
+ PgpSignEncryptResult result = op.execute(b, new CryptoInputParcel(mKeyPhrase1), data, out);
+ Assert.assertTrue("encryption must succeed", result.success());
+
+ ciphertext = out.toByteArray();
+ }
+
+ { // decryption with passphrase cached should succeed for the first key
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ByteArrayInputStream in = new ByteArrayInputStream(ciphertext);
+ InputData data = new InputData(in, in.available());
+
+ PgpDecryptVerify op = operationWithFakePassphraseCache(
+ mKeyPhrase1, mStaticRing1.getMasterKeyId(), null);
+ PgpDecryptVerifyInputParcel input = new PgpDecryptVerifyInputParcel();
+ DecryptVerifyResult result = op.execute(input, new CryptoInputParcel(), data, out);
+
+ Assert.assertTrue("decryption with cached passphrase must succeed for the first key", result.success());
+ Assert.assertArrayEquals("decrypted ciphertext with cached passphrase should equal plaintext",
+ out.toByteArray(), plaintext.getBytes());
+ Assert.assertEquals("signature should be verified and certified",
+ OpenPgpSignatureResult.SIGNATURE_SUCCESS_CERTIFIED, result.getSignatureResult().getStatus());
+
+ OpenPgpMetadata metadata = result.getDecryptMetadata();
+ Assert.assertEquals("filesize must be correct",
+ out.toByteArray().length, metadata.getOriginalSize());
+ }
+
+ { // decryption with passphrase cached should succeed for the other key if first is gone
+
+ // delete first key from database
+ new ProviderHelper(RuntimeEnvironment.application).getContentResolver().delete(
+ KeyRingData.buildPublicKeyRingUri(mStaticRing1.getMasterKeyId()), null, null
+ );
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ByteArrayInputStream in = new ByteArrayInputStream(ciphertext);
+ InputData data = new InputData(in, in.available());
+
+ PgpDecryptVerify op = operationWithFakePassphraseCache(
+ mKeyPhrase2, mStaticRing2.getMasterKeyId(), null);
+ PgpDecryptVerifyInputParcel input = new PgpDecryptVerifyInputParcel();
+ DecryptVerifyResult result = op.execute(input, new CryptoInputParcel(), data, out);
+
+ Assert.assertTrue("decryption with cached passphrase must succeed", result.success());
+ Assert.assertArrayEquals("decrypted ciphertext with cached passphrase should equal plaintext",
+ out.toByteArray(), plaintext.getBytes());
+ Assert.assertEquals("signature key should be missing",
+ OpenPgpSignatureResult.SIGNATURE_KEY_MISSING,
+ result.getSignatureResult().getStatus());
+ }
+
+ }
+
+ @Test
+ public void testForeignEncoding() throws Exception {
+ String plaintext = "ウィキペディア";
+ byte[] plaindata = plaintext.getBytes("iso-2022-jp");
+
+ { // some quick sanity checks
+ Assert.assertEquals(plaintext, new String(plaindata, "iso-2022-jp"));
+ Assert.assertNotEquals(plaintext, new String(plaindata, "utf-8"));
+ }
+
+ byte[] ciphertext;
+ { // encrypt data with a given passphrase
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ByteArrayInputStream in = new ByteArrayInputStream(plaindata);
+
+ PgpSignEncryptOperation op = new PgpSignEncryptOperation(RuntimeEnvironment.application,
+ new ProviderHelper(RuntimeEnvironment.application), null);
+
+ InputData data = new InputData(in, in.available());
+ PgpSignEncryptInputParcel b = new PgpSignEncryptInputParcel();
+
+ b.setEncryptionMasterKeyIds(new long[] { mStaticRing1.getMasterKeyId() });
+ b.setSymmetricEncryptionAlgorithm(PGPEncryptedData.AES_128);
+ // this only works with ascii armored output!
+ b.setEnableAsciiArmorOutput(true);
+ b.setCharset("iso-2022-jp");
+ PgpSignEncryptResult result = op.execute(b, new CryptoInputParcel(), data, out);
+ Assert.assertTrue("encryption must succeed", result.success());
+
+ ciphertext = out.toByteArray();
+ }
+
+ { // decryption with provided passphrase should yield the same result
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ByteArrayInputStream in = new ByteArrayInputStream(ciphertext);
+ InputData data = new InputData(in, in.available());
+
+ PgpDecryptVerify op = operationWithFakePassphraseCache(null, null, null);
+ PgpDecryptVerifyInputParcel input = new PgpDecryptVerifyInputParcel();
+ DecryptVerifyResult result = op.execute(input, new CryptoInputParcel(mKeyPhrase1), data, out);
+
+ Assert.assertTrue("decryption with provided passphrase must succeed", result.success());
+ Assert.assertArrayEquals("decrypted ciphertext should equal plaintext bytes",
+ out.toByteArray(), plaindata);
+ Assert.assertEquals("charset should be read correctly",
+ "iso-2022-jp", result.getCharset());
+ Assert.assertEquals("decrypted ciphertext should equal plaintext",
+ new String(out.toByteArray(), result.getCharset()), plaintext);
+ Assert.assertNull("signature be empty", result.getSignatureResult());
+ }
+
+ }
+
+ private PgpDecryptVerify operationWithFakePassphraseCache(
+ final Passphrase passphrase, final Long checkMasterKeyId, final Long checkSubKeyId) {
+
+ return new PgpDecryptVerify(RuntimeEnvironment.application,
+ new ProviderHelper(RuntimeEnvironment.application), null) {
+ @Override
+ public Passphrase getCachedPassphrase(long masterKeyId, long subKeyId)
+ throws NoSecretKeyException {
+ if (checkMasterKeyId != null) {
+ Assert.assertEquals("requested passphrase should be for expected master key id",
+ (long) checkMasterKeyId, masterKeyId);
+ }
+ if (checkSubKeyId != null) {
+ Assert.assertEquals("requested passphrase should be for expected sub key id",
+ (long) checkSubKeyId, subKeyId);
+ }
+ if (passphrase == null) {
+ return null;
+ }
+ return passphrase;
+ }
+ };
+ }
+} \ No newline at end of file
diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperationTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperationTest.java
new file mode 100644
index 000000000..d92e90d6a
--- /dev/null
+++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperationTest.java
@@ -0,0 +1,1379 @@
+/*
+ * 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 junit.framework.AssertionFailedError;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.runner.RunWith;
+import org.robolectric.*;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowLog;
+import org.spongycastle.bcpg.BCPGInputStream;
+import org.spongycastle.bcpg.Packet;
+import org.spongycastle.bcpg.PacketTags;
+import org.spongycastle.bcpg.S2K;
+import org.spongycastle.bcpg.SecretKeyPacket;
+import org.spongycastle.bcpg.SecretSubkeyPacket;
+import org.spongycastle.bcpg.SignaturePacket;
+import org.spongycastle.bcpg.UserAttributePacket;
+import org.spongycastle.bcpg.UserAttributeSubpacket;
+import org.spongycastle.bcpg.UserIDPacket;
+import org.spongycastle.bcpg.sig.KeyFlags;
+import org.spongycastle.jce.provider.BouncyCastleProvider;
+import org.spongycastle.openpgp.PGPSignature;
+import org.sufficientlysecure.keychain.BuildConfig;
+import org.sufficientlysecure.keychain.WorkaroundBuildConfig;
+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.CanonicalizedSecretKey.SecretKeyType;
+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.SaveKeyringParcel.SubkeyAdd;
+import org.sufficientlysecure.keychain.service.SaveKeyringParcel.SubkeyChange;
+import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
+import org.sufficientlysecure.keychain.service.input.RequiredInputParcel.RequiredInputType;
+import org.sufficientlysecure.keychain.support.KeyringBuilder;
+import org.sufficientlysecure.keychain.support.KeyringTestingHelper;
+import org.sufficientlysecure.keychain.support.KeyringTestingHelper.RawPacket;
+import org.sufficientlysecure.keychain.support.TestDataUtil;
+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.IOException;
+import java.nio.ByteBuffer;
+import java.security.Security;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Random;
+
+@RunWith(RobolectricGradleTestRunner.class)
+@Config(constants = WorkaroundBuildConfig.class, sdk = 21, manifest = "src/main/AndroidManifest.xml")
+public class PgpKeyOperationTest {
+
+ static UncachedKeyRing staticRing;
+ final static Passphrase passphrase = TestingUtils.genPassphrase();
+
+ UncachedKeyRing ring;
+ PgpKeyOperation op;
+ SaveKeyringParcel parcel;
+ ArrayList<RawPacket> onlyA = new ArrayList<RawPacket>();
+ ArrayList<RawPacket> onlyB = new ArrayList<RawPacket>();
+
+ static CryptoInputParcel cryptoInput;
+
+ @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.DSA, 1024, null, KeyFlags.SIGN_DATA, 0L));
+ parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
+ Algorithm.RSA, 2048, null, KeyFlags.ENCRYPT_COMMS, 0L));
+
+ parcel.mAddUserIds.add("twi");
+ parcel.mAddUserIds.add("pink");
+
+ {
+ int type = 42;
+ byte[] data = new byte[] { 0, 1, 2, 3, 4 };
+ WrappedUserAttribute uat = WrappedUserAttribute.fromSubpacket(type, data);
+ parcel.mAddUserAttribute.add(uat);
+ }
+
+ parcel.mNewUnlock = new ChangeUnlockParcel(passphrase);
+ PgpKeyOperation op = new PgpKeyOperation(null);
+
+ PgpEditKeyResult result = op.createSecretKeyRing(parcel);
+ Assert.assertTrue("initial test key creation must succeed", result.success());
+ Assert.assertNotNull("initial test key creation must succeed", result.getRing());
+
+ staticRing = result.getRing();
+ staticRing = staticRing.canonicalize(new OperationLog(), 0).getUncachedKeyRing();
+
+ // we sleep here for a second, to make sure all new certificates have different timestamps
+ Thread.sleep(1000);
+
+ cryptoInput = new CryptoInputParcel(new Date(), passphrase);
+
+ }
+
+ @Before public void setUp() throws Exception {
+ // show Log.x messages in system.out
+ ShadowLog.stream = System.out;
+ ring = staticRing;
+
+ // 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 = ring.getMasterKeyId();
+ parcel.mFingerprint = ring.getFingerprint();
+
+ }
+
+ @Test
+ public void createSecretKeyRingTests() {
+
+ {
+ parcel.reset();
+ parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
+ Algorithm.RSA, new Random().nextInt(256)+255, null, KeyFlags.CERTIFY_OTHER, 0L));
+ parcel.mAddUserIds.add("shy");
+ parcel.mNewUnlock = new ChangeUnlockParcel(passphrase);
+
+ assertFailure("creating ring with < 512 bytes keysize should fail", parcel,
+ LogType.MSG_CR_ERROR_KEYSIZE_512);
+ }
+
+ {
+ parcel.reset();
+ parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
+ Algorithm.ELGAMAL, 1024, null, KeyFlags.CERTIFY_OTHER, 0L));
+ parcel.mAddUserIds.add("shy");
+ parcel.mNewUnlock = new ChangeUnlockParcel(passphrase);
+
+ assertFailure("creating ring with ElGamal master key should fail", parcel,
+ LogType.MSG_CR_ERROR_FLAGS_ELGAMAL);
+ }
+
+ {
+ parcel.reset();
+ parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
+ Algorithm.RSA, 1024, null, KeyFlags.CERTIFY_OTHER, null));
+ parcel.mAddUserIds.add("lotus");
+ parcel.mNewUnlock = new ChangeUnlockParcel(passphrase);
+
+ assertFailure("creating master key with null expiry should fail", parcel,
+ LogType.MSG_CR_ERROR_NULL_EXPIRY);
+ }
+
+ {
+ parcel.reset();
+ parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
+ Algorithm.RSA, 1024, null, KeyFlags.SIGN_DATA, 0L));
+ parcel.mAddUserIds.add("shy");
+ parcel.mNewUnlock = new ChangeUnlockParcel(passphrase);
+
+ assertFailure("creating ring with non-certifying master key should fail", parcel,
+ LogType.MSG_CR_ERROR_NO_CERTIFY);
+ }
+
+ {
+ parcel.reset();
+ parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
+ Algorithm.RSA, 1024, null, KeyFlags.CERTIFY_OTHER, 0L));
+ parcel.mNewUnlock = new ChangeUnlockParcel(passphrase);
+
+ assertFailure("creating ring without user ids should fail", parcel,
+ LogType.MSG_CR_ERROR_NO_USER_ID);
+ }
+
+ {
+ parcel.reset();
+ parcel.mAddUserIds.add("shy");
+ parcel.mNewUnlock = new ChangeUnlockParcel(passphrase);
+
+ assertFailure("creating ring with no master key should fail", parcel,
+ LogType.MSG_CR_ERROR_NO_MASTER);
+ }
+
+ }
+
+ @Test
+ // this is a special case since the flags are in user id certificates rather than
+ // subkey binding certificates
+ public void testMasterFlags() throws Exception {
+ SaveKeyringParcel parcel = new SaveKeyringParcel();
+ parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
+ Algorithm.RSA, 1024, null, KeyFlags.CERTIFY_OTHER | KeyFlags.SIGN_DATA, 0L));
+ parcel.mAddUserIds.add("luna");
+ ring = assertCreateSuccess("creating ring with master key flags must succeed", parcel);
+
+ Assert.assertEquals("the keyring should contain only the master key",
+ 1, KeyringTestingHelper.itToList(ring.getPublicKeys()).size());
+ Assert.assertEquals("first (master) key must have both flags",
+ KeyFlags.CERTIFY_OTHER | KeyFlags.SIGN_DATA, (long) ring.getPublicKey().getKeyUsage());
+
+ }
+
+ @Test
+ public void testCreatedKey() throws Exception {
+
+ // an empty modification should change nothing. this also ensures the keyring
+ // is constant through canonicalization.
+ // applyModificationWithChecks(parcel, ring, onlyA, onlyB);
+
+ Assert.assertNotNull("key creation failed", ring);
+
+ Assert.assertNull("primary user id must be empty",
+ ring.getPublicKey().getPrimaryUserId());
+
+ Assert.assertEquals("number of user ids must be two",
+ 2, ring.getPublicKey().getUnorderedUserIds().size());
+
+ ArrayList<WrappedUserAttribute> attributes =
+ ring.getPublicKey().getUnorderedUserAttributes();
+ Assert.assertEquals("number of user attributes must be one",
+ 1, attributes.size());
+ Assert.assertEquals("user attribute must be correct type",
+ 42, attributes.get(0).getType());
+ Assert.assertEquals("user attribute must have one subpacket",
+ 1, attributes.get(0).getSubpackets().length);
+ Assert.assertArrayEquals("user attribute must have correct data",
+ new byte[] { 0, 1, 2, 3, 4 }, attributes.get(0).getSubpackets()[0]);
+
+ List<UncachedPublicKey> subkeys = KeyringTestingHelper.itToList(ring.getPublicKeys());
+ Assert.assertEquals("number of subkeys must be three", 3, subkeys.size());
+
+ Assert.assertTrue("key ring should have been created in the last 120 seconds",
+ ring.getPublicKey().getCreationTime().after(new Date(new Date().getTime()-1000*120)));
+
+ Assert.assertNull("key ring should not expire",
+ ring.getPublicKey().getUnsafeExpiryTimeForTesting());
+
+ Assert.assertEquals("first (master) key can certify",
+ KeyFlags.CERTIFY_OTHER, (long) subkeys.get(0).getKeyUsage());
+
+ Assert.assertEquals("second key can sign",
+ KeyFlags.SIGN_DATA, (long) subkeys.get(1).getKeyUsage());
+ ArrayList<WrappedSignature> sigs = subkeys.get(1).getSignatures().next().getEmbeddedSignatures();
+ Assert.assertEquals("signing key signature should have one embedded signature",
+ 1, sigs.size());
+ Assert.assertEquals("embedded signature should be of primary key binding type",
+ PGPSignature.PRIMARYKEY_BINDING, sigs.get(0).getSignatureType());
+ Assert.assertEquals("primary key binding signature issuer should be signing subkey",
+ subkeys.get(1).getKeyId(), sigs.get(0).getKeyId());
+
+ Assert.assertEquals("third key can encrypt",
+ KeyFlags.ENCRYPT_COMMS, (long) subkeys.get(2).getKeyUsage());
+
+ }
+
+ @Test
+ public void testBadKeyModification() throws Exception {
+
+ {
+ SaveKeyringParcel parcel = new SaveKeyringParcel();
+ // off by one
+ parcel.mMasterKeyId = ring.getMasterKeyId() -1;
+ parcel.mFingerprint = ring.getFingerprint();
+
+ assertModifyFailure("keyring modification with bad master key id should fail",
+ ring, parcel, LogType.MSG_MF_ERROR_KEYID);
+ }
+
+ {
+ SaveKeyringParcel parcel = new SaveKeyringParcel();
+ // off by one
+ parcel.mMasterKeyId = null;
+ parcel.mFingerprint = ring.getFingerprint();
+
+ assertModifyFailure("keyring modification with null master key id should fail",
+ ring, parcel, LogType.MSG_MF_ERROR_KEYID);
+ }
+
+ {
+ SaveKeyringParcel parcel = new SaveKeyringParcel();
+ parcel.mMasterKeyId = ring.getMasterKeyId();
+ parcel.mFingerprint = ring.getFingerprint();
+ // some byte, off by one
+ parcel.mFingerprint[5] += 1;
+
+ assertModifyFailure("keyring modification with bad fingerprint should fail",
+ ring, parcel, LogType.MSG_MF_ERROR_FINGERPRINT);
+ }
+
+ {
+ SaveKeyringParcel parcel = new SaveKeyringParcel();
+ parcel.mMasterKeyId = ring.getMasterKeyId();
+ parcel.mFingerprint = null;
+
+ assertModifyFailure("keyring modification with null fingerprint should fail",
+ ring, parcel, LogType.MSG_MF_ERROR_FINGERPRINT);
+ }
+
+ {
+ Passphrase badphrase = new Passphrase();
+ if (badphrase.equals(passphrase)) {
+ badphrase = new Passphrase("a");
+ }
+ parcel.mAddUserIds.add("allure");
+
+ assertModifyFailure("keyring modification with bad passphrase should fail",
+ ring, parcel, new CryptoInputParcel(badphrase), LogType.MSG_MF_UNLOCK_ERROR);
+ }
+
+ {
+ parcel.reset();
+ assertModifyFailure("no-op should fail",
+ ring, parcel, cryptoInput, LogType.MSG_MF_ERROR_NOOP);
+ }
+
+ }
+
+ @Test
+ public void testSubkeyAdd() throws Exception {
+
+ long expiry = new Date().getTime() / 1000 + 159;
+ int flags = KeyFlags.SIGN_DATA;
+ int bits = 1024 + new Random().nextInt(8);
+ parcel.mAddSubKeys.add(new SubkeyAdd(Algorithm.RSA, bits, null, flags, expiry));
+
+ UncachedKeyRing modified = applyModificationWithChecks(parcel, ring, onlyA, onlyB);
+
+ Assert.assertEquals("no extra packets in original", 0, onlyA.size());
+ Assert.assertEquals("exactly two extra packets in modified", 2, onlyB.size());
+
+ Packet p;
+
+ p = new BCPGInputStream(new ByteArrayInputStream(onlyB.get(0).buf)).readPacket();
+ Assert.assertTrue("first new packet must be secret subkey", p instanceof SecretSubkeyPacket);
+
+ p = new BCPGInputStream(new ByteArrayInputStream(onlyB.get(1).buf)).readPacket();
+ Assert.assertTrue("second new packet must be signature", p instanceof SignaturePacket);
+ Assert.assertEquals("signature type must be subkey binding certificate",
+ PGPSignature.SUBKEY_BINDING, ((SignaturePacket) p).getSignatureType());
+ Assert.assertEquals("signature must have been created by master key",
+ ring.getMasterKeyId(), ((SignaturePacket) p).getKeyID());
+
+ // get new key from ring. it should be the last one (add a check to make sure?)
+ UncachedPublicKey newKey = null;
+ {
+ Iterator<UncachedPublicKey> it = modified.getPublicKeys();
+ while (it.hasNext()) {
+ newKey = it.next();
+ }
+ }
+
+ Assert.assertNotNull("new key is not null", newKey);
+ Assert.assertNotNull("added key must have an expiry date",
+ newKey.getUnsafeExpiryTimeForTesting());
+ Assert.assertEquals("added key must have expected expiry date",
+ expiry, newKey.getUnsafeExpiryTimeForTesting().getTime()/1000);
+ Assert.assertEquals("added key must have expected flags",
+ flags, (long) newKey.getKeyUsage());
+ Assert.assertEquals("added key must have expected bitsize",
+ bits, (int) newKey.getBitStrength());
+
+ { // bad keysize should fail
+ parcel.reset();
+ parcel.mAddSubKeys.add(new SubkeyAdd(
+ Algorithm.RSA, new Random().nextInt(512), null, KeyFlags.SIGN_DATA, 0L));
+ assertModifyFailure("creating a subkey with keysize < 512 should fail", ring, parcel,
+ LogType.MSG_CR_ERROR_KEYSIZE_512);
+
+ }
+
+ {
+ parcel.reset();
+ parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
+ Algorithm.RSA, 1024, null, KeyFlags.SIGN_DATA, null));
+
+ assertModifyFailure("creating master key with null expiry should fail", ring, parcel,
+ LogType.MSG_MF_ERROR_NULL_EXPIRY);
+ }
+
+ { // a past expiry should fail
+ parcel.reset();
+ parcel.mAddSubKeys.add(new SubkeyAdd(Algorithm.RSA, 1024, null, KeyFlags.SIGN_DATA,
+ new Date().getTime()/1000-10));
+ assertModifyFailure("creating subkey with past expiry date should fail", ring, parcel,
+ LogType.MSG_MF_ERROR_PAST_EXPIRY);
+ }
+
+ }
+
+ @Test
+ public void testSubkeyModify() throws Exception {
+
+ long expiry = new Date().getTime()/1000 + 1024;
+ long keyId = KeyringTestingHelper.getSubkeyId(ring, 1);
+
+ UncachedKeyRing modified = ring;
+ {
+ parcel.mChangeSubKeys.add(new SubkeyChange(keyId, null, expiry));
+ modified = applyModificationWithChecks(parcel, modified, onlyA, onlyB);
+
+ Assert.assertEquals("one extra packet in original", 1, onlyA.size());
+ Assert.assertEquals("one extra packet in modified", 1, onlyB.size());
+
+ Assert.assertEquals("old packet must be signature",
+ PacketTags.SIGNATURE, onlyA.get(0).tag);
+
+ Packet p = new BCPGInputStream(new ByteArrayInputStream(onlyB.get(0).buf)).readPacket();
+ Assert.assertTrue("first new packet must be signature", p instanceof SignaturePacket);
+ Assert.assertEquals("signature type must be subkey binding certificate",
+ PGPSignature.SUBKEY_BINDING, ((SignaturePacket) p).getSignatureType());
+ Assert.assertEquals("signature must have been created by master key",
+ ring.getMasterKeyId(), ((SignaturePacket) p).getKeyID());
+
+ Assert.assertNotNull("modified key must have an expiry date",
+ modified.getPublicKey(keyId).getUnsafeExpiryTimeForTesting());
+ Assert.assertEquals("modified key must have expected expiry date",
+ expiry, modified.getPublicKey(keyId).getUnsafeExpiryTimeForTesting().getTime()/1000);
+ Assert.assertEquals("modified key must have same flags as before",
+ ring.getPublicKey(keyId).getKeyUsage(), modified.getPublicKey(keyId).getKeyUsage());
+ }
+
+ { // change expiry
+ expiry += 60*60*24;
+
+ parcel.mChangeSubKeys.add(new SubkeyChange(keyId, null, expiry));
+ modified = applyModificationWithChecks(parcel, modified, onlyA, onlyB);
+
+ Assert.assertNotNull("modified key must have an expiry date",
+ modified.getPublicKey(keyId).getUnsafeExpiryTimeForTesting());
+ Assert.assertEquals("modified key must have expected expiry date",
+ expiry, modified.getPublicKey(keyId).getUnsafeExpiryTimeForTesting().getTime()/1000);
+ Assert.assertEquals("modified key must have same flags as before",
+ ring.getPublicKey(keyId).getKeyUsage(), modified.getPublicKey(keyId).getKeyUsage());
+ }
+
+ {
+ int flags = KeyFlags.SIGN_DATA | KeyFlags.ENCRYPT_COMMS;
+ parcel.reset();
+ parcel.mChangeSubKeys.add(new SubkeyChange(keyId, flags, null));
+ modified = applyModificationWithChecks(parcel, modified, onlyA, onlyB);
+
+ Assert.assertEquals("old packet must be signature",
+ PacketTags.SIGNATURE, onlyA.get(0).tag);
+
+ Packet p = new BCPGInputStream(new ByteArrayInputStream(onlyB.get(0).buf)).readPacket();
+ Assert.assertTrue("first new packet must be signature", p instanceof SignaturePacket);
+ Assert.assertEquals("signature type must be subkey binding certificate",
+ PGPSignature.SUBKEY_BINDING, ((SignaturePacket) p).getSignatureType());
+ Assert.assertEquals("signature must have been created by master key",
+ ring.getMasterKeyId(), ((SignaturePacket) p).getKeyID());
+
+ Assert.assertEquals("modified key must have expected flags",
+ flags, (long) modified.getPublicKey(keyId).getKeyUsage());
+ Assert.assertNotNull("key must retain its expiry",
+ modified.getPublicKey(keyId).getUnsafeExpiryTimeForTesting());
+ Assert.assertEquals("key expiry must be unchanged",
+ expiry, modified.getPublicKey(keyId).getUnsafeExpiryTimeForTesting().getTime()/1000);
+ }
+
+ { // expiry of 0 should be "no expiry"
+ parcel.reset();
+ parcel.mChangeSubKeys.add(new SubkeyChange(keyId, null, 0L));
+ modified = applyModificationWithChecks(parcel, modified, onlyA, onlyB);
+
+ Assert.assertEquals("old packet must be signature",
+ PacketTags.SIGNATURE, onlyA.get(0).tag);
+
+ Packet p = new BCPGInputStream(new ByteArrayInputStream(onlyB.get(0).buf)).readPacket();
+ Assert.assertTrue("first new packet must be signature", p instanceof SignaturePacket);
+ Assert.assertEquals("signature type must be subkey binding certificate",
+ PGPSignature.SUBKEY_BINDING, ((SignaturePacket) p).getSignatureType());
+ Assert.assertEquals("signature must have been created by master key",
+ ring.getMasterKeyId(), ((SignaturePacket) p).getKeyID());
+
+ Assert.assertNull("key must not expire anymore", modified.getPublicKey(keyId).getUnsafeExpiryTimeForTesting());
+ }
+
+ { // a past expiry should fail
+ parcel.reset();
+ parcel.mChangeSubKeys.add(new SubkeyChange(keyId, null, new Date().getTime()/1000-10));
+
+ assertModifyFailure("setting subkey expiry to a past date should fail", ring, parcel,
+ LogType.MSG_MF_ERROR_PAST_EXPIRY);
+ }
+
+ { // modifying nonexistent subkey should fail
+ parcel.reset();
+ parcel.mChangeSubKeys.add(new SubkeyChange(123, null, null));
+
+ assertModifyFailure("modifying non-existent subkey should fail", ring, parcel,
+ LogType.MSG_MF_ERROR_SUBKEY_MISSING);
+ }
+
+ }
+
+ @Test
+ public void testMasterModify() throws Exception {
+
+ long expiry = new Date().getTime()/1000 + 1024;
+ long keyId = ring.getMasterKeyId();
+
+ UncachedKeyRing modified = ring;
+
+ // to make this check less trivial, we add a user id, change the primary one and revoke one
+ parcel.mAddUserIds.add("aloe");
+ parcel.mChangePrimaryUserId = "aloe";
+ parcel.mRevokeUserIds.add("pink");
+ modified = applyModificationWithChecks(parcel, modified, onlyA, onlyB);
+
+ {
+ parcel.reset();
+ parcel.mChangeSubKeys.add(new SubkeyChange(keyId, null, expiry));
+ modified = applyModificationWithChecks(parcel, modified, onlyA, onlyB);
+
+ // this implies that only the two non-revoked signatures were changed!
+ Assert.assertEquals("two extra packets in original", 2, onlyA.size());
+ Assert.assertEquals("two extra packets in modified", 2, onlyB.size());
+
+ Assert.assertEquals("first original packet must be a signature",
+ PacketTags.SIGNATURE, onlyA.get(0).tag);
+ Assert.assertEquals("second original packet must be a signature",
+ PacketTags.SIGNATURE, onlyA.get(1).tag);
+ Assert.assertEquals("first new packet must be signature",
+ PacketTags.SIGNATURE, onlyB.get(0).tag);
+ Assert.assertEquals("first new packet must be signature",
+ PacketTags.SIGNATURE, onlyB.get(1).tag);
+
+ Assert.assertNotNull("modified key must have an expiry date",
+ modified.getPublicKey().getUnsafeExpiryTimeForTesting());
+ Assert.assertEquals("modified key must have expected expiry date",
+ expiry, modified.getPublicKey().getUnsafeExpiryTimeForTesting().getTime() / 1000);
+ Assert.assertEquals("modified key must have same flags as before",
+ ring.getPublicKey().getKeyUsage(), modified.getPublicKey().getKeyUsage());
+ }
+
+ { // change expiry
+ expiry += 60*60*24;
+
+ parcel.mChangeSubKeys.add(new SubkeyChange(keyId, null, expiry));
+ modified = applyModificationWithChecks(parcel, modified, onlyA, onlyB);
+
+ Assert.assertNotNull("modified key must have an expiry date",
+ modified.getPublicKey(keyId).getUnsafeExpiryTimeForTesting());
+ Assert.assertEquals("modified key must have expected expiry date",
+ expiry, modified.getPublicKey(keyId).getUnsafeExpiryTimeForTesting().getTime() / 1000);
+ Assert.assertEquals("modified key must have same flags as before",
+ ring.getPublicKey(keyId).getKeyUsage(), modified.getPublicKey(keyId).getKeyUsage());
+
+ Date date = modified.canonicalize(new OperationLog(), 0).getPublicKey().getExpiryTime();
+ Assert.assertNotNull("modified key must have an expiry date", date);
+ Assert.assertEquals("modified key must have expected expiry date",
+ expiry, date.getTime() / 1000);
+
+ }
+
+ {
+ int flags = KeyFlags.CERTIFY_OTHER | KeyFlags.SIGN_DATA;
+ parcel.reset();
+ parcel.mChangeSubKeys.add(new SubkeyChange(keyId, flags, null));
+ modified = applyModificationWithChecks(parcel, modified, onlyA, onlyB);
+
+ Assert.assertEquals("modified key must have expected flags",
+ flags, (long) modified.getPublicKey(keyId).getKeyUsage());
+ Assert.assertNotNull("key must retain its expiry",
+ modified.getPublicKey(keyId).getUnsafeExpiryTimeForTesting());
+ Assert.assertEquals("key expiry must be unchanged",
+ expiry, modified.getPublicKey(keyId).getUnsafeExpiryTimeForTesting().getTime()/1000);
+ }
+
+ { // expiry of 0 should be "no expiry"
+
+ // even if there is a non-expiring user id while all others are revoked, it doesn't count!
+ // for this purpose we revoke one while they still have expiry times
+ parcel.reset();
+ parcel.mRevokeUserIds.add("aloe");
+ modified = applyModificationWithChecks(parcel, modified, onlyA, onlyB);
+
+ parcel.reset();
+ parcel.mChangeSubKeys.add(new SubkeyChange(keyId, null, 0L));
+ modified = applyModificationWithChecks(parcel, modified, onlyA, onlyB);
+
+ // for this check, it is relevant that we DON'T use the unsafe one!
+ Assert.assertNull("key must not expire anymore",
+ modified.canonicalize(new OperationLog(), 0).getPublicKey().getExpiryTime());
+ // make sure the unsafe one behaves incorrectly as expected
+ Assert.assertNotNull("unsafe expiry must yield wrong result from revoked user id",
+ modified.getPublicKey(keyId).getUnsafeExpiryTimeForTesting());
+ }
+
+ { // if we revoke everything, nothing is left to properly sign...
+ parcel.reset();
+ parcel.mRevokeUserIds.add("twi");
+ parcel.mRevokeUserIds.add("pink");
+ parcel.mChangeSubKeys.add(new SubkeyChange(keyId, KeyFlags.CERTIFY_OTHER, null));
+
+ assertModifyFailure("master key modification with all user ids revoked should fail", ring, parcel,
+ LogType.MSG_MF_ERROR_MASTER_NONE);
+ }
+
+ { // any flag not including CERTIFY_OTHER should fail
+ parcel.reset();
+ parcel.mChangeSubKeys.add(new SubkeyChange(keyId, KeyFlags.SIGN_DATA, null));
+
+ assertModifyFailure("setting master key flags without certify should fail", ring, parcel,
+ LogType.MSG_MF_ERROR_NO_CERTIFY);
+ }
+
+ { // a past expiry should fail
+ parcel.reset();
+ parcel.mChangeSubKeys.add(new SubkeyChange(keyId, null, new Date().getTime()/1000-10));
+
+ assertModifyFailure("setting subkey expiry to a past date should fail", ring, parcel,
+ LogType.MSG_MF_ERROR_PAST_EXPIRY);
+ }
+
+ }
+
+ @Test
+ public void testMasterRevoke() throws Exception {
+
+ parcel.reset();
+ parcel.mRevokeSubKeys.add(ring.getMasterKeyId());
+
+ UncachedKeyRing modified = applyModificationWithChecks(parcel, ring, onlyA, onlyB);
+
+ Assert.assertEquals("no extra packets in original", 0, onlyA.size());
+ Assert.assertEquals("exactly one extra packet in modified", 1, onlyB.size());
+
+ Packet p;
+
+ p = new BCPGInputStream(new ByteArrayInputStream(onlyB.get(0).buf)).readPacket();
+ Assert.assertTrue("first new packet must be secret subkey", p instanceof SignaturePacket);
+ Assert.assertEquals("signature type must be subkey binding certificate",
+ PGPSignature.KEY_REVOCATION, ((SignaturePacket) p).getSignatureType());
+ Assert.assertEquals("signature must have been created by master key",
+ ring.getMasterKeyId(), ((SignaturePacket) p).getKeyID());
+
+ Assert.assertTrue("subkey must actually be revoked",
+ modified.getPublicKey().isMaybeRevoked());
+
+ }
+
+ @Test
+ public void testSubkeyRevoke() throws Exception {
+
+ long keyId = KeyringTestingHelper.getSubkeyId(ring, 1);
+ int flags = ring.getPublicKey(keyId).getKeyUsage();
+
+ UncachedKeyRing modified;
+
+ {
+
+ parcel.reset();
+ parcel.mRevokeSubKeys.add(123L);
+
+ CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing(ring.getEncoded(), false, 0);
+ UncachedKeyRing otherModified = op.modifySecretKeyRing(secretRing, cryptoInput, parcel).getRing();
+
+ Assert.assertNull("revoking a nonexistent subkey should fail", otherModified);
+
+ }
+
+ { // revoked second subkey
+
+ parcel.reset();
+ parcel.mRevokeSubKeys.add(keyId);
+
+ modified = applyModificationWithChecks(parcel, ring, onlyA, onlyB,
+ new CryptoInputParcel(new Date(), passphrase));
+
+ Assert.assertEquals("no extra packets in original", 0, onlyA.size());
+ Assert.assertEquals("exactly one extra packet in modified", 1, onlyB.size());
+
+ Packet p;
+
+ p = new BCPGInputStream(new ByteArrayInputStream(onlyB.get(0).buf)).readPacket();
+ Assert.assertTrue("first new packet must be secret subkey", p instanceof SignaturePacket);
+ Assert.assertEquals("signature type must be subkey binding certificate",
+ PGPSignature.SUBKEY_REVOCATION, ((SignaturePacket) p).getSignatureType());
+ Assert.assertEquals("signature must have been created by master key",
+ ring.getMasterKeyId(), ((SignaturePacket) p).getKeyID());
+
+ Assert.assertTrue("subkey must actually be revoked",
+ modified.getPublicKey(keyId).isMaybeRevoked());
+ }
+
+ { // re-add second subkey
+
+ parcel.reset();
+ // re-certify the revoked subkey
+ parcel.mChangeSubKeys.add(new SubkeyChange(keyId, true));
+
+ modified = applyModificationWithChecks(parcel, modified, onlyA, onlyB);
+
+ Assert.assertEquals("exactly two outdated packets in original", 2, onlyA.size());
+ Assert.assertEquals("exactly one extra packet in modified", 1, onlyB.size());
+
+ Packet p;
+
+ p = new BCPGInputStream(new ByteArrayInputStream(onlyA.get(0).buf)).readPacket();
+ Assert.assertTrue("first outdated packet must be signature", p instanceof SignaturePacket);
+ Assert.assertEquals("first outdated signature type must be subkey binding certification",
+ PGPSignature.SUBKEY_BINDING, ((SignaturePacket) p).getSignatureType());
+ Assert.assertEquals("first outdated signature must have been created by master key",
+ ring.getMasterKeyId(), ((SignaturePacket) p).getKeyID());
+
+ p = new BCPGInputStream(new ByteArrayInputStream(onlyA.get(1).buf)).readPacket();
+ Assert.assertTrue("second outdated packet must be signature", p instanceof SignaturePacket);
+ Assert.assertEquals("second outdated signature type must be subkey revocation",
+ PGPSignature.SUBKEY_REVOCATION, ((SignaturePacket) p).getSignatureType());
+ Assert.assertEquals("second outdated signature must have been created by master key",
+ ring.getMasterKeyId(), ((SignaturePacket) p).getKeyID());
+
+ p = new BCPGInputStream(new ByteArrayInputStream(onlyB.get(0).buf)).readPacket();
+ Assert.assertTrue("new packet must be signature ", p instanceof SignaturePacket);
+ Assert.assertEquals("new signature type must be subkey binding certification",
+ PGPSignature.SUBKEY_BINDING, ((SignaturePacket) p).getSignatureType());
+ Assert.assertEquals("signature must have been created by master key",
+ ring.getMasterKeyId(), ((SignaturePacket) p).getKeyID());
+
+ Assert.assertFalse("subkey must no longer be revoked",
+ modified.getPublicKey(keyId).isMaybeRevoked());
+ Assert.assertEquals("subkey must have the same usage flags as before",
+ flags, (long) modified.getPublicKey(keyId).getKeyUsage());
+
+ }
+ }
+
+ @Test
+ public void testSubkeyStrip() throws Exception {
+
+ long keyId = KeyringTestingHelper.getSubkeyId(ring, 1);
+ parcel.mChangeSubKeys.add(new SubkeyChange(keyId, true, false));
+ applyModificationWithChecks(parcel, ring, onlyA, onlyB);
+
+ Assert.assertEquals("one extra packet in original", 1, onlyA.size());
+ Assert.assertEquals("one extra packet in modified", 1, onlyB.size());
+
+ Assert.assertEquals("old packet must be secret subkey",
+ PacketTags.SECRET_SUBKEY, onlyA.get(0).tag);
+ Assert.assertEquals("new packet must be secret subkey",
+ PacketTags.SECRET_SUBKEY, onlyB.get(0).tag);
+
+ Packet p = new BCPGInputStream(new ByteArrayInputStream(onlyB.get(0).buf)).readPacket();
+ Assert.assertEquals("new packet should have GNU_DUMMY S2K type",
+ S2K.GNU_DUMMY_S2K, ((SecretSubkeyPacket) p).getS2K().getType());
+ Assert.assertEquals("new packet should have GNU_DUMMY protection mode 0x1",
+ 0x1, ((SecretSubkeyPacket) p).getS2K().getProtectionMode());
+ Assert.assertEquals("new packet secret key data should have length zero",
+ 0, ((SecretSubkeyPacket) p).getSecretKeyData().length);
+ Assert.assertNull("new packet should have no iv data", ((SecretSubkeyPacket) p).getIV());
+
+ }
+
+ @Test
+ public void testMasterStrip() throws Exception {
+
+ long keyId = ring.getMasterKeyId();
+ parcel.mChangeSubKeys.add(new SubkeyChange(keyId, true, false));
+ applyModificationWithChecks(parcel, ring, onlyA, onlyB);
+
+ Assert.assertEquals("one extra packet in original", 1, onlyA.size());
+ Assert.assertEquals("one extra packet in modified", 1, onlyB.size());
+
+ Assert.assertEquals("old packet must be secret key",
+ PacketTags.SECRET_KEY, onlyA.get(0).tag);
+ Assert.assertEquals("new packet must be secret key",
+ PacketTags.SECRET_KEY, onlyB.get(0).tag);
+
+ Packet p = new BCPGInputStream(new ByteArrayInputStream(onlyB.get(0).buf)).readPacket();
+ Assert.assertEquals("new packet should have GNU_DUMMY S2K type",
+ S2K.GNU_DUMMY_S2K, ((SecretKeyPacket) p).getS2K().getType());
+ Assert.assertEquals("new packet should have GNU_DUMMY protection mode 0x1",
+ 0x1, ((SecretKeyPacket) p).getS2K().getProtectionMode());
+ Assert.assertEquals("new packet secret key data should have length zero",
+ 0, ((SecretKeyPacket) p).getSecretKeyData().length);
+ Assert.assertNull("new packet should have no iv data", ((SecretKeyPacket) p).getIV());
+ }
+
+ @Test
+ public void testRestrictedStrip() throws Exception {
+
+ long keyId = KeyringTestingHelper.getSubkeyId(ring, 1);
+ UncachedKeyRing modified;
+
+ { // we should be able to change the stripped status of subkeys without passphrase
+ parcel.reset();
+ parcel.mChangeSubKeys.add(new SubkeyChange(keyId, true, false));
+ modified = applyModificationWithChecks(parcel, ring, onlyA, onlyB, new CryptoInputParcel());
+ Assert.assertEquals("one extra packet in modified", 1, onlyB.size());
+ Packet p = new BCPGInputStream(new ByteArrayInputStream(onlyB.get(0).buf)).readPacket();
+ Assert.assertEquals("new packet should have GNU_DUMMY S2K type",
+ S2K.GNU_DUMMY_S2K, ((SecretKeyPacket) p).getS2K().getType());
+ Assert.assertEquals("new packet should have GNU_DUMMY protection mode stripped",
+ S2K.GNU_PROTECTION_MODE_NO_PRIVATE_KEY, ((SecretKeyPacket) p).getS2K().getProtectionMode());
+ }
+ }
+
+ @Test
+ public void testKeyToCard() throws Exception {
+
+ UncachedKeyRing modified;
+
+ { // keytocard should fail with BAD_NFC_SIZE when presented with the RSA-1024 key
+ long keyId = KeyringTestingHelper.getSubkeyId(ring, 0);
+ parcel.reset();
+ parcel.mChangeSubKeys.add(new SubkeyChange(keyId, false, true));
+
+ assertModifyFailure("keytocard operation should fail on invalid key size", ring,
+ parcel, cryptoInput, LogType.MSG_MF_ERROR_BAD_NFC_SIZE);
+ }
+
+ { // keytocard should fail with BAD_NFC_ALGO when presented with the DSA-1024 key
+ long keyId = KeyringTestingHelper.getSubkeyId(ring, 1);
+ parcel.reset();
+ parcel.mChangeSubKeys.add(new SubkeyChange(keyId, false, true));
+
+ assertModifyFailure("keytocard operation should fail on invalid key algorithm", ring,
+ parcel, cryptoInput, LogType.MSG_MF_ERROR_BAD_NFC_ALGO);
+ }
+
+ { // keytocard should return a pending NFC_KEYTOCARD result when presented with the RSA-2048
+ // key, and then make key divert-to-card when it gets a serial in the cryptoInputParcel.
+ long keyId = KeyringTestingHelper.getSubkeyId(ring, 2);
+ parcel.reset();
+ parcel.mChangeSubKeys.add(new SubkeyChange(keyId, false, true));
+
+ CanonicalizedSecretKeyRing secretRing =
+ new CanonicalizedSecretKeyRing(ring.getEncoded(), false, 0);
+ PgpKeyOperation op = new PgpKeyOperation(null);
+ PgpEditKeyResult result = op.modifySecretKeyRing(secretRing, cryptoInput, parcel);
+ Assert.assertTrue("keytocard operation should be pending", result.isPending());
+ Assert.assertEquals("required input should be RequiredInputType.NFC_KEYTOCARD",
+ result.getRequiredInputParcel().mType, RequiredInputType.NFC_KEYTOCARD);
+
+ // Create a cryptoInputParcel that matches what the NFCOperationActivity would return.
+ byte[] keyIdBytes = new byte[8];
+ ByteBuffer buf = ByteBuffer.wrap(keyIdBytes);
+ buf.putLong(keyId).rewind();
+ byte[] serial = new byte[] {
+ 0x6a, 0x6f, 0x6c, 0x6f, 0x73, 0x77, 0x61, 0x67,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ };
+ CryptoInputParcel inputParcel = new CryptoInputParcel();
+ inputParcel.addCryptoData(keyIdBytes, serial);
+
+ modified = applyModificationWithChecks(parcel, ring, onlyA, onlyB, inputParcel);
+ Assert.assertEquals("one extra packet in modified", 1, onlyB.size());
+ Packet p = new BCPGInputStream(new ByteArrayInputStream(onlyB.get(0).buf)).readPacket();
+ Assert.assertEquals("new packet should have GNU_DUMMY S2K type",
+ S2K.GNU_DUMMY_S2K, ((SecretKeyPacket) p).getS2K().getType());
+ Assert.assertEquals("new packet should have GNU_DUMMY protection mode divert-to-card",
+ S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD, ((SecretKeyPacket) p).getS2K().getProtectionMode());
+ Assert.assertArrayEquals("new packet should have correct serial number as iv",
+ serial, ((SecretKeyPacket) p).getIV());
+
+ }
+
+ }
+
+ @Test
+ public void testUserIdRevoke() throws Exception {
+
+ UncachedKeyRing modified;
+ String uid = ring.getPublicKey().getUnorderedUserIds().get(1);
+
+ { // revoke second user id
+
+ parcel.mRevokeUserIds.add(uid);
+
+ modified = applyModificationWithChecks(parcel, ring, onlyA, onlyB);
+
+ Assert.assertEquals("no extra packets in original", 0, onlyA.size());
+ Assert.assertEquals("exactly one extra packet in modified", 1, onlyB.size());
+
+ Packet p;
+
+ p = new BCPGInputStream(new ByteArrayInputStream(onlyB.get(0).buf)).readPacket();
+ Assert.assertTrue("first new packet must be secret subkey", p instanceof SignaturePacket);
+ Assert.assertEquals("signature type must be subkey binding certificate",
+ PGPSignature.CERTIFICATION_REVOCATION, ((SignaturePacket) p).getSignatureType());
+ Assert.assertEquals("signature must have been created by master key",
+ ring.getMasterKeyId(), ((SignaturePacket) p).getKeyID());
+
+ }
+
+ { // re-add second user id
+
+ parcel.reset();
+ parcel.mChangePrimaryUserId = uid;
+
+ assertModifyFailure("setting primary user id to a revoked user id should fail", modified, parcel,
+ LogType.MSG_MF_ERROR_REVOKED_PRIMARY);
+
+ }
+
+ { // re-add second user id
+
+ parcel.reset();
+ parcel.mAddUserIds.add(uid);
+
+ applyModificationWithChecks(parcel, modified, onlyA, onlyB);
+
+ Assert.assertEquals("exactly two outdated packets in original", 2, onlyA.size());
+ Assert.assertEquals("exactly one extra packet in modified", 1, onlyB.size());
+
+ Packet p;
+
+ p = new BCPGInputStream(new ByteArrayInputStream(onlyA.get(0).buf)).readPacket();
+ Assert.assertTrue("first outdated packet must be signature", p instanceof SignaturePacket);
+ Assert.assertEquals("first outdated signature type must be positive certification",
+ PGPSignature.POSITIVE_CERTIFICATION, ((SignaturePacket) p).getSignatureType());
+ Assert.assertEquals("first outdated signature must have been created by master key",
+ ring.getMasterKeyId(), ((SignaturePacket) p).getKeyID());
+
+ p = new BCPGInputStream(new ByteArrayInputStream(onlyA.get(1).buf)).readPacket();
+ Assert.assertTrue("second outdated packet must be signature", p instanceof SignaturePacket);
+ Assert.assertEquals("second outdated signature type must be certificate revocation",
+ PGPSignature.CERTIFICATION_REVOCATION, ((SignaturePacket) p).getSignatureType());
+ Assert.assertEquals("second outdated signature must have been created by master key",
+ ring.getMasterKeyId(), ((SignaturePacket) p).getKeyID());
+
+ p = new BCPGInputStream(new ByteArrayInputStream(onlyB.get(0).buf)).readPacket();
+ Assert.assertTrue("new packet must be signature ", p instanceof SignaturePacket);
+ Assert.assertEquals("new signature type must be positive certification",
+ PGPSignature.POSITIVE_CERTIFICATION, ((SignaturePacket) p).getSignatureType());
+ Assert.assertEquals("signature must have been created by master key",
+ ring.getMasterKeyId(), ((SignaturePacket) p).getKeyID());
+ }
+
+ { // revocation of non-existent user id should fail
+ parcel.reset();
+ parcel.mRevokeUserIds.add("nonexistent");
+
+ assertModifyFailure("revocation of nonexistent user id should fail", modified, parcel,
+ LogType.MSG_MF_ERROR_NOEXIST_REVOKE);
+ }
+
+ }
+
+ @Test
+ public void testUserIdAdd() throws Exception {
+
+ {
+ parcel.mAddUserIds.add("");
+ assertModifyFailure("adding an empty user id should fail", ring, parcel,
+ LogType.MSG_MF_UID_ERROR_EMPTY);
+ }
+
+ parcel.reset();
+ parcel.mAddUserIds.add("rainbow");
+
+ UncachedKeyRing modified = applyModificationWithChecks(parcel, ring, onlyA, onlyB);
+
+ Assert.assertTrue("keyring must contain added user id",
+ modified.getPublicKey().getUnorderedUserIds().contains("rainbow"));
+
+ Assert.assertEquals("no extra packets in original", 0, onlyA.size());
+ Assert.assertEquals("exactly two extra packets in modified", 2, onlyB.size());
+
+ Assert.assertTrue("keyring must contain added user id",
+ modified.getPublicKey().getUnorderedUserIds().contains("rainbow"));
+
+ Packet p;
+
+ p = new BCPGInputStream(new ByteArrayInputStream(onlyB.get(0).buf)).readPacket();
+ Assert.assertTrue("first new packet must be user id", p instanceof UserIDPacket);
+ Assert.assertEquals("user id packet must match added user id",
+ "rainbow", ((UserIDPacket) p).getID());
+
+ p = new BCPGInputStream(new ByteArrayInputStream(onlyB.get(1).buf)).readPacket();
+ Assert.assertTrue("second new packet must be signature", p instanceof SignaturePacket);
+ Assert.assertEquals("signature type must be positive certification",
+ PGPSignature.POSITIVE_CERTIFICATION, ((SignaturePacket) p).getSignatureType());
+
+ }
+
+ @Test
+ public void testUserAttributeAdd() throws Exception {
+
+ {
+ parcel.mAddUserAttribute.add(WrappedUserAttribute.fromData(new byte[0]));
+ assertModifyFailure("adding an empty user attribute should fail", ring, parcel,
+ LogType.MSG_MF_UAT_ERROR_EMPTY);
+ }
+
+ parcel.reset();
+
+ Random r = new Random();
+ int type = r.nextInt(110)+2; // any type except image attribute, to avoid interpretation of these
+ byte[] data = new byte[r.nextInt(2000)];
+ new Random().nextBytes(data);
+
+ WrappedUserAttribute uat = WrappedUserAttribute.fromSubpacket(type, data);
+ parcel.mAddUserAttribute.add(uat);
+
+ UncachedKeyRing modified = applyModificationWithChecks(parcel, ring, onlyA, onlyB);
+
+ Assert.assertEquals("no extra packets in original", 0, onlyA.size());
+ Assert.assertEquals("exactly two extra packets in modified", 2, onlyB.size());
+
+ Assert.assertTrue("keyring must contain added user attribute",
+ modified.getPublicKey().getUnorderedUserAttributes().contains(uat));
+
+ Packet p;
+
+ p = new BCPGInputStream(new ByteArrayInputStream(onlyB.get(0).buf)).readPacket();
+ Assert.assertTrue("first new packet must be user attribute", p instanceof UserAttributePacket);
+ {
+ UserAttributeSubpacket[] subpackets = ((UserAttributePacket) p).getSubpackets();
+ Assert.assertEquals("user attribute packet must contain one subpacket",
+ 1, subpackets.length);
+ Assert.assertEquals("user attribute subpacket type must be as specified above",
+ type, subpackets[0].getType());
+ Assert.assertArrayEquals("user attribute subpacket data must be as specified above",
+ data, subpackets[0].getData());
+ }
+
+ p = new BCPGInputStream(new ByteArrayInputStream(onlyB.get(1).buf)).readPacket();
+ Assert.assertTrue("second new packet must be signature", p instanceof SignaturePacket);
+ Assert.assertEquals("signature type must be positive certification",
+ PGPSignature.POSITIVE_CERTIFICATION, ((SignaturePacket) p).getSignatureType());
+
+ Thread.sleep(1000);
+
+ // applying the same modification AGAIN should not add more certifications but drop those
+ // as duplicates
+ modified = applyModificationWithChecks(parcel, modified, onlyA, onlyB,
+ new CryptoInputParcel(new Date(), passphrase), true, false);
+
+ Assert.assertEquals("duplicate modification: one extra packet in original", 1, onlyA.size());
+ Assert.assertEquals("duplicate modification: one extra packet in modified", 1, onlyB.size());
+
+ p = new BCPGInputStream(new ByteArrayInputStream(onlyA.get(0).buf)).readPacket();
+ Assert.assertTrue("lost packet must be signature", p instanceof SignaturePacket);
+ p = new BCPGInputStream(new ByteArrayInputStream(onlyB.get(0).buf)).readPacket();
+ Assert.assertTrue("new packet must be signature", p instanceof SignaturePacket);
+
+ }
+
+
+ @Test
+ public void testUserIdPrimary() throws Exception {
+
+ UncachedKeyRing modified = ring;
+ String uid = ring.getPublicKey().getUnorderedUserIds().get(1);
+
+ { // first part, add new user id which is also primary
+ parcel.mAddUserIds.add("jack");
+ parcel.mChangePrimaryUserId = "jack";
+
+ modified = applyModificationWithChecks(parcel, modified, onlyA, onlyB);
+
+ Assert.assertEquals("primary user id must be the one added",
+ "jack", modified.getPublicKey().getPrimaryUserId());
+ }
+
+ { // second part, change primary to a different one
+ parcel.reset();
+ parcel.mChangePrimaryUserId = uid;
+
+ modified = applyModificationWithChecks(parcel, modified, onlyA, onlyB);
+
+ Assert.assertEquals("old keyring must have two outdated certificates", 2, onlyA.size());
+ Assert.assertEquals("new keyring must have two new packets", 2, onlyB.size());
+
+ Assert.assertEquals("primary user id must be the one changed to",
+ "pink", modified.getPublicKey().getPrimaryUserId());
+ }
+
+ { // third part, change primary to a non-existent one
+ parcel.reset();
+ //noinspection SpellCheckingInspection
+ parcel.mChangePrimaryUserId = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
+ if (parcel.mChangePrimaryUserId.equals(passphrase)) {
+ parcel.mChangePrimaryUserId += "A";
+ }
+
+ assertModifyFailure("changing primary user id to a non-existent one should fail",
+ ring, parcel, LogType.MSG_MF_ERROR_NOEXIST_PRIMARY);
+ }
+
+ // check for revoked primary user id already done in revoke test
+
+ }
+
+ @Test
+ public void testPassphraseChange() throws Exception {
+
+ // change passphrase to empty
+ parcel.mNewUnlock = new ChangeUnlockParcel(new Passphrase());
+ // note that canonicalization here necessarily strips the empty notation packet
+ UncachedKeyRing modified = applyModificationWithChecks(parcel, ring, onlyA, onlyB, cryptoInput);
+
+ Assert.assertEquals("exactly three packets should have been modified (the secret keys)",
+ 3, onlyB.size());
+
+ // remember secret key packet with no passphrase for later
+ RawPacket sKeyNoPassphrase = onlyB.get(1);
+ Assert.assertEquals("extracted packet should be a secret subkey",
+ PacketTags.SECRET_SUBKEY, sKeyNoPassphrase.tag);
+
+ // modify keyring, change to non-empty passphrase
+ Passphrase otherPassphrase = TestingUtils.genPassphrase(true);
+ CryptoInputParcel otherCryptoInput = new CryptoInputParcel(otherPassphrase);
+ parcel.mNewUnlock = new ChangeUnlockParcel(otherPassphrase);
+ modified = applyModificationWithChecks(parcel, modified, onlyA, onlyB,
+ new CryptoInputParcel(new Date(), new Passphrase()));
+
+ Assert.assertEquals("exactly three packets should have been modified (the secret keys)",
+ 3, onlyB.size());
+
+ { // quick check to make sure no two secret keys have the same IV
+ HashSet<ByteBuffer> ivs = new HashSet<ByteBuffer>();
+ for (int i = 0; i < 3; i++) {
+ SecretKeyPacket p = (SecretKeyPacket) new BCPGInputStream(
+ new ByteArrayInputStream(onlyB.get(i).buf)).readPacket();
+ ByteBuffer iv = ByteBuffer.wrap(p.getIV());
+ Assert.assertFalse(
+ "no two secret keys should have the same s2k iv (slightly non-deterministic!)",
+ ivs.contains(iv)
+ );
+ ivs.add(iv);
+ }
+ }
+
+ RawPacket sKeyWithPassphrase = onlyB.get(1);
+ Assert.assertEquals("extracted packet should be a secret subkey",
+ PacketTags.SECRET_SUBKEY, sKeyNoPassphrase.tag);
+
+ Passphrase otherPassphrase2 = TestingUtils.genPassphrase(true);
+ parcel.mNewUnlock = new ChangeUnlockParcel(otherPassphrase2);
+ {
+ // if we replace a secret key with one without passphrase
+ modified = KeyringTestingHelper.removePacket(modified, sKeyNoPassphrase.position);
+ modified = KeyringTestingHelper.injectPacket(modified, sKeyNoPassphrase.buf, sKeyNoPassphrase.position);
+
+ // we should still be able to modify it (and change its passphrase) without errors
+ PgpKeyOperation op = new PgpKeyOperation(null);
+ CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing(modified.getEncoded(), false, 0);
+ PgpEditKeyResult result = op.modifySecretKeyRing(secretRing, otherCryptoInput, parcel);
+ Assert.assertTrue("key modification must succeed", result.success());
+ Assert.assertFalse("log must not contain a warning",
+ result.getLog().containsWarnings());
+ Assert.assertTrue("log must contain an empty passphrase retry notice",
+ result.getLog().containsType(LogType.MSG_MF_PASSPHRASE_EMPTY_RETRY));
+ modified = result.getRing();
+ }
+
+ {
+ // if we add one subkey with a different passphrase, that should produce a warning but also work
+ modified = KeyringTestingHelper.removePacket(modified, sKeyWithPassphrase.position);
+ modified = KeyringTestingHelper.injectPacket(modified, sKeyWithPassphrase.buf, sKeyWithPassphrase.position);
+
+ PgpKeyOperation op = new PgpKeyOperation(null);
+ CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing(modified.getEncoded(), false, 0);
+ PgpEditKeyResult result = op.modifySecretKeyRing(secretRing, new CryptoInputParcel(otherPassphrase2), parcel);
+ Assert.assertTrue("key modification must succeed", result.success());
+ Assert.assertTrue("log must contain a failed passphrase change warning",
+ result.getLog().containsType(LogType.MSG_MF_PASSPHRASE_FAIL));
+ }
+
+ }
+
+ @Test
+ public void testUnlockPin() throws Exception {
+
+ Passphrase pin = new Passphrase("5235125");
+
+ // change passphrase to a pin type
+ parcel.mNewUnlock = new ChangeUnlockParcel(null, pin);
+ UncachedKeyRing modified = applyModificationWithChecks(parcel, ring, onlyA, onlyB);
+
+ Assert.assertEquals("exactly three packets should have been added (the secret keys + notation packet)",
+ 3, onlyA.size());
+ Assert.assertEquals("exactly four packets should have been added (the secret keys + notation packet)",
+ 4, onlyB.size());
+
+ RawPacket dkSig = onlyB.get(1);
+ Assert.assertEquals("second modified packet should be notation data",
+ PacketTags.SIGNATURE, dkSig.tag);
+
+ // check that notation data contains pin
+ CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing(
+ modified.getEncoded(), false, 0);
+ Assert.assertEquals("secret key type should be 'pin' after this",
+ SecretKeyType.PIN,
+ secretRing.getSecretKey().getSecretKeyType());
+
+ // need to sleep for a sec, so the timestamp changes for notation data
+ Thread.sleep(1000);
+
+ {
+ parcel.mNewUnlock = new ChangeUnlockParcel(new Passphrase("phrayse"), null);
+ applyModificationWithChecks(parcel, modified, onlyA, onlyB, new CryptoInputParcel(pin), true, false);
+
+ Assert.assertEquals("exactly four packets should have been removed (the secret keys + notation packet)",
+ 4, onlyA.size());
+ Assert.assertEquals("exactly three packets should have been added (no more notation packet)",
+ 3, onlyB.size());
+ }
+
+ }
+
+ @Test
+ public void testRestricted () throws Exception {
+
+ CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing(ring.getEncoded(), false, 0);
+
+ parcel.mAddUserIds.add("discord");
+ PgpKeyOperation op = new PgpKeyOperation(null);
+ PgpEditKeyResult result = op.modifySecretKeyRing(secretRing, new CryptoInputParcel(new Date()), parcel);
+ Assert.assertFalse("non-restricted operations should fail without passphrase", result.success());
+ }
+
+ private static UncachedKeyRing applyModificationWithChecks(SaveKeyringParcel parcel,
+ UncachedKeyRing ring,
+ ArrayList<RawPacket> onlyA,
+ ArrayList<RawPacket> onlyB) {
+ return applyModificationWithChecks(parcel, ring, onlyA, onlyB, cryptoInput, true, true);
+ }
+
+ private static UncachedKeyRing applyModificationWithChecks(SaveKeyringParcel parcel,
+ UncachedKeyRing ring,
+ ArrayList<RawPacket> onlyA,
+ ArrayList<RawPacket> onlyB,
+ CryptoInputParcel cryptoInput) {
+ return applyModificationWithChecks(parcel, ring, onlyA, onlyB, cryptoInput, true, true);
+ }
+
+ // applies a parcel modification while running some integrity checks
+ private static UncachedKeyRing applyModificationWithChecks(SaveKeyringParcel parcel,
+ UncachedKeyRing ring,
+ ArrayList<RawPacket> onlyA,
+ ArrayList<RawPacket> onlyB,
+ CryptoInputParcel cryptoInput,
+ boolean canonicalize,
+ boolean constantCanonicalize) {
+
+ try {
+
+ Assert.assertTrue("modified keyring must be secret", ring.isSecret());
+ CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing(ring.getEncoded(), false, 0);
+
+ PgpKeyOperation op = new PgpKeyOperation(null);
+ PgpEditKeyResult result = op.modifySecretKeyRing(secretRing, cryptoInput, parcel);
+ Assert.assertTrue("key modification must succeed", result.success());
+ UncachedKeyRing rawModified = result.getRing();
+ Assert.assertNotNull("key modification must not return null", rawModified);
+
+ if (!canonicalize) {
+ Assert.assertTrue("keyring must differ from original", KeyringTestingHelper.diffKeyrings(
+ ring.getEncoded(), rawModified.getEncoded(), onlyA, onlyB));
+ return rawModified;
+ }
+
+ CanonicalizedKeyRing modified = rawModified.canonicalize(new OperationLog(), 0);
+ if (constantCanonicalize) {
+ Assert.assertTrue("key must be constant through canonicalization",
+ !KeyringTestingHelper.diffKeyrings(
+ modified.getEncoded(), rawModified.getEncoded(), onlyA, onlyB)
+ );
+ }
+ Assert.assertTrue("keyring must differ from original", KeyringTestingHelper.diffKeyrings(
+ ring.getEncoded(), modified.getEncoded(), onlyA, onlyB));
+
+ return modified.getUncachedKeyRing();
+
+ } catch (IOException e) {
+ throw new AssertionFailedError("error during encoding!");
+ }
+ }
+
+ @Test
+ public void testVerifySuccess() throws Exception {
+
+ UncachedKeyRing expectedKeyRing = KeyringBuilder.correctRing();
+ UncachedKeyRing inputKeyRing = KeyringBuilder.ringWithExtraIncorrectSignature();
+
+ CanonicalizedKeyRing canonicalized = inputKeyRing.canonicalize(new OperationLog(), 0);
+ Assert.assertNotNull("canonicalization must succeed", canonicalized);
+
+ ArrayList onlyA = new ArrayList<RawPacket>();
+ ArrayList onlyB = new ArrayList<RawPacket>();
+ //noinspection unchecked
+ Assert.assertTrue("keyrings differ", !KeyringTestingHelper.diffKeyrings(
+ expectedKeyRing.getEncoded(), expectedKeyRing.getEncoded(), onlyA, onlyB));
+
+ }
+
+ /**
+ * Just testing my own test code. Should really be using a library for this.
+ */
+ @Test
+ public void testConcat() throws Exception {
+ byte[] actual = TestDataUtil.concatAll(new byte[]{1}, new byte[]{2, -2}, new byte[]{5}, new byte[]{3});
+ byte[] expected = new byte[]{1,2,-2,5,3};
+ Assert.assertEquals(java.util.Arrays.toString(expected), java.util.Arrays.toString(actual));
+ }
+
+ private void assertFailure(String reason, SaveKeyringParcel parcel, LogType expected) {
+
+ PgpEditKeyResult result = op.createSecretKeyRing(parcel);
+
+ Assert.assertFalse(reason, result.success());
+ Assert.assertNull(reason, result.getRing());
+ Assert.assertTrue(reason + "(with correct error)",
+ result.getLog().containsType(expected));
+
+ }
+
+ private void assertModifyFailure(String reason, UncachedKeyRing ring,
+ SaveKeyringParcel parcel, CryptoInputParcel cryptoInput, LogType expected)
+ throws Exception {
+
+ CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing(ring.getEncoded(), false, 0);
+ PgpEditKeyResult result = op.modifySecretKeyRing(secretRing, cryptoInput, parcel);
+
+ Assert.assertFalse(reason, result.success());
+ Assert.assertNull(reason, result.getRing());
+ Assert.assertTrue(reason + "(with correct error)",
+ result.getLog().containsType(expected));
+
+ }
+
+ private void assertModifyFailure(String reason, UncachedKeyRing ring, SaveKeyringParcel parcel,
+ LogType expected)
+ throws Exception {
+
+ CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing(ring.getEncoded(), false, 0);
+ PgpEditKeyResult result = op.modifySecretKeyRing(secretRing, cryptoInput, parcel);
+
+ Assert.assertFalse(reason, result.success());
+ Assert.assertNull(reason, result.getRing());
+ Assert.assertTrue(reason + "(with correct error)",
+ result.getLog().containsType(expected));
+
+ }
+
+ private UncachedKeyRing assertCreateSuccess(String reason, SaveKeyringParcel parcel) {
+
+ PgpEditKeyResult result = op.createSecretKeyRing(parcel);
+
+ Assert.assertTrue(reason, result.success());
+ Assert.assertNotNull(reason, result.getRing());
+
+ return result.getRing();
+
+ }
+
+}
diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/UncachedKeyringCanonicalizeTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/UncachedKeyringCanonicalizeTest.java
new file mode 100644
index 000000000..0143ae289
--- /dev/null
+++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/UncachedKeyringCanonicalizeTest.java
@@ -0,0 +1,768 @@
+/*
+ * 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.BeforeClass;
+import org.junit.runner.RunWith;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.Before;
+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.HashAlgorithmTags;
+import org.spongycastle.bcpg.Packet;
+import org.spongycastle.bcpg.PacketTags;
+import org.spongycastle.bcpg.SymmetricKeyAlgorithmTags;
+import org.spongycastle.bcpg.UserIDPacket;
+import org.spongycastle.bcpg.sig.KeyFlags;
+import org.spongycastle.jce.provider.BouncyCastleProvider;
+import org.spongycastle.openpgp.PGPPrivateKey;
+import org.spongycastle.openpgp.PGPPublicKey;
+import org.spongycastle.openpgp.PGPSecretKey;
+import org.spongycastle.openpgp.PGPSecretKeyRing;
+import org.spongycastle.openpgp.PGPSignature;
+import org.spongycastle.openpgp.PGPSignatureGenerator;
+import org.spongycastle.openpgp.PGPSignatureSubpacketGenerator;
+import org.spongycastle.openpgp.PGPUtil;
+import org.spongycastle.openpgp.operator.PBESecretKeyDecryptor;
+import org.spongycastle.openpgp.operator.PBESecretKeyEncryptor;
+import org.spongycastle.openpgp.operator.PGPContentSignerBuilder;
+import org.spongycastle.openpgp.operator.PGPDigestCalculator;
+import org.spongycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
+import org.spongycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder;
+import org.spongycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder;
+import org.spongycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;
+import org.spongycastle.openpgp.operator.jcajce.JcePBESecretKeyEncryptorBuilder;
+import org.spongycastle.util.Strings;
+import org.sufficientlysecure.keychain.BuildConfig;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.WorkaroundBuildConfig;
+import org.sufficientlysecure.keychain.operations.results.OperationResult;
+import org.sufficientlysecure.keychain.operations.results.PgpEditKeyResult;
+import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
+import org.sufficientlysecure.keychain.service.SaveKeyringParcel.Algorithm;
+import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
+import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
+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 java.io.ByteArrayInputStream;
+import java.security.Security;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Iterator;
+
+
+/** Tests for the UncachedKeyring.canonicalize method.
+ *
+ * This is a complex and crypto-relevant method, which takes care of sanitizing keyrings.
+ * Test cases are made for all its assertions.
+ */
+
+@RunWith(RobolectricGradleTestRunner.class)
+@Config(constants = WorkaroundBuildConfig.class, sdk = 21, manifest = "src/main/AndroidManifest.xml")
+public class UncachedKeyringCanonicalizeTest {
+
+ static UncachedKeyRing staticRing;
+ static int totalPackets;
+ UncachedKeyRing ring;
+ ArrayList<RawPacket> onlyA = new ArrayList<RawPacket>();
+ ArrayList<RawPacket> onlyB = new ArrayList<RawPacket>();
+ OperationResult.OperationLog log = new OperationResult.OperationLog();
+ PGPSignatureSubpacketGenerator subHashedPacketsGen;
+ PGPSecretKey secretKey;
+
+ @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.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
+ Algorithm.RSA, 1024, null, KeyFlags.ENCRYPT_COMMS, 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);
+
+ PgpEditKeyResult result = op.createSecretKeyRing(parcel);
+ Assert.assertTrue("initial test key creation must succeed", result.success());
+ staticRing = result.getRing();
+ Assert.assertNotNull("initial test key creation must succeed", staticRing);
+
+ staticRing = staticRing.canonicalize(new OperationLog(), 0).getUncachedKeyRing();
+
+ // just for later reference
+ totalPackets = 11;
+
+ // 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;
+ ring = staticRing;
+
+ subHashedPacketsGen = new PGPSignatureSubpacketGenerator();
+ secretKey = new PGPSecretKeyRing(ring.getEncoded(), new JcaKeyFingerprintCalculator())
+ .getSecretKey();
+ }
+
+ /** Make sure the assumptions made about the generated ring packet structure are valid. */
+ @Test public void testGeneratedRingStructure() throws Exception {
+
+ Iterator<RawPacket> it = KeyringTestingHelper.parseKeyring(ring.getEncoded());
+
+ Assert.assertEquals("packet #0 should be secret key",
+ PacketTags.SECRET_KEY, it.next().tag);
+
+ Assert.assertEquals("packet #1 should be user id",
+ PacketTags.USER_ID, it.next().tag);
+ Assert.assertEquals("packet #2 should be signature",
+ PacketTags.SIGNATURE, it.next().tag);
+
+ Assert.assertEquals("packet #3 should be user id",
+ PacketTags.USER_ID, it.next().tag);
+ Assert.assertEquals("packet #4 should be signature",
+ PacketTags.SIGNATURE, it.next().tag);
+
+ Assert.assertEquals("packet #5 should be user id",
+ PacketTags.USER_ATTRIBUTE, it.next().tag);
+ Assert.assertEquals("packet #6 should be signature",
+ PacketTags.SIGNATURE, it.next().tag);
+
+ Assert.assertEquals("packet #7 should be secret subkey",
+ PacketTags.SECRET_SUBKEY, it.next().tag);
+ Assert.assertEquals("packet #8 should be signature",
+ PacketTags.SIGNATURE, it.next().tag);
+
+ Assert.assertEquals("packet #9 should be secret subkey",
+ PacketTags.SECRET_SUBKEY, it.next().tag);
+ Assert.assertEquals("packet #10 should be signature",
+ PacketTags.SIGNATURE, it.next().tag);
+
+ Assert.assertFalse("exactly 11 packets total", it.hasNext());
+
+ Assert.assertArrayEquals("created keyring should be constant through canonicalization",
+ ring.getEncoded(), ring.canonicalize(log, 0).getEncoded());
+
+ }
+
+ @Test public void testUidSignature() throws Exception {
+
+ UncachedPublicKey masterKey = ring.getPublicKey();
+ final WrappedSignature sig = masterKey.getSignaturesForRawId(Strings.toUTF8ByteArray("twi")).next();
+
+ byte[] raw = sig.getEncoded();
+ // destroy the signature
+ raw[raw.length - 5] += 1;
+ final WrappedSignature brokenSig = WrappedSignature.fromBytes(raw);
+
+ { // bad certificates get stripped
+ UncachedKeyRing modified = KeyringTestingHelper.injectPacket(ring, brokenSig.getEncoded(), 3);
+ CanonicalizedKeyRing canonicalized = modified.canonicalize(log, 0);
+
+ Assert.assertTrue("canonicalized keyring with invalid extra sig must be same as original one",
+ !KeyringTestingHelper.diffKeyrings(
+ ring.getEncoded(), canonicalized.getEncoded(), onlyA, onlyB));
+ }
+
+ // remove user id certificate for one user
+ final UncachedKeyRing base = KeyringTestingHelper.removePacket(ring, 2);
+
+ { // user id without certificate should be removed
+ CanonicalizedKeyRing modified = base.canonicalize(log, 0);
+ Assert.assertTrue("canonicalized keyring must differ", KeyringTestingHelper.diffKeyrings(
+ ring.getEncoded(), modified.getEncoded(), onlyA, onlyB));
+
+ Assert.assertEquals("two packets should be stripped after canonicalization", 2, onlyA.size());
+ Assert.assertEquals("no new packets after canonicalization", 0, onlyB.size());
+
+ Packet p = new BCPGInputStream(new ByteArrayInputStream(onlyA.get(0).buf)).readPacket();
+ Assert.assertTrue("first stripped packet must be user id", p instanceof UserIDPacket);
+ Assert.assertEquals("missing user id must be the expected one",
+ "twi", ((UserIDPacket) p).getID());
+
+ Assert.assertArrayEquals("second stripped packet must be signature we removed",
+ sig.getEncoded(), onlyA.get(1).buf);
+
+ }
+
+ { // add error to signature
+
+ UncachedKeyRing modified = KeyringTestingHelper.injectPacket(base, brokenSig.getEncoded(), 3);
+ CanonicalizedKeyRing canonicalized = modified.canonicalize(log, 0);
+
+ Assert.assertTrue("canonicalized keyring must differ", KeyringTestingHelper.diffKeyrings(
+ ring.getEncoded(), canonicalized.getEncoded(), onlyA, onlyB));
+
+ Assert.assertEquals("two packets should be missing after canonicalization", 2, onlyA.size());
+ Assert.assertEquals("no new packets after canonicalization", 0, onlyB.size());
+
+ Packet p = new BCPGInputStream(new ByteArrayInputStream(onlyA.get(0).buf)).readPacket();
+ Assert.assertTrue("first stripped packet must be user id", p instanceof UserIDPacket);
+ Assert.assertEquals("missing user id must be the expected one",
+ "twi", ((UserIDPacket) p).getID());
+
+ Assert.assertArrayEquals("second stripped packet must be signature we removed",
+ sig.getEncoded(), onlyA.get(1).buf);
+ }
+
+ }
+
+ @Test public void testUidDestroy() throws Exception {
+
+ // signature for "twi"
+ ring = KeyringTestingHelper.removePacket(ring, 2);
+ // signature for "pink"
+ ring = KeyringTestingHelper.removePacket(ring, 3);
+
+ // canonicalization should fail, because there are no valid uids left
+ CanonicalizedKeyRing canonicalized = ring.canonicalize(log, 0);
+ Assert.assertNull("canonicalization of keyring with no valid uids should fail", canonicalized);
+
+ }
+
+ @Test public void testRevocationRedundant() throws Exception {
+
+ PGPSignature revocation = forgeSignature(
+ secretKey, PGPSignature.KEY_REVOCATION, subHashedPacketsGen, secretKey.getPublicKey());
+
+ UncachedKeyRing modified = KeyringTestingHelper.injectPacket(ring, revocation.getEncoded(), 1);
+
+ // try to add the same packet again, it should be rejected in all positions
+ injectEverywhere(modified, revocation.getEncoded());
+
+ // an older (but different!) revocation should be rejected as well
+ subHashedPacketsGen.setSignatureCreationTime(false, new Date(new Date().getTime() -1000*1000));
+ revocation = forgeSignature(
+ secretKey, PGPSignature.KEY_REVOCATION, subHashedPacketsGen, secretKey.getPublicKey());
+
+ injectEverywhere(modified, revocation.getEncoded());
+
+ }
+
+ @Test public void testUidRedundant() throws Exception {
+
+ // an older uid certificate should be rejected
+ subHashedPacketsGen.setSignatureCreationTime(false, new Date(new Date().getTime() -1000*1000));
+ PGPSignature revocation = forgeSignature(
+ secretKey, PGPSignature.DEFAULT_CERTIFICATION, subHashedPacketsGen, "twi", secretKey.getPublicKey());
+
+ injectEverywhere(ring, revocation.getEncoded());
+
+ }
+
+ @Test public void testUidRevocationOutdated() throws Exception {
+ // an older uid revocation cert should be rejected
+ subHashedPacketsGen.setSignatureCreationTime(false, new Date(new Date().getTime() -1000*1000));
+ PGPSignature revocation = forgeSignature(
+ secretKey, PGPSignature.CERTIFICATION_REVOCATION, subHashedPacketsGen, "twi", secretKey.getPublicKey());
+
+ injectEverywhere(ring, revocation.getEncoded());
+
+ }
+
+ @Test public void testUidRevocationRedundant() throws Exception {
+
+ PGPSignature revocation = forgeSignature(
+ secretKey, PGPSignature.CERTIFICATION_REVOCATION, subHashedPacketsGen, "twi", secretKey.getPublicKey());
+
+ // add that revocation to the base, and check if the redundant one will be rejected as well
+ UncachedKeyRing modified = KeyringTestingHelper.injectPacket(ring, revocation.getEncoded(), 2);
+
+ injectEverywhere(modified, revocation.getEncoded());
+
+ // an older (but different!) uid revocation should be rejected as well
+ subHashedPacketsGen.setSignatureCreationTime(false, new Date(new Date().getTime() -1000*1000));
+ revocation = forgeSignature(
+ secretKey, PGPSignature.CERTIFICATION_REVOCATION, subHashedPacketsGen, "twi", secretKey.getPublicKey());
+
+ injectEverywhere(modified, revocation.getEncoded());
+
+ }
+
+ @Test public void testDuplicateUid() throws Exception {
+
+ // get subkey packets
+ Iterator<RawPacket> it = KeyringTestingHelper.parseKeyring(ring.getEncoded());
+ RawPacket uidPacket = KeyringTestingHelper.getNth(it, 3);
+ RawPacket uidSig = it.next();
+
+ // inject at a second position
+ UncachedKeyRing modified = ring;
+ modified = KeyringTestingHelper.injectPacket(modified, uidPacket.buf, 5);
+ modified = KeyringTestingHelper.injectPacket(modified, uidSig.buf, 6);
+
+ // canonicalize, and check if we lose the bad signature
+ OperationLog log = new OperationLog();
+ CanonicalizedKeyRing canonicalized = modified.canonicalize(log, 0);
+ Assert.assertNotNull("canonicalization with duplicate user id should succeed", canonicalized);
+ Assert.assertTrue("log should contain uid_dup event", log.containsType(LogType.MSG_KC_UID_DUP));
+ /* TODO actually test ths, and fix behavior
+ Assert.assertTrue("duplicate user id packets should be gone after canonicalization",
+ KeyringTestingHelper.diffKeyrings(modified.getEncoded(), canonicalized.getEncoded(),
+ onlyA, onlyB)
+ );
+ Assert.assertEquals("canonicalized keyring should have lost the two duplicate packets",
+ 2, onlyA.size());
+ Assert.assertTrue("canonicalized keyring should still contain the user id",
+ canonicalized.getUnorderedUserIds().contains(new UserIDPacket(uidPacket.buf).getID()));
+ */
+
+ }
+
+ @Test public void testSignatureBroken() throws Exception {
+
+ injectEverytype(secretKey, ring, subHashedPacketsGen, true);
+
+ }
+
+ @Test public void testForeignSignature() throws Exception {
+
+ SaveKeyringParcel parcel = new SaveKeyringParcel();
+ parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
+ Algorithm.RSA, 1024, null, KeyFlags.CERTIFY_OTHER, 0L));
+ parcel.mAddUserIds.add("trix");
+ PgpKeyOperation op = new PgpKeyOperation(null);
+
+ OperationResult.OperationLog log = new OperationResult.OperationLog();
+ UncachedKeyRing foreign = op.createSecretKeyRing(parcel).getRing();
+
+ Assert.assertNotNull("initial test key creation must succeed", foreign);
+ PGPSecretKey foreignSecretKey =
+ new PGPSecretKeyRing(foreign.getEncoded(), new JcaKeyFingerprintCalculator())
+ .getSecretKey();
+
+ injectEverytype(foreignSecretKey, ring, subHashedPacketsGen);
+
+ }
+
+ @Test public void testSignatureFuture() throws Exception {
+
+ // generate future timestamp (we allow up to one day future timestamps)
+ Calendar cal = Calendar.getInstance();
+ cal.add(Calendar.DAY_OF_YEAR, 2);
+ subHashedPacketsGen.setSignatureCreationTime(false, cal.getTime());
+
+ injectEverytype(secretKey, ring, subHashedPacketsGen);
+
+ }
+
+ @Test public void testSignatureLocal() throws Exception {
+
+ // make key local only
+ subHashedPacketsGen.setExportable(false, false);
+
+ injectEverytype(secretKey, ring, subHashedPacketsGen);
+
+ }
+
+ @Test public void testSubkeyDestroy() throws Exception {
+
+ // signature for second key (first subkey)
+ UncachedKeyRing modified = KeyringTestingHelper.removePacket(ring, 8);
+
+ // canonicalization should fail, because there are no valid uids left
+ CanonicalizedKeyRing canonicalized = modified.canonicalize(log, 0);
+ Assert.assertTrue("keyring with missing subkey binding sig should differ from intact one after canonicalization",
+ KeyringTestingHelper.diffKeyrings(ring.getEncoded(), canonicalized.getEncoded(),
+ onlyA, onlyB)
+ );
+
+ Assert.assertEquals("canonicalized keyring should have two extra packets", 2, onlyA.size());
+ Assert.assertEquals("canonicalized keyring should have no extra packets", 0, onlyB.size());
+
+ Assert.assertEquals("first missing packet should be the subkey",
+ PacketTags.SECRET_SUBKEY, onlyA.get(0).tag);
+ Assert.assertEquals("second missing packet should be subkey's signature",
+ PacketTags.SIGNATURE, onlyA.get(1).tag);
+ Assert.assertEquals("second missing packet should be next to subkey",
+ onlyA.get(0).position + 1, onlyA.get(1).position);
+
+ }
+
+ @Test public void testSubkeyBindingNoPKB() throws Exception {
+
+ UncachedPublicKey pKey = KeyringTestingHelper.getNth(ring.getPublicKeys(), 1);
+ PGPSignature sig;
+
+ subHashedPacketsGen.setKeyFlags(false, KeyFlags.SIGN_DATA);
+
+ {
+ // forge a (newer) signature, which has the sign flag but no primary key binding sig
+ PGPSignatureSubpacketGenerator unhashedSubs = new PGPSignatureSubpacketGenerator();
+
+ // just add any random signature, because why not
+ unhashedSubs.setEmbeddedSignature(false, forgeSignature(
+ secretKey, PGPSignature.POSITIVE_CERTIFICATION, subHashedPacketsGen,
+ secretKey.getPublicKey()
+ )
+ );
+
+ sig = forgeSignature(
+ secretKey, PGPSignature.SUBKEY_BINDING, subHashedPacketsGen, unhashedSubs,
+ secretKey.getPublicKey(), pKey.getPublicKey());
+
+ // inject in the right position
+ UncachedKeyRing modified = KeyringTestingHelper.injectPacket(ring, sig.getEncoded(), 8);
+
+ // canonicalize, and check if we lose the bad signature
+ CanonicalizedKeyRing canonicalized = modified.canonicalize(log, 0);
+ Assert.assertFalse("subkey binding signature should be gone after canonicalization",
+ KeyringTestingHelper.diffKeyrings(ring.getEncoded(), canonicalized.getEncoded(),
+ onlyA, onlyB)
+ );
+ }
+
+ { // now try one with a /bad/ primary key binding signature
+
+ PGPSignatureSubpacketGenerator unhashedSubs = new PGPSignatureSubpacketGenerator();
+ // this one is signed by the primary key itself, not the subkey - but it IS primary binding
+ unhashedSubs.setEmbeddedSignature(false, forgeSignature(
+ secretKey, PGPSignature.PRIMARYKEY_BINDING, subHashedPacketsGen,
+ secretKey.getPublicKey(), pKey.getPublicKey()
+ )
+ );
+
+ sig = forgeSignature(
+ secretKey, PGPSignature.SUBKEY_BINDING, subHashedPacketsGen, unhashedSubs,
+ secretKey.getPublicKey(), pKey.getPublicKey());
+
+ // inject in the right position
+ UncachedKeyRing modified = KeyringTestingHelper.injectPacket(ring, sig.getEncoded(), 8);
+
+ // canonicalize, and check if we lose the bad signature
+ CanonicalizedKeyRing canonicalized = modified.canonicalize(log, 0);
+ Assert.assertFalse("subkey binding signature should be gone after canonicalization",
+ KeyringTestingHelper.diffKeyrings(ring.getEncoded(), canonicalized.getEncoded(),
+ onlyA, onlyB)
+ );
+ }
+
+ }
+
+ @Test public void testSubkeyBindingRedundant() throws Exception {
+
+ UncachedPublicKey pKey = KeyringTestingHelper.getNth(ring.getPublicKeys(), 2);
+
+ subHashedPacketsGen.setKeyFlags(false, KeyFlags.ENCRYPT_COMMS);
+ PGPSignature sig2 = forgeSignature(
+ secretKey, PGPSignature.SUBKEY_BINDING, subHashedPacketsGen,
+ secretKey.getPublicKey(), pKey.getPublicKey());
+
+ subHashedPacketsGen.setSignatureCreationTime(false, new Date(new Date().getTime() -1000*1000));
+ PGPSignature sig1 = forgeSignature(
+ secretKey, PGPSignature.SUBKEY_REVOCATION, subHashedPacketsGen,
+ secretKey.getPublicKey(), pKey.getPublicKey());
+
+ subHashedPacketsGen = new PGPSignatureSubpacketGenerator();
+ subHashedPacketsGen.setSignatureCreationTime(false, new Date(new Date().getTime() -100*1000));
+ PGPSignature sig3 = forgeSignature(
+ secretKey, PGPSignature.SUBKEY_BINDING, subHashedPacketsGen,
+ secretKey.getPublicKey(), pKey.getPublicKey());
+
+ UncachedKeyRing modified = KeyringTestingHelper.injectPacket(ring, sig1.getEncoded(), 10);
+ modified = KeyringTestingHelper.injectPacket(modified, sig2.getEncoded(), 11);
+ modified = KeyringTestingHelper.injectPacket(modified, sig1.getEncoded(), 12);
+ modified = KeyringTestingHelper.injectPacket(modified, sig3.getEncoded(), 13);
+
+ // canonicalize, and check if we lose the bad signature
+ CanonicalizedKeyRing canonicalized = modified.canonicalize(log, 0);
+ Assert.assertTrue("subkey binding signature should be gone after canonicalization",
+ KeyringTestingHelper.diffKeyrings(modified.getEncoded(), canonicalized.getEncoded(),
+ onlyA, onlyB)
+ );
+
+ Assert.assertEquals("canonicalized keyring should have lost two packets", 3, onlyA.size());
+ Assert.assertEquals("canonicalized keyring should have no extra packets", 0, onlyB.size());
+
+ Assert.assertEquals("first missing packet should be the subkey",
+ PacketTags.SIGNATURE, onlyA.get(0).tag);
+ Assert.assertEquals("second missing packet should be a signature",
+ PacketTags.SIGNATURE, onlyA.get(1).tag);
+ Assert.assertEquals("second missing packet should be a signature",
+ PacketTags.SIGNATURE, onlyA.get(2).tag);
+
+ }
+
+ @Test
+ public void testDuplicateSubkey() throws Exception {
+
+ { // duplicate subkey
+
+ // get subkey packets
+ Iterator<RawPacket> it = KeyringTestingHelper.parseKeyring(ring.getEncoded());
+ RawPacket subKey = KeyringTestingHelper.getNth(it, 7);
+ RawPacket subSig = it.next();
+
+ // inject at a second position
+ UncachedKeyRing modified = ring;
+ modified = KeyringTestingHelper.injectPacket(modified, subKey.buf, 9);
+ modified = KeyringTestingHelper.injectPacket(modified, subSig.buf, 10);
+
+ // canonicalize, and check if we lose the bad signature
+ OperationLog log = new OperationLog();
+ CanonicalizedKeyRing canonicalized = modified.canonicalize(log, 0);
+ Assert.assertNull("canonicalization with duplicate subkey should fail", canonicalized);
+ Assert.assertTrue("log should contain dup_key event", log.containsType(LogType.MSG_KC_ERROR_DUP_KEY));
+ }
+
+ { // duplicate subkey, which is the same as the master key
+
+ // We actually encountered one of these in the wild:
+ // https://www.sparkasse-holstein.de/firmenkunden/electronic_banking/secure-e-mail/pdf/Spk_Holstein_PGP_Domain-Zertifikat.asc
+
+ CanonicalizedSecretKeyRing canonicalized = (CanonicalizedSecretKeyRing) ring.canonicalize(log, 0);
+
+ CanonicalizedSecretKey masterSecretKey = canonicalized.getSecretKey();
+ masterSecretKey.unlock(new Passphrase());
+ PGPPublicKey masterPublicKey = masterSecretKey.getPublicKey();
+ CryptoInputParcel cryptoInput = new CryptoInputParcel();
+ PGPSignature cert = PgpKeyOperation.generateSubkeyBindingSignature(
+ PgpKeyOperation.getSignatureGenerator(masterSecretKey.getSecretKey(), cryptoInput),
+ cryptoInput.getSignatureTime(),
+ masterPublicKey, masterSecretKey.getPrivateKey(), masterSecretKey.getPrivateKey(),
+ masterPublicKey, masterSecretKey.getKeyUsage(), 0);
+ PGPPublicKey subPubKey = PGPPublicKey.addSubkeyBindingCertification(masterPublicKey, cert);
+
+ PGPSecretKey sKey;
+ {
+ // Build key encrypter and decrypter based on passphrase
+ PGPDigestCalculator encryptorHashCalc = new JcaPGPDigestCalculatorProviderBuilder()
+ .build().get(HashAlgorithmTags.SHA256);
+ PBESecretKeyEncryptor keyEncryptor = new JcePBESecretKeyEncryptorBuilder(
+ SymmetricKeyAlgorithmTags.AES_256, encryptorHashCalc, 10)
+ .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build("".toCharArray());
+
+ // NOTE: only SHA1 is supported for key checksum calculations.
+ PGPDigestCalculator sha1Calc = new JcaPGPDigestCalculatorProviderBuilder()
+ .build().get(HashAlgorithmTags.SHA1);
+ sKey = new PGPSecretKey(masterSecretKey.getPrivateKey(), subPubKey, sha1Calc, false, keyEncryptor);
+ }
+
+ UncachedKeyRing modified = KeyringTestingHelper.injectPacket(ring, sKey.getEncoded(), 7);
+
+ // canonicalize, and check if we lose the bad signature
+ OperationLog log = new OperationLog();
+ CanonicalizedKeyRing result = modified.canonicalize(log, 0);
+ Assert.assertNull("canonicalization with duplicate subkey (from master) should fail", result);
+ Assert.assertTrue("log should contain dup_key event", log.containsType(LogType.MSG_KC_ERROR_DUP_KEY));
+ }
+
+ }
+
+ private static final int[] sigtypes_direct = new int[] {
+ PGPSignature.KEY_REVOCATION,
+ PGPSignature.DIRECT_KEY,
+ };
+ private static final int[] sigtypes_uid = new int[] {
+ PGPSignature.DEFAULT_CERTIFICATION,
+ PGPSignature.NO_CERTIFICATION,
+ PGPSignature.CASUAL_CERTIFICATION,
+ PGPSignature.POSITIVE_CERTIFICATION,
+ PGPSignature.CERTIFICATION_REVOCATION,
+ };
+ private static final int[] sigtypes_subkey = new int[] {
+ PGPSignature.SUBKEY_BINDING,
+ PGPSignature.PRIMARYKEY_BINDING,
+ PGPSignature.SUBKEY_REVOCATION,
+ };
+
+ private static void injectEverytype(PGPSecretKey secretKey,
+ UncachedKeyRing ring,
+ PGPSignatureSubpacketGenerator subHashedPacketsGen)
+ throws Exception {
+ injectEverytype(secretKey, ring, subHashedPacketsGen, false);
+ }
+
+ private static void injectEverytype(PGPSecretKey secretKey,
+ UncachedKeyRing ring,
+ PGPSignatureSubpacketGenerator subHashedPacketsGen,
+ boolean breakSig)
+ throws Exception {
+
+ for (int sigtype : sigtypes_direct) {
+ PGPSignature sig = forgeSignature(
+ secretKey, sigtype, subHashedPacketsGen, secretKey.getPublicKey());
+ byte[] encoded = sig.getEncoded();
+ if (breakSig) {
+ encoded[encoded.length-10] += 1;
+ }
+ injectEverywhere(ring, encoded);
+ }
+
+ for (int sigtype : sigtypes_uid) {
+ PGPSignature sig = forgeSignature(
+ secretKey, sigtype, subHashedPacketsGen, "twi", secretKey.getPublicKey());
+
+ byte[] encoded = sig.getEncoded();
+ if (breakSig) {
+ encoded[encoded.length-10] += 1;
+ }
+ injectEverywhere(ring, encoded);
+ }
+
+ for (int sigtype : sigtypes_subkey) {
+ PGPSignature sig = forgeSignature(
+ secretKey, sigtype, subHashedPacketsGen,
+ secretKey.getPublicKey(), secretKey.getPublicKey());
+
+ byte[] encoded = sig.getEncoded();
+ if (breakSig) {
+ encoded[encoded.length-10] += 1;
+ }
+ injectEverywhere(ring, encoded);
+ }
+
+ }
+
+ private static void injectEverywhere(UncachedKeyRing ring, byte[] packet) throws Exception {
+
+ OperationResult.OperationLog log = new OperationResult.OperationLog();
+
+ byte[] encodedRing = ring.getEncoded();
+
+ for(int i = 0; i < totalPackets; i++) {
+
+ byte[] brokenEncoded = KeyringTestingHelper.injectPacket(encodedRing, packet, i);
+
+ try {
+
+ UncachedKeyRing brokenRing = UncachedKeyRing.decodeFromData(brokenEncoded);
+
+ CanonicalizedKeyRing canonicalized = brokenRing.canonicalize(log, 0);
+ if (canonicalized == null) {
+ System.out.println("ok, canonicalization failed.");
+ continue;
+ }
+
+ Assert.assertArrayEquals("injected bad signature must be gone after canonicalization",
+ ring.getEncoded(), canonicalized.getEncoded());
+
+ } catch (Exception e) {
+ System.out.println("ok, rejected with: " + e.getMessage());
+ }
+ }
+
+ }
+
+ private static PGPSignature forgeSignature(PGPSecretKey key, int type,
+ PGPSignatureSubpacketGenerator subpackets,
+ PGPPublicKey publicKey)
+ throws Exception {
+
+ PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider(
+ Constants.BOUNCY_CASTLE_PROVIDER_NAME).build("".toCharArray());
+ PGPPrivateKey privateKey = key.extractPrivateKey(keyDecryptor);
+
+ PGPContentSignerBuilder signerBuilder = new JcaPGPContentSignerBuilder(
+ publicKey.getAlgorithm(), PGPUtil.SHA1)
+ .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME);
+
+ PGPSignatureGenerator sGen = new PGPSignatureGenerator(signerBuilder);
+ sGen.setHashedSubpackets(subpackets.generate());
+ sGen.init(type, privateKey);
+ return sGen.generateCertification(publicKey);
+
+ }
+
+ private static PGPSignature forgeSignature(PGPSecretKey key, int type,
+ PGPSignatureSubpacketGenerator subpackets,
+ String userId, PGPPublicKey publicKey)
+ throws Exception {
+
+ PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider(
+ Constants.BOUNCY_CASTLE_PROVIDER_NAME).build("".toCharArray());
+ PGPPrivateKey privateKey = key.extractPrivateKey(keyDecryptor);
+
+ PGPContentSignerBuilder signerBuilder = new JcaPGPContentSignerBuilder(
+ publicKey.getAlgorithm(), PGPUtil.SHA1)
+ .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME);
+
+ PGPSignatureGenerator sGen = new PGPSignatureGenerator(signerBuilder);
+ sGen.setHashedSubpackets(subpackets.generate());
+ sGen.init(type, privateKey);
+ return sGen.generateCertification(userId, publicKey);
+
+ }
+
+ private static PGPSignature forgeSignature(PGPSecretKey key, int type,
+ PGPSignatureSubpacketGenerator subpackets,
+ PGPPublicKey publicKey, PGPPublicKey signedKey)
+ throws Exception {
+
+ PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider(
+ Constants.BOUNCY_CASTLE_PROVIDER_NAME).build("".toCharArray());
+ PGPPrivateKey privateKey = key.extractPrivateKey(keyDecryptor);
+
+ PGPContentSignerBuilder signerBuilder = new JcaPGPContentSignerBuilder(
+ publicKey.getAlgorithm(), PGPUtil.SHA1)
+ .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME);
+
+ PGPSignatureGenerator sGen = new PGPSignatureGenerator(signerBuilder);
+ sGen.setHashedSubpackets(subpackets.generate());
+ sGen.init(type, privateKey);
+ return sGen.generateCertification(publicKey, signedKey);
+
+ }
+
+ private static PGPSignature forgeSignature(PGPSecretKey key, int type,
+ PGPSignatureSubpacketGenerator hashedSubs,
+ PGPSignatureSubpacketGenerator unhashedSubs,
+ PGPPublicKey publicKey, PGPPublicKey signedKey)
+ throws Exception {
+
+ PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider(
+ Constants.BOUNCY_CASTLE_PROVIDER_NAME).build("".toCharArray());
+ PGPPrivateKey privateKey = key.extractPrivateKey(keyDecryptor);
+
+ PGPContentSignerBuilder signerBuilder = new JcaPGPContentSignerBuilder(
+ publicKey.getAlgorithm(), PGPUtil.SHA1)
+ .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME);
+
+ PGPSignatureGenerator sGen = new PGPSignatureGenerator(signerBuilder);
+ sGen.setHashedSubpackets(hashedSubs.generate());
+ sGen.setUnhashedSubpackets(unhashedSubs.generate());
+ sGen.init(type, privateKey);
+ return sGen.generateCertification(publicKey, signedKey);
+
+ }
+
+}
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..cc5c487bd
--- /dev/null
+++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/UncachedKeyringMergeTest.java
@@ -0,0 +1,497 @@
+/*
+ * 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.WorkaroundBuildConfig;
+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 = WorkaroundBuildConfig.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;
+
+ }
+
+}
diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/UncachedKeyringTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/UncachedKeyringTest.java
new file mode 100644
index 000000000..e946acf01
--- /dev/null
+++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/pgp/UncachedKeyringTest.java
@@ -0,0 +1,161 @@
+/*
+ * 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.sig.KeyFlags;
+import org.sufficientlysecure.keychain.BuildConfig;
+import org.sufficientlysecure.keychain.WorkaroundBuildConfig;
+import org.sufficientlysecure.keychain.operations.results.PgpEditKeyResult;
+import org.sufficientlysecure.keychain.pgp.UncachedKeyRing.IteratorWithIOThrow;
+import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
+import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
+import org.sufficientlysecure.keychain.service.SaveKeyringParcel.Algorithm;
+import org.sufficientlysecure.keychain.service.SaveKeyringParcel.ChangeUnlockParcel;
+import org.sufficientlysecure.keychain.util.Passphrase;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.Random;
+
+@RunWith(RobolectricGradleTestRunner.class)
+@Config(constants = WorkaroundBuildConfig.class, sdk = 21, manifest = "src/main/AndroidManifest.xml")
+public class UncachedKeyringTest {
+
+ static UncachedKeyRing staticRing, staticPubRing;
+ UncachedKeyRing ring, pubRing;
+
+ @BeforeClass
+ public static void setUpOnce() throws Exception {
+ 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.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
+ Algorithm.RSA, 1024, null, KeyFlags.ENCRYPT_COMMS, 0L));
+
+ parcel.mAddUserIds.add("twi");
+ parcel.mAddUserIds.add("pink");
+ {
+ 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);
+ }
+ // passphrase is tested in PgpKeyOperationTest, just use empty here
+ parcel.mNewUnlock = new ChangeUnlockParcel(new Passphrase());
+ PgpKeyOperation op = new PgpKeyOperation(null);
+
+ PgpEditKeyResult result = op.createSecretKeyRing(parcel);
+ staticRing = result.getRing();
+ staticPubRing = staticRing.extractPublicKeyRing();
+
+ Assert.assertNotNull("initial test key creation must succeed", staticRing);
+
+ // 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;
+ ring = staticRing;
+ pubRing = staticPubRing;
+ }
+
+ @Test(expected = UnsupportedOperationException.class)
+ public void testPublicKeyItRemove() throws Exception {
+ Iterator<UncachedPublicKey> it = ring.getPublicKeys();
+ it.remove();
+ }
+
+ @Test(expected = PgpGeneralException.class)
+ public void testDecodeFromEmpty() throws Exception {
+ UncachedKeyRing.decodeFromData(new byte[0]);
+ }
+
+ @Test
+ public void testArmorIdentity() throws Exception {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ring.encodeArmored(out, "OpenKeychain");
+
+ Assert.assertArrayEquals("armor encoded and decoded ring should be identical to original",
+ ring.getEncoded(),
+ UncachedKeyRing.decodeFromData(out.toByteArray()).getEncoded());
+ }
+
+ @Test(expected = PgpGeneralException.class)
+ public void testDecodeEncodeMulti() throws Exception {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+
+ // encode secret and public ring in here
+ ring.encodeArmored(out, "OpenKeychain");
+ pubRing.encodeArmored(out, "OpenKeychain");
+
+ IteratorWithIOThrow<UncachedKeyRing> it =
+ UncachedKeyRing.fromStream(new ByteArrayInputStream(out.toByteArray()));
+ Assert.assertTrue("there should be two rings in the stream", it.hasNext());
+ Assert.assertArrayEquals("first ring should be the first we put in",
+ ring.getEncoded(), it.next().getEncoded());
+ Assert.assertTrue("there should be two rings in the stream", it.hasNext());
+ Assert.assertArrayEquals("second ring should be the second we put in",
+ pubRing.getEncoded(), it.next().getEncoded());
+ Assert.assertFalse("there should be two rings in the stream", it.hasNext());
+
+ // this should fail with PgpGeneralException, since it expects exactly one ring
+ UncachedKeyRing.decodeFromData(out.toByteArray());
+ }
+
+ @Test(expected = RuntimeException.class)
+ public void testPublicExtractPublic() throws Exception {
+ // can't do this, either!
+ pubRing.extractPublicKeyRing();
+ }
+
+ @Test(expected = IOException.class)
+ public void testBrokenVersionCert() throws Throwable {
+ // this is a test for one of the patches we use on top of stock bouncycastle, which
+ // returns an IOException rather than a RuntimeException in case of a bad certificate
+ // version byte
+ readRingFromResource("/test-keys/broken_cert_version.asc");
+ }
+
+ UncachedKeyRing readRingFromResource(String name) throws Throwable {
+ return UncachedKeyRing.fromStream(UncachedKeyringTest.class.getResourceAsStream(name)).next();
+ }
+
+}
diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/provider/ProviderHelperKeyringTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/provider/ProviderHelperKeyringTest.java
new file mode 100644
index 000000000..6779784a2
--- /dev/null
+++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/provider/ProviderHelperKeyringTest.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) Art O Cathain
+ *
+ * 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.provider;
+
+import java.util.Collections;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.ArrayList;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.*;
+import org.robolectric.annotation.Config;
+import org.sufficientlysecure.keychain.BuildConfig;
+import org.sufficientlysecure.keychain.WorkaroundBuildConfig;
+import org.sufficientlysecure.keychain.support.KeyringTestingHelper;
+
+@RunWith(RobolectricGradleTestRunner.class)
+@Config(constants = WorkaroundBuildConfig.class, sdk = 21, manifest = "src/main/AndroidManifest.xml")
+public class ProviderHelperKeyringTest {
+
+ @Test
+ public void testSavePublicKeyring() throws Exception {
+ Assert.assertTrue(new KeyringTestingHelper(RuntimeEnvironment.application).addKeyring(Collections.singleton(
+ "/public-key-for-sample.blob"
+ )));
+ }
+
+ // @Test
+ public void testSavePublicKeyringRsa() throws Exception {
+ Assert.assertTrue(new KeyringTestingHelper(RuntimeEnvironment.application).addKeyring(prependResourcePath(Arrays.asList(
+ "000001-006.public_key",
+ "000002-013.user_id",
+ "000003-002.sig",
+ "000004-012.ring_trust",
+ "000005-002.sig",
+ "000006-012.ring_trust",
+ "000007-002.sig",
+ "000008-012.ring_trust",
+ "000009-002.sig",
+ "000010-012.ring_trust",
+ "000011-002.sig",
+ "000012-012.ring_trust",
+ "000013-014.public_subkey",
+ "000014-002.sig",
+ "000015-012.ring_trust"
+ ))));
+ }
+
+ // @Test
+ public void testSavePublicKeyringDsa() throws Exception {
+ Assert.assertTrue(new KeyringTestingHelper(RuntimeEnvironment.application).addKeyring(prependResourcePath(Arrays.asList(
+ "000016-006.public_key",
+ "000017-002.sig",
+ "000018-012.ring_trust",
+ "000019-013.user_id",
+ "000020-002.sig",
+ "000021-012.ring_trust",
+ "000022-002.sig",
+ "000023-012.ring_trust",
+ "000024-014.public_subkey",
+ "000025-002.sig",
+ "000026-012.ring_trust"
+ ))));
+ }
+
+ // @Test
+ public void testSavePublicKeyringDsa2() throws Exception {
+ Assert.assertTrue(new KeyringTestingHelper(RuntimeEnvironment.application).addKeyring(prependResourcePath(Arrays.asList(
+ "000027-006.public_key",
+ "000028-002.sig",
+ "000029-012.ring_trust",
+ "000030-013.user_id",
+ "000031-002.sig",
+ "000032-012.ring_trust",
+ "000033-002.sig",
+ "000034-012.ring_trust"
+ ))));
+ }
+
+ private static Collection<String> prependResourcePath(Collection<String> files) {
+ Collection<String> prependedFiles = new ArrayList<String>();
+ for (String file: files) {
+ prependedFiles.add("/OpenPGP-Haskell/tests/data/" + file);
+ }
+ return prependedFiles;
+ }
+}
diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/provider/ProviderHelperSaveTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/provider/ProviderHelperSaveTest.java
new file mode 100644
index 000000000..04851ebbb
--- /dev/null
+++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/provider/ProviderHelperSaveTest.java
@@ -0,0 +1,256 @@
+/*
+ * 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.provider;
+
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricGradleTestRunner;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowLog;
+import org.spongycastle.bcpg.sig.KeyFlags;
+import org.spongycastle.util.encoders.Hex;
+import org.sufficientlysecure.keychain.BuildConfig;
+import org.sufficientlysecure.keychain.WorkaroundBuildConfig;
+import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing;
+import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey;
+import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType;
+import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing;
+import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
+import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
+import org.sufficientlysecure.keychain.operations.results.SaveKeyringResult;
+import org.sufficientlysecure.keychain.util.IterableIterator;
+import org.sufficientlysecure.keychain.util.ProgressScaler;
+
+import java.util.Arrays;
+import java.util.Iterator;
+
+@RunWith(RobolectricGradleTestRunner.class)
+@Config(constants = WorkaroundBuildConfig.class, sdk = 21, manifest = "src/main/AndroidManifest.xml")
+public class ProviderHelperSaveTest {
+
+ ProviderHelper mProviderHelper = new ProviderHelper(RuntimeEnvironment.application);
+
+ @BeforeClass
+ public static void setUpOnce() throws Exception {
+ ShadowLog.stream = System.out;
+ }
+
+ @Test public void testImportCooperPair() throws Exception {
+
+ // insert two keys with same long key id, make sure the second one gets rejected either way!
+ UncachedKeyRing first =
+ readRingFromResource("/test-keys/cooperpair/9E669861368BCA0BE42DAF7DDDA252EBB8EBE1AF.asc");
+ UncachedKeyRing second =
+ readRingFromResource("/test-keys/cooperpair/A55120427374F3F7AA5F1166DDA252EBB8EBE1AF.asc");
+
+ SaveKeyringResult result;
+
+ // insert both keys, second should fail
+ result = new ProviderHelper(RuntimeEnvironment.application).savePublicKeyRing(first);
+ Assert.assertTrue("first keyring import should succeed", result.success());
+ result = new ProviderHelper(RuntimeEnvironment.application).savePublicKeyRing(second);
+ Assert.assertFalse("second keyring import should fail", result.success());
+
+ new KeychainDatabase(RuntimeEnvironment.application).clearDatabase();
+
+ // and the other way around
+ result = new ProviderHelper(RuntimeEnvironment.application).savePublicKeyRing(second);
+ Assert.assertTrue("first keyring import should succeed", result.success());
+ result = new ProviderHelper(RuntimeEnvironment.application).savePublicKeyRing(first);
+ Assert.assertFalse("second keyring import should fail", result.success());
+
+ }
+
+ @Test public void testImportSymantec() throws Exception {
+
+ // symantec pgp desktop exports secret keys without self certificates. we don't support
+ // those on their own, but if they are imported together with their public key (or if
+ // the public key is already known), the self certs info will be merged in as a special
+ // case.
+ UncachedKeyRing seckey =
+ readRingFromResource("/test-keys/symantec_secret.asc");
+ UncachedKeyRing pubkey =
+ readRingFromResource("/test-keys/symantec_public.asc");
+
+ SaveKeyringResult result;
+
+ // insert secret, this should fail because of missing self-cert
+ result = new ProviderHelper(RuntimeEnvironment.application).saveSecretKeyRing(seckey, new ProgressScaler());
+ Assert.assertFalse("secret keyring import before pubring import should fail", result.success());
+
+ // insert pubkey, then seckey - both should succeed
+ result = new ProviderHelper(RuntimeEnvironment.application).savePublicKeyRing(pubkey);
+ Assert.assertTrue("public keyring import should succeed", result.success());
+ result = new ProviderHelper(RuntimeEnvironment.application).saveSecretKeyRing(seckey, new ProgressScaler());
+ Assert.assertTrue("secret keyring import after pubring import should succeed", result.success());
+
+ }
+
+ @Test public void testImportNoFlagKey() throws Exception {
+
+ UncachedKeyRing pub = readRingFromResource("/test-keys/mailvelope_07_no_key_flags.asc");
+ long keyId = pub.getMasterKeyId();
+ Assert.assertEquals("key flags should be zero",
+ 0, (long) pub.canonicalize(new OperationLog(), 0).getPublicKey().getKeyUsage());
+
+ mProviderHelper.savePublicKeyRing(pub);
+
+ CachedPublicKeyRing cachedRing = mProviderHelper.getCachedPublicKeyRing(keyId);
+ CanonicalizedPublicKeyRing pubRing = mProviderHelper.getCanonicalizedPublicKeyRing(keyId);
+
+ Assert.assertEquals("master key should be encryption key", keyId, pubRing.getEncryptId());
+ Assert.assertEquals("master key should be encryption key (cached)", keyId, cachedRing.getEncryptId());
+
+ Assert.assertEquals("canonicalized key flags should be zero",
+ 0, (long) pubRing.getPublicKey().getKeyUsage());
+ Assert.assertTrue("master key should be able to certify", pubRing.getPublicKey().canCertify());
+ Assert.assertTrue("master key should be allowed to sign", pubRing.getPublicKey().canSign());
+ Assert.assertTrue("master key should be able to encrypt", pubRing.getPublicKey().canEncrypt());
+
+ }
+
+ @Test public void testImportDivertToCard() throws Exception {
+
+ UncachedKeyRing sec = readRingFromResource("/test-keys/divert_to_card_sec.asc");
+ long keyId = sec.getMasterKeyId();
+
+ SaveKeyringResult result;
+
+ result = mProviderHelper.saveSecretKeyRing(sec, new ProgressScaler());
+ Assert.assertTrue("import of secret keyring should succeed", result.success());
+
+ // make sure both the CanonicalizedSecretKeyRing as well as the CachedPublicKeyRing correctly
+ // indicate the secret key type
+ CachedPublicKeyRing cachedRing = mProviderHelper.getCachedPublicKeyRing(keyId);
+ CanonicalizedSecretKeyRing secRing = mProviderHelper.getCanonicalizedSecretKeyRing(keyId);
+
+ Iterator<CanonicalizedSecretKey> it = secRing.secretKeyIterator().iterator();
+
+ { // first subkey
+ Assert.assertTrue("keyring should have 3 subkeys (1)", it.hasNext());
+ CanonicalizedSecretKey key = it.next();
+ Assert.assertEquals("first subkey should be of type sign+certify",
+ KeyFlags.CERTIFY_OTHER | KeyFlags.SIGN_DATA, (int) key.getKeyUsage());
+ Assert.assertEquals("first subkey should be divert-to-card",
+ SecretKeyType.DIVERT_TO_CARD, key.getSecretKeyType());
+ Assert.assertTrue("canCertify() should be true", key.canCertify());
+ Assert.assertTrue("canSign() should be true", key.canSign());
+
+ // cached
+ Assert.assertEquals("all subkeys from CachedPublicKeyRing should be divert-to-key",
+ SecretKeyType.DIVERT_TO_CARD, cachedRing.getSecretKeyType(key.getKeyId()));
+ }
+
+ { // second subkey
+ Assert.assertTrue("keyring should have 3 subkeys (2)", it.hasNext());
+ CanonicalizedSecretKey key = it.next();
+ Assert.assertEquals("second subkey should be of type authenticate",
+ KeyFlags.AUTHENTICATION, (int) key.getKeyUsage());
+ Assert.assertEquals("second subkey should be divert-to-card",
+ SecretKeyType.DIVERT_TO_CARD, key.getSecretKeyType());
+ Assert.assertTrue("canAuthenticate() should be true", key.canAuthenticate());
+
+ // cached
+ Assert.assertEquals("all subkeys from CachedPublicKeyRing should be divert-to-key",
+ SecretKeyType.DIVERT_TO_CARD, cachedRing.getSecretKeyType(key.getKeyId()));
+ }
+
+ { // third subkey
+ Assert.assertTrue("keyring should have 3 subkeys (3)", it.hasNext());
+ CanonicalizedSecretKey key = it.next();
+ Assert.assertEquals("first subkey should be of type encrypt (both types)",
+ KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE, (int) key.getKeyUsage());
+ Assert.assertEquals("third subkey should be divert-to-card",
+ SecretKeyType.DIVERT_TO_CARD, key.getSecretKeyType());
+ Assert.assertTrue("canEncrypt() should be true", key.canEncrypt());
+
+ // cached
+ Assert.assertEquals("all subkeys from CachedPublicKeyRing should be divert-to-key",
+ SecretKeyType.DIVERT_TO_CARD, cachedRing.getSecretKeyType(key.getKeyId()));
+ }
+
+ Assert.assertFalse("keyring should have 3 subkeys (4)", it.hasNext());
+
+ }
+
+ @Test public void testImportBadEncodedUserId() throws Exception {
+
+ UncachedKeyRing key = readRingFromResource("/test-keys/bad_user_id_encoding.asc");
+ long keyId = key.getMasterKeyId();
+
+ SaveKeyringResult result;
+
+ result = mProviderHelper.savePublicKeyRing(key, new ProgressScaler());
+ Assert.assertTrue("import of keyring should succeed", result.success());
+
+ CanonicalizedPublicKeyRing ring = mProviderHelper.getCanonicalizedPublicKeyRing(keyId);
+ boolean found = false;
+ byte[] badUserId = Hex.decode("436c61757320467261656e6b656c203c436c6175732e4672e46e6b656c4068616c696661782e727774682d61616368656e2e64653e");
+ for (byte[] rawUserId : new IterableIterator<byte[]>(
+ ring.getUnorderedRawUserIds().iterator())) {
+ if (Arrays.equals(rawUserId, badUserId)) {
+ found = true;
+ }
+ }
+
+ Assert.assertTrue("import of the badly encoded user id should succeed", found);
+ }
+
+ @Test
+ /** Tests a master key which may sign, but is stripped. In this case, if there is a different
+ * subkey available which can sign, that one should be selected.
+ */
+ public void testImportStrippedFlags() throws Exception {
+
+ UncachedKeyRing key = readRingFromResource("/test-keys/stripped_flags.asc");
+ long masterKeyId = key.getMasterKeyId();
+
+ SaveKeyringResult result;
+
+ result = mProviderHelper.saveSecretKeyRing(key, new ProgressScaler());
+ Assert.assertTrue("import of keyring should succeed", result.success());
+
+ long signId;
+ {
+ CanonicalizedSecretKeyRing ring = mProviderHelper.getCanonicalizedSecretKeyRing(masterKeyId);
+ Assert.assertTrue("master key should have sign flag", ring.getPublicKey().canSign());
+ Assert.assertTrue("master key should have encrypt flag", ring.getPublicKey().canEncrypt());
+
+ signId = ring.getSecretSignId();
+ Assert.assertNotEquals("encrypt id should not be 0", 0, signId);
+ Assert.assertNotEquals("encrypt key should be different from master key", masterKeyId, signId);
+ }
+
+ {
+ CachedPublicKeyRing ring = mProviderHelper.getCachedPublicKeyRing(masterKeyId);
+ Assert.assertEquals("signing key should be same id cached as uncached", signId, ring.getSecretSignId());
+ }
+
+ }
+
+ UncachedKeyRing readRingFromResource(String name) throws Exception {
+ return UncachedKeyRing.fromStream(ProviderHelperSaveTest.class.getResourceAsStream(name)).next();
+ }
+
+} \ No newline at end of file
diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/support/KeyringBuilder.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/support/KeyringBuilder.java
new file mode 100644
index 000000000..94193bbcb
--- /dev/null
+++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/support/KeyringBuilder.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright (C) Art O Cathain
+ *
+ * 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.support;
+
+import org.spongycastle.bcpg.CompressionAlgorithmTags;
+import org.spongycastle.bcpg.ContainedPacket;
+import org.spongycastle.bcpg.HashAlgorithmTags;
+import org.spongycastle.bcpg.MPInteger;
+import org.spongycastle.bcpg.PublicKeyAlgorithmTags;
+import org.spongycastle.bcpg.PublicKeyPacket;
+import org.spongycastle.bcpg.PublicSubkeyPacket;
+import org.spongycastle.bcpg.RSAPublicBCPGKey;
+import org.spongycastle.bcpg.SignaturePacket;
+import org.spongycastle.bcpg.SignatureSubpacket;
+import org.spongycastle.bcpg.SignatureSubpacketInputStream;
+import org.spongycastle.bcpg.SignatureSubpacketTags;
+import org.spongycastle.bcpg.SymmetricKeyAlgorithmTags;
+import org.spongycastle.bcpg.UserIDPacket;
+import org.spongycastle.bcpg.sig.Features;
+import org.spongycastle.bcpg.sig.IssuerKeyID;
+import org.spongycastle.bcpg.sig.KeyExpirationTime;
+import org.spongycastle.bcpg.sig.KeyFlags;
+import org.spongycastle.bcpg.sig.PreferredAlgorithms;
+import org.spongycastle.bcpg.sig.SignatureCreationTime;
+import org.spongycastle.openpgp.PGPSignature;
+import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Helps create correct and incorrect keyrings for tests.
+ *
+ * The original "correct" keyring was generated by GnuPG.
+ */
+public class KeyringBuilder {
+
+
+ private static final BigInteger PUBLIC_KEY_MODULUS = new BigInteger(
+ "cbab78d90d5f2cc0c54dd3c3953005a1e6b521f1ffa5465a102648bf7b91ec72" +
+ "f9c180759301587878caeb73332156209f81ca5b3b94309d96110f6972cfc56a" +
+ "37fd6279f61d71f19b8f64b288e338299dce133520f5b9b4253e6f4ba31ca36a" +
+ "fd87c2081b15f0b283e9350e370e181a23d31379101f17a23ae9192250db6540" +
+ "2e9cab2a275bc5867563227b197c8b136c832a94325b680e144ed864fb00b9b8" +
+ "b07e13f37b40d5ac27dae63cd6a470a7b40fa3c7479b5b43e634850cc680b177" +
+ "8dd6b1b51856f36c3520f258f104db2f96b31a53dd74f708ccfcefccbe420a90" +
+ "1c37f1f477a6a4b15f5ecbbfd93311a647bcc3f5f81c59dfe7252e3cd3be6e27"
+ , 16
+ );
+
+ private static final BigInteger PUBLIC_SUBKEY_MODULUS = new BigInteger(
+ "e8e2e2a33102649f19f8a07486fb076a1406ca888d72ae05d28f0ef372b5408e" +
+ "45132c69f6e5cb6a79bb8aed84634196731393a82d53e0ddd42f28f92cc15850" +
+ "8ce3b7ca1a9830502745aee774f86987993df984781f47c4a2910f95cf4c950c" +
+ "c4c6cccdc134ad408a0c5418b5e360c9781a8434d366053ea6338b975fae88f9" +
+ "383a10a90e7b2caa9ddb95708aa9d8a90246e29b04dbd6136613085c9a287315" +
+ "c6e9c7ff4012defc1713875e3ff6073333a1c93d7cd75ebeaaf16b8b853d96ba" +
+ "7003258779e8d2f70f1bc0bcd3ef91d7a9ccd8e225579b2d6fcae32799b0a6c0" +
+ "e7305fc65dc4edc849c6130a0d669c90c193b1e746c812510f9d600a208be4a5"
+ , 16
+ );
+
+ private static final Date SIGNATURE_DATE = new Date(1404566755000L);
+
+ private static final BigInteger EXPONENT = BigInteger.valueOf(0x010001);
+
+ private static final String USER_ID_STRING = "OpenKeychain User (NOT A REAL KEY) <openkeychain@example.com>";
+
+ public static final BigInteger CORRECT_SIGNATURE = new BigInteger(
+ "b065c071d3439d5610eb22e5b4df9e42ed78b8c94f487389e4fc98e8a75a043f" +
+ "14bf57d591811e8e7db2d31967022d2ee64372829183ec51d0e20c42d7a1e519" +
+ "e9fa22cd9db90f0fd7094fd093b78be2c0db62022193517404d749152c71edc6" +
+ "fd48af3416038d8842608ecddebbb11c5823a4321d2029b8993cb017fa8e5ad7" +
+ "8a9a618672d0217c4b34002f1a4a7625a514b6a86475e573cb87c64d7069658e" +
+ "627f2617874007a28d525e0f87d93ca7b15ad10dbdf10251e542afb8f9b16cbf" +
+ "7bebdb5fe7e867325a44e59cad0991cb239b1c859882e2ebb041b80e5cdc3b40" +
+ "ed259a8a27d63869754c0881ccdcb50f0564fecdc6966be4a4b87a3507a9d9be"
+ , 16
+ );
+ public static final BigInteger CORRECT_SUBKEY_SIGNATURE = new BigInteger(
+ "9c40543e646cfa6d3d1863d91a4e8f1421d0616ddb3187505df75fbbb6c59dd5" +
+ "3136b866f246a0320e793cb142c55c8e0e521d1e8d9ab864650f10690f5f1429" +
+ "2eb8402a3b1f82c01079d12f5c57c43fce524a530e6f49f6f87d984e26db67a2" +
+ "d469386dac87553c50147ebb6c2edd9248325405f737b815253beedaaba4f5c9" +
+ "3acd5d07fe6522ceda1027932d849e3ec4d316422cd43ea6e506f643936ab0be" +
+ "8246e546bb90d9a83613185047566864ffe894946477e939725171e0e15710b2" +
+ "089f78752a9cb572f5907323f1b62f14cb07671aeb02e6d7178f185467624ec5" +
+ "74e4a73c439a12edba200a4832106767366a1e6f63da0a42d593fa3914deee2b"
+ , 16
+ );
+ public static final BigInteger KEY_ID = BigInteger.valueOf(0x15130BCF071AE6BFL);
+
+ public static UncachedKeyRing correctRing() {
+ return convertToKeyring(correctKeyringPackets());
+ }
+
+ public static UncachedKeyRing ringWithExtraIncorrectSignature() {
+ List<ContainedPacket> packets = correctKeyringPackets();
+ SignaturePacket incorrectSignaturePacket = createSignaturePacket(CORRECT_SIGNATURE.subtract(BigInteger.ONE));
+ packets.add(2, incorrectSignaturePacket);
+ return convertToKeyring(packets);
+ }
+
+ private static UncachedKeyRing convertToKeyring(List<ContainedPacket> packets) {
+ try {
+ return UncachedKeyRing.decodeFromData(TestDataUtil.concatAll(packets));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static List<ContainedPacket> correctKeyringPackets() {
+ PublicKeyPacket publicKey = createPgpPublicKey(PUBLIC_KEY_MODULUS);
+ UserIDPacket userId = createUserId(USER_ID_STRING);
+ SignaturePacket signaturePacket = createSignaturePacket(CORRECT_SIGNATURE);
+ PublicKeyPacket subKey = createPgpPublicSubKey(PUBLIC_SUBKEY_MODULUS);
+ SignaturePacket subKeySignaturePacket = createSubkeySignaturePacket();
+
+ return new ArrayList<ContainedPacket>(Arrays.asList(
+ publicKey,
+ userId,
+ signaturePacket,
+ subKey,
+ subKeySignaturePacket
+ ));
+ }
+
+ private static SignaturePacket createSignaturePacket(BigInteger signature) {
+ MPInteger[] signatureArray = new MPInteger[]{
+ new MPInteger(signature)
+ };
+
+ int signatureType = PGPSignature.POSITIVE_CERTIFICATION;
+ int keyAlgorithm = SignaturePacket.RSA_GENERAL;
+ int hashAlgorithm = HashAlgorithmTags.SHA1;
+
+ SignatureSubpacket[] hashedData = new SignatureSubpacket[]{
+ new SignatureCreationTime(false, SIGNATURE_DATE),
+ new KeyFlags(false, KeyFlags.CERTIFY_OTHER + KeyFlags.SIGN_DATA),
+ new KeyExpirationTime(false, TimeUnit.DAYS.toSeconds(2)),
+ new PreferredAlgorithms(SignatureSubpacketTags.PREFERRED_SYM_ALGS, false, new int[]{
+ SymmetricKeyAlgorithmTags.AES_256,
+ SymmetricKeyAlgorithmTags.AES_192,
+ SymmetricKeyAlgorithmTags.AES_128,
+ SymmetricKeyAlgorithmTags.CAST5,
+ SymmetricKeyAlgorithmTags.TRIPLE_DES
+ }),
+ new PreferredAlgorithms(SignatureSubpacketTags.PREFERRED_HASH_ALGS, false, new int[]{
+ HashAlgorithmTags.SHA256,
+ HashAlgorithmTags.SHA1,
+ HashAlgorithmTags.SHA384,
+ HashAlgorithmTags.SHA512,
+ HashAlgorithmTags.SHA224
+ }),
+ new PreferredAlgorithms(SignatureSubpacketTags.PREFERRED_COMP_ALGS, false, new int[]{
+ CompressionAlgorithmTags.ZLIB,
+ CompressionAlgorithmTags.BZIP2,
+ CompressionAlgorithmTags.ZIP
+ }),
+ new Features(false, Features.FEATURE_MODIFICATION_DETECTION),
+ createPreferencesSignatureSubpacket()
+ };
+ SignatureSubpacket[] unhashedData = new SignatureSubpacket[]{
+ new IssuerKeyID(false, KEY_ID.toByteArray())
+ };
+ byte[] fingerPrint = new BigInteger("522c", 16).toByteArray();
+
+ return new SignaturePacket(signatureType,
+ KEY_ID.longValue(),
+ keyAlgorithm,
+ hashAlgorithm,
+ hashedData,
+ unhashedData,
+ fingerPrint,
+ signatureArray);
+ }
+
+ /**
+ * There is no Preferences subpacket in BouncyCastle, so we have
+ * to create one manually.
+ */
+ private static SignatureSubpacket createPreferencesSignatureSubpacket() {
+ SignatureSubpacket prefs;
+ try {
+ prefs = new SignatureSubpacketInputStream(new ByteArrayInputStream(
+ new byte[]{2, SignatureSubpacketTags.KEY_SERVER_PREFS, (byte) 0x80})
+ ).readPacket();
+ } catch (IOException ex) {
+ throw new RuntimeException(ex);
+ }
+ return prefs;
+ }
+
+ private static SignaturePacket createSubkeySignaturePacket() {
+ int signatureType = PGPSignature.SUBKEY_BINDING;
+ int keyAlgorithm = SignaturePacket.RSA_GENERAL;
+ int hashAlgorithm = HashAlgorithmTags.SHA1;
+
+ SignatureSubpacket[] hashedData = new SignatureSubpacket[]{
+ new SignatureCreationTime(false, SIGNATURE_DATE),
+ new KeyFlags(false, KeyFlags.ENCRYPT_COMMS + KeyFlags.ENCRYPT_STORAGE),
+ new KeyExpirationTime(false, TimeUnit.DAYS.toSeconds(2)),
+ };
+ SignatureSubpacket[] unhashedData = new SignatureSubpacket[]{
+ new IssuerKeyID(false, KEY_ID.toByteArray())
+ };
+ byte[] fingerPrint = new BigInteger("234a", 16).toByteArray();
+ MPInteger[] signature = new MPInteger[]{
+ new MPInteger(CORRECT_SUBKEY_SIGNATURE)
+ };
+ return new SignaturePacket(signatureType,
+ KEY_ID.longValue(),
+ keyAlgorithm,
+ hashAlgorithm,
+ hashedData,
+ unhashedData,
+ fingerPrint,
+ signature);
+ }
+
+ private static PublicKeyPacket createPgpPublicKey(BigInteger modulus) {
+ return new PublicKeyPacket(PublicKeyAlgorithmTags.RSA_GENERAL, SIGNATURE_DATE, new RSAPublicBCPGKey(modulus, EXPONENT));
+ }
+
+ private static PublicKeyPacket createPgpPublicSubKey(BigInteger modulus) {
+ return new PublicSubkeyPacket(PublicKeyAlgorithmTags.RSA_GENERAL, SIGNATURE_DATE, new RSAPublicBCPGKey(modulus, EXPONENT));
+ }
+
+ private static UserIDPacket createUserId(String userId) {
+ return new UserIDPacket(userId);
+ }
+
+}
diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/support/KeyringTestingHelper.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/support/KeyringTestingHelper.java
new file mode 100644
index 000000000..4ac994c99
--- /dev/null
+++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/support/KeyringTestingHelper.java
@@ -0,0 +1,365 @@
+/*
+ * Copyright (C) Art O Cathain
+ * 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.support;
+
+import android.content.Context;
+
+import org.spongycastle.util.Arrays;
+import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
+import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.operations.results.SaveKeyringResult;
+import org.sufficientlysecure.keychain.util.ProgressScaler;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+
+/** Helper methods for keyring tests. */
+public class KeyringTestingHelper {
+
+ private final Context context;
+
+ public KeyringTestingHelper(Context robolectricContext) {
+ this.context = robolectricContext;
+ }
+
+ public boolean addKeyring(Collection<String> blobFiles) throws Exception {
+
+ ProviderHelper providerHelper = new ProviderHelper(context);
+
+ byte[] data = TestDataUtil.readAllFully(blobFiles);
+ UncachedKeyRing ring = UncachedKeyRing.decodeFromData(data);
+ long masterKeyId = ring.getMasterKeyId();
+
+ // Should throw an exception; key is not yet saved
+ retrieveKeyAndExpectNotFound(providerHelper, masterKeyId);
+
+ SaveKeyringResult saveKeyringResult = providerHelper.savePublicKeyRing(ring, new ProgressScaler());
+
+ boolean saveSuccess = saveKeyringResult.success();
+
+ // Now re-retrieve the saved key. Should not throw an exception.
+ providerHelper.getCanonicalizedPublicKeyRing(masterKeyId);
+
+ // A different ID should still fail
+ retrieveKeyAndExpectNotFound(providerHelper, masterKeyId - 1);
+
+ return saveSuccess;
+ }
+
+ public static UncachedKeyRing removePacket(UncachedKeyRing ring, int position)
+ throws IOException, PgpGeneralException {
+ return UncachedKeyRing.decodeFromData(removePacket(ring.getEncoded(), position));
+ }
+
+ public static byte[] removePacket(byte[] ring, int position) throws IOException {
+ Iterator<RawPacket> it = parseKeyring(ring);
+ ByteArrayOutputStream out = new ByteArrayOutputStream(ring.length);
+
+ int i = 0;
+ while(it.hasNext()) {
+ // at the right position, skip the packet
+ if(i++ == position) {
+ it.next();
+ continue;
+ }
+ // write the old one
+ out.write(it.next().buf);
+ }
+
+ if (i <= position) {
+ throw new IndexOutOfBoundsException("injection index did not not occur in stream!");
+ }
+
+ return out.toByteArray();
+ }
+
+ public static UncachedKeyRing injectPacket(UncachedKeyRing ring, byte[] inject, int position)
+ throws IOException, PgpGeneralException {
+ return UncachedKeyRing.decodeFromData(injectPacket(ring.getEncoded(), inject, position));
+ }
+
+ public static byte[] injectPacket(byte[] ring, byte[] inject, int position) throws IOException {
+
+ Iterator<RawPacket> it = parseKeyring(ring);
+ ByteArrayOutputStream out = new ByteArrayOutputStream(ring.length + inject.length);
+
+ int i = 0;
+ while(it.hasNext()) {
+ // at the right position, inject the new packet
+ if(i++ == position) {
+ out.write(inject);
+ }
+ // write the old one
+ out.write(it.next().buf);
+ }
+
+ if (i <= position) {
+ throw new IndexOutOfBoundsException("injection index did not not occur in stream!");
+ }
+
+ return out.toByteArray();
+
+ }
+
+ /** This class contains a single pgp packet, together with information about its position
+ * in the keyring and its packet tag.
+ */
+ public static class RawPacket {
+ public int position;
+
+ // packet tag for convenience, this can also be read from the header
+ public int tag;
+
+ public int headerLength, length;
+ // this buf includes the header, so its length is headerLength + length!
+ public byte[] buf;
+
+ @Override
+ public boolean equals(Object other) {
+ return other instanceof RawPacket && Arrays.areEqual(this.buf, ((RawPacket) other).buf);
+ }
+
+ @Override
+ public int hashCode() {
+ return Arrays.hashCode(buf);
+ }
+ }
+
+ /** A comparator which compares RawPackets by their position */
+ public static final Comparator<RawPacket> packetOrder = new Comparator<RawPacket>() {
+ public int compare(RawPacket left, RawPacket right) {
+ return Integer.compare(left.position, right.position);
+ }
+ };
+
+ /** Diff two keyrings, returning packets only present in one keyring in its associated List.
+ *
+ * Packets in the returned lists are annotated and ordered by their original order of appearance
+ * in their origin keyrings.
+ *
+ * @return true if keyrings differ in at least one packet
+ */
+ public static boolean diffKeyrings(byte[] ringA, byte[] ringB,
+ List<RawPacket> onlyA, List<RawPacket> onlyB)
+ throws IOException {
+ Iterator<RawPacket> streamA = parseKeyring(ringA);
+ Iterator<RawPacket> streamB = parseKeyring(ringB);
+
+ HashSet<RawPacket> a = new HashSet<RawPacket>(), b = new HashSet<RawPacket>();
+
+ RawPacket p;
+ int pos = 0;
+ while(true) {
+ p = streamA.next();
+ if (p == null) {
+ break;
+ }
+ p.position = pos++;
+ a.add(p);
+ }
+ pos = 0;
+ while(true) {
+ p = streamB.next();
+ if (p == null) {
+ break;
+ }
+ p.position = pos++;
+ b.add(p);
+ }
+
+ onlyA.clear();
+ onlyB.clear();
+
+ onlyA.addAll(a);
+ onlyA.removeAll(b);
+ onlyB.addAll(b);
+ onlyB.removeAll(a);
+
+ Collections.sort(onlyA, packetOrder);
+ Collections.sort(onlyB, packetOrder);
+
+ return !onlyA.isEmpty() || !onlyB.isEmpty();
+ }
+
+ /** Creates an iterator of RawPackets over a binary keyring. */
+ public static Iterator<RawPacket> parseKeyring(byte[] ring) {
+
+ final InputStream stream = new ByteArrayInputStream(ring);
+
+ return new Iterator<RawPacket>() {
+ RawPacket next;
+
+ @Override
+ public boolean hasNext() {
+ if (next == null) try {
+ next = readPacket(stream);
+ } catch (IOException e) {
+ return false;
+ }
+ return next != null;
+ }
+
+ @Override
+ public RawPacket next() {
+ if (!hasNext()) {
+ return null;
+ }
+ try {
+ return next;
+ } finally {
+ next = null;
+ }
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+ };
+
+ }
+
+ /** Read a single (raw) pgp packet from an input stream.
+ *
+ * Note that the RawPacket.position field is NOT set here!
+ *
+ * Variable length packets are not handled here. we don't use those in our test classes, and
+ * otherwise rely on BouncyCastle's own unit tests to handle those correctly.
+ */
+ private static RawPacket readPacket(InputStream in) throws IOException {
+
+ // save here. this is tag + length, max 6 bytes
+ in.mark(6);
+
+ int hdr = in.read();
+ int headerLength = 1;
+
+ if (hdr < 0) {
+ return null;
+ }
+
+ if ((hdr & 0x80) == 0) {
+ throw new IOException("invalid header encountered");
+ }
+
+ boolean newPacket = (hdr & 0x40) != 0;
+ int tag;
+ int bodyLen;
+
+ if (newPacket) {
+ tag = hdr & 0x3f;
+
+ int l = in.read();
+ headerLength += 1;
+
+ if (l < 192) {
+ bodyLen = l;
+ } else if (l <= 223) {
+ int b = in.read();
+ headerLength += 1;
+
+ bodyLen = ((l - 192) << 8) + (b) + 192;
+ } else if (l == 255) {
+ bodyLen = (in.read() << 24) | (in.read() << 16) | (in.read() << 8) | in.read();
+ headerLength += 4;
+ } else {
+ // bodyLen = 1 << (l & 0x1f);
+ throw new IOException("no support for partial bodies in test classes");
+ }
+ } else {
+ int lengthType = hdr & 0x3;
+
+ tag = (hdr & 0x3f) >> 2;
+
+ switch (lengthType) {
+ case 0:
+ bodyLen = in.read();
+ headerLength += 1;
+ break;
+ case 1:
+ bodyLen = (in.read() << 8) | in.read();
+ headerLength += 2;
+ break;
+ case 2:
+ bodyLen = (in.read() << 24) | (in.read() << 16) | (in.read() << 8) | in.read();
+ headerLength += 4;
+ break;
+ case 3:
+ // bodyLen = 1 << (l & 0x1f);
+ throw new IOException("no support for partial bodies in test classes");
+ default:
+ throw new IOException("unknown length type encountered");
+ }
+ }
+
+ in.reset();
+
+ // read the entire packet INCLUDING the header here
+ byte[] buf = new byte[headerLength+bodyLen];
+ if (in.read(buf) != headerLength+bodyLen) {
+ throw new IOException("read length mismatch!");
+ }
+ RawPacket p = new RawPacket();
+ p.tag = tag;
+ p.headerLength = headerLength;
+ p.length = bodyLen;
+ p.buf = buf;
+ return p;
+
+ }
+
+ public static <E> E getNth(Iterator<E> it, int position) {
+ while(position-- > 0) {
+ it.next();
+ }
+ return it.next();
+ }
+
+ public static long getSubkeyId(UncachedKeyRing ring, int position) {
+ return getNth(ring.getPublicKeys(), position).getKeyId();
+ }
+
+ private void retrieveKeyAndExpectNotFound(ProviderHelper providerHelper, long masterKeyId) {
+ try {
+ providerHelper.getCanonicalizedPublicKeyRing(masterKeyId);
+ throw new AssertionError("Was expecting the previous call to fail!");
+ } catch (ProviderHelper.NotFoundException expectedException) {
+ // good
+ }
+ }
+
+ public static <E> List<E> itToList(Iterator<E> it) {
+ List<E> result = new ArrayList<E>();
+ while(it.hasNext()) {
+ result.add(it.next());
+ }
+ return result;
+ }
+
+}
diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/support/ProviderHelperStub.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/support/ProviderHelperStub.java
new file mode 100644
index 000000000..0573f07fb
--- /dev/null
+++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/support/ProviderHelperStub.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) Art O Cathain
+ *
+ * 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.support;
+
+import android.content.Context;
+import android.net.Uri;
+
+import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+
+class ProviderHelperStub extends ProviderHelper {
+ public ProviderHelperStub(Context context) {
+ super(context);
+ }
+
+ @Override
+ public CanonicalizedPublicKeyRing getCanonicalizedPublicKeyRing(Uri id) throws NotFoundException {
+ byte[] data = TestDataUtil.readFully(getClass().getResourceAsStream("/public-key-for-sample.blob"));
+ return new CanonicalizedPublicKeyRing(data, 0);
+ }
+}
diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/support/TestDataUtil.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/support/TestDataUtil.java
new file mode 100644
index 000000000..f2b3c0996
--- /dev/null
+++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/support/TestDataUtil.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) Art O Cathain
+ *
+ * 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.support;
+
+import org.spongycastle.bcpg.ContainedPacket;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Collection;
+import java.util.Iterator;
+
+/**
+ * Misc support functions. Would just use Guava / Apache Commons but
+ * avoiding extra dependencies.
+ */
+public class TestDataUtil {
+ public static byte[] readFully(InputStream input) {
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ appendToOutput(input, output);
+ return output.toByteArray();
+ }
+
+ public static void appendToOutput(InputStream input, OutputStream output) {
+ byte[] buffer = new byte[8192];
+ int bytesRead;
+ try {
+ while ((bytesRead = input.read(buffer)) != -1) {
+ output.write(buffer, 0, bytesRead);
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static byte[] readAllFully(Collection<String> inputResources) {
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+
+ for (String inputResource : inputResources) {
+ appendToOutput(getResourceAsStream(inputResource), output);
+ }
+ return output.toByteArray();
+ }
+
+ public static InputStream getResourceAsStream(String resourceName) {
+ return TestDataUtil.class.getResourceAsStream(resourceName);
+ }
+
+ /**
+ * Null-safe equivalent of {@code a.equals(b)}.
+ */
+ public static boolean equals(Object a, Object b) {
+ return (a == null) ? (b == null) : a.equals(b);
+ }
+
+ public static <T> boolean iterEquals(Iterator<T> a, Iterator<T> b, EqualityChecker<T> comparator) {
+ while (a.hasNext()) {
+ T aObject = a.next();
+ if (!b.hasNext()) {
+ return false;
+ }
+ T bObject = b.next();
+ if (!comparator.areEquals(aObject, bObject)) {
+ return false;
+ }
+ }
+
+ if (b.hasNext()) {
+ return false;
+ }
+
+ return true;
+ }
+
+
+ public static <T> boolean iterEquals(Iterator<T> a, Iterator<T> b) {
+ return iterEquals(a, b, new EqualityChecker<T>() {
+ @Override
+ public boolean areEquals(T lhs, T rhs) {
+ return TestDataUtil.equals(lhs, rhs);
+ }
+ });
+ }
+
+ public static interface EqualityChecker<T> {
+ public boolean areEquals(T lhs, T rhs);
+ }
+
+
+ public static byte[] concatAll(java.util.List<ContainedPacket> packets) {
+ byte[][] byteArrays = new byte[packets.size()][];
+ try {
+ for (int i = 0; i < packets.size(); i++) {
+ byteArrays[i] = packets.get(i).getEncoded();
+ }
+ } catch (IOException ex) {
+ throw new RuntimeException(ex);
+ }
+
+ return concatAll(byteArrays);
+ }
+
+ public static byte[] concatAll(byte[]... byteArrays) {
+ if (byteArrays.length == 1) {
+ return byteArrays[0];
+ } else if (byteArrays.length == 2) {
+ return concat(byteArrays[0], byteArrays[1]);
+ } else {
+ byte[] first = concat(byteArrays[0], byteArrays[1]);
+ byte[][] remainingArrays = new byte[byteArrays.length - 1][];
+ remainingArrays[0] = first;
+ System.arraycopy(byteArrays, 2, remainingArrays, 1, byteArrays.length - 2);
+ return concatAll(remainingArrays);
+ }
+ }
+
+ private static byte[] concat(byte[] a, byte[] b) {
+ int aLen = a.length;
+ int bLen = b.length;
+ byte[] c = new byte[aLen + bLen];
+ System.arraycopy(a, 0, c, 0, aLen);
+ System.arraycopy(b, 0, c, aLen, bLen);
+ return c;
+ }
+
+}
diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/util/Iso7816TLVTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/util/Iso7816TLVTest.java
new file mode 100644
index 000000000..f6888588e
--- /dev/null
+++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/util/Iso7816TLVTest.java
@@ -0,0 +1,103 @@
+/* Copyright (C) 2014 Vincent Breitmoser <v.breitmoser@mugenguild.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.util;
+
+import org.junit.Assert;
+import org.junit.Before;
+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.util.encoders.Hex;
+import org.sufficientlysecure.keychain.BuildConfig;
+import org.sufficientlysecure.keychain.WorkaroundBuildConfig;
+import org.sufficientlysecure.keychain.util.Iso7816TLV.Iso7816CompositeTLV;
+
+@RunWith(RobolectricGradleTestRunner.class)
+@Config(constants = WorkaroundBuildConfig.class, sdk = 21, manifest = "src/main/AndroidManifest.xml")
+public class Iso7816TLVTest {
+
+ @Before
+ public void setUp() throws Exception {
+ ShadowLog.stream = System.out;
+ }
+
+ @Test
+ public void testDecode() throws Exception {
+
+ // this is an Application Related Data packet, received from my Yubikey
+ String input = "6e81dd4f10d27600012401020000000000000100005f520f0073000080000000000000000000007300c00af00000ff04c000ff00ffc106010800001103c206010800001103c306010800001103c407007f7f7f030303c53c1efdb4845ca242ca6977fddb1f788094fd3b430af1114c28a08d8c5afda81191cc50ca9bf51bc99fe8e6ca03a9d4d40e7b5925cd154813df381655b2c63c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cd0c5423590e5423590e5423590e9000";
+ byte[] data = Hex.decode(input);
+
+ Iso7816TLV tlv = Iso7816TLV.readSingle(data, true);
+ Assert.assertNotNull("tlv parse must succeed", tlv);
+
+ Assert.assertEquals("top packet must be 'application related data' tag", 0x6e, tlv.mT);
+ Assert.assertEquals("length must be correct", 221, tlv.mL);
+
+ Assert.assertTrue("top packet must be composite", tlv instanceof Iso7816CompositeTLV);
+
+ Iso7816CompositeTLV ctlv = (Iso7816CompositeTLV) tlv;
+
+ Assert.assertEquals("top packet must have 11 sub packets", 11, ctlv.mSubs.length);
+
+ Assert.assertEquals("sub packet #1 must have expected tag", 0x4f, ctlv.mSubs[0].mT);
+ Assert.assertEquals("sub packet #1 must have expected length", 16, ctlv.mSubs[0].mL);
+
+ Assert.assertEquals("sub packet #2 must have expected tag", 0x5f52, ctlv.mSubs[1].mT);
+ Assert.assertEquals("sub packet #2 must have expected length", 15, ctlv.mSubs[1].mL);
+
+ Assert.assertEquals("sub packet #3 must have expected tag", 0x73, ctlv.mSubs[2].mT);
+ Assert.assertEquals("sub packet #3 must have expected length", 0, ctlv.mSubs[2].mL);
+ Assert.assertTrue("sub packet #3 muse be composite", ctlv.mSubs[2] instanceof Iso7816CompositeTLV);
+
+ Assert.assertEquals("sub packet #4 must have expected tag", 0xc0, ctlv.mSubs[3].mT);
+ Assert.assertEquals("sub packet #4 must have expected length", 10, ctlv.mSubs[3].mL);
+
+ Assert.assertEquals("sub packet #5 must have expected tag", 0xc1, ctlv.mSubs[4].mT);
+ Assert.assertEquals("sub packet #5 must have expected length", 6, ctlv.mSubs[4].mL);
+
+ Assert.assertEquals("sub packet #6 must have expected tag", 0xc2, ctlv.mSubs[5].mT);
+ Assert.assertEquals("sub packet #6 must have expected length", 6, ctlv.mSubs[5].mL);
+
+ Assert.assertEquals("sub packet #7 must have expected tag", 0xc3, ctlv.mSubs[6].mT);
+ Assert.assertEquals("sub packet #7 must have expected length", 6, ctlv.mSubs[6].mL);
+
+ Assert.assertEquals("sub packet #8 must have expected tag", 0xc4, ctlv.mSubs[7].mT);
+ Assert.assertEquals("sub packet #8 must have expected length", 7, ctlv.mSubs[7].mL);
+
+ Assert.assertEquals("sub packet #9 must have expected tag", 0xc5, ctlv.mSubs[8].mT);
+ Assert.assertEquals("sub packet #9 must have expected length", 60, ctlv.mSubs[8].mL);
+
+ {
+ // this is my pubkey fingerprint
+ String fingerprint = "1efdb4845ca242ca6977fddb1f788094fd3b430a";
+ byte[] V1 = new byte[20];
+ System.arraycopy(ctlv.mSubs[8].mV, 0, V1, 0, 20);
+ Assert.assertArrayEquals("fingerprint must match", V1, Hex.decode(fingerprint));
+ }
+
+ Assert.assertEquals("sub packet #10 must have expected tag", 0xc6, ctlv.mSubs[9].mT);
+ Assert.assertEquals("sub packet #10 must have expected length", 60, ctlv.mSubs[9].mL);
+
+ Assert.assertEquals("sub packet #11 must have expected tag", 0xcd, ctlv.mSubs[10].mT);
+ Assert.assertEquals("sub packet #11 must have expected length", 12, ctlv.mSubs[10].mL);
+
+ }
+
+}
diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/util/ParcelableFileCacheTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/util/ParcelableFileCacheTest.java
new file mode 100644
index 000000000..7b97ebdae
--- /dev/null
+++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/util/ParcelableFileCacheTest.java
@@ -0,0 +1,82 @@
+/*
+ * 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.util;
+
+import android.os.Bundle;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricGradleTestRunner;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowLog;
+import org.sufficientlysecure.keychain.BuildConfig;
+import org.sufficientlysecure.keychain.WorkaroundBuildConfig;
+import org.sufficientlysecure.keychain.util.ParcelableFileCache.IteratorWithSize;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+@RunWith(RobolectricGradleTestRunner.class)
+@Config(constants = WorkaroundBuildConfig.class, sdk = 21, manifest = "src/main/AndroidManifest.xml")
+public class ParcelableFileCacheTest {
+
+ @Before
+ public void setUp() throws Exception {
+ ShadowLog.stream = System.out;
+ }
+
+ @Test
+ public void testInputOutput() throws Exception {
+
+ ParcelableFileCache<Bundle> cache = new ParcelableFileCache<Bundle>(RuntimeEnvironment.application, "test.pcl");
+
+ ArrayList<Bundle> list = new ArrayList<Bundle>();
+
+ for (int i = 0; i < 50; i++) {
+ Bundle b = new Bundle();
+ b.putInt("key1", i);
+ b.putString("key2", Integer.toString(i));
+ list.add(b);
+ }
+
+ // write to cache file
+ cache.writeCache(list.size(), list.iterator());
+
+ // read back
+ IteratorWithSize<Bundle> it = cache.readCache();
+
+ Assert.assertEquals("number of entries must be correct", list.size(), it.getSize());
+
+ while (it.hasNext()) {
+ Bundle b = it.next();
+ Assert.assertEquals("input values should be equal to output values",
+ b.getInt("key1"), b.getInt("key1"));
+ Assert.assertEquals("input values should be equal to output values",
+ b.getString("key2"), b.getString("key2"));
+ }
+
+ }
+
+}
diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/util/TestingUtils.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/util/TestingUtils.java
new file mode 100644
index 000000000..1d7952464
--- /dev/null
+++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/util/TestingUtils.java
@@ -0,0 +1,38 @@
+/*
+ * 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.util;
+
+import java.util.Random;
+
+public class TestingUtils {
+ public static Passphrase genPassphrase() {
+ return genPassphrase(false);
+ }
+
+ public static Passphrase genPassphrase(boolean noEmpty) {
+ String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789!@#$%^&*()-_=";
+ Random r = new Random();
+ StringBuilder passbuilder = new StringBuilder();
+ // 20% chance for an empty passphrase
+ for(int i = 0, j = noEmpty || r.nextInt(10) > 2 ? r.nextInt(20)+1 : 0; i < j; i++) {
+ passbuilder.append(chars.charAt(r.nextInt(chars.length())));
+ }
+ System.out.println("Generated passphrase: '" + passbuilder.toString() + "'");
+ return new Passphrase(passbuilder.toString());
+ }
+}
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/COPYING b/OpenKeychain/src/test/resources/OpenPGP-Haskell/COPYING
new file mode 100644
index 000000000..55234e7a0
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/COPYING
@@ -0,0 +1,13 @@
+Copyright © 2011, Stephen Paul Weber <singpolyma.net>
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/README b/OpenKeychain/src/test/resources/OpenPGP-Haskell/README
new file mode 100644
index 000000000..cff696c83
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/README
@@ -0,0 +1,26 @@
+These test files were copied from the OpenPGP Haskell project.
+
+
+
+Original README
+===============
+
+This is an OpenPGP library inspired by my work on OpenPGP libraries in
+Ruby <https://github.com/singpolyma/openpgp>,
+PHP <http://github.com/singpolyma/openpgp-php>,
+and Python <https://github.com/singpolyma/OpenPGP-Python>.
+
+It defines types to represent OpenPGP messages as a series of packets
+and then defines instances of Data.Binary for each to facilitate
+encoding/decoding.
+
+For performing cryptography, see
+<http://hackage.haskell.org/package/openpgp-crypto-api> or
+<http://hackage.haskell.org/package/openpgp-Crypto>
+
+For dealing with ASCII armor, see
+<http://hackage.haskell.org/package/openpgp-asciiarmor>
+
+It is intended that you use qualified imports with this library.
+
+> import qualified Data.OpenPGP as OpenPGP
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000001-006.public_key b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000001-006.public_key
new file mode 100644
index 000000000..7cbab1782
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000001-006.public_key
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000002-013.user_id b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000002-013.user_id
new file mode 100644
index 000000000..759449bb4
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000002-013.user_id
@@ -0,0 +1 @@
+´$Test Key (RSA) <testkey@example.org> \ No newline at end of file
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000003-002.sig b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000003-002.sig
new file mode 100644
index 000000000..1e0656d27
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000003-002.sig
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000004-012.ring_trust b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000004-012.ring_trust
new file mode 100644
index 000000000..ffa57e57a
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000004-012.ring_trust
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000005-002.sig b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000005-002.sig
new file mode 100644
index 000000000..108b99842
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000005-002.sig
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000006-012.ring_trust b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000006-012.ring_trust
new file mode 100644
index 000000000..ffa57e57a
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000006-012.ring_trust
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000007-002.sig b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000007-002.sig
new file mode 100644
index 000000000..14276d0a5
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000007-002.sig
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000008-012.ring_trust b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000008-012.ring_trust
new file mode 100644
index 000000000..ffa57e57a
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000008-012.ring_trust
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000009-002.sig b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000009-002.sig
new file mode 100644
index 000000000..4a282dd68
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000009-002.sig
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000010-012.ring_trust b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000010-012.ring_trust
new file mode 100644
index 000000000..ffa57e57a
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000010-012.ring_trust
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000011-002.sig b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000011-002.sig
new file mode 100644
index 000000000..cae1b7391
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000011-002.sig
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000012-012.ring_trust b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000012-012.ring_trust
new file mode 100644
index 000000000..ffa57e57a
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000012-012.ring_trust
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000013-014.public_subkey b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000013-014.public_subkey
new file mode 100644
index 000000000..08676d067
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000013-014.public_subkey
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000014-002.sig b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000014-002.sig
new file mode 100644
index 000000000..dd601807f
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000014-002.sig
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000015-012.ring_trust b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000015-012.ring_trust
new file mode 100644
index 000000000..ffa57e57a
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000015-012.ring_trust
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000016-006.public_key b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000016-006.public_key
new file mode 100644
index 000000000..c9dccbf1e
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000016-006.public_key
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000017-002.sig b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000017-002.sig
new file mode 100644
index 000000000..e734505a7
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000017-002.sig
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000018-012.ring_trust b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000018-012.ring_trust
new file mode 100644
index 000000000..ffa57e57a
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000018-012.ring_trust
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000019-013.user_id b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000019-013.user_id
new file mode 100644
index 000000000..ab3f51d91
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000019-013.user_id
@@ -0,0 +1 @@
+´$Test Key (DSA) <testkey@example.com> \ No newline at end of file
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000020-002.sig b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000020-002.sig
new file mode 100644
index 000000000..8588489a7
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000020-002.sig
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000021-012.ring_trust b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000021-012.ring_trust
new file mode 100644
index 000000000..ffa57e57a
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000021-012.ring_trust
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000022-002.sig b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000022-002.sig
new file mode 100644
index 000000000..fefcb5fea
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000022-002.sig
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000023-012.ring_trust b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000023-012.ring_trust
new file mode 100644
index 000000000..ffa57e57a
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000023-012.ring_trust
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000024-014.public_subkey b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000024-014.public_subkey
new file mode 100644
index 000000000..2e8deea28
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000024-014.public_subkey
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000025-002.sig b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000025-002.sig
new file mode 100644
index 000000000..a3eea0a20
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000025-002.sig
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000026-012.ring_trust b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000026-012.ring_trust
new file mode 100644
index 000000000..ffa57e57a
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000026-012.ring_trust
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000027-006.public_key b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000027-006.public_key
new file mode 100644
index 000000000..5817e0037
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000027-006.public_key
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000028-002.sig b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000028-002.sig
new file mode 100644
index 000000000..5194b7840
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000028-002.sig
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000029-012.ring_trust b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000029-012.ring_trust
new file mode 100644
index 000000000..ffa57e57a
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000029-012.ring_trust
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000030-013.user_id b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000030-013.user_id
new file mode 100644
index 000000000..fb3f49e0d
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000030-013.user_id
@@ -0,0 +1 @@
+´+Test Key (DSA sign-only) <test@example.net> \ No newline at end of file
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000031-002.sig b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000031-002.sig
new file mode 100644
index 000000000..f69f6875b
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000031-002.sig
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000032-012.ring_trust b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000032-012.ring_trust
new file mode 100644
index 000000000..ffa57e57a
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000032-012.ring_trust
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000033-002.sig b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000033-002.sig
new file mode 100644
index 000000000..2bb55d4fe
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000033-002.sig
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000034-012.ring_trust b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000034-012.ring_trust
new file mode 100644
index 000000000..ffa57e57a
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000034-012.ring_trust
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000035-006.public_key b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000035-006.public_key
new file mode 100644
index 000000000..5980638c4
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000035-006.public_key
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000036-013.user_id b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000036-013.user_id
new file mode 100644
index 000000000..5d0d46e5d
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000036-013.user_id
@@ -0,0 +1 @@
+´.Test Key (RSA sign-only) <testkey@example.net> \ No newline at end of file
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000037-002.sig b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000037-002.sig
new file mode 100644
index 000000000..833b563b2
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000037-002.sig
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000038-012.ring_trust b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000038-012.ring_trust
new file mode 100644
index 000000000..ffa57e57a
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000038-012.ring_trust
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000039-002.sig b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000039-002.sig
new file mode 100644
index 000000000..89c34fa5d
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000039-002.sig
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000040-012.ring_trust b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000040-012.ring_trust
new file mode 100644
index 000000000..ffa57e57a
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000040-012.ring_trust
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000041-017.attribute b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000041-017.attribute
new file mode 100644
index 000000000..a21a82fb1
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000041-017.attribute
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000042-002.sig b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000042-002.sig
new file mode 100644
index 000000000..fc6267fd0
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000042-002.sig
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000043-012.ring_trust b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000043-012.ring_trust
new file mode 100644
index 000000000..ffa57e57a
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000043-012.ring_trust
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000044-014.public_subkey b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000044-014.public_subkey
new file mode 100644
index 000000000..06bf50e4f
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000044-014.public_subkey
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000045-002.sig b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000045-002.sig
new file mode 100644
index 000000000..336eb0f24
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000045-002.sig
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000046-012.ring_trust b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000046-012.ring_trust
new file mode 100644
index 000000000..ffa57e57a
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000046-012.ring_trust
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000047-005.secret_key b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000047-005.secret_key
new file mode 100644
index 000000000..77b5d428a
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000047-005.secret_key
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000048-013.user_id b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000048-013.user_id
new file mode 100644
index 000000000..759449bb4
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000048-013.user_id
@@ -0,0 +1 @@
+´$Test Key (RSA) <testkey@example.org> \ No newline at end of file
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000049-002.sig b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000049-002.sig
new file mode 100644
index 000000000..14276d0a5
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000049-002.sig
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000050-012.ring_trust b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000050-012.ring_trust
new file mode 100644
index 000000000..b1eeabb95
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000050-012.ring_trust
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000051-007.secret_subkey b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000051-007.secret_subkey
new file mode 100644
index 000000000..b4e65c92f
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000051-007.secret_subkey
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000052-002.sig b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000052-002.sig
new file mode 100644
index 000000000..dd601807f
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000052-002.sig
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000053-012.ring_trust b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000053-012.ring_trust
new file mode 100644
index 000000000..b1eeabb95
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000053-012.ring_trust
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000054-005.secret_key b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000054-005.secret_key
new file mode 100644
index 000000000..f153e5932
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000054-005.secret_key
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000055-002.sig b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000055-002.sig
new file mode 100644
index 000000000..e734505a7
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000055-002.sig
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000056-012.ring_trust b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000056-012.ring_trust
new file mode 100644
index 000000000..b1eeabb95
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000056-012.ring_trust
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000057-013.user_id b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000057-013.user_id
new file mode 100644
index 000000000..ab3f51d91
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000057-013.user_id
@@ -0,0 +1 @@
+´$Test Key (DSA) <testkey@example.com> \ No newline at end of file
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000058-002.sig b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000058-002.sig
new file mode 100644
index 000000000..8588489a7
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000058-002.sig
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000059-012.ring_trust b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000059-012.ring_trust
new file mode 100644
index 000000000..b1eeabb95
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000059-012.ring_trust
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000060-007.secret_subkey b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000060-007.secret_subkey
new file mode 100644
index 000000000..9df45f395
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000060-007.secret_subkey
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000061-002.sig b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000061-002.sig
new file mode 100644
index 000000000..639494223
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000061-002.sig
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000062-012.ring_trust b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000062-012.ring_trust
new file mode 100644
index 000000000..b1eeabb95
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000062-012.ring_trust
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000063-005.secret_key b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000063-005.secret_key
new file mode 100644
index 000000000..2f4268ee1
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000063-005.secret_key
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000064-002.sig b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000064-002.sig
new file mode 100644
index 000000000..5194b7840
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000064-002.sig
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000065-012.ring_trust b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000065-012.ring_trust
new file mode 100644
index 000000000..b1eeabb95
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000065-012.ring_trust
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000066-013.user_id b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000066-013.user_id
new file mode 100644
index 000000000..fb3f49e0d
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000066-013.user_id
@@ -0,0 +1 @@
+´+Test Key (DSA sign-only) <test@example.net> \ No newline at end of file
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000067-002.sig b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000067-002.sig
new file mode 100644
index 000000000..d354e79df
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000067-002.sig
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000068-012.ring_trust b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000068-012.ring_trust
new file mode 100644
index 000000000..b1eeabb95
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000068-012.ring_trust
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000069-005.secret_key b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000069-005.secret_key
new file mode 100644
index 000000000..17a2c354d
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000069-005.secret_key
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000070-013.user_id b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000070-013.user_id
new file mode 100644
index 000000000..5d0d46e5d
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000070-013.user_id
@@ -0,0 +1 @@
+´.Test Key (RSA sign-only) <testkey@example.net> \ No newline at end of file
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000071-002.sig b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000071-002.sig
new file mode 100644
index 000000000..833b563b2
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000071-002.sig
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000072-012.ring_trust b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000072-012.ring_trust
new file mode 100644
index 000000000..b1eeabb95
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000072-012.ring_trust
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000073-017.attribute b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000073-017.attribute
new file mode 100644
index 000000000..a21a82fb1
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000073-017.attribute
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000074-002.sig b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000074-002.sig
new file mode 100644
index 000000000..fc6267fd0
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000074-002.sig
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000075-012.ring_trust b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000075-012.ring_trust
new file mode 100644
index 000000000..b1eeabb95
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000075-012.ring_trust
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000076-007.secret_subkey b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000076-007.secret_subkey
new file mode 100644
index 000000000..b380339a4
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000076-007.secret_subkey
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000077-002.sig b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000077-002.sig
new file mode 100644
index 000000000..336eb0f24
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000077-002.sig
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000078-012.ring_trust b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000078-012.ring_trust
new file mode 100644
index 000000000..b1eeabb95
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/000078-012.ring_trust
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/002182-002.sig b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/002182-002.sig
new file mode 100644
index 000000000..2bc6679f4
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/002182-002.sig
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/3F5BBA0B0694BEB6000005-002.sig b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/3F5BBA0B0694BEB6000005-002.sig
new file mode 100644
index 000000000..94055af66
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/3F5BBA0B0694BEB6000005-002.sig
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/3F5BBA0B0694BEB6000017-002.sig b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/3F5BBA0B0694BEB6000017-002.sig
new file mode 100644
index 000000000..b22f23b91
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/3F5BBA0B0694BEB6000017-002.sig
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/compressedsig-bzip2.gpg b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/compressedsig-bzip2.gpg
new file mode 100644
index 000000000..87539dbe8
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/compressedsig-bzip2.gpg
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/compressedsig-zlib.gpg b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/compressedsig-zlib.gpg
new file mode 100644
index 000000000..4da4dfa99
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/compressedsig-zlib.gpg
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/compressedsig.gpg b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/compressedsig.gpg
new file mode 100644
index 000000000..dd617de13
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/compressedsig.gpg
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/onepass_sig b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/onepass_sig
new file mode 100644
index 000000000..87b2895ea
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/onepass_sig
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/pubring.gpg b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/pubring.gpg
new file mode 100644
index 000000000..a1519ee74
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/pubring.gpg
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/secring.gpg b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/secring.gpg
new file mode 100644
index 000000000..13598756a
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/secring.gpg
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/symmetrically_encrypted b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/symmetrically_encrypted
new file mode 100644
index 000000000..129155aa2
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/symmetrically_encrypted
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/uncompressed-ops-dsa-sha384.txt.gpg b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/uncompressed-ops-dsa-sha384.txt.gpg
new file mode 100644
index 000000000..39828fcae
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/uncompressed-ops-dsa-sha384.txt.gpg
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/uncompressed-ops-dsa.gpg b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/uncompressed-ops-dsa.gpg
new file mode 100644
index 000000000..97e7a267b
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/uncompressed-ops-dsa.gpg
Binary files differ
diff --git a/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/uncompressed-ops-rsa.gpg b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/uncompressed-ops-rsa.gpg
new file mode 100644
index 000000000..7ae453da6
--- /dev/null
+++ b/OpenKeychain/src/test/resources/OpenPGP-Haskell/tests/data/uncompressed-ops-rsa.gpg
Binary files differ
diff --git a/OpenKeychain/src/test/resources/public-key-canonicalize.blob b/OpenKeychain/src/test/resources/public-key-canonicalize.blob
new file mode 100644
index 000000000..3450824c1
--- /dev/null
+++ b/OpenKeychain/src/test/resources/public-key-canonicalize.blob
Binary files differ
diff --git a/OpenKeychain/src/test/resources/public-key-for-sample.blob b/OpenKeychain/src/test/resources/public-key-for-sample.blob
new file mode 100644
index 000000000..4aa91510b
--- /dev/null
+++ b/OpenKeychain/src/test/resources/public-key-for-sample.blob
Binary files differ
diff --git a/OpenKeychain/src/test/resources/sample-altered.txt b/OpenKeychain/src/test/resources/sample-altered.txt
new file mode 100644
index 000000000..458821f81
--- /dev/null
+++ b/OpenKeychain/src/test/resources/sample-altered.txt
@@ -0,0 +1,26 @@
+-----BEGIN PGP SIGNED MESSAGE-----
+Hash: SHA1
+
+This is a simple text document, which is used to illustrate
+the concept of signing simple text files. There are no
+control characters or special formatting commands in this
+text, just simple printable ASCII characters.
+MALICIOUS TEXT
+To make this a slightly less uninteresting document, there
+follows a short shopping list.
+
+ eggs, 1 doz
+ milk, 1 gal
+ bacon, 1 lb
+ olive oil
+ bread, 1 loaf
+
+That's all there is to this document.
+
+-----BEGIN PGP SIGNATURE-----
+Version: PGPfreeware 5.5.5 for non-commercial use <http://www.nai.com>
+
+iQA/AwUBN78ib3S9RCOKzj55EQKqDACg1NV2/iyPKrDlOVJvJwz6ArcQ0UQAnjNC
+CDxKAFyaaGa835l1vpbFkAJk
+=3r/N
+-----END PGP SIGNATURE-----
diff --git a/OpenKeychain/src/test/resources/sample.txt b/OpenKeychain/src/test/resources/sample.txt
new file mode 100644
index 000000000..c0065f78d
--- /dev/null
+++ b/OpenKeychain/src/test/resources/sample.txt
@@ -0,0 +1,26 @@
+-----BEGIN PGP SIGNED MESSAGE-----
+Hash: SHA1
+
+This is a simple text document, which is used to illustrate
+the concept of signing simple text files. There are no
+control characters or special formatting commands in this
+text, just simple printable ASCII characters.
+
+To make this a slightly less uninteresting document, there
+follows a short shopping list.
+
+ eggs, 1 doz
+ milk, 1 gal
+ bacon, 1 lb
+ olive oil
+ bread, 1 loaf
+
+That's all there is to this document.
+
+-----BEGIN PGP SIGNATURE-----
+Version: PGPfreeware 5.5.5 for non-commercial use <http://www.nai.com>
+
+iQA/AwUBN78ib3S9RCOKzj55EQKqDACg1NV2/iyPKrDlOVJvJwz6ArcQ0UQAnjNC
+CDxKAFyaaGa835l1vpbFkAJk
+=3r/N
+-----END PGP SIGNATURE-----
diff --git a/OpenKeychain/src/test/resources/test-keys/bad_user_id_encoding.asc b/OpenKeychain/src/test/resources/test-keys/bad_user_id_encoding.asc
new file mode 100644
index 000000000..332747f84
--- /dev/null
+++ b/OpenKeychain/src/test/resources/test-keys/bad_user_id_encoding.asc
@@ -0,0 +1,60 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: SKS 1.1.5
+Comment: Hostname: keyserver.codinginfinity.com
+
+mQGiBDhOth4RBADOAh3T2bWx+os6LBE+C55EBykg2WdC5AOnRe457ro5X1YuhZomY31hcxvH
+m+hro4Lh43SP2xTcyAz4G2UcLJch9qdgJ7W32wb+kOyEi/WTkfLpU2OiXRS975euqex4kARK
+l2alJfmu8FI4p1WinbGDDPyb6DyiqhW3KP/i3ukQwQCg/zOQEvnI0RgHwFSi1V+dvvCR5ZUD
+/0WCch+cD8Aje0EUtztBx2DL/JKXRA0B/9jivwd/QgQy2hIYYXL5+OFrRBWcpPNFKqhZLyXn
+82OEaZZpJGHEpfGeb8uHZ3fv8NTaPxNGwYx99AwiOVatNX88gCnlQ2ZBWQuuN1rrK/oKduPK
+35yZbNSR3nT8fJsWg/+h1L2eB00qA/0cgovkdYnjVzq3ztt43jXQEzrZzYMRJCK9q1CYSeZ4
+g/zjFwCROgQoVnVcLYfJDCJWM/3zJvlqfgGikhKcT4UepGadLD0ACJ9gchsc0Zrx+gUlgr/K
+aWGdRh+j4i5ykc0HNO/TFo5ESBBrGx3WtyxI1ob+2MAH8RAa18HZq2M1irQgQ2xhdXMgRnJh
+ZW5rZWwgPGFsaWVuQG9ubGluZS5kZT6IRgQQEQIABgUCOFQy1gAKCRA81crJrp6K4xFhAJ93
+JV8PJRdgcHrLrlgKrG9zSpjqRQCgvbImT0pBuZIj058FoklOw7OiBjaISwQQEQIACwUCOE62
+7AQLAwIBAAoJELXPGfg2YSW2elEAn1hrbuR8/I3/OBxReVor0wzQ3tYTAJ44UFUL3VmHweHB
+t+kfPDVl93bMY7QiQ2xhdXMgRnJhZW5rZWwgPGNmcmFlbmtlbEBnbXgubmV0PohGBBARAgAG
+BQI4VDKRAAoJEDzVysmunorjI3AAn2nfyP1nsRpSMzPs8Vm+KUyvTY68AJ9q3evrzGG1Qbtl
+VZgtcbprQKnaX4hGBBARAgAGBQI6uRZFAAoJEDxoxauoLKe5e6sAn3ZDhTsVaNOgFrpzNZQ6
+meeoJTjgAKDdSqyUit+pSzK//pqEdKJUJ2N93ohGBBARAgAGBQI8Niv5AAoJEIPL/xBv5hfH
+GgEAoKMcltZIsfaagIV86tlQsETsleJeAJ9Puwxt/n455EQWHkAQz109noNIVIhLBBARAgAL
+BQI4TrYeBAsDAgEACgkQtc8Z+DZhJbbveQCfUOPT34HElA7KEPTchx4/nBINqKAAoMIk8G+r
+UFzHJo3w7mjeS2OEHCoutCJDbGF1cyBGcmFlbmtlbCA8Y2ZyYWVua2VsQHVzYS5uZXQ+iEYE
+EBECAAYFAjhUMp4ACgkQPNXKya6eiuMuqgCg1WcZAJYFF3/jUA85GJ4BVLdQkpsAoK37bsBz
+tvhUQE/dObJduMsK9462iEsEEBECAAsFAjhOtj4ECwMCAQAKCRC1zxn4NmElts+/AKD9cnve
+ShBQe95eF98yomHFJ8CgHQCfTNyhyJgLhTfYd2wI8jrTePr2zhi0JENsYXVzIEZyYWVua2Vs
+IDxjY19mYXN0amFja0BnbXgubmV0PohGBBARAgAGBQI4VDKsAAoJEDzVysmunorj2mEAoLf9
+yqSFRla7rfVSD2cTSozHrGt4AJ9L4VDELJjDoaTC0ZTxwuVaSPxSxIhLBBARAgALBQI4TrZZ
+BAsDAgEACgkQtc8Z+DZhJbZC2gCgvjEKlK22N+RSKbPEN4XyjrNLh3QAoPVHXqRght5VG3+k
+XzA/zDYWQ+/2tDBDbGF1cyBGcmFlbmtlbCA8ZmFzdGphY2tAaGFsaWZheC5yd3RoLWFhY2hl
+bi5kZT6IRgQQEQIABgUCOFQyyQAKCRA81crJrp6K41yrAKDrCcfzexdQCDFtfbxuwyeUibST
+zgCbBGPP4qeXfAf0l07kY5Of1/PyniiISwQQEQIACwUCOE62uwQLAwIBAAoJELXPGfg2YSW2
+nhoAnRu89DXOd1zOUagpwcqLmj542JwOAKDhN736B2Zd9xtxaw2mT46Qu6YTULQ1Q2xhdXMg
+RnJhZW5rZWwgPENsYXVzLkZy5G5rZWxAaGFsaWZheC5yd3RoLWFhY2hlbi5kZT6IRgQQEQIA
+BgUCOFQy6QAKCRA81crJrp6K47VGAKDvlogjYHaN6t650M6CteLCXh5SqQCghfGa7tk327WE
+St4WER8cXbADd62ISwQQEQIACwUCOE63LwQLAwIBAAoJELXPGfg2YSW2FuUAmQGG/lfpeNad
+fgB61x37ugaFgE9oAJ0QgfdXhalOPRjHnBksxYzzrWFCVLkEDQQ4TrYeEBAA+RigfloGYXpD
+kJXcBWyHhuxh7M1FHw7Y4KN5xsncegus5D/jRpS2MEpT13wCFkiAtRXlKZmpnwd00//jocWW
+IE6YZbjYDe4QXau2FxxR2FDKIldDKb6V6FYrOHhcC9v4TE3V46pGzPvOF+gqnRRh44SpT9GD
+hKh5tu+Pp0NGCMbMHXdXJDhK4sTw6I4TZ5dOkhNh9tvrJQ4X/faY98h8ebByHTh1+/bBc8SD
+ESYrQ2DD4+jWCv2hKCYLrqmus2UPogBTAaB81qujEh76DyrOH3SET8rzF/OkQOnX0ne2Qi0C
+NsEmy2henXyYCQqNfi3t5F159dSST5sYjvwqp0t8MvZCV7cIfwgXcqK61qlC8wXo+VMROU+2
+8W65Szgg2gGnVqMU6Y9AVfPQB8bLQ6mUrfdMZIZJ+AyDvWXpF9Sh01D49Vlf3HZSTz09jdvO
+meFXklnN/biudE/F/Ha8g8VHMGHOfMlm/xX5u/2RXscBqtNbno2gpXI61Brwv0YAWCvl9Ij9
+WE5J280gtJ3kkQc2azNsOA1FHQ98iLMcfFstjvbzySPAQ/ClWxiNjrtVjLhdONM0/XwXV0Oj
+HRhs3jMhLLUq/zzhsSlAGBGNfISnCnLWhsQDGcgHKXrKlQzZlp+r0ApQmwJG0wg9ZqRdQZ+c
+fL2JSyIZJrqrol7DVes91hcAAgIQAIbS27fz+cIoDbWDQ4UhfHFGqPK51uixRfw2fIWmXsV0
+yFChqjaPRieERsP4Y26IGL7jB+yr+Q8fNp+YcevH2YWluMcXAZMXkEMi8Lvz9lDUVZxrgILm
+ZHyBycHXXhn8mFZJufUnn33cr8JC/6D+QBHdF1lly40s1877Om10W14bHTo1SBNN7PoAO/fC
+w+GL0QEIK3iTqEI+bEs+vxLqBKZa1+NkKg/s5L6OXMgBTFuXqZvzWAHLK7wR1QT3DgKgY4go
+YZeJkSzGmmQkZRCVBLZ0MQx1x6CiqMI9VN/5/7h5jA7QuhXhHZHuNmucITU05wHQty2yOH+D
+nWsPCcWRjaXVe0pFqD6pJGBZB58B1N/7uGdn+PkR3TSDOt9IdXqWoY/dvLNc38uoL31FXko8
+ITwzua5LzwGhjwYoNiM31Cehx/zfzzLsQlzIvVjgdNX4rZcMTfi3cJ/RkVue2bOlrRSvCCzk
+58lW+3PsHJ/Rk4yGngOimEK1okcjgSrVQamuK1kG0aKKv//ql1Ehd0WibKZTlq/n644Yb3Yd
+HrVXK2JhdJ1opTmYHbGDS9rE6HUc+cq9DsL4ZZDHwPnsi5FXtjCs3q3k5d2y9P1szFgVXm54
+vNbolRY2J3/shMf0382KfyVNWLjqlC1DEtxNP+Xfk9XJsBe+PD3ClLfbPxG989BJiEYEGBEC
+AAYFAjhOth4ACgkQtc8Z+DZhJbZvegCeJEWNkQYaPDTAx19s+GBBZJo1K6AAmwc006dQTrDf
+ykezF64bvFWd+vuO
+=5FoN
+-----END PGP PUBLIC KEY BLOCK-----
+
diff --git a/OpenKeychain/src/test/resources/test-keys/broken_cert_version.asc b/OpenKeychain/src/test/resources/test-keys/broken_cert_version.asc
new file mode 100644
index 000000000..e2d2abd8e
--- /dev/null
+++ b/OpenKeychain/src/test/resources/test-keys/broken_cert_version.asc
@@ -0,0 +1,17 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQENBFSl5DIBCADqsGJJ8RhV4Uw6a3Q48QyTMrUtvZquOTlLVaqOdEFZNL5/OBal
+prft9LNkcOkIVA89Hdn715WwkmG2OJiJoQ/ZAKwal1CPGm4Q8kZIM7k57ISJL6J5
+300e7UIznc74XbG7eFNxNcjCM9wG12vW2rFwc+ogJtkBSf0IXukPwtUkRK+H5ufO
+lpqS5NNZfiGbNQCrb+YsGZNRk4QTGR6WGyaIRHlcG8G00VPGNSauTqe/11MO9MoF
+BvPgFeur3nefWunCQ+uDmzIEs8r94gaHu3LWbctd5w5x/o/PDfTSSiO+U8zzXrKC
+4ZpEl5bk7t7jH1hYMLWyO6nn0vWTOMO1EYLBABEBAAG0GGJyb2tlbiBzaWduYXR1
+cmUgdmVyc2lvbokBOMATAQIAIgUCVKXkMgIbAwYLCQgHAwIGFQgCCQoLBBYCAwEC
+HgECF4AACgkQDe00lH/2SnprLggAh64TsdHDfIhTNc1DeJLCuvuHsitAcUdEEnue
+yJjodxboKNSplIwnmb5CpM3P8f736dNaW77Yd6aO4IeAy6cBlxT1tSRkJMsp+cBt
+kBa3lRr+GnWZlLZs3coL2g0t5RbuyYKyQxm2qvgFJGi/7Qfty5nJOW5U1ElT3VT8
+jISNdQdDAIaBsCE+TuyW3VsP3PqnJ7x14K7VhkFuCyvYB9paLcJBnan93R0Ja0Ip
+Cv1pbrNxXp0UELf0RYc2X5C1m6otZ9LKf3PmzxlEkApkb1TZUEBak2Za5p99koZT
++pg/XpZPyawi+gZeYkBAohxRGmzG/a4L+YacAZHbchfN0eG7lg==
+=mxTR
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/OpenKeychain/src/test/resources/test-keys/cooperpair/9E669861368BCA0BE42DAF7DDDA252EBB8EBE1AF.asc b/OpenKeychain/src/test/resources/test-keys/cooperpair/9E669861368BCA0BE42DAF7DDDA252EBB8EBE1AF.asc
new file mode 100644
index 000000000..4f51252da
--- /dev/null
+++ b/OpenKeychain/src/test/resources/test-keys/cooperpair/9E669861368BCA0BE42DAF7DDDA252EBB8EBE1AF.asc
@@ -0,0 +1,29 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: GnuPG v1
+
+mQINBFJtd/UBEACpw/psXoGNM8RHczviD7FnGdjMQPEJQ+nuWQ2AEGYouulg5hFv
+0ChuSQVLiqQht2k5K2liyW1MeXoJ8tr9nSn/Zi9nttc0Wo6K7pvrDD40r2HNg305
+qLCzItr5st3x8cq2cIXvN4LOm2rqpBLZ/sqMmNiW2Y7/aAQqV1xtR35joHqamWHD
+UPOmzBMs07YSUjXgC1EMx8kWQSV6cuARj93kxWj8R6eoYHHfrWCEGR313wov6QST
+zIfVU7FqQqOmdLW3LaPHxcrI/TjsnkUN99qdlpjJH/YW925LDPJHAkliqPP5AvhU
+F9KbY2F8mcIZBCDd8TH+xXynuN3BbIU4kCwVbdx/tcpO1npuJcKB1Go/udyow/Ei
+Z3nHzJsCVkezvopek77wnwPaP0nAb7f4iIY3gJCoGirOx6N075TgF6MBe00q9oFE
+y4rvnUnU9/QzOOes95eUMhM+9eK1cuLFEV5t47DfxRdq+fQip3FJ2l6v19sZvQ0G
+j06pjYqg0of273rG8oXcDrFjb1Zqhj8x1mLl6u7d/ide5wTm9HylBWcYKQjIJJAi
+WIScxEPIOINDJKgsKTuKtoyNvISJ3xUeS1yzxiIb3YGLIyPgFFx0vFyqJfbkXq70
+m1n2xnJlkTidfzbZvc6EA7vRGSDYK6FqqhlGhc7UypUEVW8FM/jZNAOS6QARAUGt
+tCg5RTY2OTg2MTM2OEJDQTBCRTQyREFGN0REREEyNTJFQkI4RUJFMUFGiQI3BBMB
+CgAhBQJSg/uTAhsDBQsJCAcDBRUKCQgLBRYCAwEAAh4BAheAAAoJEN2iUuu46+Gv
++Z0P+wQhkLwm+WGcEsS98Lei9O7hit/k4g/VkLUUQV7BOR3n8uRZIFkdOtpvrFU3
+aKf246uCy6GM48Oh+1U2cv5InX/WEuKaFo5uF6t79wyt18BUn1weDcU+DQdOSG4f
+fSnNa55wkN0l0svW4fGIthjmDTz6HZFntYD+9A20wZAqpPIs+vyG9Jp+e9E9Y/W/
+EFQbNlxHHb9+BMT2+DtNP+HSl3MPFlQPKOLZxyLAU5uzT0Sa0LxhrQy5FgkW6Jog
+sbAJVM9z0pZw+grzGPciM66ZW1rxeICvbYsdWLytRjqxpY8GS8XudyseUGd+dZim
+ptarsrE5yfSMg2gW5Z1PTc0tEMXJLUwtpyzQjpFpbb7dPuo2TUp09LgZKX63WCbS
+Nb1RTaGfkeYudOTo2rh4Jfg+Tb/JRpO6clo0rxAq8nPH2WmG+9TB8Zbb7YRzGWuV
+/e5SeVNR+zY8tXZKnmUIH1HIprc+BtT6Bupdvd0CT14Mg9MmsFvUXofwHLa4gahr
+8/iG9y3uHSA6Rhz++yOpyOmNvO1LDxsYNaRCIXQJbqgNwF5YNYlMPsEeY/CG7FOb
+Afv7rHiYtRRQfz2P4OF900DJO7QL9gdNXJ1+Hajy/5Lvvl7qwqMG4GvVQEsgFc5O
+jjFCUhE2i20j2kEMxvA5RLBH/fOoGARn87tiKSfb+pqLNZQb
+=fDJ8
+-----END PGP PUBLIC KEY BLOCK----- \ No newline at end of file
diff --git a/OpenKeychain/src/test/resources/test-keys/cooperpair/A55120427374F3F7AA5F1166DDA252EBB8EBE1AF.asc b/OpenKeychain/src/test/resources/test-keys/cooperpair/A55120427374F3F7AA5F1166DDA252EBB8EBE1AF.asc
new file mode 100644
index 000000000..549bc51a2
--- /dev/null
+++ b/OpenKeychain/src/test/resources/test-keys/cooperpair/A55120427374F3F7AA5F1166DDA252EBB8EBE1AF.asc
@@ -0,0 +1,29 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: GnuPG v1
+
+mQINBFKD+38BEADSv5l4xOx9hCRJVcybq6yK5hTpGSFf3xo1bkhoMvyC62ehb4jD
+MDLwwNRyzCBEWQJLbq/LLizPFN2qXFJpXJcsuqsHNYRtDqDBEjtriRQwSqHnqTXt
+c0K46FYHldCJQ4/tBXxPI+WwtXjcNRWaV7n2BvR/Jk+B5e4Zz3LPnN0C4w5vORHs
+hN1jil8A3Hs/F+OmlQYrU8ZtNwTpSo2EXxe2fVgSDCsKRyNsPZj++OyujPzW+yaN
+lJ9I/q6s9gvX9o9o7nwZbqBETipWsdRK6RfBdTKpnyLNordbWwWTk6GxN8T5Ppit
+P6a3UlQ71VuflcswCTmEQ1pEfZrlRFKa9psBOW+cZLNxT9h0jGFMh6/B3w48Sag+
+cFcPBFWParC+cAXBIURDxT9G6bzNLogg7YKoaPsyiXnLDH2VJUCXs27D2wPJL24Q
+S7npvsg63MPPssWgG5cauLznmNR4y5pQi6oH/C10v0zrUJy6FPJzQhYRhWOvhtz6
+j88RGMrFNNCdB2VACtn699D+ixu3nRlXHIKCT+xLSfgslVYifmJOCNljBLGHOQ1e
+FJxQuNVpmmxjvk/8kqK+pHLB9Qn6M1ZYzip7OyUL3OAWabCabgEw2bQmUhiBWD3u
+buv0WAVOJEAFvBCAeYNQzrQMY+Rc3RnvynG4pI6Tbo8wC6/IJcDOw516JwARASB3
+tChBNTUxMjA0MjczNzRGM0Y3QUE1RjExNjZEREEyNTJFQkI4RUJFMUFGiQI3BBMB
+CgAhBQJSg/uTAhsDBQsJCAcDBRUKCQgLBRYCAwEAAh4BAheAAAoJEN2iUuu46+Gv
+9L0P/3tFu0LOZ/dAPjUNfKJCZqcIuVnD5xShMTsUbVx+QoXMy7rt4iRLD7ofGi/I
+vTAZehxk3sk/Slx5nbews+3NItyw6mcaP9HlmwKNr6k7BC2kJHcCxH4DNzhmIx1H
+3T/CggtHX42JBYKlGf22y+M8jAbvsPOUfTznx96mYNrOY6s1dJyn0kRleqJ8+tGj
+/5+0y90iZnGCa0FtacQkKUPkXwVodeZVxk8z5OEipShYKc+8dl+5WsvOzHqLC/KY
+xCGRb4JaqEMwouLNg8dTNAXXUvFGqJNDX4+andggogmI1hdD9xExfSU9cAGegg2t
+vvveC4S+CCHd+zt88iK5ze6F61RxwYhhNbkuFGjdgNGCpHtG/BQhKnYJuKEbq3oi
+mgNyxJERlfgaWXveiMG0AmACXN+jCkTtqZjQnsg2N2QDL3tjY7usmuiwRL1aVOFG
+Kw5/Cc+2nDeANS3Xi1403Ni269b1c6kNSoLe4zd0WsbO3Kouds8F8EQfeheXQe97
+ZxuvBOMsR9wHC3f0sl/vfxCGdUC+khmKk5taKnUeUFJmVmh5ghlVy8FySHGB0QHO
+zd8GUl59rFpQJNpNFQW2YKDhrcjxIr2AeJrdoDI6NsQ02+Qtep/bbq53hqtAD4jF
+t3S8vBbTXtRk6g2qn4ojF4SOIc8SAiZcURgVFuSJX8ngFbO4
+=OEw/
+-----END PGP PUBLIC KEY BLOCK----- \ No newline at end of file
diff --git a/OpenKeychain/src/test/resources/test-keys/cooperpair/readme b/OpenKeychain/src/test/resources/test-keys/cooperpair/readme
new file mode 100644
index 000000000..fecb372d9
--- /dev/null
+++ b/OpenKeychain/src/test/resources/test-keys/cooperpair/readme
@@ -0,0 +1,3 @@
+"Cooperpair" testcase under public domain license, by @coruus:
+
+https://github.com/coruus/cooperpair/tree/master/pgpv4
diff --git a/OpenKeychain/src/test/resources/test-keys/divert_to_card_sec.asc b/OpenKeychain/src/test/resources/test-keys/divert_to_card_sec.asc
new file mode 100644
index 000000000..3c3bbebe4
--- /dev/null
+++ b/OpenKeychain/src/test/resources/test-keys/divert_to_card_sec.asc
@@ -0,0 +1,44 @@
+-----BEGIN PGP PRIVATE KEY BLOCK-----
+Version: GnuPG v1.4.12 (GNU/Linux)
+
+lQEmBFPHw2oBCAC3GlG03VJ2axLekrafwFuTZOysS30rTpUF5yrJC9XHgV/W7/Hk
+QAWKMOwPcOVRiuBwUgHKcn9Oy/pBBzu0kSF/ttMUe9wOFwEkants2Yap87hRa5po
+g7lCgtvmKBE6qEPr0EcTKzUif+UcPBXpRs74BtJyaMLQgA3C+wPDZ1MTWf9rkRTp
+lif7xnAClP89yYvhV9KwDYostVxzc7eCe0hrYXXZjN68z30l1aPxbJbzliTrXFyX
+w6m4KZsd07dY1rRlamJIMI0HUgCYTPDoJAv8ezpYern1bebb2TEAqsEn6QPJHSHz
+dbXvPk4PEtor7c8J3H47aaBInVsAulUWDCpNABEBAAH/AGUAR05VAhDSdgABJAEC
+AAAAAAAAAQAAtCVIZXJiZXJ0IE11ZWxsZXIgPGhlcmJlcnRAZXhhbXBsZS5jb20+
+iQE4BBMBAgAiBQJTx8NqAhsDBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRBM
+tukD/9+35ck9CAC08bV32LYEwxsWS4FKeGDoExdN4IWi/GXS+WjYm5DxBYIeHC5i
+2K/dsdOSUs6x8+90+U1lBv9IhDn5tj9OJGE4w49blGTF6dKhkKK9vwxzZhgC36cU
+glbUljfRhu0DRX3yZmd1OIEG24UvcnR077nnMQi/086zNvtCDIdyNz5thWOeOaxX
+z3C3UXVjzSYXOWVPtOnuyXBe44vnBV90VI5K8CCwO8QKzUGvWGZAIIr4cTRXVY47
+lTT9qIRnVNmouLBtte4yzD8JisQ/glK6hnXher2rCWPxD4JpUUARgB2CHuIBWMOu
+E8Oha6bUCqFSNgrcvag4fSWgXfJv9LmQdYpJnQEmBFPHw2oBCACt7nYc4+0+lv/R
+hGJ8Uzq63n1RP/5EA1e/4AhlXV6W2inXeqLw9gSneD4P9RuVH2SSCoRf8RN0Srhw
+WSlvuCBJ2MHVyAjxtCBmfpJKrcTccHFQin4ElOa7nvOux9srhB/qwFy9NRt6SKTt
++xeeRtdv84BZPt8XkNItQhHnzf0g5wUEA/XAmHl3HKKhz8BSoMGC56oXSDbt7nbh
+AF2a9lQBHi0vm4SW24A0V0co9CLVSFAok2TK5drslhWLZ78J7ZdUWGI/g25+BOH/
+241dN5u3kgF+XeuaMqDKb5YnSCTopAEw0x/o5orT8YYGlhHHO65zoqBAmF8Sb15m
+haDe/tu1ABEBAAH/AGUAR05VAhDSdgABJAECAAAAAAAAAQAAiQEfBBgBAgAJBQJT
+x8NqAhsgAAoJEEy26QP/37flu+sH/37DW7TdG5IIML2gvFciivr90D8g1PhZycKt
+E4nTsxcRk5Fnh1vFaTc3lYsTPoAv5KTvePS3xMEBU5yPjIdprMdRu4XaZNAY3IoN
+ikV8V8S1lRxQjK74w4y+u2ewvo0RMenMC+6iGGrEFkSFAO8XgyCumTPBtQcRq77d
+U/darBj+P3pgFjuPi6lPiOmRAyPonjBOxKHAeIOIPqB8S0LNy4H96Z9pW553EE24
+js8GRWrA2npIEZKA9J8+6Z/Qdka7I3GYwKX25a/oXW06gKCIfINlftA1kl0w51A6
+vl8+qruzM1ISK581ghaV6cX0UBjRUEc4PpVJv9XAOZ/qb2S4BgudASYEU8fDagEI
+AITU8pl36lynZSo5RCqaEl14jCnWU6kftlTsJEigMP4sQQhjQEnQKt7lFlfNQDl2
+EDbP1rEoO0oxQyrDBM83UOl4/OQWADOYCHs1ykZdN8GQ+a1SeS53JlaBCCrX3Jyy
+k90adY0CqzDmCG3NCx95tGJuqVPASTBkzBdKqcPyjs2Rme1TCEJJBj4CiwA/7Xqs
+eRvm4zWCtN8AWTHwxb4h2/rOSCfUG9YeckMSg3ds9M63fpuCwYS4eDNykMIuR1Bf
+HYY1YCR0kV9MnBvjz/5iKUXyAL7hHMp+2/BWoPya6gg00RdbQu+BhMszo/tRd3sV
+3m9e2DaJf5vODbLflwSbtdUAEQEAAf8AZQBHTlUCENJ2AAEkAQIAAAAAAAABAACJ
+AR8EGAECAAkFAlPHw2oCGwwACgkQTLbpA//ft+VoCAf+Mm7INmrtS9EbXJZBQvko
+hyuk0TymI/GV4nng5bBeEuW2xE+3ppnBOTULJ2tlpgboIcVoKcXNKYNiw08bGSMS
+pFPzpyPZDHqau2fiGUlmZl9Pft4yWgoqCB7KcIk1Lf0F2Pg87dxSfhR+9lJoJnim
+7waWFCbjNWT1nuTG/30TJPjN6wUG0iQ7zDwskxBJy+FusWaB64CzA6bGWzmT3PxL
+N2Hmc2xWI5Xn/3Iho6jaVt6mypmBU3ybn7ltzMft9oTiVHQxJGwk2faA3Qv+JI3I
+4kLwYezEteIZEPkr04Zt5/HiU3Rg9DZsKKWp4En/LdYj1T4I01bkZQ4jDPCcvEqk
+Yg==
+=WUIv
+-----END PGP PRIVATE KEY BLOCK-----
diff --git a/OpenKeychain/src/test/resources/test-keys/mailvelope_07_no_key_flags.asc b/OpenKeychain/src/test/resources/test-keys/mailvelope_07_no_key_flags.asc
new file mode 100644
index 000000000..30e6b85e1
--- /dev/null
+++ b/OpenKeychain/src/test/resources/test-keys/mailvelope_07_no_key_flags.asc
@@ -0,0 +1,20 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: Mailvelope v0.7.0
+Comment: Email security by Mailvelope - http://www.mailvelope.com
+
+xsBNBFL9xMwBCACAa8z36B4Z4Yvpo7s4Y8jQsHNrJygyd8mh9+Bd3Sg+zt0Z
+UjfNS3SfwXocBTGOXrFlS0bRKd2CRXeXdTO0HqjCEKhGKHX73UhAcpBUoTV7
+6cgpYtKZL4tkIYWn47UPoASujjpwHxbDGaas/kmsTFPrlh8x5VOdEEVFkqPC
+wP1tnX1WALO58BWbs4R1vqrqBrOrGclm9cF2mErWP9QBLR/nH+soaVPK3Hrj
+/fX4S43xIxuhKmdZsljrfK7jsUkcF187QkK7xpyS5XSyoosPYIF/d06sVNlZ
+XlFFlJXuyndwnRA/VJHowa4hqFFQf1PjxV3g7IVqZfbtOX+SpcLCW507ABEB
+AAHNKlRlc3QgTWFpbCA8bWFpbHZlbG9wZUBzb21ld2hlcmUtZ21haWwuY29t
+PsLAXAQQAQgAEAUCUv3EzwkQ3CnDbfC4HTYAAIMAB/9AolqHiQun6rxyKgF5
+u9hgufo6FziYAtCQelRnUtONsGGUz6mvPsVliNsfbg0BAqwzeExXGBtsIoOv
+YAQ3a9zkK3lZSzO7lJIrg4UJyvqZAB6Vp5kJLJnWO1sfe4Pg8Xr8heawp2Hk
+na3MPma+ISb2jiioq8wGZHfSMdTYWPYSHf6645yKvfIZGSaG8TOtFYsASSHh
+ECfo6mQ+N/akF9pNI1S54RiZinssDn/vy2ZUUPpJGpaHDJ7zf6dATobaV+dV
+uqroe00SyJbYPzwKhEo7NgTEGdvICGnCXVoqDkS4j+lK07e5ztxE3OAIy5Un
+fC5CQLlbzR9jHSpe2FD6S9H+
+=hu6f
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/OpenKeychain/src/test/resources/test-keys/stripped_flags.asc b/OpenKeychain/src/test/resources/test-keys/stripped_flags.asc
new file mode 100644
index 000000000..0f4728297
--- /dev/null
+++ b/OpenKeychain/src/test/resources/test-keys/stripped_flags.asc
@@ -0,0 +1,60 @@
+-----BEGIN PGP PRIVATE KEY BLOCK-----
+
+lQIVBFQtfsYBEADhyTqxezePQuP9PCmzdXYavyaaBPCJLpfNhrufe++7xMmkIzGO
+l7X8hVrrnqhRs1pVe/rpaSJ8iUVpsMN2BWRrrsmpysf/idjE4OXDGvA5dCRJOhqy
+waHA7dO94x2mvcUCVWo5V4cu5+Qv2GejhorPsTNuXXUZrFKWSgEwLpg9lY7M39wB
+1SIeQLxMLwixCod87b0A0p9UBCU+QeBx4WFl0NPE3qneQgmFe1Idk6nfwSZFEfYM
+6iWepEzhv3GQjdNfXZxLIPqeo4Jowen53HlYYNp/d5fIOpyJ7FxWsutltcGA4GrD
+FSfhi/Wh+1/KKwRwCqhFQEZIQjWbmaiQlf3anERfL8Dy+WWiJmZt9W4TDZGY+3Ay
+3nDyqVHZJVo4gE+BUiNdx6oITM/mhwVJYpaOnHGMAmWz0Nj0OO5pa86Un/V25gaG
+/lmpkvrzHeTpM109xHJt4/WBWt+gUClDd32KCVaSx1b+ECagAf/PJJWpbQspLsNq
+DwJCyE8pYnGcoChIVIAPvE5wrAH890FgE43f0utlYW13vp76TXSwuk18XYYCglMl
+zuOTFSOkOEflZP4UvJTTjlHoKMJ+iOW4yIS+vvt9jCIYAGp6Fso4pcjakTZ1iSSD
+j5QN0ybcJHxKN+fSXZspuW2LijK8aZS2xhWjWwhWJWMDvl5pu9Nx5u4SwwARAQAB
+/wNlAkdOVQG0FVN0cmlwcGVkIFRlc3QgPHhAeS56PokCNwQTAQoAIQULCQgHAwYV
+CgkLCAMEFgIDAQIZAQWCVC1+xgKeAQKbDwAKCRCf3UpNJ7bxZ9GDEADDEsKrTKvz
+c1H6MbBNqsKOWI6Wc8D5MgggphuBmZwXP1FSYVsYTf9UMFsjrlE/JoFr4Yfjslav
+ywxbC5Vr74OAL7r25n9MuH7V8xcL14vDssbuIB/aOEmjOkHS2bRgR3hzkGfCSo5Y
+zb+uGZrYyJ+72FlTIbV3xAGtX+jYCVXCfzxuavmR5OlVFIWE4d/3sWFmyJG85Twh
+9JAl66eP+OK5UbfP1Y01SFmSR/g6Rb0splNq9BOSiurr/cs4z57lDNOheE10UODe
+TWtZLuwwy9+ajy1cCPAHtHZIx17d5VFc0I1u2UFoPq3HCQX+PNS8Maq2Nl83ZQNk
+z8+k1ZF58ojRgc5KX/wNI6t+aYZQCwEzzTiHAYxkQu8nvCC9M2QnU/LF5uHJyVi8
+QlVCwiuLqwV0PnNBdaLysNpeAZ7B21DA/cwliphOiK8qTIHwbTQvaOTzuahPizt9
+zCDzdzgrNu4RmJmscvh/PQHZTU4wpX3q0Vx/BpZdEIHFCJ0NnHwfXk2uXkbbwQnA
+6mvdtGteCL0ffEnjIbKPkjJ8qgWGVrIQP5XrJFTmHBHTdrA4sSCBRm7R0TtmBcOt
+JZhUXwPqpfZCcik5BYpptskwwi9J+Di3caCcYFak6xOe0hrYTBOtP3Ztca64hTzW
+Q66EYC4TOIqj0Cp5RFXKc6blIcCymbDvI5ylBFQtfscTCCqGSM49AwEHAgMENK58
+neORF9s3/idis8T/u45bCa5Az5hcNlrgX+UFcNoVBESsoIaJT/EGN+8/wIJgIsvo
+dUMQXrLvoLuZKw0wzv4JAwha337PCMiUiZDitD0nW3W1nso1Cilb/DmAXfxkZ6zh
+D+CU6Km3w3BcIChFQW8R/J+b598UQuq0dhtgg527pxveEfOCPhLwLKAAtBtOCUHU
+iQJ/BBgBCgAJBYJULX7HApsCAGoJEJ/dSk0ntvFnX6AEGRMKAAYFAlQtfscACgkQ
+tRqCgorJb7ZqQQEAoUsJ5P8GSiLriVSMPqNaFjjL/RBQ6ITwj8SaOyheJicA/1mJ
+f1XqAae/Bf9I6Dn5km7RQ6Z9wjL43w/N/E6mIhvgFswP/jv93r0JomWcXienLxQw
+2scLjwTH/BpIrf9vGLyzlfkh1T+S3roM2Ul3j5Kc7ycaumMgKNxJpBpMjnpn4Unr
+pq0P7oVtjonKs53UqfVUl+/ZNuYG1vqEVH5clW1QL4Xvir6bOM4TXHOXvWQozy5w
+z9A5gkzWMMNnXov60J7o7QHbZOBgzsLLvXmP/HiXJoS95RLW8/yHH2I5hBkSbkqm
+NIE7g/gD2diVACy/mEmmVmkmdDRnBzknicxoSi1z2r9OrEgDlGeX4iRlgwTVKXf0
+t65wh8fc0tBuPggZrw2mdI8CGDhvDzaQLbwf549XbBgaggAIBzne0V7LsqXDHp5m
+qQx7j7WaLuhcv0DkHcTDxXa48Xt4Auhzw64VxqzaleLrsdv0klCmIM+oxZWND2pZ
++VK2pouFNGALJJcbt4sgaX9BkhKqJgW8Rtc7l5OqsqAt7WlmHbOn1E4b6lGAXftH
+pX59k3LzVX+KHdIdUhXm0bShEa4GhiQLrP1pMJ1js+JWir3r4uHHwDRM1jt/n899
+llGnGpz/SmyMSLeQ9LjJK7Pt8JcLmK5SKj3FUrl1+Aa+KQNLDJGOvx8cjcFd09p0
+UHi2zF8YbnXbFmj50OVGvFkUbLhYY11t3JLrnLNug2CkcygL40FbBszjrcJxMucJ
++ZSMgM4OkWsqtqRmU4WtGFIQnKkEVC1+xxIIKoZIzj0DAQcCAwTVE/OsK5w9j1Wl
+R2U2KtBTCPpc+ED3niUS63kKawp7pudX4BJqnbXwX4DqnMA+iMYX9rPvXymAyjT/
+yEhfGlaHAwEIB/4JAwha337PCMiUiZAtspy93vg+lA7XwKz0K4zScQRW0JiFWEp0
+j8BdiH3B5TOuTD+D5HMhx2hxek/DtW337zQbh5SS0RFckCYOK/qa1qysCNhgiQIf
+BBgBCgAJBYJULX7HApsMAAoJEJ/dSk0ntvFnMbYQAJgXLnV7d3xv6hJlCOI8A9Wk
+qt5aF032hzsQzd+lhSb1kveuEt07XNZ07Plj/MOdrdNduqVMqJ21A3Pqo1iUr6PC
+B+co/BGqUmbkx+16Ebj20SKb48xeHrFtQZb3ciDMzcixXY8pfeFUdb7O2M/3NURV
+caDuU4e3FW+eNOnTriW8beRkC2FYud+kMiLfbYT76MufVLERQN94x0T0OCrCI7nQ
+GfM8lxgRVJ4hoTygadlv19LCq16wGCVFKIPw/DtFcavkdoN3TboNh/aHia6moKR1
+RSC6II9IcKLMSbnqZdBIJpqpXJIbkCgAOV6Fr3blVg/sub2Mpe9XRT879sO1I8sf
+vAP1VI+/I3WE3mZyelJ8xQlR6t81upfpN29DGfKq/194P/mq8b8LYcvO6xdVNkUl
+2ZO4ojqY0PBdiFG0VMeGLgzmuxROYVCNV66hg9GvbVPnILFreL4RHy+mDPBa1XQW
+YsxIrz1wElOud2HjSLIHdwDEiNuqqQEY4yEmRaD0ULmRXTWINvcMz6mQ3343Np1z
+s4ppb6W4OJoVnkW8OIQzTJaPHIItTfLLQFLSWf8L00FVOsV3WtauCO9B688H5JNw
+RMmNFoqHMKmplEveTG1beRBRZ7UaLJXh9mO96nS1G1YRTPv5+BWRf9ZhKxoat3GN
+9uUAwtb1XXM6DVP1glMx
+=kEZ2
+-----END PGP PRIVATE KEY BLOCK-----
diff --git a/OpenKeychain/src/test/resources/test-keys/symantec_public.asc b/OpenKeychain/src/test/resources/test-keys/symantec_public.asc
new file mode 100644
index 000000000..f4148935c
--- /dev/null
+++ b/OpenKeychain/src/test/resources/test-keys/symantec_public.asc
@@ -0,0 +1,37 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: Encryption Desktop 10.3.1 (Build 13100)
+
+mQENBFRHpJ8BCADIzN2EB8emV3l1wZ+GyKiC3roHEUnWcIj2ZksEJ1ZBfUC/En4/
+wjNSAsJCcECRUDRaoMH1NI2w6BOrODt+7EsBqndGtKi0LzcDvHj9WNe9XfwXWRAH
+tnaTtzqLyPsqkKWJQzzyqpys8sy58qIb3gqpRSXL8UST3OtIGesYmeFSI8swL/0l
+vabQ7JJjImFyuwiel5TE5A5sRr/Tqd26jW1Y1cInqtSQqeSwzUwi0uNMCZrNJNs6
+fk902U8xanLk4TkIJT6nPVzoGOcczPES4U8p27m82IuqKeua+Yzwmeitllhm98mw
+k9u3jSM36FwHRomdBmWBPn6supMbrPR+wrYXABEBAAG0F1Rlc3QgPHRlc3RAYmxh
+aGJsYWguZGU+iQFyBBABAgBcBQJUR6SfMBSAAAAAACAAB3ByZWZlcnJlZC1lbWFp
+bC1lbmNvZGluZ0BwZ3AuY29tcGdwbWltZQgLCQgHAwIBCgIZAQUbAwAAAAUWAAMC
+AQUeAQAAAAYVCAkKAwIACgkQuR01xc4cnZxtTwf/QJ0G4ME/XhdPxfHIHzG9H1uo
+dtFPjf2Bpt1TKZNrf+Cf+7P+M1HvgRhhR4pp04lFCBYhaworaX0bqyGCqKmON11M
+cCAUC63ygrKmuGTmU1vnLcct/j7+k3cpX0Q4JZjQNzQ5ebSQP3PQyTfcd1SuZL8f
+XA1xBjPXMNhgqkLC1qzyVaJMmlBouXeW/fICWDz6Cwl2kw/9BgxaWZVAJvIxuspN
+1i3mTQvZ5Ztyhc1++GES+BxcxSzjcd43OaPJVWJW68MqcjwQTUBT3s5tz+QqYN3s
+zVT9piZ72E9xflkBvQYq/ez9eloXiMiguiix3FvYJWcZivKRph76IWhgPoPW+LkB
+DQRUR6SfAQgAyOwGhDRxB93kFGTjjYwUbxR+dQ7lDZA5pmmLQ62wMLsqI5J0SPQ9
+DEjjRpO0E4s7ASzsySjnJQ5yvjQCVAdIER1rTIwPwAz9U7vhdxXUfypirO5OBO0d
+bAJCnCEGNZ15uaZwClKep7G0e/tqh3nMdr0YY0LG1kIKfozUZYPv3QyjcrPHe9kC
+tYveiuh78hcGixUVa9LUfQreVbicx+bombY6j5BuPjoYbvStaILpN/EyVtzpsBj8
+NqXci6llO9RzmctSgOE3SplTLeJdnsfFR/XWzI55/aZlFczU275odDyhTEXsuH5a
+VBx8sGqtVScQYEw/vghIXjkXXni5gqC42QARAQABiQJBBBgBAgErBQJUR6SgBRsM
+AAAAwF0gBBkBCAAGBQJUR6SfAAoJELXmxUk/pkE59bkIAMCAMoNijiQfmOPv1Wya
+K5q/whZXzbWkVEuhTLYj2pyOpUeFZgQgnXe4niZG06lTDSbJGwH4LJXKOyssWicZ
+XRJwmtwWEnlo5+XBSiW9u8XiQTJExe6Go3zY3OJpAmLj2DMoGYT/Zyccld+C9VN3
+qgJ+DSQvv/zV2R5h94cZefaGh5PoXw5usz9hHMz2+VUR7iy1rY53Fv0qwSYib6cQ
+SqrhoIoFEYUhFJZ4zc9LQzPVlwND0hpN5e7clr5A3HNbUb1GC1J9X1P+mQaLdysx
+Iv2MZnRtcgyn8ci5I9xm2mB09qaMLU05iZK/hkWlAH5LMrn/e4xtcGMsYm26SAK4
+g6wACgkQuR01xc4cnZzC2Qf/dtJRT0S1PGIl8uGTF2tBukCZBOXqtCxfWR/EHlK4
+AwOpwAJ8dgIV9qDkw36lC6bdxeu/qm4EXcczxxYc2TVGHaj3QH2sjU0MUmsXj2Ri
+ybdS62pXl/0cI2dBNGnWmcydm6NHNidymJ9LeL5+NQv5lPh1k12UDpj8PCz7TNqw
+0XwWww11fizmW6I5bIkkdaPzXg14jBQkY5YIt63sDyMCFJectYHNyobgWxzEy/Kj
+NxkZjNhL2olwk8HctIWzAEFNSOzJZ+Y6hNePN5ARmjaJeve7cNU1cxgeY4SSmZsm
+uNTAvlX6Lqi4l3p/Uroz3T4UltFIDUbzhqQbNQ902Hz6ag==
+=c8vd
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/OpenKeychain/src/test/resources/test-keys/symantec_secret.asc b/OpenKeychain/src/test/resources/test-keys/symantec_secret.asc
new file mode 100644
index 000000000..94451c1bf
--- /dev/null
+++ b/OpenKeychain/src/test/resources/test-keys/symantec_secret.asc
@@ -0,0 +1,46 @@
+-----BEGIN PGP PRIVATE KEY BLOCK-----
+Version: Encryption Desktop 10.3.1 (Build 13100)
+
+lQPGBFRHpJ8BCADIzN2EB8emV3l1wZ+GyKiC3roHEUnWcIj2ZksEJ1ZBfUC/En4/
+wjNSAsJCcECRUDRaoMH1NI2w6BOrODt+7EsBqndGtKi0LzcDvHj9WNe9XfwXWRAH
+tnaTtzqLyPsqkKWJQzzyqpys8sy58qIb3gqpRSXL8UST3OtIGesYmeFSI8swL/0l
+vabQ7JJjImFyuwiel5TE5A5sRr/Tqd26jW1Y1cInqtSQqeSwzUwi0uNMCZrNJNs6
+fk902U8xanLk4TkIJT6nPVzoGOcczPES4U8p27m82IuqKeua+Yzwmeitllhm98mw
+k9u3jSM36FwHRomdBmWBPn6supMbrPR+wrYXABEBAAH+CQMCgd+uT2AR0iSohaM1
+J4VJkc9XImdR0sOx38MaTA/wQYUMEgU4SHlHcRGb7cUluYNysSTTRnYZxE1SU1t7
+0wN13ce3DELMEahBnEsRKE9a1COUnpkBsfw/1qWWyeX4iJPKiMBKABiLiZ8PxkI2
+Rhi2c0epWb2xsw4KZtB+c4UcCIkvRgrgJrEPIwL0DBCgtBd1jqKHa58pyPo28Y8Q
+Wf9hxRGyRz6QErcfZE7JiAsBE9kHZufo/NzrYEps4gtuZkmwYojT0NuzWIS64P1D
+AQe/ay3av406xQsoVmamYNklgDUVf+YrTw6Rv/0G2ocaqTbCKqoaDi1bpdYDQpzw
+2QdMlzZ9HpBSbY9DQJSmJRTq98jOOo6qNGzv2W34YF5RE1xZL/GsQ9a7fx+SwGCF
+hIQRC43q3tKMqfhacUf6UxNB3EaPt5SPo35Khbp/BLNXAMjXyZJuZd50fEA+mivX
+f5Q/GdZ8NCcnOtwK/fhepShaNkQYtJIYqr5LqYZqNdyDcV7y/4L0TIxwdVKc5skN
+dRtM+Q/6rN/Ewxgxa/WI2fgUG2Hgez/jVWJoCZzatP+zUgtsGS6cr3jmhFHimuJu
+RCsoTl5vJciVH8sTb+6/oUvWAbE11Ks8KptZoO8xv+pYvpcCZkfaPVF2E1NVd1kW
+cwqijc0bwudKq40A7y82lfcBzKnKR51eOf3xCzfk67pOhaVqkQBMdNJQGztmuYC9
+JBfHSZqoTQEfZr1gWl0hhPGC+LldzfwjwBQTLbfUh4V5Zboj1SY9JAu1WxwIieGw
+Z/3tq4KAriramkEXiFAdslnqvxu9iXErsxzln2fFjSW+rm3USFaz8ZHQQ1iCz4V0
+6uPGG2TWqpsEYluuepVSdnqpBtzE5I2fjmgdcey4MoITGB66qEAp0o7c/yYuCSlz
+WZpb0sJuNQAOtBdUZXN0IDx0ZXN0QGJsYWhibGFoLmRlPp0DxgRUR6SfAQgAyOwG
+hDRxB93kFGTjjYwUbxR+dQ7lDZA5pmmLQ62wMLsqI5J0SPQ9DEjjRpO0E4s7ASzs
+ySjnJQ5yvjQCVAdIER1rTIwPwAz9U7vhdxXUfypirO5OBO0dbAJCnCEGNZ15uaZw
+ClKep7G0e/tqh3nMdr0YY0LG1kIKfozUZYPv3QyjcrPHe9kCtYveiuh78hcGixUV
+a9LUfQreVbicx+bombY6j5BuPjoYbvStaILpN/EyVtzpsBj8NqXci6llO9RzmctS
+gOE3SplTLeJdnsfFR/XWzI55/aZlFczU275odDyhTEXsuH5aVBx8sGqtVScQYEw/
+vghIXjkXXni5gqC42QARAQAB/gkDAjh+yK9prL3PqHOsnl83foztKdNlyBq0WF4g
+pYN0K4ldqMAdOh0ffs7Syi+48GHx9E+e79QwHfgSvszxIirGVXw8wlju2hCh544U
+0qj1CHfeZ02Qww+gViQikn0PAONeqdKBztDKUgys/dyarC4ROhB16qfyYUJgSy5M
+lENBSLrKf1AZoWTARw6wl0a20pDI1qk/SAh8Dyi+kNk7yX9wzYnaCKGE6pnydSJF
+HvK3tUHbMmYroucbRSWW7AdeoznBt1801kvuPIKEP0CexSZeW72GgpF3ZDSXmyeS
+gXpE7rgYD/yttPkzMRM/f2VwHov78jVPDnA1Z5hzi0I2vBbFLpdli1o4Wqazffvm
+4dR7Bfd6sfbTNi3hB5rwEcCKvKQZ4HdUe7+8f7VzzqCEuRaVcpsAn5sejn0dcvdg
+ON0sWUvGSS/hcl92aKhrYRqSfycZiT5snzYQ79II3Mv9e/juB5LdFOOEK1XMo0+L
+WvV5TfgwnLTYNQBUkhRioJl5YM4xFTCgzP2e0kLDMuSfQa8BiRB7ip27YxonTWQW
++nwD+3WmO5DKChAp0reljQSsCfxr7QVmNNf2oCmlO9TQHEDjJx3CkC/lb07igbel
+SABIRkfAm8ylMINalatd5TO8Pk1+spCKJav1wbYjyttyy97IFV/DhVUp5lxdUPxS
+7QEi8BGPbAPQNrLwckktK4EcCwgjIS4KuFe7awQfM8VUSkpRx7g/6YpC7aN0AkBA
+ao9YTXGsf9TOegzXOChwCrcnskWfkydIyYzG4enxeR7c3hiyyh4NiHzZ0hLCr/Pt
+wImpcHWRyWoIU7R7oNnOaWfpH5P2mv83ZvffGfVZQdjoj6gUinHqtU+tlEwkhbua
+lyhs+g99VJbxJchFM1JADgokcc31enYM41J239Y9ScoPjQh+Kz9UJzQjmg==
+=jUby
+-----END PGP PRIVATE KEY BLOCK-----