aboutsummaryrefslogtreecommitdiffstats
path: root/OpenKeychain-Test/src/test/java/org
diff options
context:
space:
mode:
Diffstat (limited to 'OpenKeychain-Test/src/test/java/org')
-rw-r--r--OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/support/KeyringBuilder.java254
-rw-r--r--OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/support/KeyringTestingHelper.java363
-rw-r--r--OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/support/PgpVerifyTestingHelper.java65
-rw-r--r--OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/support/ProviderHelperStub.java39
-rw-r--r--OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/support/TestDataUtil.java142
-rw-r--r--OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/tests/PgpDecryptVerifyTest.java53
-rw-r--r--OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/tests/PgpKeyOperationTest.java770
-rw-r--r--OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/tests/ProviderHelperKeyringTest.java101
-rw-r--r--OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/tests/UncachedKeyringCanonicalizeTest.java627
-rw-r--r--OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/tests/UncachedKeyringMergeTest.java397
-rw-r--r--OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/tests/UncachedKeyringTest.java125
11 files changed, 2936 insertions, 0 deletions
diff --git a/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/support/KeyringBuilder.java b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/support/KeyringBuilder.java
new file mode 100644
index 000000000..94193bbcb
--- /dev/null
+++ b/OpenKeychain-Test/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-Test/src/test/java/org/sufficientlysecure/keychain/support/KeyringTestingHelper.java b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/support/KeyringTestingHelper.java
new file mode 100644
index 000000000..015e134ea
--- /dev/null
+++ b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/support/KeyringTestingHelper.java
@@ -0,0 +1,363 @@
+/*
+ * Copyright (C) Art O Cathain, Vincent Breitmoser
+ *
+ * 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.NullProgressable;
+import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
+import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.service.OperationResults;
+
+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);
+
+ OperationResults.SaveKeyringResult saveKeyringResult = providerHelper.savePublicKeyRing(ring, new NullProgressable());
+
+ 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-Test/src/test/java/org/sufficientlysecure/keychain/support/PgpVerifyTestingHelper.java b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/support/PgpVerifyTestingHelper.java
new file mode 100644
index 000000000..dd5786512
--- /dev/null
+++ b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/support/PgpVerifyTestingHelper.java
@@ -0,0 +1,65 @@
+package org.sufficientlysecure.keychain.support;
+/*
+ * 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/>.
+ */
+
+import android.content.Context;
+
+import org.sufficientlysecure.keychain.pgp.PgpDecryptVerify;
+import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyResult;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.util.InputData;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * For functional tests of PgpDecryptVerify
+ */
+public class PgpVerifyTestingHelper {
+
+ private final Context context;
+
+ public PgpVerifyTestingHelper(Context robolectricContext) {
+ this.context = robolectricContext;
+ }
+
+ public int doTestFile(String testFileName) throws Exception {
+ ProviderHelper providerHelper = new ProviderHelperStub(context);
+
+ PgpDecryptVerify.PassphraseCache passphraseCache = new PgpDecryptVerify.PassphraseCache() {
+ public String getCachedPassphrase(long masterKeyId) {
+ return "I am a passphrase";
+ }
+ };
+
+ byte[] sampleInputBytes = TestDataUtil.readFully(getClass().getResourceAsStream(testFileName));
+
+ InputStream sampleInput = new ByteArrayInputStream(sampleInputBytes);
+
+ InputData data = new InputData(sampleInput, sampleInputBytes.length);
+ OutputStream outStream = new ByteArrayOutputStream();
+
+ PgpDecryptVerify verify = new PgpDecryptVerify.Builder(providerHelper, passphraseCache, data, outStream).build();
+ PgpDecryptVerifyResult result = verify.execute();
+
+ return result.getSignatureResult().getStatus();
+ }
+
+
+}
diff --git a/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/support/ProviderHelperStub.java b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/support/ProviderHelperStub.java
new file mode 100644
index 000000000..2cd0c67a2
--- /dev/null
+++ b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/support/ProviderHelperStub.java
@@ -0,0 +1,39 @@
+/*
+ * 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;
+
+/**
+ * Created by art on 21/06/14.
+ */
+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-Test/src/test/java/org/sufficientlysecure/keychain/support/TestDataUtil.java b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/support/TestDataUtil.java
new file mode 100644
index 000000000..f2b3c0996
--- /dev/null
+++ b/OpenKeychain-Test/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-Test/src/test/java/org/sufficientlysecure/keychain/tests/PgpDecryptVerifyTest.java b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/tests/PgpDecryptVerifyTest.java
new file mode 100644
index 000000000..158650012
--- /dev/null
+++ b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/tests/PgpDecryptVerifyTest.java
@@ -0,0 +1,53 @@
+/*
+ * 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 tests;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.*;
+import org.openintents.openpgp.OpenPgpSignatureResult;
+import org.sufficientlysecure.keychain.support.PgpVerifyTestingHelper;
+
+@RunWith(RobolectricTestRunner.class)
+@org.robolectric.annotation.Config(emulateSdk = 18) // Robolectric doesn't yet support 19
+public class PgpDecryptVerifyTest {
+
+ @Test
+ public void testVerifySuccess() throws Exception {
+
+ String testFileName = "/sample.txt";
+ int expectedSignatureResult = OpenPgpSignatureResult.SIGNATURE_SUCCESS_UNCERTIFIED;
+
+ int status = new PgpVerifyTestingHelper(Robolectric.application).doTestFile(testFileName);
+
+ Assert.assertEquals(expectedSignatureResult, status);
+ }
+
+ @Test
+ public void testVerifyFailure() throws Exception {
+
+ String testFileName = "/sample-altered.txt";
+ int expectedSignatureResult = OpenPgpSignatureResult.SIGNATURE_ERROR;
+
+ int status = new PgpVerifyTestingHelper(Robolectric.application).doTestFile(testFileName);
+
+ Assert.assertEquals(expectedSignatureResult, status);
+ }
+
+}
diff --git a/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/tests/PgpKeyOperationTest.java b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/tests/PgpKeyOperationTest.java
new file mode 100644
index 000000000..4f6694049
--- /dev/null
+++ b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/tests/PgpKeyOperationTest.java
@@ -0,0 +1,770 @@
+package org.sufficientlysecure.keychain.tests;
+
+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.shadows.ShadowLog;
+import org.spongycastle.bcpg.BCPGInputStream;
+import org.spongycastle.bcpg.Packet;
+import org.spongycastle.bcpg.PacketTags;
+import org.spongycastle.bcpg.SecretSubkeyPacket;
+import org.spongycastle.bcpg.SignaturePacket;
+import org.spongycastle.bcpg.UserIDPacket;
+import org.spongycastle.bcpg.sig.KeyFlags;
+import org.spongycastle.openpgp.PGPSignature;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Constants.choice.algorithm;
+import org.sufficientlysecure.keychain.pgp.CanonicalizedKeyRing;
+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.pgp.WrappedSignature;
+import org.sufficientlysecure.keychain.service.OperationResultParcel.OperationLog;
+import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
+import org.sufficientlysecure.keychain.service.SaveKeyringParcel.SubkeyAdd;
+import org.sufficientlysecure.keychain.service.SaveKeyringParcel.SubkeyChange;
+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.ProgressScaler;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.Random;
+
+@RunWith(RobolectricTestRunner.class)
+@org.robolectric.annotation.Config(emulateSdk = 18) // Robolectric doesn't yet support 19
+public class PgpKeyOperationTest {
+
+ static UncachedKeyRing staticRing;
+ static String passphrase;
+
+ UncachedKeyRing ring;
+ PgpKeyOperation op;
+ SaveKeyringParcel parcel;
+ ArrayList<RawPacket> onlyA = new ArrayList<RawPacket>();
+ ArrayList<RawPacket> onlyB = new ArrayList<RawPacket>();
+
+ @BeforeClass public static void setUpOnce() throws Exception {
+ ShadowLog.stream = System.out;
+
+ {
+ String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789!@#$%^&*()-_=";
+ Random r = new Random();
+ StringBuilder passbuilder = new StringBuilder();
+ // 20% chance for an empty passphrase
+ for(int i = 0, j = r.nextInt(10) > 2 ? r.nextInt(20) : 0; i < j; i++) {
+ passbuilder.append(chars.charAt(r.nextInt(chars.length())));
+ }
+ passphrase = passbuilder.toString();
+ System.out.println("Passphrase is '" + passphrase + "'");
+ }
+
+ SaveKeyringParcel parcel = new SaveKeyringParcel();
+ parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
+ Constants.choice.algorithm.rsa, 1024, KeyFlags.CERTIFY_OTHER, null));
+ parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
+ Constants.choice.algorithm.rsa, 1024, KeyFlags.SIGN_DATA, null));
+ parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
+ Constants.choice.algorithm.rsa, 1024, KeyFlags.ENCRYPT_COMMS, null));
+
+ parcel.mAddUserIds.add("twi");
+ parcel.mAddUserIds.add("pink");
+ parcel.mNewPassphrase = passphrase;
+ PgpKeyOperation op = new PgpKeyOperation(null);
+
+ staticRing = op.createSecretKeyRing(parcel).getRing();
+
+ 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;
+
+ // 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(
+ Constants.choice.algorithm.rsa, new Random().nextInt(256)+255, KeyFlags.CERTIFY_OTHER, null));
+ parcel.mAddUserIds.add("shy");
+ parcel.mNewPassphrase = passphrase;
+
+ UncachedKeyRing ring = op.createSecretKeyRing(parcel).getRing();
+
+ Assert.assertNull("creating ring with < 512 bytes keysize should fail", ring);
+ }
+
+ {
+ parcel.reset();
+ parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
+ Constants.choice.algorithm.elgamal, 1024, KeyFlags.CERTIFY_OTHER, null));
+ parcel.mAddUserIds.add("shy");
+ parcel.mNewPassphrase = passphrase;
+
+ UncachedKeyRing ring = op.createSecretKeyRing(parcel).getRing();
+
+ Assert.assertNull("creating ring with ElGamal master key should fail", ring);
+ }
+
+ {
+ parcel.reset();
+ parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
+ 12345, 1024, KeyFlags.CERTIFY_OTHER, null));
+ parcel.mAddUserIds.add("shy");
+ parcel.mNewPassphrase = passphrase;
+
+ UncachedKeyRing ring = op.createSecretKeyRing(parcel).getRing();
+ Assert.assertNull("creating ring with bad algorithm choice should fail", ring);
+ }
+
+ {
+ parcel.reset();
+ parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
+ Constants.choice.algorithm.rsa, 1024, KeyFlags.SIGN_DATA, null));
+ parcel.mAddUserIds.add("shy");
+ parcel.mNewPassphrase = passphrase;
+
+ UncachedKeyRing ring = op.createSecretKeyRing(parcel).getRing();
+ Assert.assertNull("creating ring with non-certifying master key should fail", ring);
+ }
+
+ {
+ parcel.reset();
+ parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
+ Constants.choice.algorithm.rsa, 1024, KeyFlags.CERTIFY_OTHER, null));
+ parcel.mNewPassphrase = passphrase;
+
+ UncachedKeyRing ring = op.createSecretKeyRing(parcel).getRing();
+ Assert.assertNull("creating ring without user ids should fail", ring);
+ }
+
+ {
+ parcel.reset();
+ parcel.mAddUserIds.add("shy");
+ parcel.mNewPassphrase = passphrase;
+
+ UncachedKeyRing ring = op.createSecretKeyRing(parcel).getRing();
+ Assert.assertNull("creating ring without subkeys should fail", ring);
+ }
+
+ }
+
+ @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(
+ Constants.choice.algorithm.rsa, 1024, KeyFlags.CERTIFY_OTHER | KeyFlags.SIGN_DATA, null));
+ parcel.mAddUserIds.add("luna");
+ ring = op.createSecretKeyRing(parcel).getRing();
+
+ 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, 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());
+
+ Assert.assertEquals("number of subkeys must be three",
+ 3, KeyringTestingHelper.itToList(ring.getPublicKeys()).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().getExpiryTime());
+
+ Iterator<UncachedPublicKey> it = ring.getPublicKeys();
+
+ Assert.assertEquals("first (master) key can certify",
+ KeyFlags.CERTIFY_OTHER, it.next().getKeyUsage());
+
+ UncachedPublicKey signingKey = it.next();
+ Assert.assertEquals("second key can sign",
+ KeyFlags.SIGN_DATA, signingKey.getKeyUsage());
+ ArrayList<WrappedSignature> sigs = signingKey.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",
+ signingKey.getKeyId(), sigs.get(0).getKeyId());
+
+ Assert.assertEquals("third key can encrypt",
+ KeyFlags.ENCRYPT_COMMS, it.next().getKeyUsage());
+
+ }
+
+ @Test
+ public void testBadKeyModification() throws Exception {
+
+ {
+ SaveKeyringParcel parcel = new SaveKeyringParcel();
+ // off by one
+ parcel.mMasterKeyId = ring.getMasterKeyId() -1;
+ parcel.mFingerprint = ring.getFingerprint();
+
+ CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing(ring.getEncoded(), false, 0);
+ UncachedKeyRing modified = op.modifySecretKeyRing(secretRing, parcel, passphrase).getRing();
+
+ Assert.assertNull("keyring modification with bad master key id should fail", modified);
+ }
+
+ {
+ SaveKeyringParcel parcel = new SaveKeyringParcel();
+ // off by one
+ parcel.mMasterKeyId = null;
+ parcel.mFingerprint = ring.getFingerprint();
+
+ CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing(ring.getEncoded(), false, 0);
+ UncachedKeyRing modified = op.modifySecretKeyRing(secretRing, parcel, passphrase).getRing();
+
+ Assert.assertNull("keyring modification with null master key id should fail", modified);
+ }
+
+ {
+ SaveKeyringParcel parcel = new SaveKeyringParcel();
+ parcel.mMasterKeyId = ring.getMasterKeyId();
+ parcel.mFingerprint = ring.getFingerprint();
+ // some byte, off by one
+ parcel.mFingerprint[5] += 1;
+
+ CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing(ring.getEncoded(), false, 0);
+ UncachedKeyRing modified = op.modifySecretKeyRing(secretRing, parcel, passphrase).getRing();
+
+ Assert.assertNull("keyring modification with bad fingerprint should fail", modified);
+ }
+
+ {
+ SaveKeyringParcel parcel = new SaveKeyringParcel();
+ parcel.mMasterKeyId = ring.getMasterKeyId();
+ parcel.mFingerprint = null;
+
+ CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing(ring.getEncoded(), false, 0);
+ UncachedKeyRing modified = op.modifySecretKeyRing(secretRing, parcel, passphrase).getRing();
+
+ Assert.assertNull("keyring modification with null fingerprint should fail", modified);
+ }
+
+ {
+ String badphrase = "";
+ if (badphrase.equals(passphrase)) {
+ badphrase = "a";
+ }
+ CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing(ring.getEncoded(), false, 0);
+ UncachedKeyRing modified = op.modifySecretKeyRing(secretRing, parcel, badphrase).getRing();
+
+ Assert.assertNull("keyring modification with bad passphrase should fail", modified);
+ }
+
+ }
+
+ @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, 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.getExpiryTime());
+ Assert.assertEquals("added key must have expected expiry date",
+ expiry, newKey.getExpiryTime().getTime()/1000);
+ Assert.assertEquals("added key must have expected flags",
+ flags, newKey.getKeyUsage());
+ Assert.assertEquals("added key must have expected bitsize",
+ bits, newKey.getBitStrength());
+
+ { // bad keysize should fail
+ parcel.reset();
+ parcel.mAddSubKeys.add(new SubkeyAdd(
+ algorithm.rsa, new Random().nextInt(512), KeyFlags.SIGN_DATA, null));
+
+ CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing(ring.getEncoded(), false, 0);
+ modified = op.modifySecretKeyRing(secretRing, parcel, passphrase).getRing();
+
+ Assert.assertNull("creating a subkey with keysize < 512 should fail", modified);
+ }
+
+ { // a past expiry should fail
+ parcel.reset();
+ parcel.mAddSubKeys.add(new SubkeyAdd(algorithm.rsa, 1024, KeyFlags.SIGN_DATA,
+ new Date().getTime()/1000-10));
+
+ CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing(ring.getEncoded(), false, 0);
+ modified = op.modifySecretKeyRing(secretRing, parcel, passphrase).getRing();
+
+ Assert.assertNull("creating subkey with past expiry date should fail", modified);
+ }
+
+ }
+
+ @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).getExpiryTime());
+ Assert.assertEquals("modified key must have expected expiry date",
+ expiry, modified.getPublicKey(keyId).getExpiryTime().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, modified.getPublicKey(keyId).getKeyUsage());
+ Assert.assertNotNull("key must retain its expiry",
+ modified.getPublicKey(keyId).getExpiryTime());
+ Assert.assertEquals("key expiry must be unchanged",
+ expiry, modified.getPublicKey(keyId).getExpiryTime().getTime()/1000);
+ }
+
+ { // a past expiry should fail
+ parcel.reset();
+ parcel.mChangeSubKeys.add(new SubkeyChange(keyId, null, new Date().getTime()/1000-10));
+
+ CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing(ring.getEncoded(), false, 0);
+ modified = op.modifySecretKeyRing(secretRing, parcel, passphrase).getRing();
+
+ Assert.assertNull("setting subkey expiry to a past date should fail", modified);
+ }
+
+ { // modifying nonexistent keyring should fail
+ parcel.reset();
+ parcel.mChangeSubKeys.add(new SubkeyChange(123, null, null));
+
+ CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing(ring.getEncoded(), false, 0);
+ modified = op.modifySecretKeyRing(secretRing, parcel, passphrase).getRing();
+
+ Assert.assertNull("modifying non-existent subkey should fail", modified);
+ }
+
+ }
+
+ @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, parcel, passphrase).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);
+
+ 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).isRevoked());
+ }
+
+ { // re-add second subkey
+
+ parcel.reset();
+ parcel.mChangeSubKeys.add(new SubkeyChange(keyId, null, null));
+
+ 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).isRevoked());
+ Assert.assertEquals("subkey must have the same usage flags as before",
+ flags, modified.getPublicKey(keyId).getKeyUsage());
+
+ }
+ }
+
+ @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;
+
+ CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing(modified.getEncoded(), false, 0);
+ UncachedKeyRing otherModified = op.modifySecretKeyRing(secretRing, parcel, passphrase).getRing();
+
+ Assert.assertNull("setting primary user id to a revoked user id should fail", otherModified);
+
+ }
+
+ { // 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());
+ }
+
+ }
+
+ @Test
+ public void testUserIdAdd() throws Exception {
+
+ {
+ parcel.mAddUserIds.add("");
+ CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing(ring.getEncoded(), false, 0);
+ UncachedKeyRing modified = op.modifySecretKeyRing(secretRing, parcel, passphrase).getRing();
+ Assert.assertNull("adding an empty user id should fail", modified);
+ }
+
+ 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 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";
+ }
+
+ CanonicalizedSecretKeyRing secretRing = new CanonicalizedSecretKeyRing(ring.getEncoded(), false, 0);
+ modified = op.modifySecretKeyRing(secretRing, parcel, passphrase).getRing();
+
+ Assert.assertNull("changing primary user id to a non-existent one should fail", modified);
+ }
+
+ // check for revoked primary user id already done in revoke test
+
+ }
+
+
+ private static UncachedKeyRing applyModificationWithChecks(SaveKeyringParcel parcel,
+ UncachedKeyRing ring,
+ ArrayList<RawPacket> onlyA,
+ ArrayList<RawPacket> onlyB) {
+ return applyModificationWithChecks(parcel, ring, onlyA, onlyB, 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,
+ 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);
+ UncachedKeyRing rawModified = op.modifySecretKeyRing(secretRing, parcel, passphrase).getRing();
+ Assert.assertNotNull("key modification failed", 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));
+ }
+
+
+}
diff --git a/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/tests/ProviderHelperKeyringTest.java b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/tests/ProviderHelperKeyringTest.java
new file mode 100644
index 000000000..665e8ef2b
--- /dev/null
+++ b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/tests/ProviderHelperKeyringTest.java
@@ -0,0 +1,101 @@
+/*
+ * 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 tests;
+
+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.sufficientlysecure.keychain.support.KeyringTestingHelper;
+
+@RunWith(RobolectricTestRunner.class)
+@org.robolectric.annotation.Config(emulateSdk = 18) // Robolectric doesn't yet support 19
+public class ProviderHelperKeyringTest {
+
+ @Test
+ public void testSavePublicKeyring() throws Exception {
+ Assert.assertTrue(new KeyringTestingHelper(Robolectric.application).addKeyring(Collections.singleton(
+ "/public-key-for-sample.blob"
+ )));
+ }
+
+ // @Test
+ public void testSavePublicKeyringRsa() throws Exception {
+ Assert.assertTrue(new KeyringTestingHelper(Robolectric.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(Robolectric.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(Robolectric.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-Test/src/test/java/org/sufficientlysecure/keychain/tests/UncachedKeyringCanonicalizeTest.java b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/tests/UncachedKeyringCanonicalizeTest.java
new file mode 100644
index 000000000..97e0d8a68
--- /dev/null
+++ b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/tests/UncachedKeyringCanonicalizeTest.java
@@ -0,0 +1,627 @@
+package org.sufficientlysecure.keychain.tests;
+
+import org.junit.BeforeClass;
+import org.junit.runner.RunWith;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.Before;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.shadows.ShadowLog;
+import org.spongycastle.bcpg.BCPGInputStream;
+import org.spongycastle.bcpg.Packet;
+import org.spongycastle.bcpg.PacketTags;
+import org.spongycastle.bcpg.UserIDPacket;
+import org.spongycastle.bcpg.sig.KeyFlags;
+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.PGPContentSignerBuilder;
+import org.spongycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
+import org.spongycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder;
+import org.spongycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.pgp.CanonicalizedKeyRing;
+import org.sufficientlysecure.keychain.pgp.PgpKeyOperation;
+import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
+import org.sufficientlysecure.keychain.pgp.UncachedPublicKey;
+import org.sufficientlysecure.keychain.pgp.WrappedSignature;
+import org.sufficientlysecure.keychain.service.OperationResultParcel;
+import org.sufficientlysecure.keychain.service.OperationResults.EditKeyResult;
+import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
+import org.sufficientlysecure.keychain.support.KeyringTestingHelper;
+import org.sufficientlysecure.keychain.support.KeyringTestingHelper.RawPacket;
+
+import java.io.ByteArrayInputStream;
+import java.util.ArrayList;
+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(RobolectricTestRunner.class)
+@org.robolectric.annotation.Config(emulateSdk = 18) // Robolectric doesn't yet support 19
+public class UncachedKeyringCanonicalizeTest {
+
+ static UncachedKeyRing staticRing;
+ static int totalPackets;
+ UncachedKeyRing ring;
+ ArrayList<RawPacket> onlyA = new ArrayList<RawPacket>();
+ ArrayList<RawPacket> onlyB = new ArrayList<RawPacket>();
+ OperationResultParcel.OperationLog log = new OperationResultParcel.OperationLog();
+ PGPSignatureSubpacketGenerator subHashedPacketsGen;
+ PGPSecretKey secretKey;
+
+ @BeforeClass
+ public static void setUpOnce() throws Exception {
+ ShadowLog.stream = System.out;
+
+ SaveKeyringParcel parcel = new SaveKeyringParcel();
+ parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
+ Constants.choice.algorithm.rsa, 1024, KeyFlags.CERTIFY_OTHER, null));
+ parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
+ Constants.choice.algorithm.rsa, 1024, KeyFlags.SIGN_DATA, null));
+ parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
+ Constants.choice.algorithm.rsa, 1024, KeyFlags.ENCRYPT_COMMS, null));
+
+ parcel.mAddUserIds.add("twi");
+ parcel.mAddUserIds.add("pink");
+ // passphrase is tested in PgpKeyOperationTest, just use empty here
+ parcel.mNewPassphrase = "";
+ PgpKeyOperation op = new PgpKeyOperation(null);
+
+ EditKeyResult 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);
+
+ // just for later reference
+ totalPackets = 9;
+
+ // 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 #1 should be secret key",
+ PacketTags.SECRET_KEY, it.next().tag);
+
+ Assert.assertEquals("packet #2 should be user id",
+ PacketTags.USER_ID, it.next().tag);
+ Assert.assertEquals("packet #3 should be signature",
+ PacketTags.SIGNATURE, it.next().tag);
+
+ Assert.assertEquals("packet #4 should be user id",
+ PacketTags.USER_ID, it.next().tag);
+ Assert.assertEquals("packet #5 should be signature",
+ PacketTags.SIGNATURE, it.next().tag);
+
+ Assert.assertEquals("packet #6 should be secret subkey",
+ PacketTags.SECRET_SUBKEY, it.next().tag);
+ Assert.assertEquals("packet #7 should be signature",
+ PacketTags.SIGNATURE, it.next().tag);
+
+ Assert.assertEquals("packet #8 should be secret subkey",
+ PacketTags.SECRET_SUBKEY, it.next().tag);
+ Assert.assertEquals("packet #9 should be signature",
+ PacketTags.SIGNATURE, it.next().tag);
+
+ Assert.assertFalse("exactly 9 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.getSignaturesForId("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 testSignatureBroken() throws Exception {
+
+ injectEverytype(secretKey, ring, subHashedPacketsGen, true);
+
+ }
+
+ @Test public void testForeignSignature() throws Exception {
+
+ SaveKeyringParcel parcel = new SaveKeyringParcel();
+ parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
+ Constants.choice.algorithm.rsa, 1024, KeyFlags.CERTIFY_OTHER, null));
+ parcel.mAddUserIds.add("trix");
+ PgpKeyOperation op = new PgpKeyOperation(null);
+
+ OperationResultParcel.OperationLog log = new OperationResultParcel.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
+ subHashedPacketsGen.setSignatureCreationTime(false, new Date(new Date().getTime() + 1000 * 1000));
+
+ injectEverytype(secretKey, ring, subHashedPacketsGen);
+
+
+ }
+
+ @Test public void testSignatureLocal() throws Exception {
+
+ // generate future
+ subHashedPacketsGen.setSignatureCreationTime(false, new Date());
+ 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, 6);
+
+ // 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);
+ Assert.assertTrue("second subkey must be able to sign", pKey.canSign());
+
+ 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(), 6);
+
+ // 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(), 6);
+
+ // 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(), 8);
+ modified = KeyringTestingHelper.injectPacket(modified, sig2.getEncoded(), 9);
+ modified = KeyringTestingHelper.injectPacket(modified, sig1.getEncoded(), 10);
+ modified = KeyringTestingHelper.injectPacket(modified, sig3.getEncoded(), 11);
+
+ // 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);
+
+ }
+
+ 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 {
+
+ OperationResultParcel.OperationLog log = new OperationResultParcel.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-Test/src/test/java/org/sufficientlysecure/keychain/tests/UncachedKeyringMergeTest.java b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/tests/UncachedKeyringMergeTest.java
new file mode 100644
index 000000000..977ddfc52
--- /dev/null
+++ b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/tests/UncachedKeyringMergeTest.java
@@ -0,0 +1,397 @@
+package org.sufficientlysecure.keychain.tests;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.shadows.ShadowLog;
+import org.spongycastle.bcpg.PacketTags;
+import org.spongycastle.bcpg.sig.KeyFlags;
+import org.sufficientlysecure.keychain.Constants;
+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.pgp.CanonicalizedPublicKeyRing;
+import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey;
+import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing;
+import org.sufficientlysecure.keychain.service.OperationResultParcel;
+import org.sufficientlysecure.keychain.service.OperationResults.EditKeyResult;
+import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
+import org.sufficientlysecure.keychain.support.KeyringTestingHelper;
+import org.sufficientlysecure.keychain.support.KeyringTestingHelper.RawPacket;
+import org.sufficientlysecure.keychain.util.ProgressScaler;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+
+/** 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(RobolectricTestRunner.class)
+@org.robolectric.annotation.Config(emulateSdk = 18) // Robolectric doesn't yet support 19
+public class UncachedKeyringMergeTest {
+
+ static UncachedKeyRing staticRingA, staticRingB;
+ UncachedKeyRing ringA, ringB;
+ ArrayList<RawPacket> onlyA = new ArrayList<RawPacket>();
+ ArrayList<RawPacket> onlyB = new ArrayList<RawPacket>();
+ OperationResultParcel.OperationLog log = new OperationResultParcel.OperationLog();
+ PgpKeyOperation op;
+ SaveKeyringParcel parcel;
+
+ @BeforeClass
+ public static void setUpOnce() throws Exception {
+ ShadowLog.stream = System.out;
+
+ {
+ SaveKeyringParcel parcel = new SaveKeyringParcel();
+ parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
+ Constants.choice.algorithm.rsa, 1024, KeyFlags.CERTIFY_OTHER, null));
+ parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
+ Constants.choice.algorithm.rsa, 1024, KeyFlags.SIGN_DATA, null));
+
+ parcel.mAddUserIds.add("twi");
+ parcel.mAddUserIds.add("pink");
+ // passphrase is tested in PgpKeyOperationTest, just use empty here
+ parcel.mNewPassphrase = "";
+ PgpKeyOperation op = new PgpKeyOperation(null);
+
+ OperationResultParcel.OperationLog log = new OperationResultParcel.OperationLog();
+
+ EditKeyResult result = op.createSecretKeyRing(parcel);
+ staticRingA = result.getRing();
+ }
+
+ {
+ SaveKeyringParcel parcel = new SaveKeyringParcel();
+ parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
+ Constants.choice.algorithm.rsa, 1024, KeyFlags.CERTIFY_OTHER, null));
+
+ parcel.mAddUserIds.add("shy");
+ // passphrase is tested in PgpKeyOperationTest, just use empty here
+ parcel.mNewPassphrase = "";
+ PgpKeyOperation op = new PgpKeyOperation(null);
+
+ OperationResultParcel.OperationLog log = new OperationResultParcel.OperationLog();
+ EditKeyResult result = op.createSecretKeyRing(parcel);
+ staticRingB = result.getRing();
+ }
+
+ 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, parcel, "").getRing();
+
+ parcel.reset();
+ parcel.mAddUserIds.add("flam");
+ modifiedB = op.modifySecretKeyRing(secretRing, 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(
+ Constants.choice.algorithm.rsa, 1024, KeyFlags.SIGN_DATA, null));
+ modifiedA = op.modifySecretKeyRing(secretRing, parcel, "").getRing();
+ modifiedB = op.modifySecretKeyRing(secretRing, 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, 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("");
+ // sign all user ids
+ modified = secretKey.certifyUserIds(publicRing, publicRing.getPublicKey().getUnorderedUserIds());
+ }
+
+ {
+ 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().getSignaturesForId("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)
+ );
+ }
+ }
+
+ 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, pub + sec, and pub + pub
+
+ try {
+ resultB = a.merge(pubB, log, 0);
+ Assert.assertNotNull("merge must succeed as sec(a)+pub(b)", resultA);
+
+ Assert.assertFalse("result of sec(a)+pub(b) must be same as sec(a)+sec(b)",
+ KeyringTestingHelper.diffKeyrings(
+ resultA.getEncoded(), resultB.getEncoded(), onlyA, onlyB)
+ );
+ } catch (RuntimeException e) {
+ System.out.println("special case, dummy key generation not in yet");
+ }
+
+ 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-Test/src/test/java/org/sufficientlysecure/keychain/tests/UncachedKeyringTest.java b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/tests/UncachedKeyringTest.java
new file mode 100644
index 000000000..15aaa4c5d
--- /dev/null
+++ b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/tests/UncachedKeyringTest.java
@@ -0,0 +1,125 @@
+package org.sufficientlysecure.keychain.tests;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.shadows.ShadowLog;
+import org.spongycastle.bcpg.sig.KeyFlags;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.pgp.PgpKeyOperation;
+import org.sufficientlysecure.keychain.pgp.UncachedKeyRing;
+import org.sufficientlysecure.keychain.pgp.UncachedPublicKey;
+import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
+import org.sufficientlysecure.keychain.service.OperationResultParcel;
+import org.sufficientlysecure.keychain.service.OperationResults.EditKeyResult;
+import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
+import org.sufficientlysecure.keychain.support.KeyringTestingHelper.RawPacket;
+import org.sufficientlysecure.keychain.util.ProgressScaler;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+@org.robolectric.annotation.Config(emulateSdk = 18) // Robolectric doesn't yet support 19
+public class UncachedKeyringTest {
+
+ static UncachedKeyRing staticRing, staticPubRing;
+ UncachedKeyRing ring, pubRing;
+ ArrayList<RawPacket> onlyA = new ArrayList<RawPacket>();
+ ArrayList<RawPacket> onlyB = new ArrayList<RawPacket>();
+ PgpKeyOperation op;
+ SaveKeyringParcel parcel;
+
+ @BeforeClass
+ public static void setUpOnce() throws Exception {
+ ShadowLog.stream = System.out;
+
+ SaveKeyringParcel parcel = new SaveKeyringParcel();
+ parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
+ Constants.choice.algorithm.rsa, 1024, KeyFlags.CERTIFY_OTHER, null));
+ parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
+ Constants.choice.algorithm.rsa, 1024, KeyFlags.SIGN_DATA, null));
+ parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(
+ Constants.choice.algorithm.rsa, 1024, KeyFlags.ENCRYPT_COMMS, null));
+
+ parcel.mAddUserIds.add("twi");
+ parcel.mAddUserIds.add("pink");
+ // passphrase is tested in PgpKeyOperationTest, just use empty here
+ parcel.mNewPassphrase = "";
+ PgpKeyOperation op = new PgpKeyOperation(null);
+
+ EditKeyResult 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");
+
+ Iterator<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();
+ }
+
+}