diff options
Diffstat (limited to 'OpenKeychain-Test/src/test/java')
10 files changed, 2171 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..398b2393e --- /dev/null +++ b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/support/KeyringTestingHelper.java @@ -0,0 +1,331 @@ +/* + * 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.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.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.getWrappedPublicKeyRing(masterKeyId); + +        // A different ID should still fail +        retrieveKeyAndExpectNotFound(providerHelper, masterKeyId - 1); + +        return saveSuccess; +    } + +    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) { +                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 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; + +    } + +    private void retrieveKeyAndExpectNotFound(ProviderHelper providerHelper, long masterKeyId) { +        try { +            providerHelper.getWrappedPublicKeyRing(masterKeyId); +            throw new AssertionError("Was expecting the previous call to fail!"); +        } catch (ProviderHelper.NotFoundException expectedException) { +            // good +        } +    } + +} 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..f06fe0072 --- /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.WrappedPublicKeyRing; +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 WrappedPublicKeyRing getWrappedPublicKeyRing(Uri id) throws NotFoundException { +        byte[] data = TestDataUtil.readFully(getClass().getResourceAsStream("/public-key-for-sample.blob")); +        return new WrappedPublicKeyRing(data, false, 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/support/UncachedKeyringTestingHelper.java b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/support/UncachedKeyringTestingHelper.java new file mode 100644 index 000000000..6467d3f32 --- /dev/null +++ b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/support/UncachedKeyringTestingHelper.java @@ -0,0 +1,297 @@ +/* + * 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.BCPGKey; +import org.spongycastle.bcpg.PublicKeyPacket; +import org.spongycastle.bcpg.SignatureSubpacket; +import org.spongycastle.openpgp.PGPException; +import org.spongycastle.openpgp.PGPPublicKey; +import org.spongycastle.openpgp.PGPSignature; +import org.spongycastle.openpgp.PGPSignatureSubpacketVector; +import org.spongycastle.openpgp.PGPUserAttributeSubpacketVector; +import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; +import org.sufficientlysecure.keychain.pgp.UncachedPublicKey; +import org.sufficientlysecure.keychain.service.OperationResultParcel; + +import java.util.Arrays; + +/** + * Created by art on 28/06/14. + */ +public class UncachedKeyringTestingHelper { + +    public static boolean compareRing(UncachedKeyRing keyRing1, UncachedKeyRing keyRing2) { +        OperationResultParcel.OperationLog operationLog = new OperationResultParcel.OperationLog(); +        UncachedKeyRing canonicalized = keyRing1.canonicalize(operationLog, 0); + +        if (canonicalized == null) { +            throw new AssertionError("Canonicalization failed; messages: [" + operationLog.toList() + "]"); +        } + +        return TestDataUtil.iterEquals(canonicalized.getPublicKeys(), keyRing2.getPublicKeys(), new +                TestDataUtil.EqualityChecker<UncachedPublicKey>() { +                    @Override +                    public boolean areEquals(UncachedPublicKey lhs, UncachedPublicKey rhs) { +                        return comparePublicKey(lhs, rhs); +                    } +                }); +    } + +    public static boolean comparePublicKey(UncachedPublicKey key1, UncachedPublicKey key2) { +        boolean equal = true; + +        if (key1.canAuthenticate() != key2.canAuthenticate()) { +            return false; +        } +        if (key1.canCertify() != key2.canCertify()) { +            return false; +        } +        if (key1.canEncrypt() != key2.canEncrypt()) { +            return false; +        } +        if (key1.canSign() != key2.canSign()) { +            return false; +        } +        if (key1.getAlgorithm() != key2.getAlgorithm()) { +            return false; +        } +        if (key1.getBitStrength() != key2.getBitStrength()) { +            return false; +        } +        if (!TestDataUtil.equals(key1.getCreationTime(), key2.getCreationTime())) { +            return false; +        } +        if (!TestDataUtil.equals(key1.getExpiryTime(), key2.getExpiryTime())) { +            return false; +        } +        if (!Arrays.equals(key1.getFingerprint(), key2.getFingerprint())) { +            return false; +        } +        if (key1.getKeyId() != key2.getKeyId()) { +            return false; +        } +        if (key1.getKeyUsage() != key2.getKeyUsage()) { +            return false; +        } +        if (!TestDataUtil.equals(key1.getPrimaryUserId(), key2.getPrimaryUserId())) { +            return false; +        } + +        // Ooops, getPublicKey is due to disappear. But then how to compare? +        if (!keysAreEqual(key1.getPublicKey(), key2.getPublicKey())) { +            return false; +        } + +        return equal; +    } + +    public static boolean keysAreEqual(PGPPublicKey a, PGPPublicKey b) { + +        if (a.getAlgorithm() != b.getAlgorithm()) { +            return false; +        } + +        if (a.getBitStrength() != b.getBitStrength()) { +            return false; +        } + +        if (!TestDataUtil.equals(a.getCreationTime(), b.getCreationTime())) { +            return false; +        } + +        if (!Arrays.equals(a.getFingerprint(), b.getFingerprint())) { +            return false; +        } + +        if (a.getKeyID() != b.getKeyID()) { +            return false; +        } + +        if (!pubKeyPacketsAreEqual(a.getPublicKeyPacket(), b.getPublicKeyPacket())) { +            return false; +        } + +        if (a.getVersion() != b.getVersion()) { +            return false; +        } + +        if (a.getValidDays() != b.getValidDays()) { +            return false; +        } + +        if (a.getValidSeconds() != b.getValidSeconds()) { +            return false; +        } + +        if (!Arrays.equals(a.getTrustData(), b.getTrustData())) { +            return false; +        } + +        if (!TestDataUtil.iterEquals(a.getUserIDs(), b.getUserIDs())) { +            return false; +        } + +        if (!TestDataUtil.iterEquals(a.getUserAttributes(), b.getUserAttributes(), +                new TestDataUtil.EqualityChecker<PGPUserAttributeSubpacketVector>() { +                    public boolean areEquals(PGPUserAttributeSubpacketVector lhs, PGPUserAttributeSubpacketVector rhs) { +                        // For once, BC defines equals, so we use it implicitly. +                        return TestDataUtil.equals(lhs, rhs); +                    } +                } +        )) { +            return false; +        } + + +        if (!TestDataUtil.iterEquals(a.getSignatures(), b.getSignatures(), +                new TestDataUtil.EqualityChecker<PGPSignature>() { +                    public boolean areEquals(PGPSignature lhs, PGPSignature rhs) { +                        return signaturesAreEqual(lhs, rhs); +                    } +                } +        )) { +            return false; +        } + +        return true; +    } + +    public static boolean signaturesAreEqual(PGPSignature a, PGPSignature b) { + +        if (a.getVersion() != b.getVersion()) { +            return false; +        } + +        if (a.getKeyAlgorithm() != b.getKeyAlgorithm()) { +            return false; +        } + +        if (a.getHashAlgorithm() != b.getHashAlgorithm()) { +            return false; +        } + +        if (a.getSignatureType() != b.getSignatureType()) { +            return false; +        } + +        try { +            if (!Arrays.equals(a.getSignature(), b.getSignature())) { +                return false; +            } +        } catch (PGPException ex) { +            throw new RuntimeException(ex); +        } + +        if (a.getKeyID() != b.getKeyID()) { +            return false; +        } + +        if (!TestDataUtil.equals(a.getCreationTime(), b.getCreationTime())) { +            return false; +        } + +        if (!Arrays.equals(a.getSignatureTrailer(), b.getSignatureTrailer())) { +            return false; +        } + +        if (!subPacketVectorsAreEqual(a.getHashedSubPackets(), b.getHashedSubPackets())) { +            return false; +        } + +        if (!subPacketVectorsAreEqual(a.getUnhashedSubPackets(), b.getUnhashedSubPackets())) { +            return false; +        } + +        return true; +    } + +    private static boolean subPacketVectorsAreEqual(PGPSignatureSubpacketVector aHashedSubPackets, PGPSignatureSubpacketVector bHashedSubPackets) { +        for (int i = 0; i < Byte.MAX_VALUE; i++) { +            if (!TestDataUtil.iterEquals(Arrays.asList(aHashedSubPackets.getSubpackets(i)).iterator(), +                    Arrays.asList(bHashedSubPackets.getSubpackets(i)).iterator(), +                    new TestDataUtil.EqualityChecker<SignatureSubpacket>() { +                        @Override +                        public boolean areEquals(SignatureSubpacket lhs, SignatureSubpacket rhs) { +                            return signatureSubpacketsAreEqual(lhs, rhs); +                        } +                    } +            )) { +                return false; +            } + +        } +        return true; +    } + +    private static boolean signatureSubpacketsAreEqual(SignatureSubpacket lhs, SignatureSubpacket rhs) { +        if (lhs.getType() != rhs.getType()) { +            return false; +        } +        if (!Arrays.equals(lhs.getData(), rhs.getData())) { +            return false; +        } +        return true; +    } + +    public static boolean pubKeyPacketsAreEqual(PublicKeyPacket a, PublicKeyPacket b) { + +        if (a.getAlgorithm() != b.getAlgorithm()) { +            return false; +        } + +        if (!bcpgKeysAreEqual(a.getKey(), b.getKey())) { +            return false; +        } + +        if (!TestDataUtil.equals(a.getTime(), b.getTime())) { +            return false; +        } + +        if (a.getValidDays() != b.getValidDays()) { +            return false; +        } + +        if (a.getVersion() != b.getVersion()) { +            return false; +        } + +        return true; +    } + +    public static boolean bcpgKeysAreEqual(BCPGKey a, BCPGKey b) { + +        if (!TestDataUtil.equals(a.getFormat(), b.getFormat())) { +            return false; +        } + +        if (!Arrays.equals(a.getEncoded(), b.getEncoded())) { +            return false; +        } + +        return true; +    } + + +    public void doTestCanonicalize(UncachedKeyRing inputKeyRing, UncachedKeyRing expectedKeyRing) { +        if (!compareRing(inputKeyRing, expectedKeyRing)) { +            throw new AssertionError("Expected [" + inputKeyRing + "] to match [" + expectedKeyRing + "]"); +        } +    } + +} 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..5c6072c25 --- /dev/null +++ b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/tests/PgpKeyOperationTest.java @@ -0,0 +1,783 @@ +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.PgpKeyOperation; +import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; +import org.sufficientlysecure.keychain.pgp.UncachedPublicKey; +import org.sufficientlysecure.keychain.pgp.WrappedSecretKeyRing; +import org.sufficientlysecure.keychain.pgp.WrappedSignature; +import org.sufficientlysecure.keychain.service.OperationResultParcel; +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); + +        OperationResultParcel.OperationLog log = new OperationResultParcel.OperationLog(); +        staticRing = op.createSecretKeyRing(parcel, log, 0); + +        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 testAlgorithmChoice() { + +        OperationResultParcel.OperationLog log = new OperationResultParcel.OperationLog(); + +        { +            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, log, 0); + +            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, log, 0); + +            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, log, 0); +            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, log, 0); +            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, log, 0); +            Assert.assertNull("creating ring without user ids should fail", ring); +        } + +        { +            parcel.reset(); +            parcel.mAddUserIds.add("shy"); +            parcel.mNewPassphrase = passphrase; + +            UncachedKeyRing ring = op.createSecretKeyRing(parcel, log, 0); +            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"); +        OperationResultParcel.OperationLog log = new OperationResultParcel.OperationLog(); +        ring = op.createSecretKeyRing(parcel, log, 0); + +        Assert.assertEquals("the keyring should contain only the master key", +                1, ring.getAvailableSubkeys().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, ring.getAvailableSubkeys().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(); + +            WrappedSecretKeyRing secretRing = new WrappedSecretKeyRing(ring.getEncoded(), false, 0); +            OperationResultParcel.OperationLog log = new OperationResultParcel.OperationLog(); +            UncachedKeyRing modified = op.modifySecretKeyRing(secretRing, parcel, passphrase, log, 0); + +            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(); + +            WrappedSecretKeyRing secretRing = new WrappedSecretKeyRing(ring.getEncoded(), false, 0); +            OperationResultParcel.OperationLog log = new OperationResultParcel.OperationLog(); +            UncachedKeyRing modified = op.modifySecretKeyRing(secretRing, parcel, passphrase, log, 0); + +            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; + +            WrappedSecretKeyRing secretRing = new WrappedSecretKeyRing(ring.getEncoded(), false, 0); +            OperationResultParcel.OperationLog log = new OperationResultParcel.OperationLog(); +            UncachedKeyRing modified = op.modifySecretKeyRing(secretRing, parcel, passphrase, log, 0); + +            Assert.assertNull("keyring modification with bad fingerprint should fail", modified); +        } + +        { +            SaveKeyringParcel parcel = new SaveKeyringParcel(); +            parcel.mMasterKeyId = ring.getMasterKeyId(); +            parcel.mFingerprint = null; + +            WrappedSecretKeyRing secretRing = new WrappedSecretKeyRing(ring.getEncoded(), false, 0); +            OperationResultParcel.OperationLog log = new OperationResultParcel.OperationLog(); +            UncachedKeyRing modified = op.modifySecretKeyRing(secretRing, parcel, passphrase, log, 0); + +            Assert.assertNull("keyring modification with null fingerprint should fail", modified); +        } + +        { +            WrappedSecretKeyRing secretRing = new WrappedSecretKeyRing(ring.getEncoded(), false, 0); +            OperationResultParcel.OperationLog log = new OperationResultParcel.OperationLog(); +            UncachedKeyRing modified = op.modifySecretKeyRing(secretRing, parcel, "bad passphrase", log, 0); + +            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, 77, KeyFlags.SIGN_DATA, null)); + +            WrappedSecretKeyRing secretRing = new WrappedSecretKeyRing(ring.getEncoded(), false, 0); +            OperationResultParcel.OperationLog log = new OperationResultParcel.OperationLog(); +            modified = op.modifySecretKeyRing(secretRing, parcel, passphrase, log, 0); + +            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)); + +            WrappedSecretKeyRing secretRing = new WrappedSecretKeyRing(ring.getEncoded(), false, 0); +            OperationResultParcel.OperationLog log = new OperationResultParcel.OperationLog(); +            modified = op.modifySecretKeyRing(secretRing, parcel, passphrase, log, 0); + +            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; +        { +            Iterator<UncachedPublicKey> it = ring.getPublicKeys(); +            it.next(); +            keyId = it.next().getKeyId(); +        } + +        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)); + +            WrappedSecretKeyRing secretRing = new WrappedSecretKeyRing(ring.getEncoded(), false, 0); +            OperationResultParcel.OperationLog log = new OperationResultParcel.OperationLog(); +            modified = op.modifySecretKeyRing(secretRing, parcel, passphrase, log, 0); + +            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)); + +            WrappedSecretKeyRing secretRing = new WrappedSecretKeyRing(ring.getEncoded(), false, 0); +            OperationResultParcel.OperationLog log = new OperationResultParcel.OperationLog(); +            modified = op.modifySecretKeyRing(secretRing, parcel, passphrase, log, 0); + +            Assert.assertNull("modifying non-existent subkey should fail", modified); +        } + +    } + +    @Test +    public void testSubkeyRevoke() throws Exception { + +        long keyId; +        { +            Iterator<UncachedPublicKey> it = ring.getPublicKeys(); +            it.next(); +            keyId = it.next().getKeyId(); +        } + +        int flags = ring.getPublicKey(keyId).getKeyUsage(); + +        UncachedKeyRing modified; + +        { + +            parcel.reset(); +            parcel.mRevokeSubKeys.add(123L); + +            WrappedSecretKeyRing secretRing = new WrappedSecretKeyRing(ring.getEncoded(), false, 0); +            OperationResultParcel.OperationLog log = new OperationResultParcel.OperationLog(); +            UncachedKeyRing otherModified = op.modifySecretKeyRing(secretRing, parcel, passphrase, log, 0); + +            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; + +            WrappedSecretKeyRing secretRing = new WrappedSecretKeyRing(modified.getEncoded(), false, 0); +            OperationResultParcel.OperationLog log = new OperationResultParcel.OperationLog(); +            UncachedKeyRing otherModified = op.modifySecretKeyRing(secretRing, parcel, passphrase, log, 0); + +            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("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"; + +            WrappedSecretKeyRing secretRing = new WrappedSecretKeyRing(ring.getEncoded(), false, 0); +            OperationResultParcel.OperationLog log = new OperationResultParcel.OperationLog(); +            modified = op.modifySecretKeyRing(secretRing, parcel, passphrase, log, 0); + +            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()); +            WrappedSecretKeyRing secretRing = new WrappedSecretKeyRing(ring.getEncoded(), false, 0); + +            PgpKeyOperation op = new PgpKeyOperation(null); +            OperationResultParcel.OperationLog log = new OperationResultParcel.OperationLog(); +            UncachedKeyRing rawModified = op.modifySecretKeyRing(secretRing, parcel, passphrase, log, 0); +            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; +            } + +            UncachedKeyRing modified = rawModified.canonicalize(log, 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; + +        } catch (IOException e) { +            throw new AssertionFailedError("error during encoding!"); +        } +    } + +    @Test +    public void testVerifySuccess() throws Exception { + +        UncachedKeyRing expectedKeyRing = KeyringBuilder.correctRing(); +        UncachedKeyRing inputKeyRing = KeyringBuilder.ringWithExtraIncorrectSignature(); + +        OperationResultParcel.OperationLog log = new OperationResultParcel.OperationLog(); +        UncachedKeyRing canonicalizedRing = inputKeyRing.canonicalize(log, 0); + +        if (canonicalizedRing == null) { +            throw new AssertionError("Canonicalization failed; messages: [" + log + "]"); +        } + +        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..ab5c1f1ec --- /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("/extern/OpenPGP-Haskell/tests/data/" + file); +        } +        return prependedFiles; +    } +} 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..66e31c272 --- /dev/null +++ b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/tests/UncachedKeyringTest.java @@ -0,0 +1,106 @@ +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.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.WrappedSignature; +import org.sufficientlysecure.keychain.service.OperationResultParcel; +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.Iterator; + +@RunWith(RobolectricTestRunner.class) +@org.robolectric.annotation.Config(emulateSdk = 18) // Robolectric doesn't yet support 19 +public class UncachedKeyringTest { + +    static UncachedKeyRing staticRing; +    UncachedKeyRing ring; +    ArrayList<RawPacket> onlyA = new ArrayList<RawPacket>(); +    ArrayList<RawPacket> onlyB = new ArrayList<RawPacket>(); +    OperationResultParcel.OperationLog log = new OperationResultParcel.OperationLog(); + +    @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); + +        OperationResultParcel.OperationLog log = new OperationResultParcel.OperationLog(); +        staticRing = op.createSecretKeyRing(parcel, log, 0); + +        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; +    } + +    @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 secret key", +                PacketTags.USER_ID, it.next().tag); +        Assert.assertEquals("packet #3 should be secret key", +                PacketTags.SIGNATURE, it.next().tag); + +        Assert.assertEquals("packet #4 should be secret key", +                PacketTags.USER_ID, it.next().tag); +        Assert.assertEquals("packet #5 should be secret key", +                PacketTags.SIGNATURE, it.next().tag); + +        Assert.assertEquals("packet #6 should be secret key", +                PacketTags.SECRET_SUBKEY, it.next().tag); +        Assert.assertEquals("packet #7 should be secret key", +                PacketTags.SIGNATURE, it.next().tag); + +        Assert.assertEquals("packet #8 should be secret key", +                PacketTags.SECRET_SUBKEY, it.next().tag); +        Assert.assertEquals("packet #9 should be secret key", +                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()); + +    } + +}  | 
