diff options
68 files changed, 3342 insertions, 1093 deletions
| diff --git a/.gitmodules b/.gitmodules index b7b0e1173..956362d4b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -31,6 +31,6 @@  [submodule "extern/minidns"]  	path = extern/minidns  	url = https://github.com/open-keychain/minidns.git -[submodule "OpenKeychain/src/test/resources/extern/OpenPGP-Haskell"] -	path = OpenKeychain/src/test/resources/extern/OpenPGP-Haskell +[submodule "OpenKeychain-Test/src/test/resources/extern/OpenPGP-Haskell"] +	path = OpenKeychain-Test/src/test/resources/extern/OpenPGP-Haskell  	url = https://github.com/singpolyma/OpenPGP-Haskell.git diff --git a/.travis.yml b/.travis.yml index 5da831e61..fd6e55ea7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@  language: java -jdk: oraclejdk7 +jdk: openjdk7  before_install:      # Install base Android SDK      - sudo apt-get update -qq @@ -12,6 +12,9 @@ before_install:      # Install required Android components.      #- echo "y" | android update sdk -a --filter build-tools-19.1.0,android-19,platform-tools,extra-android-support,extra-android-m2repository --no-ui --force      - ( sleep 5 && while [ 1 ]; do sleep 1; echo y; done ) | android update sdk --no-ui --all --force --filter build-tools-19.1.0,android-19,platform-tools,extra-android-support,extra-android-m2repository +    - ./install-custom-gradle-test-plugin.sh  install: echo "Installation done" -script: gradle assemble -S -q +script: +    - gradle assemble -S -q +    - gradle --info OpenKeychain-Test:testDebug diff --git a/OpenKeychain-Test/build.gradle b/OpenKeychain-Test/build.gradle new file mode 100644 index 000000000..d795ace3d --- /dev/null +++ b/OpenKeychain-Test/build.gradle @@ -0,0 +1,80 @@ +apply plugin: 'java' +apply plugin: 'android-test' + +dependencies { +    testCompile 'junit:junit:4.11' +    testCompile 'com.google.android:android:4.1.1.4' +    testCompile('com.squareup:fest-android:1.0.+') { exclude module: 'support-v4' } +    testCompile ('org.robolectric:robolectric:2.3') { +        exclude module: 'classworlds' +        exclude module: 'maven-artifact' +        exclude module: 'maven-artifact-manager' +        exclude module: 'maven-error-diagnostics' +        exclude module: 'maven-model' +        exclude module: 'maven-plugin-registry' +        exclude module: 'maven-profile' +        exclude module: 'maven-project' +        exclude module: 'maven-settings' +        exclude module: 'nekohtml' +        exclude module: 'plexus-container-default' +        exclude module: 'plexus-interpolation' +        exclude module: 'plexus-utils' +        exclude module: 'support-v4' // crazy but my android studio don't like this dependency and to fix it remove .idea and re import project +        exclude module: 'wagon-file' +        exclude module: 'wagon-http-lightweight' +        exclude module: 'wagon-http-shared' +        exclude module: 'wagon-provider-api' +    } +} + +android { +    projectUnderTest ':OpenKeychain' +} + +// new workaround to force add custom output dirs for android studio +task addTest { +    def file = file(project.name + ".iml") +    doLast { +        try { +            def parsedXml = (new XmlParser()).parse(file) +            def node = parsedXml.component[1] +            def outputNode = parsedXml.component[1].output[0] +            def outputTestNode = parsedXml.component[1].'output-test'[0] +            def rewrite = false + +            new Node(node, 'sourceFolder', ['url': 'file://$MODULE_DIR$/' + "${it}", 'isTestSource': "true"]) + +            if(outputNode == null) { +                new Node(node, 'output', ['url': 'file://$MODULE_DIR$/build/resources/testDebug']) +            } else { +                if(outputNode.attributes['url'] != 'file://$MODULE_DIR$/build/resources/testDebug') { +                    outputNode.attributes = ['url': 'file://$MODULE_DIR$/build/resources/testDebug'] +                    rewrite = true +                } +            } + +            if(outputTestNode == null) { +                new Node(node, 'output-test', ['url': 'file://$MODULE_DIR$/build/test-classes/debug']) +            } else { +                if(outputTestNode.attributes['url'] != 'file://$MODULE_DIR$/build/test-classes/debug') { +                    outputTestNode.attributes = ['url': 'file://$MODULE_DIR$/build/test-classes/debug'] +                    rewrite = true +                } +            } + +            if(rewrite) { +                def writer = new StringWriter() +                new XmlNodePrinter(new PrintWriter(writer)).print(parsedXml) +                file.text = writer.toString() +            } +        } catch (FileNotFoundException e) { +            // iml not found, common on command line only builds +        } + +    } +} + +// always do the addtest on prebuild +gradle.projectsEvaluated { +    testDebugClasses.dependsOn(addTest) +} 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/src/main/java/org/sufficientlysecure/keychain/testsupport/PgpVerifyTestingHelper.java b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/support/PgpVerifyTestingHelper.java index 1ab5878cc..dd5786512 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/testsupport/PgpVerifyTestingHelper.java +++ b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/support/PgpVerifyTestingHelper.java @@ -1,4 +1,20 @@ -package org.sufficientlysecure.keychain.testsupport; +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; 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/src/test/java/tests/PgpDecryptVerifyTest.java b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/tests/PgpDecryptVerifyTest.java index d759bce05..158650012 100644 --- a/OpenKeychain/src/test/java/tests/PgpDecryptVerifyTest.java +++ b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/tests/PgpDecryptVerifyTest.java @@ -1,3 +1,20 @@ +/* + * 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; @@ -5,7 +22,7 @@ import org.junit.Test;  import org.junit.runner.RunWith;  import org.robolectric.*;  import org.openintents.openpgp.OpenPgpSignatureResult; -import org.sufficientlysecure.keychain.testsupport.PgpVerifyTestingHelper; +import org.sufficientlysecure.keychain.support.PgpVerifyTestingHelper;  @RunWith(RobolectricTestRunner.class)  @org.robolectric.annotation.Config(emulateSdk = 18) // Robolectric doesn't yet support 19 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/src/test/java/tests/ProviderHelperKeyringTest.java b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/tests/ProviderHelperKeyringTest.java index 3d48c2f97..ab5c1f1ec 100644 --- a/OpenKeychain/src/test/java/tests/ProviderHelperKeyringTest.java +++ b/OpenKeychain-Test/src/test/java/org/sufficientlysecure/keychain/tests/ProviderHelperKeyringTest.java @@ -1,3 +1,20 @@ +/* + * 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; @@ -9,9 +26,7 @@ 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.testsupport.KeyringTestingHelper; -import org.sufficientlysecure.keychain.testsupport.PgpVerifyTestingHelper; +import org.sufficientlysecure.keychain.support.KeyringTestingHelper;  @RunWith(RobolectricTestRunner.class)  @org.robolectric.annotation.Config(emulateSdk = 18) // Robolectric doesn't yet support 19 @@ -24,7 +39,7 @@ public class ProviderHelperKeyringTest {          )));      } -    @Test +    // @Test      public void testSavePublicKeyringRsa() throws Exception {          Assert.assertTrue(new KeyringTestingHelper(Robolectric.application).addKeyring(prependResourcePath(Arrays.asList(                          "000001-006.public_key", @@ -45,7 +60,7 @@ public class ProviderHelperKeyringTest {                  ))));      } -    @Test +    // @Test      public void testSavePublicKeyringDsa() throws Exception {          Assert.assertTrue(new KeyringTestingHelper(Robolectric.application).addKeyring(prependResourcePath(Arrays.asList(                          "000016-006.public_key", @@ -62,7 +77,7 @@ public class ProviderHelperKeyringTest {                  ))));      } -    @Test +    // @Test      public void testSavePublicKeyringDsa2() throws Exception {          Assert.assertTrue(new KeyringTestingHelper(Robolectric.application).addKeyring(prependResourcePath(Arrays.asList(                          "000027-006.public_key", 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()); + +    } + +} diff --git a/OpenKeychain-Test/src/test/resources/public-key-canonicalize.blob b/OpenKeychain-Test/src/test/resources/public-key-canonicalize.blobBinary files differ new file mode 100644 index 000000000..3450824c1 --- /dev/null +++ b/OpenKeychain-Test/src/test/resources/public-key-canonicalize.blob diff --git a/OpenKeychain/src/test/resources/public-key-for-sample.blob b/OpenKeychain-Test/src/test/resources/public-key-for-sample.blobBinary files differ index 4aa91510b..4aa91510b 100644 --- a/OpenKeychain/src/test/resources/public-key-for-sample.blob +++ b/OpenKeychain-Test/src/test/resources/public-key-for-sample.blob diff --git a/OpenKeychain/src/test/resources/sample-altered.txt b/OpenKeychain-Test/src/test/resources/sample-altered.txt index 458821f81..458821f81 100644 --- a/OpenKeychain/src/test/resources/sample-altered.txt +++ b/OpenKeychain-Test/src/test/resources/sample-altered.txt diff --git a/OpenKeychain/src/test/resources/sample.txt b/OpenKeychain-Test/src/test/resources/sample.txt index c0065f78d..c0065f78d 100644 --- a/OpenKeychain/src/test/resources/sample.txt +++ b/OpenKeychain-Test/src/test/resources/sample.txt diff --git a/OpenKeychain/build.gradle b/OpenKeychain/build.gradle index f419141b4..a6c01543c 100644 --- a/OpenKeychain/build.gradle +++ b/OpenKeychain/build.gradle @@ -21,13 +21,6 @@ dependencies {      compile project(':extern:minidns')      compile project(':extern:KeybaseLib:Lib') - -    // Unit tests are run with Robolectric -    testCompile 'junit:junit:4.11' -    testCompile 'org.robolectric:robolectric:2.3' -    testCompile 'com.squareup:fest-android:1.0.8' -    testCompile 'com.google.android:android:4.1.1.4' -    // compile dependencies are automatically also included in testCompile  }  android { @@ -86,7 +79,7 @@ android {  // NOTE: This disables Lint!  tasks.whenTaskAdded { task -> -    if (task.name.equals("lint")) { +    if (task.name.contains("lint")) {          task.enabled = false      }  } diff --git a/OpenKeychain/src/main/AndroidManifest.xml b/OpenKeychain/src/main/AndroidManifest.xml index 2283235e7..af09019e8 100644 --- a/OpenKeychain/src/main/AndroidManifest.xml +++ b/OpenKeychain/src/main/AndroidManifest.xml @@ -81,9 +81,14 @@              </intent-filter>          </activity>          <activity -            android:name=".ui.WizardActivity" +            android:name=".ui.FirstTimeActivity"              android:configChanges="orientation|screenSize|keyboardHidden|keyboard" -            android:label="@string/title_wizard" +            android:label="@string/app_name" +            android:windowSoftInputMode="stateHidden" /> +        <activity +            android:name=".ui.CreateKeyActivity" +            android:configChanges="orientation|screenSize|keyboardHidden|keyboard" +            android:label="@string/title_create_key"              android:windowSoftInputMode="stateHidden" />          <activity              android:name=".ui.EditKeyActivityOld" diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java index 86bc3fa5b..33ab52bca 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java @@ -69,6 +69,7 @@ public final class Constants {          public static final String KEY_SERVERS = "keyServers";          public static final String KEY_SERVERS_DEFAULT_VERSION = "keyServersDefaultVersion";          public static final String CONCEAL_PGP_APPLICATION = "concealPgpApplication"; +        public static final String FIRST_TIME = "firstTime";      }      public static final class Defaults { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/helper/Preferences.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/helper/Preferences.java index fd8267a59..e55c14a2a 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/helper/Preferences.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/helper/Preferences.java @@ -139,6 +139,16 @@ public class Preferences {          editor.commit();      } +    public boolean getFirstTime() { +        return mSharedPreferences.getBoolean(Constants.Pref.FIRST_TIME, true); +    } + +    public void setFirstTime(boolean value) { +        SharedPreferences.Editor editor = mSharedPreferences.edit(); +        editor.putBoolean(Constants.Pref.FIRST_TIME, value); +        editor.commit(); +    } +      public String[] getKeyServers() {          String rawData = mSharedPreferences.getString(Constants.Pref.KEY_SERVERS,                  Constants.Defaults.KEY_SERVERS); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/ImportKeysListEntry.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/ImportKeysListEntry.java index 0a49cb629..30e93f957 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/ImportKeysListEntry.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/keyimport/ImportKeysListEntry.java @@ -250,7 +250,7 @@ public class ImportKeysListEntry implements Serializable, Parcelable {          mHashCode = key.hashCode(); -        mPrimaryUserId = key.getPrimaryUserId(); +        mPrimaryUserId = key.getPrimaryUserIdWithFallback();          mUserIds = key.getUnorderedUserIds();          // if there was no user id flagged as primary, use the first one diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/KeyRing.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/KeyRing.java index 47b827677..129ffba3e 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/KeyRing.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/KeyRing.java @@ -22,8 +22,10 @@ public abstract class KeyRing {      abstract public String getPrimaryUserId() throws PgpGeneralException; -    public String[] getSplitPrimaryUserId() throws PgpGeneralException { -        return splitUserId(getPrimaryUserId()); +    abstract public String getPrimaryUserIdWithFallback() throws PgpGeneralException; + +    public String[] getSplitPrimaryUserIdWithFallback() throws PgpGeneralException { +        return splitUserId(getPrimaryUserIdWithFallback());      }      abstract public boolean isRevoked() throws PgpGeneralException; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerify.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerify.java index a5ccfbd3b..db9e2c6c6 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerify.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerify.java @@ -258,7 +258,7 @@ public class PgpDecryptVerify {                      continue;                  }                  // get subkey which has been used for this encryption packet -                secretEncryptionKey = secretKeyRing.getSubKey(encData.getKeyID()); +                secretEncryptionKey = secretKeyRing.getSecretKey(encData.getKeyID());                  if (secretEncryptionKey == null) {                      // continue with the next packet in the while loop                      continue; @@ -393,7 +393,7 @@ public class PgpDecryptVerify {                      signingRing = mProviderHelper.getWrappedPublicKeyRing(                              KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(sigKeyId)                      ); -                    signingKey = signingRing.getSubkey(sigKeyId); +                    signingKey = signingRing.getPublicKey(sigKeyId);                      signatureIndex = i;                  } catch (ProviderHelper.NotFoundException e) {                      Log.d(Constants.TAG, "key not found!"); @@ -409,7 +409,7 @@ public class PgpDecryptVerify {                  signatureResultBuilder.knownKey(true);                  signatureResultBuilder.keyId(signingRing.getMasterKeyId());                  try { -                    signatureResultBuilder.userId(signingRing.getPrimaryUserId()); +                    signatureResultBuilder.userId(signingRing.getPrimaryUserIdWithFallback());                  } catch(PgpGeneralException e) {                      Log.d(Constants.TAG, "No primary user id in key " + signingRing.getMasterKeyId());                  } @@ -578,7 +578,7 @@ public class PgpDecryptVerify {                  signingRing = mProviderHelper.getWrappedPublicKeyRing(                          KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(sigKeyId)                  ); -                signingKey = signingRing.getSubkey(sigKeyId); +                signingKey = signingRing.getPublicKey(sigKeyId);                  signatureIndex = i;              } catch (ProviderHelper.NotFoundException e) {                  Log.d(Constants.TAG, "key not found!"); @@ -596,7 +596,7 @@ public class PgpDecryptVerify {              signatureResultBuilder.knownKey(true);              signatureResultBuilder.keyId(signingRing.getMasterKeyId());              try { -                signatureResultBuilder.userId(signingRing.getPrimaryUserId()); +                signatureResultBuilder.userId(signingRing.getPrimaryUserIdWithFallback());              } catch(PgpGeneralException e) {                  Log.d(Constants.TAG, "No primary user id in key " + signingRing.getMasterKeyId());              } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperation.java index 9a08290e4..bd8a9201e 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpKeyOperation.java @@ -65,10 +65,8 @@ import java.security.NoSuchProviderException;  import java.security.SecureRandom;  import java.security.SignatureException;  import java.util.Arrays; -import java.util.Calendar;  import java.util.Date;  import java.util.Iterator; -import java.util.TimeZone;  /**   * This class is the single place where ALL operations that actually modify a PGP public or secret @@ -104,11 +102,12 @@ public class PgpKeyOperation {      }      /** Creates new secret key. */ -    private PGPKeyPair createKey(int algorithmChoice, int keySize) throws PgpGeneralMsgIdException { +    private PGPKeyPair createKey(int algorithmChoice, int keySize, OperationLog log, int indent) {          try {              if (keySize < 512) { -                throw new PgpGeneralMsgIdException(R.string.error_key_size_minimum512bit); +                log.add(LogLevel.ERROR, LogType.MSG_CR_ERROR_KEYSIZE_512, indent); +                return null;              }              int algorithm; @@ -143,7 +142,8 @@ public class PgpKeyOperation {                  }                  default: { -                    throw new PgpGeneralMsgIdException(R.string.error_unknown_algorithm_choice); +                    log.add(LogLevel.ERROR, LogType.MSG_CR_ERROR_UNKNOWN_ALGO, indent); +                    return null;                  }              } @@ -157,7 +157,9 @@ public class PgpKeyOperation {          } catch(InvalidAlgorithmParameterException e) {              throw new RuntimeException(e);          } catch(PGPException e) { -            throw new PgpGeneralMsgIdException(R.string.msg_mf_error_pgp, e); +            Log.e(Constants.TAG, "internal pgp error", e); +            log.add(LogLevel.ERROR, LogType.MSG_CR_ERROR_INTERNAL_PGP, indent); +            return null;          }      } @@ -166,20 +168,36 @@ public class PgpKeyOperation {          try { -            log.add(LogLevel.ERROR, LogType.MSG_MF_ERROR_KEYID, indent); +            log.add(LogLevel.START, LogType.MSG_CR, indent);              indent += 1;              updateProgress(R.string.progress_building_key, 0, 100); -            if (saveParcel.addSubKeys == null || saveParcel.addSubKeys.isEmpty()) { +            if (saveParcel.mAddSubKeys.isEmpty()) {                  log.add(LogLevel.ERROR, LogType.MSG_CR_ERROR_NO_MASTER, indent);                  return null;              } -            SubkeyAdd add = saveParcel.addSubKeys.remove(0); -            PGPKeyPair keyPair = createKey(add.mAlgorithm, add.mKeysize); +            if (saveParcel.mAddUserIds.isEmpty()) { +                log.add(LogLevel.ERROR, LogType.MSG_CR_ERROR_NO_USER_ID, indent); +                return null; +            } + +            SubkeyAdd add = saveParcel.mAddSubKeys.remove(0); +            if ((add.mFlags & KeyFlags.CERTIFY_OTHER) != KeyFlags.CERTIFY_OTHER) { +                log.add(LogLevel.ERROR, LogType.MSG_CR_ERROR_NO_CERTIFY, indent); +                return null; +            }              if (add.mAlgorithm == Constants.choice.algorithm.elgamal) { -                throw new PgpGeneralMsgIdException(R.string.error_master_key_must_not_be_el_gamal); +                log.add(LogLevel.ERROR, LogType.MSG_CR_ERROR_MASTER_ELGAMAL, indent); +                return null; +            } + +            PGPKeyPair keyPair = createKey(add.mAlgorithm, add.mKeysize, log, indent); + +            // return null if this failed (an error will already have been logged by createKey) +            if (keyPair == null) { +                return null;              }              // define hashing and signing algos @@ -195,17 +213,15 @@ public class PgpKeyOperation {              PGPSecretKeyRing sKR = new PGPSecretKeyRing(                      masterSecretKey.getEncoded(), new JcaKeyFingerprintCalculator()); -            return internal(sKR, masterSecretKey, saveParcel, "", log, indent); +            return internal(sKR, masterSecretKey, add.mFlags, saveParcel, "", log, indent);          } catch (PGPException e) { +            log.add(LogLevel.ERROR, LogType.MSG_CR_ERROR_INTERNAL_PGP, indent);              Log.e(Constants.TAG, "pgp error encoding key", e);              return null;          } catch (IOException e) {              Log.e(Constants.TAG, "io error encoding key", e);              return null; -        } catch (PgpGeneralMsgIdException e) { -            Log.e(Constants.TAG, "pgp msg id error", e); -            return null;          }      } @@ -257,11 +273,16 @@ public class PgpKeyOperation {              return null;          } -        return internal(sKR, masterSecretKey, saveParcel, passphrase, log, indent); +        // read masterKeyFlags, and use the same as before. +        // since this is the master key, this contains at least CERTIFY_OTHER +        int masterKeyFlags = readKeyFlags(masterSecretKey.getPublicKey()) | KeyFlags.CERTIFY_OTHER; + +        return internal(sKR, masterSecretKey, masterKeyFlags, saveParcel, passphrase, log, indent);      }      private UncachedKeyRing internal(PGPSecretKeyRing sKR, PGPSecretKey masterSecretKey, +                                     int masterKeyFlags,                                       SaveKeyringParcel saveParcel, String passphrase,                                       OperationLog log, int indent) { @@ -289,7 +310,7 @@ public class PgpKeyOperation {              PGPPublicKey modifiedPublicKey = masterPublicKey;              // 2a. Add certificates for new user ids -            for (String userId : saveParcel.addUserIds) { +            for (String userId : saveParcel.mAddUserIds) {                  log.add(LogLevel.INFO, LogType.MSG_MF_UID_ADD, indent);                  // this operation supersedes all previous binding and revocation certificates, @@ -298,9 +319,10 @@ public class PgpKeyOperation {                  Iterator<PGPSignature> it = modifiedPublicKey.getSignaturesForID(userId);                  if (it != null) {                      for (PGPSignature cert : new IterableIterator<PGPSignature>(it)) { -                        // if it's not a self cert, never mind                          if (cert.getKeyID() != masterPublicKey.getKeyID()) { -                            continue; +                            // foreign certificate?! error error error +                            log.add(LogLevel.ERROR, LogType.MSG_MF_ERROR_INTEGRITY, indent); +                            return null;                          }                          if (cert.getSignatureType() == PGPSignature.CERTIFICATION_REVOCATION                                  || cert.getSignatureType() == PGPSignature.NO_CERTIFICATION @@ -314,27 +336,31 @@ public class PgpKeyOperation {                  }                  // if it's supposed to be primary, we can do that here as well -                boolean isPrimary = saveParcel.changePrimaryUserId != null -                        && userId.equals(saveParcel.changePrimaryUserId); +                boolean isPrimary = saveParcel.mChangePrimaryUserId != null +                        && userId.equals(saveParcel.mChangePrimaryUserId);                  // generate and add new certificate                  PGPSignature cert = generateUserIdSignature(masterPrivateKey, -                        masterPublicKey, userId, isPrimary); -                modifiedPublicKey = PGPPublicKey.addCertification(masterPublicKey, userId, cert); +                        masterPublicKey, userId, isPrimary, masterKeyFlags); +                modifiedPublicKey = PGPPublicKey.addCertification(modifiedPublicKey, userId, cert);              }              // 2b. Add revocations for revoked user ids -            for (String userId : saveParcel.revokeUserIds) { +            for (String userId : saveParcel.mRevokeUserIds) {                  log.add(LogLevel.INFO, LogType.MSG_MF_UID_REVOKE, indent); -                // a duplicate revocatin will be removed during canonicalization, so no need to +                // a duplicate revocation will be removed during canonicalization, so no need to                  // take care of that here.                  PGPSignature cert = generateRevocationSignature(masterPrivateKey,                          masterPublicKey, userId); -                modifiedPublicKey = PGPPublicKey.addCertification(masterPublicKey, userId, cert); +                modifiedPublicKey = PGPPublicKey.addCertification(modifiedPublicKey, userId, cert);              }              // 3. If primary user id changed, generate new certificates for both old and new -            if (saveParcel.changePrimaryUserId != null) { +            if (saveParcel.mChangePrimaryUserId != null) { + +                // keep track if we actually changed one +                boolean ok = false;                  log.add(LogLevel.INFO, LogType.MSG_MF_UID_PRIMARY, indent); +                indent += 1;                  // we work on the modifiedPublicKey here, to respect new or newly revoked uids                  // noinspection unchecked @@ -343,10 +369,11 @@ public class PgpKeyOperation {                      PGPSignature currentCert = null;                      // noinspection unchecked                      for (PGPSignature cert : new IterableIterator<PGPSignature>( -                            masterPublicKey.getSignaturesForID(userId))) { -                        // if it's not a self cert, never mind +                            modifiedPublicKey.getSignaturesForID(userId))) {                          if (cert.getKeyID() != masterPublicKey.getKeyID()) { -                            continue; +                            // foreign certificate?! error error error +                            log.add(LogLevel.ERROR, LogType.MSG_MF_ERROR_INTEGRITY, indent); +                            return null;                          }                          // we know from canonicalization that if there is any revocation here, it                          // is valid and not superseded by a newer certification. @@ -373,7 +400,7 @@ public class PgpKeyOperation {                      // we definitely should not update certifications of revoked keys, so just leave it.                      if (isRevoked) {                          // revoked user ids cannot be primary! -                        if (userId.equals(saveParcel.changePrimaryUserId)) { +                        if (userId.equals(saveParcel.mChangePrimaryUserId)) {                              log.add(LogLevel.ERROR, LogType.MSG_MF_ERROR_REVOKED_PRIMARY, indent);                              return null;                          } @@ -383,14 +410,16 @@ public class PgpKeyOperation {                      // if this is~ the/a primary user id                      if (currentCert.hasSubpackets() && currentCert.getHashedSubPackets().isPrimaryUserID()) {                          // if it's the one we want, just leave it as is -                        if (userId.equals(saveParcel.changePrimaryUserId)) { +                        if (userId.equals(saveParcel.mChangePrimaryUserId)) { +                            ok = true;                              continue;                          }                          // otherwise, generate new non-primary certification +                        log.add(LogLevel.DEBUG, LogType.MSG_MF_PRIMARY_REPLACE_OLD, indent);                          modifiedPublicKey = PGPPublicKey.removeCertification(                                  modifiedPublicKey, userId, currentCert);                          PGPSignature newCert = generateUserIdSignature( -                                masterPrivateKey, masterPublicKey, userId, false); +                                masterPrivateKey, masterPublicKey, userId, false, masterKeyFlags);                          modifiedPublicKey = PGPPublicKey.addCertification(                                  modifiedPublicKey, userId, newCert);                          continue; @@ -399,20 +428,28 @@ public class PgpKeyOperation {                      // if we are here, this is not currently a primary user id                      // if it should be -                    if (userId.equals(saveParcel.changePrimaryUserId)) { +                    if (userId.equals(saveParcel.mChangePrimaryUserId)) {                          // add shiny new primary user id certificate +                        log.add(LogLevel.DEBUG, LogType.MSG_MF_PRIMARY_NEW, indent);                          modifiedPublicKey = PGPPublicKey.removeCertification(                                  modifiedPublicKey, userId, currentCert);                          PGPSignature newCert = generateUserIdSignature( -                                masterPrivateKey, masterPublicKey, userId, true); +                                masterPrivateKey, masterPublicKey, userId, true, masterKeyFlags);                          modifiedPublicKey = PGPPublicKey.addCertification(                                  modifiedPublicKey, userId, newCert); +                        ok = true;                      }                      // user id is not primary and is not supposed to be - nothing to do here.                  } +                indent -= 1; + +                if (!ok) { +                    log.add(LogLevel.ERROR, LogType.MSG_MF_ERROR_NOEXIST_PRIMARY, indent); +                    return null; +                }              }              // Update the secret key ring @@ -423,9 +460,16 @@ public class PgpKeyOperation {              }              // 4a. For each subkey change, generate new subkey binding certificate -            for (SaveKeyringParcel.SubkeyChange change : saveParcel.changeSubKeys) { +            for (SaveKeyringParcel.SubkeyChange change : saveParcel.mChangeSubKeys) {                  log.add(LogLevel.INFO, LogType.MSG_MF_SUBKEY_CHANGE,                          indent, PgpKeyHelper.convertKeyIdToHex(change.mKeyId)); + +                // TODO allow changes in master key? this implies generating new user id certs... +                if (change.mKeyId == masterPublicKey.getKeyID()) { +                    Log.e(Constants.TAG, "changing the master key not supported"); +                    return null; +                } +                  PGPSecretKey sKey = sKR.getSecretKey(change.mKeyId);                  if (sKey == null) {                      log.add(LogLevel.ERROR, LogType.MSG_MF_SUBKEY_MISSING, @@ -434,22 +478,45 @@ public class PgpKeyOperation {                  }                  PGPPublicKey pKey = sKey.getPublicKey(); -                if (change.mExpiry != null && new Date(change.mExpiry).before(new Date())) { +                // expiry must not be in the past +                if (change.mExpiry != null && new Date(change.mExpiry*1000).before(new Date())) {                      log.add(LogLevel.ERROR, LogType.MSG_MF_SUBKEY_PAST_EXPIRY,                              indent + 1, PgpKeyHelper.convertKeyIdToHex(change.mKeyId));                      return null;                  } -                // generate and add new signature. we can be sloppy here and just leave the old one, -                // it will be removed during canonicalization +                // keep old flags, or replace with new ones +                int flags = change.mFlags == null ? readKeyFlags(pKey) : change.mFlags; +                long expiry; +                if (change.mExpiry == null) { +                    long valid = pKey.getValidSeconds(); +                    expiry = valid == 0 +                            ? 0 +                            : pKey.getCreationTime().getTime() / 1000 + pKey.getValidSeconds(); +                } else { +                    expiry = change.mExpiry; +                } + +                // drop all old signatures, they will be superseded by the new one +                //noinspection unchecked +                for (PGPSignature sig : new IterableIterator<PGPSignature>(pKey.getSignatures())) { +                    // special case: if there is a revocation, don't use expiry from before +                    if (change.mExpiry == null +                            && sig.getSignatureType() == PGPSignature.SUBKEY_REVOCATION) { +                        expiry = 0; +                    } +                    pKey = PGPPublicKey.removeCertification(pKey, sig); +                } + +                // generate and add new signature                  PGPSignature sig = generateSubkeyBindingSignature(masterPublicKey, masterPrivateKey, -                        sKey, pKey, change.mFlags, change.mExpiry, passphrase); +                        sKey, pKey, flags, expiry, passphrase);                  pKey = PGPPublicKey.addCertification(pKey, sig);                  sKR = PGPSecretKeyRing.insertSecretKey(sKR, PGPSecretKey.replacePublicKey(sKey, pKey));              }              // 4b. For each subkey revocation, generate new subkey revocation certificate -            for (long revocation : saveParcel.revokeSubKeys) { +            for (long revocation : saveParcel.mRevokeSubKeys) {                  log.add(LogLevel.INFO, LogType.MSG_MF_SUBKEY_REVOKE,                          indent, PgpKeyHelper.convertKeyIdToHex(revocation));                  PGPSecretKey sKey = sKR.getSecretKey(revocation); @@ -468,52 +535,51 @@ public class PgpKeyOperation {              }              // 5. Generate and add new subkeys -            for (SaveKeyringParcel.SubkeyAdd add : saveParcel.addSubKeys) { -                try { - -                    if (add.mExpiry != null && new Date(add.mExpiry).before(new Date())) { -                        log.add(LogLevel.ERROR, LogType.MSG_MF_SUBKEY_PAST_EXPIRY, indent +1); -                        return null; -                    } - -                    log.add(LogLevel.INFO, LogType.MSG_MF_SUBKEY_NEW, indent); +            for (SaveKeyringParcel.SubkeyAdd add : saveParcel.mAddSubKeys) { -                    // generate a new secret key (privkey only for now) -                    PGPKeyPair keyPair = createKey(add.mAlgorithm, add.mKeysize); - -                    // add subkey binding signature (making this a sub rather than master key) -                    PGPPublicKey pKey = keyPair.getPublicKey(); -                    PGPSignature cert = generateSubkeyBindingSignature( -                            masterPublicKey, masterPrivateKey, keyPair.getPrivateKey(), pKey, -                            add.mFlags, add.mExpiry); -                    pKey = PGPPublicKey.addSubkeyBindingCertification(pKey, cert); +                if (add.mExpiry != null && new Date(add.mExpiry*1000).before(new Date())) { +                    log.add(LogLevel.ERROR, LogType.MSG_MF_SUBKEY_PAST_EXPIRY, indent +1); +                    return null; +                } -                    PGPSecretKey sKey; { -                        // define hashing and signing algos -                        PGPDigestCalculator sha1Calc = new JcaPGPDigestCalculatorProviderBuilder() -                                .build().get(HashAlgorithmTags.SHA1); +                log.add(LogLevel.INFO, LogType.MSG_MF_SUBKEY_NEW, indent); -                        // Build key encrypter and decrypter based on passphrase -                        PBESecretKeyEncryptor keyEncryptor = new JcePBESecretKeyEncryptorBuilder( -                                PGPEncryptedData.CAST5, sha1Calc) -                                .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build("".toCharArray()); +                // generate a new secret key (privkey only for now) +                PGPKeyPair keyPair = createKey(add.mAlgorithm, add.mKeysize, log, indent); +                if(keyPair == null) { +                    return null; +                } -                        sKey = new PGPSecretKey(keyPair.getPrivateKey(), pKey, -                                sha1Calc, false, keyEncryptor); -                    } +                // add subkey binding signature (making this a sub rather than master key) +                PGPPublicKey pKey = keyPair.getPublicKey(); +                PGPSignature cert = generateSubkeyBindingSignature( +                        masterPublicKey, masterPrivateKey, keyPair.getPrivateKey(), pKey, +                        add.mFlags, add.mExpiry == null ? 0 : add.mExpiry); +                pKey = PGPPublicKey.addSubkeyBindingCertification(pKey, cert); + +                PGPSecretKey sKey; { +                    // define hashing and signing algos +                    PGPDigestCalculator sha1Calc = new JcaPGPDigestCalculatorProviderBuilder() +                            .build().get(HashAlgorithmTags.SHA1); + +                    // Build key encrypter and decrypter based on passphrase +                    PBESecretKeyEncryptor keyEncryptor = new JcePBESecretKeyEncryptorBuilder( +                            PGPEncryptedData.CAST5, sha1Calc) +                            .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build("".toCharArray()); + +                    sKey = new PGPSecretKey(keyPair.getPrivateKey(), pKey, +                            sha1Calc, false, keyEncryptor); +                } -                    log.add(LogLevel.DEBUG, LogType.MSG_MF_SUBKEY_NEW_ID, -                            indent+1, PgpKeyHelper.convertKeyIdToHex(sKey.getKeyID())); +                log.add(LogLevel.DEBUG, LogType.MSG_MF_SUBKEY_NEW_ID, +                        indent+1, PgpKeyHelper.convertKeyIdToHex(sKey.getKeyID())); -                    sKR = PGPSecretKeyRing.insertSecretKey(sKR, sKey); +                sKR = PGPSecretKeyRing.insertSecretKey(sKR, sKey); -                } catch (PgpGeneralMsgIdException e) { -                    return null; -                }              }              // 6. If requested, change passphrase -            if (saveParcel.newPassphrase != null) { +            if (saveParcel.mNewPassphrase != null) {                  log.add(LogLevel.INFO, LogType.MSG_MF_PASSPHRASE, indent);                  PGPDigestCalculator sha1Calc = new JcaPGPDigestCalculatorProviderBuilder().build()                          .get(HashAlgorithmTags.SHA1); @@ -523,7 +589,7 @@ public class PgpKeyOperation {                  PBESecretKeyEncryptor keyEncryptorNew = new JcePBESecretKeyEncryptorBuilder(                          PGPEncryptedData.CAST5, sha1Calc)                          .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build( -                                saveParcel.newPassphrase.toCharArray()); +                                saveParcel.mNewPassphrase.toCharArray());                  sKR = PGPSecretKeyRing.copyWithNewPassword(sKR, keyDecryptor, keyEncryptorNew);              } @@ -546,7 +612,7 @@ public class PgpKeyOperation {      }      private static PGPSignature generateUserIdSignature( -            PGPPrivateKey masterPrivateKey, PGPPublicKey pKey, String userId, boolean primary) +            PGPPrivateKey masterPrivateKey, PGPPublicKey pKey, String userId, boolean primary, int flags)              throws IOException, PGPException, SignatureException {          PGPContentSignerBuilder signerBuilder = new JcaPGPContentSignerBuilder(                  pKey.getAlgorithm(), PGPUtil.SHA1) @@ -558,6 +624,7 @@ public class PgpKeyOperation {          subHashedPacketsGen.setPreferredHashAlgorithms(true, PREFERRED_HASH_ALGORITHMS);          subHashedPacketsGen.setPreferredCompressionAlgorithms(true, PREFERRED_COMPRESSION_ALGORITHMS);          subHashedPacketsGen.setPrimaryUserID(false, primary); +        subHashedPacketsGen.setKeyFlags(false, flags);          sGen.setHashedSubpackets(subHashedPacketsGen.generate());          sGen.init(PGPSignature.POSITIVE_CERTIFICATION, masterPrivateKey);          return sGen.generateCertification(userId, pKey); @@ -599,7 +666,7 @@ public class PgpKeyOperation {      private static PGPSignature generateSubkeyBindingSignature(              PGPPublicKey masterPublicKey, PGPPrivateKey masterPrivateKey, -            PGPSecretKey sKey, PGPPublicKey pKey, int flags, Long expiry, String passphrase) +            PGPSecretKey sKey, PGPPublicKey pKey, int flags, long expiry, String passphrase)              throws IOException, PGPException, SignatureException {          PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder()                  .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME).build( @@ -611,7 +678,7 @@ public class PgpKeyOperation {      private static PGPSignature generateSubkeyBindingSignature(              PGPPublicKey masterPublicKey, PGPPrivateKey masterPrivateKey, -            PGPPrivateKey subPrivateKey, PGPPublicKey pKey, int flags, Long expiry) +            PGPPrivateKey subPrivateKey, PGPPublicKey pKey, int flags, long expiry)              throws IOException, PGPException, SignatureException {          // date for signing @@ -640,17 +707,9 @@ public class PgpKeyOperation {              hashedPacketsGen.setKeyFlags(false, flags);          } -        if (expiry != null) { -            Calendar creationDate = Calendar.getInstance(TimeZone.getTimeZone("UTC")); -            creationDate.setTime(pKey.getCreationTime()); - -            // (Just making sure there's no programming error here, this MUST have been checked above!) -            if (new Date(expiry).before(todayDate)) { -                throw new RuntimeException("Bad subkey creation date, this is a bug!"); -            } -            hashedPacketsGen.setKeyExpirationTime(false, expiry - creationDate.getTimeInMillis()); -        } else { -            hashedPacketsGen.setKeyExpirationTime(false, 0); +        if (expiry > 0) { +            long creationTime = pKey.getCreationTime().getTime() / 1000; +            hashedPacketsGen.setKeyExpirationTime(false, expiry - creationTime);          }          PGPContentSignerBuilder signerBuilder = new JcaPGPContentSignerBuilder( @@ -665,4 +724,22 @@ public class PgpKeyOperation {      } +    /** Returns all flags valid for this key. +     * +     * This method does not do any validity checks on the signature, so it should not be used on +     * a non-canonicalized key! +     * +     */ +    private static int readKeyFlags(PGPPublicKey key) { +        int flags = 0; +        //noinspection unchecked +        for(PGPSignature sig : new IterableIterator<PGPSignature>(key.getSignatures())) { +            if (!sig.hasSubpackets()) { +                continue; +            } +            flags |= sig.getHashedSubPackets().getKeyFlags(); +        } +        return flags; +    } +  } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java index 441e2762a..9ddfd3405 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedKeyRing.java @@ -81,6 +81,10 @@ public class UncachedKeyRing {          return new UncachedPublicKey(mRing.getPublicKey());      } +    public UncachedPublicKey getPublicKey(long keyId) { +        return new UncachedPublicKey(mRing.getPublicKey(keyId)); +    } +      public Iterator<UncachedPublicKey> getPublicKeys() {          final Iterator<PGPPublicKey> it = mRing.getPublicKeys();          return new Iterator<UncachedPublicKey>() { @@ -558,7 +562,7 @@ public class UncachedKeyRing {                      // make sure the certificate checks out                      try {                          cert.init(masterKey); -                        if (!cert.verifySignature(key)) { +                        if (!cert.verifySignature(masterKey, key)) {                              log.add(LogLevel.WARN, LogType.MSG_KC_SUB_REVOKE_BAD, indent);                              badCerts += 1;                              continue; @@ -744,8 +748,12 @@ public class UncachedKeyRing {              } -            log.add(LogLevel.DEBUG, LogType.MSG_MG_FOUND_NEW, -                    indent, Integer.toString(newCerts)); +            if (newCerts > 0) { +                log.add(LogLevel.DEBUG, LogType.MSG_MG_FOUND_NEW, indent, +                        Integer.toString(newCerts)); +            } else { +                log.add(LogLevel.DEBUG, LogType.MSG_MG_UNCHANGED, indent); +            }              return new UncachedKeyRing(result); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedPublicKey.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedPublicKey.java index 33db7771b..ef5aa8e86 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedPublicKey.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/UncachedPublicKey.java @@ -9,6 +9,7 @@ import org.spongycastle.openpgp.PGPSignatureSubpacketVector;  import org.spongycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider;  import org.sufficientlysecure.keychain.Constants;  import org.sufficientlysecure.keychain.util.IterableIterator; +import org.sufficientlysecure.keychain.util.Log;  import java.security.SignatureException;  import java.util.ArrayList; @@ -44,14 +45,19 @@ public class UncachedPublicKey {      }      public Date getExpiryTime() { -        Date creationDate = getCreationTime(); -        if (mPublicKey.getValidDays() == 0) { +        long seconds = mPublicKey.getValidSeconds(); +        if (seconds > Integer.MAX_VALUE) { +            Log.e(Constants.TAG, "error, expiry time too large"); +            return null; +        } +        if (seconds == 0) {              // no expiry              return null;          } +        Date creationDate = getCreationTime();          Calendar calendar = GregorianCalendar.getInstance();          calendar.setTime(creationDate); -        calendar.add(Calendar.DATE, mPublicKey.getValidDays()); +        calendar.add(Calendar.SECOND, (int) seconds);          return calendar.getTime();      } @@ -77,26 +83,76 @@ public class UncachedPublicKey {          return mPublicKey.getBitStrength();      } +    /** Returns the primary user id, as indicated by the public key's self certificates. +     * +     * This is an expensive operation, since potentially a lot of certificates (and revocations) +     * have to be checked, and even then the result is NOT guaranteed to be constant through a +     * canonicalization operation. +     * +     * Returns null if there is no primary user id (as indicated by certificates) +     * +     */      public String getPrimaryUserId() { +        String found = null; +        PGPSignature foundSig = null;          for (String userId : new IterableIterator<String>(mPublicKey.getUserIDs())) { +            PGPSignature revocation = null; +              for (PGPSignature sig : new IterableIterator<PGPSignature>(mPublicKey.getSignaturesForID(userId))) { -                if (sig.getHashedSubPackets() != null -                        && sig.getHashedSubPackets().hasSubpacket(SignatureSubpacketTags.PRIMARY_USER_ID)) { -                    try { +                try { + +                    // if this is a revocation, this is not the user id +                    if (sig.getSignatureType() == PGPSignature.CERTIFICATION_REVOCATION) { +                        // make sure it's actually valid +                        sig.init(new JcaPGPContentVerifierBuilderProvider().setProvider( +                                Constants.BOUNCY_CASTLE_PROVIDER_NAME), mPublicKey); +                        if (!sig.verifyCertification(userId, mPublicKey)) { +                            continue; +                        } +                        if (found != null && found.equals(userId)) { +                            found = null; +                        } +                        revocation = sig; +                        // this revocation may still be overridden by a newer cert +                        continue; +                    } + +                    if (sig.getHashedSubPackets() != null && sig.getHashedSubPackets().isPrimaryUserID()) { +                        if (foundSig != null && sig.getCreationTime().before(foundSig.getCreationTime())) { +                            continue; +                        } +                        // ignore if there is a newer revocation for this user id +                        if (revocation != null && sig.getCreationTime().before(revocation.getCreationTime())) { +                            continue; +                        }                          // make sure it's actually valid                          sig.init(new JcaPGPContentVerifierBuilderProvider().setProvider(                                  Constants.BOUNCY_CASTLE_PROVIDER_NAME), mPublicKey);                          if (sig.verifyCertification(userId, mPublicKey)) { -                            return userId; +                            found = userId; +                            foundSig = sig; +                            // this one can't be relevant anymore at this point +                            revocation = null;                          } -                    } catch (Exception e) { -                        // nothing bad happens, the key is just not considered the primary key id                      } -                } +                } catch (Exception e) { +                    // nothing bad happens, the key is just not considered the primary key id +                }              }          } -        return null; +        return found; +    } + +    /** +     * Returns primary user id if existing. If not, return first encountered user id. +     */ +    public String getPrimaryUserIdWithFallback()  { +        String userId = getPrimaryUserId(); +        if (userId == null) { +            userId = (String) mPublicKey.getUserIDs().next(); +        } +        return userId;      }      public ArrayList<String> getUnorderedUserIds() { @@ -186,6 +242,21 @@ public class UncachedPublicKey {          return mPublicKey;      } +    public Iterator<WrappedSignature> getSignatures() { +        final Iterator<PGPSignature> it = mPublicKey.getSignatures(); +        return new Iterator<WrappedSignature>() { +            public void remove() { +                it.remove(); +            } +            public WrappedSignature next() { +                return new WrappedSignature(it.next()); +            } +            public boolean hasNext() { +                return it.hasNext(); +            } +        }; +    } +      public Iterator<WrappedSignature> getSignaturesForId(String userId) {          final Iterator<PGPSignature> it = mPublicKey.getSignaturesForID(userId);          return new Iterator<WrappedSignature>() { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/WrappedKeyRing.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/WrappedKeyRing.java index 6f3068261..a054255dc 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/WrappedKeyRing.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/WrappedKeyRing.java @@ -39,8 +39,12 @@ public abstract class WrappedKeyRing extends KeyRing {      }      public String getPrimaryUserId() throws PgpGeneralException { -        return (String) getRing().getPublicKey().getUserIDs().next(); -    }; +        return getPublicKey().getPrimaryUserId(); +    } + +    public String getPrimaryUserIdWithFallback() throws PgpGeneralException { +        return getPublicKey().getPrimaryUserIdWithFallback(); +    }      public boolean isRevoked() throws PgpGeneralException {          // Is the master key revoked? @@ -101,8 +105,16 @@ public abstract class WrappedKeyRing extends KeyRing {      abstract public IterableIterator<WrappedPublicKey> publicKeyIterator(); -    public UncachedKeyRing getUncached() { -        return new UncachedKeyRing(getRing()); +    public WrappedPublicKey getPublicKey() { +        return new WrappedPublicKey(this, getRing().getPublicKey()); +    } + +    public WrappedPublicKey getPublicKey(long id) { +        return new WrappedPublicKey(this, getRing().getPublicKey(id)); +    } + +    public byte[] getEncoded() throws IOException { +        return getRing().getEncoded();      }  } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/WrappedPublicKeyRing.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/WrappedPublicKeyRing.java index b2abf15a4..57d84072a 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/WrappedPublicKeyRing.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/WrappedPublicKeyRing.java @@ -44,14 +44,6 @@ public class WrappedPublicKeyRing extends WrappedKeyRing {          getRing().encode(stream);      } -    public WrappedPublicKey getSubkey() { -        return new WrappedPublicKey(this, getRing().getPublicKey()); -    } - -    public WrappedPublicKey getSubkey(long id) { -        return new WrappedPublicKey(this, getRing().getPublicKey(id)); -    } -      /** Getter that returns the subkey that should be used for signing. */      WrappedPublicKey getEncryptionSubKey() throws PgpGeneralException {          PGPPublicKey key = getRing().getPublicKey(getEncryptId()); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/WrappedSecretKey.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/WrappedSecretKey.java index ef8044a9b..98ad2b706 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/WrappedSecretKey.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/WrappedSecretKey.java @@ -97,7 +97,7 @@ public class WrappedSecretKey extends WrappedPublicKey {              signatureGenerator.init(signatureType, mPrivateKey);              PGPSignatureSubpacketGenerator spGen = new PGPSignatureSubpacketGenerator(); -            spGen.setSignerUserID(false, mRing.getPrimaryUserId()); +            spGen.setSignerUserID(false, mRing.getPrimaryUserIdWithFallback());              signatureGenerator.setHashedSubpackets(spGen.generate());              return signatureGenerator;          } catch(PGPException e) { @@ -175,7 +175,7 @@ public class WrappedSecretKey extends WrappedPublicKey {          }          // get the master subkey (which we certify for) -        PGPPublicKey publicKey = publicKeyRing.getSubkey().getPublicKey(); +        PGPPublicKey publicKey = publicKeyRing.getPublicKey().getPublicKey();          // fetch public key ring, add the certification and return it          for (String userId : new IterableIterator<String>(userIds.iterator())) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/WrappedSecretKeyRing.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/WrappedSecretKeyRing.java index c737b7c46..5cb24cf88 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/WrappedSecretKeyRing.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/WrappedSecretKeyRing.java @@ -41,11 +41,11 @@ public class WrappedSecretKeyRing extends WrappedKeyRing {          return mRing;      } -    public WrappedSecretKey getSubKey() { +    public WrappedSecretKey getSecretKey() {          return new WrappedSecretKey(this, mRing.getSecretKey());      } -    public WrappedSecretKey getSubKey(long id) { +    public WrappedSecretKey getSecretKey(long id) {          return new WrappedSecretKey(this, mRing.getSecretKey(id));      } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/WrappedSignature.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/WrappedSignature.java index 196ac1dee..df19930c4 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/WrappedSignature.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/WrappedSignature.java @@ -15,6 +15,7 @@ import org.sufficientlysecure.keychain.util.Log;  import java.io.IOException;  import java.security.SignatureException; +import java.util.ArrayList;  import java.util.Date;  /** OpenKeychain wrapper around PGPSignature objects. @@ -55,12 +56,37 @@ public class WrappedSignature {          return mSig.getCreationTime();      } +    public ArrayList<WrappedSignature> getEmbeddedSignatures() { +        ArrayList<WrappedSignature> sigs = new ArrayList<WrappedSignature>(); +        if (!mSig.hasSubpackets()) { +            return sigs; +        } +        try { +            PGPSignatureList list; +            list = mSig.getHashedSubPackets().getEmbeddedSignatures(); +            for(int i = 0; i < list.size(); i++) { +                sigs.add(new WrappedSignature(list.get(i))); +            } +            list = mSig.getUnhashedSubPackets().getEmbeddedSignatures(); +            for(int i = 0; i < list.size(); i++) { +                sigs.add(new WrappedSignature(list.get(i))); +            } +        } catch (PGPException e) { +            // no matter +            Log.e(Constants.TAG, "exception reading embedded signatures", e); +        } catch (IOException e) { +            // no matter +            Log.e(Constants.TAG, "exception reading embedded signatures", e); +        } +        return sigs; +    } +      public byte[] getEncoded() throws IOException {          return mSig.getEncoded();      }      public boolean isRevocation() { -        return mSig.getHashedSubPackets().hasSubpacket(SignatureSubpacketTags.REVOCATION_REASON); +        return mSig.getSignatureType() == PGPSignature.CERTIFICATION_REVOCATION;      }      public boolean isPrimaryUserId() { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/CachedPublicKeyRing.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/CachedPublicKeyRing.java index 48d40430a..34de0024d 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/CachedPublicKeyRing.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/CachedPublicKeyRing.java @@ -70,6 +70,10 @@ public class CachedPublicKeyRing extends KeyRing {          }      } +    public String getPrimaryUserIdWithFallback() throws PgpGeneralException { +        return getPrimaryUserId(); +    } +      public boolean isRevoked() throws PgpGeneralException {          try {              Object data = mProviderHelper.getGenericData(mUri, diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java index b5609a327..2d524f5b0 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java @@ -199,7 +199,7 @@ public class ProviderHelper {                  byte[] blob = cursor.getBlob(3);                  if (blob != null) {                      result.put(masterKeyId, -                            new WrappedPublicKeyRing(blob, hasAnySecret, verified).getSubkey()); +                            new WrappedPublicKeyRing(blob, hasAnySecret, verified).getPublicKey());                  }              } while (cursor.moveToNext()); @@ -450,8 +450,7 @@ public class ProviderHelper {                              if (cert.verifySignature(masterKey, userId)) {                                  item.trustedCerts.add(cert);                                  log(LogLevel.INFO, LogType.MSG_IP_UID_CERT_GOOD, -                                        PgpKeyHelper.convertKeyIdToHexShort(trustedKey.getKeyId()), -                                        trustedKey.getPrimaryUserId() +                                        PgpKeyHelper.convertKeyIdToHexShort(trustedKey.getKeyId())                                  );                              } else {                                  log(LogLevel.WARN, LogType.MSG_IP_UID_CERT_BAD); @@ -670,7 +669,7 @@ public class ProviderHelper {              // If there is an old keyring, merge it              try { -                UncachedKeyRing oldPublicRing = getWrappedPublicKeyRing(masterKeyId).getUncached(); +                UncachedKeyRing oldPublicRing = getWrappedPublicKeyRing(masterKeyId).getUncachedKeyRing();                  // Merge data from new public ring into the old one                  publicRing = oldPublicRing.merge(publicRing, mLog, mIndent); @@ -706,7 +705,7 @@ public class ProviderHelper {              // If there is a secret key, merge new data (if any) and save the key for later              UncachedKeyRing secretRing;              try { -                secretRing = getWrappedSecretKeyRing(publicRing.getMasterKeyId()).getUncached(); +                secretRing = getWrappedSecretKeyRing(publicRing.getMasterKeyId()).getUncachedKeyRing();                  // Merge data from new public ring into secret one                  secretRing = secretRing.merge(publicRing, mLog, mIndent); @@ -754,10 +753,10 @@ public class ProviderHelper {              // If there is an old secret key, merge it.              try { -                UncachedKeyRing oldSecretRing = getWrappedSecretKeyRing(masterKeyId).getUncached(); +                UncachedKeyRing oldSecretRing = getWrappedSecretKeyRing(masterKeyId).getUncachedKeyRing();                  // Merge data from new secret ring into old one -                secretRing = oldSecretRing.merge(secretRing, mLog, mIndent); +                secretRing = secretRing.merge(oldSecretRing, mLog, mIndent);                  // If this is null, there is an error in the log so we can just return                  if (secretRing == null) { @@ -791,9 +790,9 @@ public class ProviderHelper {              // Merge new data into public keyring as well, if there is any              UncachedKeyRing publicRing;              try { -                UncachedKeyRing oldPublicRing = getWrappedPublicKeyRing(masterKeyId).getUncached(); +                UncachedKeyRing oldPublicRing = getWrappedPublicKeyRing(masterKeyId).getUncachedKeyRing(); -                // Merge data from new public ring into secret one +                // Merge data from new secret ring into public one                  publicRing = oldPublicRing.merge(secretRing, mLog, mIndent);                  if (publicRing == null) {                      return new SaveKeyringResult(SaveKeyringResult.RESULT_ERROR, mLog); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java index 10faa8786..9a4cef2f1 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java @@ -44,7 +44,6 @@ import org.sufficientlysecure.keychain.pgp.PgpKeyOperation;  import org.sufficientlysecure.keychain.pgp.PgpSignEncrypt;  import org.sufficientlysecure.keychain.pgp.Progressable;  import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; -import org.sufficientlysecure.keychain.pgp.WrappedPublicKey;  import org.sufficientlysecure.keychain.pgp.WrappedPublicKeyRing;  import org.sufficientlysecure.keychain.pgp.WrappedSecretKey;  import org.sufficientlysecure.keychain.pgp.WrappedSecretKeyRing; @@ -350,9 +349,9 @@ public class KeychainIntentService extends IntentService                      providerHelper.saveSecretKeyRing(ring, new ProgressScaler(this, 10, 95, 100));                      // cache new passphrase -                    if (saveParcel.newPassphrase != null) { +                    if (saveParcel.mNewPassphrase != null) {                          PassphraseCacheService.addCachedPassphrase(this, ring.getMasterKeyId(), -                                saveParcel.newPassphrase, ring.getPublicKey().getPrimaryUserId()); +                                saveParcel.mNewPassphrase, ring.getPublicKey().getPrimaryUserIdWithFallback());                      }                  } catch (ProviderHelper.NotFoundException e) {                      sendErrorToHandler(e); @@ -546,7 +545,7 @@ public class KeychainIntentService extends IntentService                  ProviderHelper providerHelper = new ProviderHelper(this);                  WrappedPublicKeyRing publicRing = providerHelper.getWrappedPublicKeyRing(pubKeyId);                  WrappedSecretKeyRing secretKeyRing = providerHelper.getWrappedSecretKeyRing(masterKeyId); -                WrappedSecretKey certificationKey = secretKeyRing.getSubKey(); +                WrappedSecretKey certificationKey = secretKeyRing.getSecretKey();                  if(!certificationKey.unlock(signaturePassphrase)) {                      throw new PgpGeneralException("Error extracting key (bad passphrase?)");                  } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/OperationResultParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/OperationResultParcel.java index f868d931c..705d6afaf 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/OperationResultParcel.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/OperationResultParcel.java @@ -9,6 +9,7 @@ import org.sufficientlysecure.keychain.util.IterableIterator;  import org.sufficientlysecure.keychain.util.Log;  import java.util.ArrayList; +import java.util.Arrays;  import java.util.Iterator;  import java.util.List; @@ -70,9 +71,6 @@ public class OperationResultParcel implements Parcelable {              mParameters = parameters;              mIndent = indent;          } -        public LogEntryParcel(LogLevel level, LogType type, Object... parameters) { -            this(level, type, 0, parameters); -        }          public LogEntryParcel(Parcel source) {              mLevel = LogLevel.values()[source.readInt()]; @@ -104,6 +102,15 @@ public class OperationResultParcel implements Parcelable {              }          }; +        @Override +        public String toString() { +            return "LogEntryParcel{" + +                    "mLevel=" + mLevel + +                    ", mType=" + mType + +                    ", mParameters=" + Arrays.toString(mParameters) + +                    ", mIndent=" + mIndent + +                    '}'; +        }      }      /** This is an enum of all possible log events. @@ -235,9 +242,17 @@ public class OperationResultParcel implements Parcelable {          MSG_MG_HETEROGENEOUS (R.string.msg_mg_heterogeneous),          MSG_MG_NEW_SUBKEY (R.string.msg_mg_new_subkey),          MSG_MG_FOUND_NEW (R.string.msg_mg_found_new), +        MSG_MG_UNCHANGED (R.string.msg_mg_unchanged),          // secret key create -        MSG_CR_ERROR_NO_MASTER (R.string.msg_mr), +        MSG_CR (R.string.msg_cr), +        MSG_CR_ERROR_NO_MASTER (R.string.msg_cr_error_no_master), +        MSG_CR_ERROR_NO_USER_ID (R.string.msg_cr_error_no_user_id), +        MSG_CR_ERROR_NO_CERTIFY (R.string.msg_cr_error_no_certify), +        MSG_CR_ERROR_KEYSIZE_512 (R.string.msg_cr_error_keysize_512), +        MSG_CR_ERROR_UNKNOWN_ALGO (R.string.msg_cr_error_unknown_algo), +        MSG_CR_ERROR_INTERNAL_PGP (R.string.msg_cr_error_internal_pgp), +        MSG_CR_ERROR_MASTER_ELGAMAL (R.string.msg_cr_error_master_elgamal),          // secret key modify          MSG_MF (R.string.msg_mr), @@ -245,10 +260,13 @@ public class OperationResultParcel implements Parcelable {          MSG_MF_ERROR_FINGERPRINT (R.string.msg_mf_error_fingerprint),          MSG_MF_ERROR_KEYID (R.string.msg_mf_error_keyid),          MSG_MF_ERROR_INTEGRITY (R.string.msg_mf_error_integrity), +        MSG_MF_ERROR_NOEXIST_PRIMARY (R.string.msg_mf_error_noexist_primary),          MSG_MF_ERROR_REVOKED_PRIMARY (R.string.msg_mf_error_revoked_primary),          MSG_MF_ERROR_PGP (R.string.msg_mf_error_pgp),          MSG_MF_ERROR_SIG (R.string.msg_mf_error_sig),          MSG_MF_PASSPHRASE (R.string.msg_mf_passphrase), +        MSG_MF_PRIMARY_REPLACE_OLD (R.string.msg_mf_primary_replace_old), +        MSG_MF_PRIMARY_NEW (R.string.msg_mf_primary_new),          MSG_MF_SUBKEY_CHANGE (R.string.msg_mf_subkey_change),          MSG_MF_SUBKEY_MISSING (R.string.msg_mf_subkey_missing),          MSG_MF_SUBKEY_NEW_ID (R.string.msg_mf_subkey_new_id), @@ -314,6 +332,7 @@ public class OperationResultParcel implements Parcelable {          }          public void add(LogLevel level, LogType type, int indent) { +            Log.d(Constants.TAG, type.toString());              mParcels.add(new OperationResultParcel.LogEntryParcel(level, type, indent, (Object[]) null));          } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/PassphraseCacheService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/PassphraseCacheService.java index 72c1a8f67..13d9b497f 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/PassphraseCacheService.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/PassphraseCacheService.java @@ -191,7 +191,7 @@ public class PassphraseCacheService extends Service {                  Log.d(Constants.TAG, "Key has no passphrase! Caches and returns empty passphrase!");                  try { -                    addCachedPassphrase(this, keyId, "", key.getPrimaryUserId()); +                    addCachedPassphrase(this, keyId, "", key.getPrimaryUserIdWithFallback());                  } catch (PgpGeneralException e) {                      Log.d(Constants.TAG, "PgpGeneralException occured");                  } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/SaveKeyringParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/SaveKeyringParcel.java index a56095767..5e90b396e 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/SaveKeyringParcel.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/SaveKeyringParcel.java @@ -27,23 +27,19 @@ public class SaveKeyringParcel implements Parcelable {      // the key fingerprint, for safety. MUST be null for a new key.      public byte[] mFingerprint; -    public String newPassphrase; +    public String mNewPassphrase; -    public ArrayList<String> addUserIds; -    public ArrayList<SubkeyAdd> addSubKeys; +    public ArrayList<String> mAddUserIds; +    public ArrayList<SubkeyAdd> mAddSubKeys; -    public ArrayList<SubkeyChange> changeSubKeys; -    public String changePrimaryUserId; +    public ArrayList<SubkeyChange> mChangeSubKeys; +    public String mChangePrimaryUserId; -    public ArrayList<String> revokeUserIds; -    public ArrayList<Long> revokeSubKeys; +    public ArrayList<String> mRevokeUserIds; +    public ArrayList<Long> mRevokeSubKeys;      public SaveKeyringParcel() { -        addUserIds = new ArrayList<String>(); -        addSubKeys = new ArrayList<SubkeyAdd>(); -        changeSubKeys = new ArrayList<SubkeyChange>(); -        revokeUserIds = new ArrayList<String>(); -        revokeSubKeys = new ArrayList<Long>(); +        reset();      }      public SaveKeyringParcel(long masterKeyId, byte[] fingerprint) { @@ -52,6 +48,16 @@ public class SaveKeyringParcel implements Parcelable {          mFingerprint = fingerprint;      } +    public void reset() { +        mNewPassphrase = null; +        mAddUserIds = new ArrayList<String>(); +        mAddSubKeys = new ArrayList<SubkeyAdd>(); +        mChangePrimaryUserId = null; +        mChangeSubKeys = new ArrayList<SubkeyChange>(); +        mRevokeUserIds = new ArrayList<String>(); +        mRevokeSubKeys = new ArrayList<Long>(); +    } +      // performance gain for using Parcelable here would probably be negligible,      // use Serializable instead.      public static class SubkeyAdd implements Serializable { @@ -70,6 +76,7 @@ public class SaveKeyringParcel implements Parcelable {      public static class SubkeyChange implements Serializable {          public long mKeyId;          public Integer mFlags; +        // this is a long unix timestamp, in seconds (NOT MILLISECONDS!)          public Long mExpiry;          public SubkeyChange(long keyId, Integer flags, Long expiry) {              mKeyId = keyId; @@ -82,16 +89,16 @@ public class SaveKeyringParcel implements Parcelable {          mMasterKeyId = source.readInt() != 0 ? source.readLong() : null;          mFingerprint = source.createByteArray(); -        newPassphrase = source.readString(); +        mNewPassphrase = source.readString(); -        addUserIds = source.createStringArrayList(); -        addSubKeys = (ArrayList<SubkeyAdd>) source.readSerializable(); +        mAddUserIds = source.createStringArrayList(); +        mAddSubKeys = (ArrayList<SubkeyAdd>) source.readSerializable(); -        changeSubKeys = (ArrayList<SubkeyChange>) source.readSerializable(); -        changePrimaryUserId = source.readString(); +        mChangeSubKeys = (ArrayList<SubkeyChange>) source.readSerializable(); +        mChangePrimaryUserId = source.readString(); -        revokeUserIds = source.createStringArrayList(); -        revokeSubKeys = (ArrayList<Long>) source.readSerializable(); +        mRevokeUserIds = source.createStringArrayList(); +        mRevokeSubKeys = (ArrayList<Long>) source.readSerializable();      }      @Override @@ -102,16 +109,16 @@ public class SaveKeyringParcel implements Parcelable {          }          destination.writeByteArray(mFingerprint); -        destination.writeString(newPassphrase); +        destination.writeString(mNewPassphrase); -        destination.writeStringList(addUserIds); -        destination.writeSerializable(addSubKeys); +        destination.writeStringList(mAddUserIds); +        destination.writeSerializable(mAddSubKeys); -        destination.writeSerializable(changeSubKeys); -        destination.writeString(changePrimaryUserId); +        destination.writeSerializable(mChangeSubKeys); +        destination.writeString(mChangePrimaryUserId); -        destination.writeStringList(revokeUserIds); -        destination.writeSerializable(revokeSubKeys); +        destination.writeStringList(mRevokeUserIds); +        destination.writeSerializable(mRevokeSubKeys);      }      public static final Creator<SaveKeyringParcel> CREATOR = new Creator<SaveKeyringParcel>() { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/testsupport/KeyringTestingHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/testsupport/KeyringTestingHelper.java deleted file mode 100644 index a565f0707..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/testsupport/KeyringTestingHelper.java +++ /dev/null @@ -1,56 +0,0 @@ -package org.sufficientlysecure.keychain.testsupport; - -import android.content.Context; - -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.util.Collection; - -/** - * Helper for tests of the Keyring import in ProviderHelper. - */ -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; -    } - - -    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/src/main/java/org/sufficientlysecure/keychain/testsupport/ProviderHelperStub.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/testsupport/ProviderHelperStub.java deleted file mode 100644 index c6d834bf9..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/testsupport/ProviderHelperStub.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.sufficientlysecure.keychain.testsupport; - -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/src/main/java/org/sufficientlysecure/keychain/testsupport/TestDataUtil.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/testsupport/TestDataUtil.java deleted file mode 100644 index 9e6ede761..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/testsupport/TestDataUtil.java +++ /dev/null @@ -1,44 +0,0 @@ -package org.sufficientlysecure.keychain.testsupport; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.Collection; - -/** - * 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(); -    } - -    private static void appendToOutput(InputStream input, ByteArrayOutputStream 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); -    } -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/testsupport/package-info.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/testsupport/package-info.java deleted file mode 100644 index 1cc0f9a95..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/testsupport/package-info.java +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Test support classes. - * This is only in main code because of gradle-Android Studio-robolectric issues. Having - * classes in main code means IDE autocomplete, class detection, etc., all works. - * TODO Move into test package when possible - */ -package org.sufficientlysecure.keychain.testsupport; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyActivity.java new file mode 100644 index 000000000..d2073d9a7 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyActivity.java @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program.  If not, see <http://www.gnu.org/licenses/>. + */ + +package org.sufficientlysecure.keychain.ui; + +import android.app.ProgressDialog; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.Message; +import android.os.Messenger; +import android.support.v7.app.ActionBarActivity; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.Patterns; +import android.view.View; +import android.widget.ArrayAdapter; +import android.widget.AutoCompleteTextView; +import android.widget.Button; +import android.widget.EditText; + +import org.spongycastle.bcpg.sig.KeyFlags; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.helper.ContactHelper; +import org.sufficientlysecure.keychain.service.KeychainIntentService; +import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler; +import org.sufficientlysecure.keychain.service.SaveKeyringParcel; + +import java.util.regex.Matcher; + +public class CreateKeyActivity extends ActionBarActivity { + +    AutoCompleteTextView nameEdit; +    AutoCompleteTextView emailEdit; +    EditText passphraseEdit; +    Button createButton; + +    @Override +    protected void onCreate(Bundle savedInstanceState) { +        super.onCreate(savedInstanceState); + +        setContentView(R.layout.create_key_activity); + +        nameEdit = (AutoCompleteTextView) findViewById(R.id.name); +        emailEdit = (AutoCompleteTextView) findViewById(R.id.email); +        passphraseEdit = (EditText) findViewById(R.id.passphrase); +        createButton = (Button) findViewById(R.id.create_key_button); + +        emailEdit.setThreshold(1); // Start working from first character +        emailEdit.setAdapter( +                new ArrayAdapter<String> +                        (this, android.R.layout.simple_spinner_dropdown_item, +                                ContactHelper.getPossibleUserEmails(this) +                        ) +        ); +        emailEdit.addTextChangedListener(new TextWatcher() { +            @Override +            public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) { +            } + +            @Override +            public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) { +            } + +            @Override +            public void afterTextChanged(Editable editable) { +                String email = editable.toString(); +                if (email.length() > 0) { +                    Matcher emailMatcher = Patterns.EMAIL_ADDRESS.matcher(email); +                    if (emailMatcher.matches()) { +                        emailEdit.setCompoundDrawablesWithIntrinsicBounds(0, 0, +                                R.drawable.uid_mail_ok, 0); +                    } else { +                        emailEdit.setCompoundDrawablesWithIntrinsicBounds(0, 0, +                                R.drawable.uid_mail_bad, 0); +                    } +                } else { +                    // remove drawable if email is empty +                    emailEdit.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); +                } +            } +        }); +        nameEdit.setThreshold(1); // Start working from first character +        nameEdit.setAdapter( +                new ArrayAdapter<String> +                        (this, android.R.layout.simple_spinner_dropdown_item, +                                ContactHelper.getPossibleUserNames(this) +                        ) +        ); + +        createButton.setOnClickListener(new View.OnClickListener() { +            @Override +            public void onClick(View v) { +                createKeyCheck(); +            } +        }); + +    } + +    private void createKeyCheck() { +        if (isEditTextNotEmpty(this, nameEdit) +                && isEditTextNotEmpty(this, emailEdit) +                && isEditTextNotEmpty(this, passphraseEdit)) { +            createKey(); +        } +    } + +    private void createKey() { +        Intent intent = new Intent(this, KeychainIntentService.class); +        intent.setAction(KeychainIntentService.ACTION_SAVE_KEYRING); + +        // Message is received after importing is done in KeychainIntentService +        KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler( +                this, +                getString(R.string.progress_importing), +                ProgressDialog.STYLE_HORIZONTAL) { +            public void handleMessage(Message message) { +                // handle messages by standard KeychainIntentServiceHandler first +                super.handleMessage(message); + +                if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) { +                    CreateKeyActivity.this.finish(); +                } +            } +        }; + +        // fill values for this action +        Bundle data = new Bundle(); + +        SaveKeyringParcel parcel = new SaveKeyringParcel(); +        parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(Constants.choice.algorithm.rsa, 4096, KeyFlags.CERTIFY_OTHER, null)); +        parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(Constants.choice.algorithm.rsa, 4096, KeyFlags.SIGN_DATA, null)); +        parcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(Constants.choice.algorithm.rsa, 4096, KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE, null)); +        String userId = nameEdit.getText().toString() + " <" + emailEdit.getText().toString() + ">"; +        parcel.mAddUserIds.add(userId); +        parcel.mNewPassphrase = passphraseEdit.getText().toString(); + +        // get selected key entries +        data.putParcelable(KeychainIntentService.SAVE_KEYRING_PARCEL, parcel); + +        intent.putExtra(KeychainIntentService.EXTRA_DATA, data); + +        // Create a new Messenger for the communication back +        Messenger messenger = new Messenger(saveHandler); +        intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger); + +        saveHandler.showProgressDialog(this); + +        startService(intent); +    } + +    /** +     * Checks if text of given EditText is not empty. If it is empty an error is +     * set and the EditText gets the focus. +     * +     * @param context +     * @param editText +     * @return true if EditText is not empty +     */ +    private static boolean isEditTextNotEmpty(Context context, EditText editText) { +        boolean output = true; +        if (editText.getText().toString().length() == 0) { +            editText.setError("empty!"); +            editText.requestFocus(); +            output = false; +        } else { +            editText.setError(null); +        } + +        return output; +    } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyActivityOld.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyActivityOld.java index 51457cd45..70ccb8800 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyActivityOld.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyActivityOld.java @@ -280,7 +280,7 @@ public class EditKeyActivityOld extends ActionBarActivity implements EditorListe                  Uri secretUri = KeyRings.buildUnifiedKeyRingUri(mDataUri);                  WrappedSecretKeyRing keyRing = new ProviderHelper(this).getWrappedSecretKeyRing(secretUri); -                mMasterCanSign = keyRing.getSubKey().canCertify(); +                mMasterCanSign = keyRing.getSecretKey().canCertify();                  for (WrappedSecretKey key : keyRing.secretKeyIterator()) {                      // Turn into uncached instance                      mKeys.add(key.getUncached()); @@ -288,7 +288,7 @@ public class EditKeyActivityOld extends ActionBarActivity implements EditorListe                  }                  boolean isSet = false; -                for (String userId : keyRing.getSubKey().getUserIds()) { +                for (String userId : keyRing.getSecretKey().getUserIds()) {                      Log.d(Constants.TAG, "Added userId " + userId);                      if (!isSet) {                          isSet = true; @@ -556,7 +556,7 @@ public class EditKeyActivityOld extends ActionBarActivity implements EditorListe              saveParams.deletedKeys = mKeysView.getDeletedKeys();              saveParams.keysExpiryDates = getKeysExpiryDates(mKeysView);              saveParams.keysUsages = getKeysUsages(mKeysView); -            saveParams.newPassphrase = mNewPassphrase; +            saveParams.mNewPassphrase = mNewPassphrase;              saveParams.oldPassphrase = mCurrentPassphrase;              saveParams.newKeys = toPrimitiveArray(mKeysView.getNewKeysArray());              saveParams.keys = getKeys(mKeysView); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyFragment.java index 94ca883d6..b41871a39 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyFragment.java @@ -228,7 +228,7 @@ public class EditKeyFragment extends LoaderFragment implements              }          }); -        mSubkeysAddedAdapter = new SubkeysAddedAdapter(getActivity(), mSaveKeyringParcel.addSubKeys); +        mSubkeysAddedAdapter = new SubkeysAddedAdapter(getActivity(), mSaveKeyringParcel.mAddSubKeys);          mSubkeysAddedList.setAdapter(mSubkeysAddedAdapter);          // Prepare the loaders. Either re-connect with an existing ones, @@ -298,7 +298,7 @@ public class EditKeyFragment extends LoaderFragment implements                      Bundle data = message.getData();                      // cache new returned passphrase! -                    mSaveKeyringParcel.newPassphrase = data +                    mSaveKeyringParcel.mNewPassphrase = data                              .getString(SetPassphraseDialogFragment.MESSAGE_NEW_PASSPHRASE);                  }              } @@ -320,19 +320,19 @@ public class EditKeyFragment extends LoaderFragment implements                  switch (message.what) {                      case EditUserIdDialogFragment.MESSAGE_CHANGE_PRIMARY_USER_ID:                          // toggle -                        if (mSaveKeyringParcel.changePrimaryUserId != null -                                && mSaveKeyringParcel.changePrimaryUserId.equals(userId)) { -                            mSaveKeyringParcel.changePrimaryUserId = null; +                        if (mSaveKeyringParcel.mChangePrimaryUserId != null +                                && mSaveKeyringParcel.mChangePrimaryUserId.equals(userId)) { +                            mSaveKeyringParcel.mChangePrimaryUserId = null;                          } else { -                            mSaveKeyringParcel.changePrimaryUserId = userId; +                            mSaveKeyringParcel.mChangePrimaryUserId = userId;                          }                          break;                      case EditUserIdDialogFragment.MESSAGE_REVOKE:                          // toggle -                        if (mSaveKeyringParcel.revokeUserIds.contains(userId)) { -                            mSaveKeyringParcel.revokeUserIds.remove(userId); +                        if (mSaveKeyringParcel.mRevokeUserIds.contains(userId)) { +                            mSaveKeyringParcel.mRevokeUserIds.remove(userId);                          } else { -                            mSaveKeyringParcel.revokeUserIds.add(userId); +                            mSaveKeyringParcel.mRevokeUserIds.add(userId);                          }                          break;                  } @@ -363,10 +363,10 @@ public class EditKeyFragment extends LoaderFragment implements                          break;                      case EditSubkeyDialogFragment.MESSAGE_REVOKE:                          // toggle -                        if (mSaveKeyringParcel.revokeSubKeys.contains(keyId)) { -                            mSaveKeyringParcel.revokeSubKeys.remove(keyId); +                        if (mSaveKeyringParcel.mRevokeSubKeys.contains(keyId)) { +                            mSaveKeyringParcel.mRevokeSubKeys.remove(keyId);                          } else { -                            mSaveKeyringParcel.revokeSubKeys.add(keyId); +                            mSaveKeyringParcel.mRevokeSubKeys.add(keyId);                          }                          break;                  } @@ -450,10 +450,11 @@ public class EditKeyFragment extends LoaderFragment implements      }      private void save(String passphrase) { -        Log.d(Constants.TAG, "add userids to parcel: " + mUserIdsAddedAdapter.getDataAsStringList()); -        Log.d(Constants.TAG, "mSaveKeyringParcel.newPassphrase: " + mSaveKeyringParcel.newPassphrase); +        mSaveKeyringParcel.mAddUserIds = mUserIdsAddedAdapter.getDataAsStringList(); -        mSaveKeyringParcel.addUserIds = mUserIdsAddedAdapter.getDataAsStringList(); +        Log.d(Constants.TAG, "mSaveKeyringParcel.mAddUserIds: " + mSaveKeyringParcel.mAddUserIds); +        Log.d(Constants.TAG, "mSaveKeyringParcel.mNewPassphrase: " + mSaveKeyringParcel.mNewPassphrase); +        Log.d(Constants.TAG, "mSaveKeyringParcel.mRevokeUserIds: " + mSaveKeyringParcel.mRevokeUserIds);          // Message is received after importing is done in KeychainIntentService          KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler( @@ -509,4 +510,4 @@ public class EditKeyFragment extends LoaderFragment implements          // start service with intent          getActivity().startService(intent);      } -}
\ No newline at end of file +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptAsymmetricFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptAsymmetricFragment.java index 51963e963..dc0510189 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptAsymmetricFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/EncryptAsymmetricFragment.java @@ -198,7 +198,7 @@ public class EncryptAsymmetricFragment extends Fragment {              String[] userId;              try {                  userId = mProviderHelper.getCachedPublicKeyRing( -                        KeyRings.buildUnifiedKeyRingUri(mSecretKeyId)).getSplitPrimaryUserId(); +                        KeyRings.buildUnifiedKeyRingUri(mSecretKeyId)).getSplitPrimaryUserIdWithFallback();              } catch (PgpGeneralException e) {                  userId = null;              } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/FirstTimeActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/FirstTimeActivity.java new file mode 100644 index 000000000..aa4120d2c --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/FirstTimeActivity.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program.  If not, see <http://www.gnu.org/licenses/>. + */ + +package org.sufficientlysecure.keychain.ui; + +import android.content.Intent; +import android.os.Bundle; +import android.support.v7.app.ActionBarActivity; +import android.view.View; +import android.view.Window; +import android.widget.Button; + +import org.sufficientlysecure.keychain.R; + +public class FirstTimeActivity extends ActionBarActivity { + +    Button mCreateKey; +    Button mImportKey; +    Button mSkipSetup; + +    @Override +    protected void onCreate(Bundle savedInstanceState) { +        super.onCreate(savedInstanceState); + +        supportRequestWindowFeature(Window.FEATURE_NO_TITLE); + +        setContentView(R.layout.first_time_activity); + +        mCreateKey = (Button) findViewById(R.id.first_time_create_key); +        mImportKey = (Button) findViewById(R.id.first_time_import_key); +        mSkipSetup = (Button) findViewById(R.id.first_time_cancel); + +        mSkipSetup.setOnClickListener(new View.OnClickListener() { +            @Override +            public void onClick(View v) { +                Intent intent = new Intent(FirstTimeActivity.this, KeyListActivity.class); +                startActivity(intent); +                finish(); +            } +        }); + +        mImportKey.setOnClickListener(new View.OnClickListener() { +            @Override +            public void onClick(View v) { +                Intent intent = new Intent(FirstTimeActivity.this, ImportKeysActivity.class); +                intent.setAction(ImportKeysActivity.ACTION_IMPORT_KEY_FROM_FILE_AND_RETURN); +                startActivity(intent); +                finish(); +            } +        }); + +        mCreateKey.setOnClickListener(new View.OnClickListener() { +            @Override +            public void onClick(View v) { +                Intent intent = new Intent(FirstTimeActivity.this, CreateKeyActivity.class); +                startActivity(intent); +                finish(); +            } +        }); + +    } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListActivity.java index 849576284..9cc623c83 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListActivity.java @@ -32,8 +32,7 @@ import org.sufficientlysecure.keychain.Constants;  import org.sufficientlysecure.keychain.Constants.choice.algorithm;  import org.sufficientlysecure.keychain.R;  import org.sufficientlysecure.keychain.helper.ExportHelper; -import org.sufficientlysecure.keychain.helper.OtherHelper; -import org.sufficientlysecure.keychain.keyimport.ImportKeysListEntry; +import org.sufficientlysecure.keychain.helper.Preferences;  import org.sufficientlysecure.keychain.provider.KeychainContract;  import org.sufficientlysecure.keychain.provider.KeychainDatabase;  import org.sufficientlysecure.keychain.service.KeychainIntentService; @@ -43,7 +42,6 @@ import org.sufficientlysecure.keychain.service.SaveKeyringParcel.SubkeyAdd;  import org.sufficientlysecure.keychain.util.Log;  import java.io.IOException; -import java.util.ArrayList;  public class KeyListActivity extends DrawerActivity { @@ -53,6 +51,15 @@ public class KeyListActivity extends DrawerActivity {      public void onCreate(Bundle savedInstanceState) {          super.onCreate(savedInstanceState); +        Preferences prefs = Preferences.getPreferences(this); +        if (prefs.getFirstTime()) { +            prefs.setFirstTime(false); +            Intent intent = new Intent(this, FirstTimeActivity.class); +            startActivity(intent); +            finish(); +            return; +        } +          mExportHelper = new ExportHelper(this);          setContentView(R.layout.key_list_activity); @@ -66,9 +73,10 @@ public class KeyListActivity extends DrawerActivity {          super.onCreateOptionsMenu(menu);          getMenuInflater().inflate(R.menu.key_list, menu); -        if(Constants.DEBUG) { +        if (Constants.DEBUG) {              menu.findItem(R.id.menu_key_list_debug_read).setVisible(true);              menu.findItem(R.id.menu_key_list_debug_write).setVisible(true); +            menu.findItem(R.id.menu_key_list_debug_first_time).setVisible(true);          }          return true; @@ -85,10 +93,6 @@ public class KeyListActivity extends DrawerActivity {                  createKey();                  return true; -            case R.id.menu_key_list_create_expert: -                createKeyExpert(); -                return true; -              case R.id.menu_key_list_export:                  mExportHelper.showExportKeysDialog(null, Constants.Path.APP_DIR_FILE, true);                  return true; @@ -98,7 +102,7 @@ public class KeyListActivity extends DrawerActivity {                      KeychainDatabase.debugRead(this);                      AppMsg.makeText(this, "Restored from backup", AppMsg.STYLE_CONFIRM).show();                      getContentResolver().notifyChange(KeychainContract.KeyRings.CONTENT_URI, null); -                } catch(IOException e) { +                } catch (IOException e) {                      Log.e(Constants.TAG, "IO Error", e);                      AppMsg.makeText(this, "IO Error: " + e.getMessage(), AppMsg.STYLE_ALERT).show();                  } @@ -108,12 +112,18 @@ public class KeyListActivity extends DrawerActivity {                  try {                      KeychainDatabase.debugWrite(this);                      AppMsg.makeText(this, "Backup successful", AppMsg.STYLE_CONFIRM).show(); -                } catch(IOException e) { +                } catch (IOException e) {                      Log.e(Constants.TAG, "IO Error", e);                      AppMsg.makeText(this, "IO Error: " + e.getMessage(), AppMsg.STYLE_ALERT).show();                  }                  return true; +            case R.id.menu_key_list_debug_first_time: +                Intent intent = new Intent(this, FirstTimeActivity.class); +                startActivity(intent); +                finish(); +                return true; +              default:                  return super.onOptionsItemSelected(item);          } @@ -125,50 +135,8 @@ public class KeyListActivity extends DrawerActivity {      }      private void createKey() { -        Intent intent = new Intent(this, WizardActivity.class); -//        intent.setAction(EditKeyActivity.ACTION_CREATE_KEY); -//        intent.putExtra(EditKeyActivity.EXTRA_GENERATE_DEFAULT_KEYS, true); -//        intent.putExtra(EditKeyActivity.EXTRA_USER_IDS, ""); // show user id view -        startActivityForResult(intent, 0); +        Intent intent = new Intent(this, CreateKeyActivity.class); +        startActivity(intent);      } -    private void createKeyExpert() { -        Intent intent = new Intent(this, KeychainIntentService.class); -        intent.setAction(KeychainIntentService.ACTION_SAVE_KEYRING); - -        // Message is received after importing is done in KeychainIntentService -        KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler( -                this, -                getString(R.string.progress_importing), -                ProgressDialog.STYLE_HORIZONTAL) { -            public void handleMessage(Message message) { -                // handle messages by standard KeychainIntentServiceHandler first -                super.handleMessage(message); -                Bundle data = message.getData(); -                // OtherHelper.logDebugBundle(data, "message reply"); -            } -        }; - -        // fill values for this action -        Bundle data = new Bundle(); - -        SaveKeyringParcel parcel = new SaveKeyringParcel(); -        parcel.addSubKeys.add(new SubkeyAdd(algorithm.rsa, 1024, KeyFlags.CERTIFY_OTHER, null)); -        parcel.addSubKeys.add(new SubkeyAdd(algorithm.rsa, 1024, KeyFlags.SIGN_DATA, null)); -        parcel.addUserIds.add("swagerinho"); -        parcel.newPassphrase = "swag"; - -        // get selected key entries -        data.putParcelable(KeychainIntentService.SAVE_KEYRING_PARCEL, parcel); - -        intent.putExtra(KeychainIntentService.EXTRA_DATA, data); - -        // Create a new Messenger for the communication back -        Messenger messenger = new Messenger(saveHandler); -        intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger); - -        saveHandler.showProgressDialog(this); - -        startService(intent); -    }  }
\ No newline at end of file diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewCertActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewCertActivity.java index c7fffe263..cfdea0611 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewCertActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewCertActivity.java @@ -149,8 +149,8 @@ public class ViewCertActivity extends ActionBarActivity                          providerHelper.getWrappedPublicKeyRing(sig.getKeyId());                  try { -                    sig.init(signerRing.getSubkey()); -                    if (sig.verifySignature(signeeRing.getSubkey(), signeeUid)) { +                    sig.init(signerRing.getPublicKey()); +                    if (sig.verifySignature(signeeRing.getPublicKey(), signeeUid)) {                          mStatus.setText(R.string.cert_verify_ok);                          mStatus.setTextColor(getResources().getColor(R.color.result_green));                      } else { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/WizardActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/WizardActivity.java deleted file mode 100644 index 601fc09f9..000000000 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/WizardActivity.java +++ /dev/null @@ -1,466 +0,0 @@ -/* - * Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program.  If not, see <http://www.gnu.org/licenses/>. - */ - -package org.sufficientlysecure.keychain.ui; - -import android.app.Activity; -import android.content.ActivityNotFoundException; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.Bundle; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; -import android.support.v4.app.FragmentTransaction; -import android.support.v7.app.ActionBarActivity; -import android.text.Editable; -import android.text.TextWatcher; -import android.util.Patterns; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.inputmethod.InputMethodManager; -import android.widget.ArrayAdapter; -import android.widget.AutoCompleteTextView; -import android.widget.Button; -import android.widget.EditText; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.ProgressBar; -import android.widget.RadioGroup; -import android.widget.TextView; - -import org.sufficientlysecure.htmltextview.HtmlTextView; -import org.sufficientlysecure.keychain.Constants; -import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.helper.ContactHelper; -import org.sufficientlysecure.keychain.util.Log; - -import java.util.regex.Matcher; - - -public class WizardActivity extends ActionBarActivity { - -    private State mCurrentState; - -    // values for mCurrentScreen -    private enum State { -        START, CREATE_KEY, IMPORT_KEY, K9 -    } - -    public static final int REQUEST_CODE_IMPORT = 0x00007703; - -    Button mBackButton; -    Button mNextButton; -    StartFragment mStartFragment; -    CreateKeyFragment mCreateKeyFragment; -    K9Fragment mK9Fragment; - -    private static final String K9_PACKAGE = "com.fsck.k9"; -    //    private static final String K9_MARKET_INTENT_URI_BASE = "market://details?id=%s"; -//    private static final Intent K9_MARKET_INTENT = new Intent(Intent.ACTION_VIEW, Uri.parse( -//            String.format(K9_MARKET_INTENT_URI_BASE, K9_PACKAGE))); -    private static final Intent K9_MARKET_INTENT = new Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/k9mail/k-9/releases/tag/4.904")); - -    LinearLayout mProgressLayout; -    View mProgressLine; -    ProgressBar mProgressBar; -    ImageView mProgressImage; -    TextView mProgressText; - -    /** -     * Checks if text of given EditText is not empty. If it is empty an error is -     * set and the EditText gets the focus. -     * -     * @param context -     * @param editText -     * @return true if EditText is not empty -     */ -    private static boolean isEditTextNotEmpty(Context context, EditText editText) { -        boolean output = true; -        if (editText.getText().toString().length() == 0) { -            editText.setError("empty!"); -            editText.requestFocus(); -            output = false; -        } else { -            editText.setError(null); -        } - -        return output; -    } - -    public static class StartFragment extends Fragment { -        public static StartFragment newInstance() { -            StartFragment myFragment = new StartFragment(); - -            Bundle args = new Bundle(); -            myFragment.setArguments(args); - -            return myFragment; -        } - -        @Override -        public View onCreateView(LayoutInflater inflater, ViewGroup container, -                                 Bundle savedInstanceState) { -            return inflater.inflate(R.layout.wizard_start_fragment, -                    container, false); -        } -    } - -    public static class CreateKeyFragment extends Fragment { -        public static CreateKeyFragment newInstance() { -            CreateKeyFragment myFragment = new CreateKeyFragment(); - -            Bundle args = new Bundle(); -            myFragment.setArguments(args); - -            return myFragment; -        } - -        @Override -        public View onCreateView(LayoutInflater inflater, ViewGroup container, -                                 Bundle savedInstanceState) { -            View view = inflater.inflate(R.layout.wizard_create_key_fragment, -                    container, false); - -            final AutoCompleteTextView emailView = (AutoCompleteTextView) view.findViewById(R.id.email); -            emailView.setThreshold(1); // Start working from first character -            emailView.setAdapter( -                    new ArrayAdapter<String> -                            (getActivity(), android.R.layout.simple_spinner_dropdown_item, -                                    ContactHelper.getPossibleUserEmails(getActivity()) -                            ) -            ); -            emailView.addTextChangedListener(new TextWatcher() { -                @Override -                public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) { -                } - -                @Override -                public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) { -                } - -                @Override -                public void afterTextChanged(Editable editable) { -                    String email = editable.toString(); -                    if (email.length() > 0) { -                        Matcher emailMatcher = Patterns.EMAIL_ADDRESS.matcher(email); -                        if (emailMatcher.matches()) { -                            emailView.setCompoundDrawablesWithIntrinsicBounds(0, 0, -                                    R.drawable.uid_mail_ok, 0); -                        } else { -                            emailView.setCompoundDrawablesWithIntrinsicBounds(0, 0, -                                    R.drawable.uid_mail_bad, 0); -                        } -                    } else { -                        // remove drawable if email is empty -                        emailView.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); -                    } -                } -            }); -            final AutoCompleteTextView nameView = (AutoCompleteTextView) view.findViewById(R.id.name); -            nameView.setThreshold(1); // Start working from first character -            nameView.setAdapter( -                    new ArrayAdapter<String> -                            (getActivity(), android.R.layout.simple_spinner_dropdown_item, -                                    ContactHelper.getPossibleUserNames(getActivity()) -                            ) -            ); -            return view; -        } -    } - -    public static class K9Fragment extends Fragment { -        public static K9Fragment newInstance() { -            K9Fragment myFragment = new K9Fragment(); - -            Bundle args = new Bundle(); -            myFragment.setArguments(args); - -            return myFragment; -        } - -        @Override -        public View onCreateView(LayoutInflater inflater, ViewGroup container, -                                 Bundle savedInstanceState) { -            View v = inflater.inflate(R.layout.wizard_k9_fragment, -                    container, false); - -            HtmlTextView text = (HtmlTextView) v -                    .findViewById(R.id.wizard_k9_text); -            text.setHtmlFromString("Install K9. It's good for you! Here is a screenhot how to enable OK in K9: (TODO)", true); - -            return v; -        } - -    } - -    /** -     * Loads new fragment -     * -     * @param fragment -     */ -    private void loadFragment(Fragment fragment) { -        FragmentManager fragmentManager = getSupportFragmentManager(); -        FragmentTransaction fragmentTransaction = fragmentManager -                .beginTransaction(); -        fragmentTransaction.replace(R.id.wizard_container, -                fragment); -        fragmentTransaction.commit(); -    } - -    /** -     * Instantiate View and initialize fragments for this Activity -     */ -    @Override -    protected void onCreate(Bundle savedInstanceState) { -        super.onCreate(savedInstanceState); - -        setContentView(R.layout.wizard_activity); -        mBackButton = (Button) findViewById(R.id.wizard_back); -        mNextButton = (Button) findViewById(R.id.wizard_next); - -        // progress layout -        mProgressLayout = (LinearLayout) findViewById(R.id.wizard_progress); -        mProgressLine = findViewById(R.id.wizard_progress_line); -        mProgressBar = (ProgressBar) findViewById(R.id.wizard_progress_progressbar); -        mProgressImage = (ImageView) findViewById(R.id.wizard_progress_image); -        mProgressText = (TextView) findViewById(R.id.wizard_progress_text); - -        changeToState(State.START); -    } - -    private enum ProgressState { -        WORKING, ENABLED, DISABLED, ERROR -    } - -    private void showProgress(ProgressState state, String text) { -        switch (state) { -            case WORKING: -                mProgressBar.setVisibility(View.VISIBLE); -                mProgressImage.setVisibility(View.GONE); -                break; -            case ENABLED: -                mProgressBar.setVisibility(View.GONE); -                mProgressImage.setVisibility(View.VISIBLE); -//			mProgressImage.setImageDrawable(getResources().getDrawable( -//					R.drawable.status_enabled)); -                break; -            case DISABLED: -                mProgressBar.setVisibility(View.GONE); -                mProgressImage.setVisibility(View.VISIBLE); -//			mProgressImage.setImageDrawable(getResources().getDrawable( -//					R.drawable.status_disabled)); -                break; -            case ERROR: -                mProgressBar.setVisibility(View.GONE); -                mProgressImage.setVisibility(View.VISIBLE); -//			mProgressImage.setImageDrawable(getResources().getDrawable( -//					R.drawable.status_fail)); -                break; - -            default: -                break; -        } -        mProgressText.setText(text); - -        mProgressLine.setVisibility(View.VISIBLE); -        mProgressLayout.setVisibility(View.VISIBLE); -    } - -    private void hideProgress() { -        mProgressLine.setVisibility(View.GONE); -        mProgressLayout.setVisibility(View.GONE); -    } - -    public void nextOnClick(View view) { -        // close keyboard -        if (getCurrentFocus() != null) { -            InputMethodManager inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); -            inputManager.hideSoftInputFromWindow(getCurrentFocus() -                    .getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); -        } - -        switch (mCurrentState) { -            case START: { -                RadioGroup radioGroup = (RadioGroup) findViewById(R.id.wizard_start_radio_group); -                int selectedId = radioGroup.getCheckedRadioButtonId(); -                switch (selectedId) { -                    case R.id.wizard_start_new_key: { -                        changeToState(State.CREATE_KEY); -                        break; -                    } -                    case R.id.wizard_start_import: { -                        changeToState(State.IMPORT_KEY); -                        break; -                    } -                    case R.id.wizard_start_skip: { -                        finish(); -                        break; -                    } -                } - -                mBackButton.setText(R.string.btn_back); -                break; -            } -            case CREATE_KEY: -                EditText nameEdit = (EditText) findViewById(R.id.name); -                EditText emailEdit = (EditText) findViewById(R.id.email); -                EditText passphraseEdit = (EditText) findViewById(R.id.passphrase); - -                if (isEditTextNotEmpty(this, nameEdit) -                        && isEditTextNotEmpty(this, emailEdit) -                        && isEditTextNotEmpty(this, passphraseEdit)) { - -//                    SaveKeyringParcel newKey = new SaveKeyringParcel(); -//                    newKey.addUserIds.add(nameEdit.getText().toString() + " <" -//                            + emailEdit.getText().toString() + ">"); - - -                    AsyncTask<String, Boolean, Boolean> generateTask = new AsyncTask<String, Boolean, Boolean>() { - -                        @Override -                        protected void onPreExecute() { -                            super.onPreExecute(); - -                            showProgress(ProgressState.WORKING, "generating key..."); -                        } - -                        @Override -                        protected Boolean doInBackground(String... params) { -                            return true; -                        } - -                        @Override -                        protected void onPostExecute(Boolean result) { -                            super.onPostExecute(result); - -                            if (result) { -                                showProgress(ProgressState.ENABLED, "key generated successfully!"); - -                                changeToState(State.K9); -                            } else { -                                showProgress(ProgressState.ERROR, "error in key gen"); -                            } -                        } - -                    }; - -                    generateTask.execute(""); -                } -                break; -            case K9: { -                RadioGroup radioGroup = (RadioGroup) findViewById(R.id.wizard_k9_radio_group); -                int selectedId = radioGroup.getCheckedRadioButtonId(); -                switch (selectedId) { -                    case R.id.wizard_k9_install: { -                        try { -                            startActivity(K9_MARKET_INTENT); -                        } catch (ActivityNotFoundException e) { -                            Log.e(Constants.TAG, "Activity not found for: " + K9_MARKET_INTENT); -                        } -                        break; -                    } -                    case R.id.wizard_k9_skip: { -                        finish(); -                        break; -                    } -                } - -                finish(); -                break; -            } -            default: -                break; -        } -    } - -    @Override -    protected void onActivityResult(int requestCode, int resultCode, Intent data) { -        super.onActivityResult(requestCode, resultCode, data); -        switch (requestCode) { -            case REQUEST_CODE_IMPORT: { -                if (resultCode == Activity.RESULT_OK) { -                    // imported now... -                    changeToState(State.K9); -                } else { -                    // back to start -                    changeToState(State.START); -                } -                break; -            } - -            default: { -                super.onActivityResult(requestCode, resultCode, data); - -                break; -            } -        } -    } - -    public void backOnClick(View view) { -        switch (mCurrentState) { -            case START: -                finish(); -                break; -            case CREATE_KEY: -                changeToState(State.START); -                break; -            case IMPORT_KEY: -                changeToState(State.START); -                break; -            default: -                changeToState(State.START); -                break; -        } -    } - -    private void changeToState(State state) { -        switch (state) { -            case START: { -                mCurrentState = State.START; -                mStartFragment = StartFragment.newInstance(); -                loadFragment(mStartFragment); -                mBackButton.setText(android.R.string.cancel); -                mNextButton.setText(R.string.btn_next); -                break; -            } -            case CREATE_KEY: { -                mCurrentState = State.CREATE_KEY; -                mCreateKeyFragment = CreateKeyFragment.newInstance(); -                loadFragment(mCreateKeyFragment); -                break; -            } -            case IMPORT_KEY: { -                mCurrentState = State.IMPORT_KEY; -                Intent intent = new Intent(this, ImportKeysActivity.class); -                intent.setAction(ImportKeysActivity.ACTION_IMPORT_KEY_FROM_FILE_AND_RETURN); -                startActivityForResult(intent, REQUEST_CODE_IMPORT); -                break; -            } -            case K9: { -                mCurrentState = State.K9; -                mBackButton.setEnabled(false); // don't go back to import/create key -                mK9Fragment = K9Fragment.newInstance(); -                loadFragment(mK9Fragment); -                break; -            } -        } -    } - -} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/SubkeysAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/SubkeysAdapter.java index ccc5960c8..e5dbebe01 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/SubkeysAdapter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/SubkeysAdapter.java @@ -143,7 +143,7 @@ public class SubkeysAdapter extends CursorAdapter {          // for edit key          if (mSaveKeyringParcel != null) { -            boolean revokeThisSubkey = (mSaveKeyringParcel.revokeSubKeys.contains(keyId)); +            boolean revokeThisSubkey = (mSaveKeyringParcel.mRevokeSubKeys.contains(keyId));              if (revokeThisSubkey) {                  if (!isRevoked) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserIdsAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserIdsAdapter.java index 10e299ae3..18312660a 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserIdsAdapter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/UserIdsAdapter.java @@ -131,10 +131,10 @@ public class UserIdsAdapter extends CursorAdapter implements AdapterView.OnItemC          // for edit key          if (mSaveKeyringParcel != null) { -            boolean changeAnyPrimaryUserId = (mSaveKeyringParcel.changePrimaryUserId != null); -            boolean changeThisPrimaryUserId = (mSaveKeyringParcel.changePrimaryUserId != null -                    && mSaveKeyringParcel.changePrimaryUserId.equals(userId)); -            boolean revokeThisUserId = (mSaveKeyringParcel.revokeUserIds.contains(userId)); +            boolean changeAnyPrimaryUserId = (mSaveKeyringParcel.mChangePrimaryUserId != null); +            boolean changeThisPrimaryUserId = (mSaveKeyringParcel.mChangePrimaryUserId != null +                    && mSaveKeyringParcel.mChangePrimaryUserId.equals(userId)); +            boolean revokeThisUserId = (mSaveKeyringParcel.mRevokeUserIds.contains(userId));              // only if primary user id will be changed              // (this is not triggered if the user id is currently the primary one) diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/PassphraseDialogFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/PassphraseDialogFragment.java index 4d0b73d30..d723f88af 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/PassphraseDialogFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/dialog/PassphraseDialogFragment.java @@ -152,7 +152,7 @@ public class PassphraseDialogFragment extends DialogFragment implements OnEditor                  // above can't be statically verified to have been set in all cases because                  // the catch clause doesn't return.                  try { -                    userId = secretRing.getPrimaryUserId(); +                    userId = secretRing.getPrimaryUserIdWithFallback();                  } catch (PgpGeneralException e) {                      userId = null;                  } @@ -232,7 +232,7 @@ public class PassphraseDialogFragment extends DialogFragment implements OnEditor                  try {                      PassphraseCacheService.addCachedPassphrase(activity, masterKeyId, passphrase, -                        secretRing.getPrimaryUserId()); +                        secretRing.getPrimaryUserIdWithFallback());                  } catch(PgpGeneralException e) {                      Log.e(Constants.TAG, "adding of a passhrase failed", e);                  } @@ -240,7 +240,7 @@ public class PassphraseDialogFragment extends DialogFragment implements OnEditor                  if (unlockedSecretKey.getKeyId() != masterKeyId) {                      PassphraseCacheService.addCachedPassphrase(                              activity, unlockedSecretKey.getKeyId(), passphrase, -                            unlockedSecretKey.getPrimaryUserId()); +                            unlockedSecretKey.getPrimaryUserIdWithFallback());                  }                  // also return passphrase back to activity diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ProgressScaler.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ProgressScaler.java index 5553ea5d2..869eea03f 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ProgressScaler.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/ProgressScaler.java @@ -39,15 +39,21 @@ public class ProgressScaler implements Progressable {       * Set progress of ProgressDialog by sending message to handler on UI thread       */      public void setProgress(String message, int progress, int max) { -        mWrapped.setProgress(message, mFrom + progress * (mTo - mFrom) / max, mMax); +        if (mWrapped != null) { +            mWrapped.setProgress(message, mFrom + progress * (mTo - mFrom) / max, mMax); +        }      }      public void setProgress(int resourceId, int progress, int max) { -        mWrapped.setProgress(resourceId, progress, mMax); +        if (mWrapped != null) { +            mWrapped.setProgress(resourceId, progress, mMax); +        }      }      public void setProgress(int progress, int max) { -        mWrapped.setProgress(progress, max); +        if (mWrapped != null) { +            mWrapped.setProgress(progress, max); +        }      }  } diff --git a/OpenKeychain/src/main/res/drawable/first_time_1.png b/OpenKeychain/src/main/res/drawable/first_time_1.pngBinary files differ new file mode 100644 index 000000000..1f340df5c --- /dev/null +++ b/OpenKeychain/src/main/res/drawable/first_time_1.png diff --git a/OpenKeychain/src/main/res/layout/wizard_create_key_fragment.xml b/OpenKeychain/src/main/res/layout/create_key_activity.xml index 258ea7223..673f43084 100644 --- a/OpenKeychain/src/main/res/layout/wizard_create_key_fragment.xml +++ b/OpenKeychain/src/main/res/layout/create_key_activity.xml @@ -2,6 +2,7 @@  <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"      android:layout_width="match_parent"      android:layout_height="wrap_content" +    android:padding="8dp"      android:orientation="vertical">      <TextView @@ -38,4 +39,16 @@          android:layout_gravity="center_horizontal" /> +    <Button +        android:id="@+id/create_key_button" +        android:layout_width="match_parent" +        android:layout_height="wrap_content" +        android:layout_weight="1" +        android:layout_gravity="center_horizontal" +        android:layout_margin="8dp" +        android:text="@string/first_time_create_key" +        android:background="@drawable/button_edgy" +        android:drawableLeft="@drawable/ic_action_new_account" /> + +  </LinearLayout>
\ No newline at end of file diff --git a/OpenKeychain/src/main/res/layout/first_time_activity.xml b/OpenKeychain/src/main/res/layout/first_time_activity.xml new file mode 100644 index 000000000..514f34212 --- /dev/null +++ b/OpenKeychain/src/main/res/layout/first_time_activity.xml @@ -0,0 +1,89 @@ +<?xml version="1.0" encoding="utf-8"?> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" +    android:layout_width="wrap_content" +    android:layout_height="wrap_content" +    android:paddingTop="16dp" +    android:paddingBottom="8dp"> + +    <LinearLayout +        android:layout_width="match_parent" +        android:layout_height="match_parent" +        android:orientation="vertical"> + +        <TextView +            android:layout_width="wrap_content" +            android:layout_height="wrap_content" +            android:layout_marginLeft="8dp" +            android:textAppearance="?android:attr/textAppearanceLarge" +            android:text="@string/app_name" +            android:drawableLeft="@drawable/ic_launcher" +            android:drawablePadding="16dp" +            android:gravity="center" +            android:layout_gravity="center_horizontal" /> + +        <ImageView +            android:layout_width="wrap_content" +            android:layout_marginLeft="64dp" +            android:layout_marginRight="64dp" +            android:layout_marginTop="16dp" +            android:layout_marginBottom="16dp" +            android:layout_height="256dp" +            android:src="@drawable/first_time_1" /> + +        <TextView +            android:layout_width="wrap_content" +            android:layout_height="wrap_content" +            android:layout_marginLeft="64dp" +            android:layout_marginRight="64dp" +            android:textAppearance="?android:attr/textAppearanceMedium" +            android:text="@string/first_time_text1" +            android:layout_gravity="center_horizontal" +            android:gravity="center_horizontal" /> + + +    </LinearLayout> + +    <Button +        android:id="@+id/first_time_cancel" +        android:layout_alignParentBottom="true" +        android:layout_width="match_parent" +        android:layout_height="wrap_content" +        android:layout_marginBottom="8dp" +        android:layout_marginLeft="8dp" +        android:layout_marginRight="8dp" +        android:text="@string/first_time_skip" +        android:background="@drawable/button_edgy" /> + +    <LinearLayout +        android:layout_width="match_parent" +        android:layout_height="wrap_content" +        android:layout_above="@id/first_time_cancel" +        android:orientation="horizontal"> + +        <Button +            android:id="@+id/first_time_create_key" +            android:layout_width="match_parent" +            android:layout_height="wrap_content" +            android:layout_gravity="center_horizontal" +            android:layout_weight="1" +            android:layout_marginLeft="8dp" +            android:layout_marginRight="8dp" +            android:text="@string/first_time_create_key" +            android:background="@drawable/button_edgy" +            android:drawableLeft="@drawable/ic_action_new_account" /> + +        <Button +            android:id="@+id/first_time_import_key" +            android:layout_width="match_parent" +            android:layout_height="wrap_content" +            android:layout_weight="1" +            android:layout_gravity="center_horizontal" +            android:layout_marginLeft="8dp" +            android:layout_marginRight="8dp" +            android:text="@string/first_time_import_key" +            android:background="@drawable/button_edgy" +            android:drawableLeft="@drawable/ic_action_download" /> +    </LinearLayout> + + +</RelativeLayout> diff --git a/OpenKeychain/src/main/res/layout/wizard_activity.xml b/OpenKeychain/src/main/res/layout/wizard_activity.xml deleted file mode 100644 index 299d07a76..000000000 --- a/OpenKeychain/src/main/res/layout/wizard_activity.xml +++ /dev/null @@ -1,98 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" -    android:layout_width="match_parent" -    android:layout_height="match_parent"> - -    <LinearLayout -        android:id="@+id/wizard_buttons" -        android:layout_width="match_parent" -        android:layout_height="wrap_content" -        android:layout_alignParentBottom="true" -        android:orientation="horizontal"> - -        <Button -            android:id="@+id/wizard_back" -            android:layout_width="match_parent" -            android:layout_height="wrap_content" -            android:layout_weight="1" -            android:onClick="backOnClick" -            android:text="cancel" -            style="@style/SelectableItem" /> - -        <View -            android:layout_width="1dip" -            android:layout_height="match_parent" -            android:layout_marginBottom="4dip" -            android:layout_marginTop="4dip" -            android:background="?android:attr/listDivider" /> - -        <Button -            android:id="@+id/wizard_next" -            android:layout_width="match_parent" -            android:layout_height="wrap_content" -            android:layout_weight="1" -            android:onClick="nextOnClick" -            android:text="next" -            style="@style/SelectableItem" /> -    </LinearLayout> - -    <View -        android:id="@+id/wizard_progress_line" -        android:layout_width="match_parent" -        android:layout_height="1dip" -        android:layout_above="@+id/wizard_buttons" -        android:layout_marginLeft="4dip" -        android:layout_marginRight="4dip" -        android:background="?android:attr/listDivider" -        android:visibility="gone" /> - -    <LinearLayout -        android:id="@+id/wizard_progress" -        android:layout_width="match_parent" -        android:layout_height="wrap_content" -        android:layout_above="@+id/wizard_progress_line" -        android:visibility="gone"> - -        <ProgressBar -            android:id="@+id/wizard_progress_progressbar" -            android:layout_width="wrap_content" -            android:layout_height="wrap_content" /> - -        <ImageView -            android:id="@+id/wizard_progress_image" -            android:layout_width="wrap_content" -            android:layout_height="wrap_content" -            android:src="@drawable/icon_light_refresh" /> - -        <TextView -            android:id="@+id/wizard_progress_text" -            android:layout_width="wrap_content" -            android:layout_height="wrap_content" -            android:layout_gravity="center_vertical" -            android:text="asd" -            android:textAppearance="?android:attr/textAppearanceMedium" /> -    </LinearLayout> - -    <View -        android:id="@+id/wizard_line2" -        android:layout_width="match_parent" -        android:layout_height="1dip" -        android:layout_above="@+id/wizard_progress" -        android:layout_marginLeft="4dip" -        android:layout_marginRight="4dip" -        android:background="?android:attr/listDivider" /> - -    <ScrollView -        android:layout_width="match_parent" -        android:layout_height="match_parent" -        android:layout_above="@+id/wizard_line2"> - -        <LinearLayout -            android:id="@+id/wizard_container" -            android:layout_width="match_parent" -            android:layout_height="wrap_content" -            android:orientation="vertical" -            android:padding="16dp" /> -    </ScrollView> - -</RelativeLayout>
\ No newline at end of file diff --git a/OpenKeychain/src/main/res/layout/wizard_k9_fragment.xml b/OpenKeychain/src/main/res/layout/wizard_k9_fragment.xml deleted file mode 100644 index 342adc37e..000000000 --- a/OpenKeychain/src/main/res/layout/wizard_k9_fragment.xml +++ /dev/null @@ -1,43 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" -    android:layout_width="match_parent" -    android:layout_height="wrap_content" -    android:orientation="vertical" > - -    <org.sufficientlysecure.htmltextview.HtmlTextView -        android:id="@+id/wizard_k9_text" -        android:layout_width="match_parent" -        android:layout_height="wrap_content" -        android:paddingBottom="4dp" -        android:text="Text..." -        android:textAppearance="?android:attr/textAppearanceMedium" /> - -    <RadioGroup -        android:id="@+id/wizard_k9_radio_group" -        android:layout_width="match_parent" -        android:layout_height="wrap_content"> - -        <RadioButton -            android:layout_width="match_parent" -            android:layout_height="?android:attr/listPreferredItemHeight" -            android:checked="true" -            android:textAppearance="?android:attr/textAppearanceMedium" -            style="@style/SelectableItem" -            android:text="install K9" -            android:id="@+id/wizard_k9_install" /> - -        <View -            android:layout_width="match_parent" -            android:layout_height="1dip" -            android:background="?android:attr/listDivider" /> - -        <RadioButton -            android:layout_width="match_parent" -            android:layout_height="?android:attr/listPreferredItemHeight" -            android:textAppearance="?android:attr/textAppearanceMedium" -            style="@style/SelectableItem" -            android:text="skip install" -            android:id="@+id/wizard_k9_skip" /> -    </RadioGroup> - -</LinearLayout>
\ No newline at end of file diff --git a/OpenKeychain/src/main/res/layout/wizard_start_fragment.xml b/OpenKeychain/src/main/res/layout/wizard_start_fragment.xml deleted file mode 100644 index 9e1403f74..000000000 --- a/OpenKeychain/src/main/res/layout/wizard_start_fragment.xml +++ /dev/null @@ -1,63 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" -    android:layout_width="match_parent" -    android:layout_height="wrap_content" -    android:orientation="vertical"> - -    <TextView -        android:layout_width="match_parent" -        android:layout_height="wrap_content" -        android:paddingBottom="4dp" -        android:text="Welcome to OpenKeychain" -        android:textAppearance="?android:attr/textAppearanceMedium" /> - -    <TextView -        style="@style/SectionHeader" -        android:layout_width="wrap_content" -        android:layout_height="0dp" -        android:layout_marginTop="14dp" -        android:text="What you wanna do today?" -        android:layout_weight="1" /> - -    <RadioGroup -        android:id="@+id/wizard_start_radio_group" -        android:layout_width="match_parent" -        android:layout_height="wrap_content"> - -        <RadioButton -            android:layout_width="match_parent" -            android:layout_height="?android:attr/listPreferredItemHeight" -            android:checked="true" -            android:textAppearance="?android:attr/textAppearanceMedium" -            style="@style/SelectableItem" -            android:text="new key" -            android:id="@+id/wizard_start_new_key" /> - -        <View -            android:layout_width="match_parent" -            android:layout_height="1dip" -            android:background="?android:attr/listDivider" /> - -        <RadioButton -            android:layout_width="match_parent" -            android:layout_height="?android:attr/listPreferredItemHeight" -            android:textAppearance="?android:attr/textAppearanceMedium" -            style="@style/SelectableItem" -            android:text="import existing key" -            android:id="@+id/wizard_start_import" /> - -        <View -            android:layout_width="match_parent" -            android:layout_height="1dip" -            android:background="?android:attr/listDivider" /> - -        <RadioButton -            android:layout_width="match_parent" -            android:layout_height="?android:attr/listPreferredItemHeight" -            android:textAppearance="?android:attr/textAppearanceMedium" -            style="@style/SelectableItem" -            android:text="skip wizard" -            android:id="@+id/wizard_start_skip" /> -    </RadioGroup> - -</LinearLayout>
\ No newline at end of file diff --git a/OpenKeychain/src/main/res/menu/key_list.xml b/OpenKeychain/src/main/res/menu/key_list.xml index ebb7314b8..e865df182 100644 --- a/OpenKeychain/src/main/res/menu/key_list.xml +++ b/OpenKeychain/src/main/res/menu/key_list.xml @@ -27,11 +27,6 @@          android:title="@string/menu_create_key" />      <item -        android:id="@+id/menu_key_list_create_expert" -        app:showAsAction="never" -        android:title="@string/menu_create_key_expert" /> - -    <item          android:id="@+id/menu_key_list_debug_read"          app:showAsAction="never"          android:title="Debug / DB restore" @@ -43,4 +38,10 @@          android:title="Debug / DB backup"          android:visible="false" /> +    <item +        android:id="@+id/menu_key_list_debug_first_time" +        app:showAsAction="never" +        android:title="Debug / Show first time screen" +        android:visible="false" /> +  </menu> diff --git a/OpenKeychain/src/main/res/values/strings.xml b/OpenKeychain/src/main/res/values/strings.xml index 686460f75..50f73e73b 100644 --- a/OpenKeychain/src/main/res/values/strings.xml +++ b/OpenKeychain/src/main/res/values/strings.xml @@ -9,7 +9,6 @@      <string name="title_authentication">Passphrase</string>      <string name="title_create_key">Create Key</string>      <string name="title_edit_key">Edit Key</string> -    <string name="title_wizard">Welcome to OpenKeychain</string>      <string name="title_preferences">Preferences</string>      <string name="title_api_registered_apps">Apps</string>      <string name="title_key_server_preference">Keyserver Preference</string> @@ -511,7 +510,7 @@      <string name="cert_casual">casual</string>      <string name="cert_positive">positive</string>      <string name="cert_revoke">revoked</string> -    <string name="cert_verify_ok">ok</string> +    <string name="cert_verify_ok">OK</string>      <string name="cert_verify_failed">failed!</string>      <string name="cert_verify_error">error!</string>      <string name="cert_verify_unavailable">key unavailable</string> @@ -556,7 +555,7 @@      <string name="msg_ip_reinsert_secret">Re-inserting secret key</string>      <string name="msg_ip_uid_cert_bad">Encountered bad certificate!</string>      <string name="msg_ip_uid_cert_error">Error processing certificate!</string> -    <string name="msg_ip_uid_cert_good">User id is certified by %1$s (%2$s)</string> +    <string name="msg_ip_uid_cert_good">User id is certified by %1$s</string>      <plurals name="msg_ip_uid_certs_unknown">          <item quantity="one">Ignoring one certificate issued by an unknown public key</item>          <item quantity="other">Ignoring %s certificates issued by unknown public keys</item> @@ -637,6 +636,17 @@      <string name="msg_mg_heterogeneous">Tried to consolidate heterogeneous keyrings</string>      <string name="msg_mg_new_subkey">Adding new subkey %s</string>      <string name="msg_mg_found_new">Found %s new certificates in keyring</string> +    <string name="msg_mg_unchanged">No new certificates</string> + +    <!-- createSecretKeyRing --> +    <string name="msg_cr">Generating new master key</string> +    <string name="msg_cr_error_no_master">No master key options specified!</string> +    <string name="msg_cr_error_no_user_id">Keyrings must be created with at least one user id!</string> +    <string name="msg_cr_error_no_certify">Master key must have certify flag!</string> +    <string name="msg_cr_error_keysize_512">Key size must be greater or equal 512!</string> +    <string name="msg_cr_error_internal_pgp">Internal PGP error!</string> +    <string name="msg_cr_error_unknown_algo">Bad algorithm choice!</string> +    <string name="msg_cr_error_master_elgamal">Master key must not be of type ElGamal!</string>      <!-- modifySecretKeyRing -->      <string name="msg_mr">Modifying keyring %s</string> @@ -644,10 +654,13 @@      <string name="msg_mf_error_fingerprint">Actual key fingerprint does not match the expected one!</string>      <string name="msg_mf_error_keyid">No key ID. This is an internal error, please file a bug report!</string>      <string name="msg_mf_error_integrity">Internal error, integrity check failed!</string> +    <string name="msg_mf_error_noexist_primary">Bad primary user id specified!</string>      <string name="msg_mf_error_revoked_primary">Revoked user ids cannot be primary!</string>      <string name="msg_mf_error_pgp">PGP internal exception!</string>      <string name="msg_mf_error_sig">Signature exception!</string>      <string name="msg_mf_passphrase">Changing passphrase</string> +    <string name="msg_mf_primary_replace_old">Replacing certificate of previous primary user id</string> +    <string name="msg_mf_primary_new">Generating new certificate for new primary user id</string>      <string name="msg_mf_subkey_change">Modifying subkey %s</string>      <string name="msg_mf_subkey_missing">Tried to operate on missing subkey %s!</string>      <string name="msg_mf_subkey_new">Generating new %1$s bit %2$s subkey</string> @@ -691,4 +704,10 @@      <string name="info_no_manual_account_creation">Do not create OpenKeychain-Accounts manually.\nFor more information, see Help.</string>      <string name="contact_show_key">Show key (%s)</string> +    <!-- First Time --> +    <string name="first_time_text1">Take back your privacy with OpenKeychain!</string> +    <string name="first_time_create_key">Create Key</string> +    <string name="first_time_import_key">Import Key</string> +    <string name="first_time_skip">Skip Setup</string> +  </resources> @@ -37,8 +37,10 @@ Expand the Tools directory and select "Android SDK Build-tools (Version 19.1)".  Expand the Extras directory and install "Android Support Repository"    Select everything for the newest SDK Platform (API-Level 19)  4. Export ANDROID_HOME pointing to your Android SDK -5. Execute ``./gradlew build`` -6. You can install the app with ``adb install -r OpenKeychain/build/outputs/apk/OpenKeychain-debug-unaligned.apk`` +5. Use OpenJDK 7 instead of Oracle JDK (this is required for OpenKeychain's tests based on Bouncy Castle) +6. Execute ``./install-custom-gradle-test-plugin.sh`` +7. Execute ``./gradlew build`` +8. You can install the app with ``adb install -r OpenKeychain/build/outputs/apk/OpenKeychain-debug-unaligned.apk``  ### Build API Demo with Gradle diff --git a/Resources/gnupg-infographic/first_time_1.png b/Resources/gnupg-infographic/first_time_1.pngBinary files differ new file mode 100644 index 000000000..1f340df5c --- /dev/null +++ b/Resources/gnupg-infographic/first_time_1.png diff --git a/Resources/gnupg-infographic/first_time_1.svg b/Resources/gnupg-infographic/first_time_1.svg new file mode 100644 index 000000000..1f40c5ff3 --- /dev/null +++ b/Resources/gnupg-infographic/first_time_1.svg @@ -0,0 +1,351 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg +   xmlns:dc="http://purl.org/dc/elements/1.1/" +   xmlns:cc="http://creativecommons.org/ns#" +   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" +   xmlns:svg="http://www.w3.org/2000/svg" +   xmlns="http://www.w3.org/2000/svg" +   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" +   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" +   width="512" +   height="512" +   id="svg4325" +   version="1.1" +   inkscape:version="0.48.3.1 r9886" +   sodipodi:docname="ok_start.svg"> +  <defs +     id="defs4327" /> +  <sodipodi:namedview +     id="base" +     pagecolor="#ffffff" +     bordercolor="#666666" +     borderopacity="1.0" +     inkscape:pageopacity="0.0" +     inkscape:pageshadow="2" +     inkscape:zoom="1.979899" +     inkscape:cx="405.16912" +     inkscape:cy="246.12576" +     inkscape:document-units="px" +     inkscape:current-layer="layer1" +     showgrid="false" +     inkscape:window-width="2558" +     inkscape:window-height="1419" +     inkscape:window-x="0" +     inkscape:window-y="19" +     inkscape:window-maximized="1" /> +  <metadata +     id="metadata4330"> +    <rdf:RDF> +      <cc:Work +         rdf:about=""> +        <dc:format>image/svg+xml</dc:format> +        <dc:type +           rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> +        <dc:title></dc:title> +      </cc:Work> +    </rdf:RDF> +  </metadata> +  <g +     inkscape:label="Layer 1" +     inkscape:groupmode="layer" +     id="layer1" +     transform="translate(0,-540.36218)"> +    <path +       sodipodi:type="arc" +       style="opacity:0.59999999999999998;fill:#9933cc;fill-opacity:1;stroke:none;stroke-width:2;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" +       id="path4501" +       sodipodi:cx="227.53687" +       sodipodi:cy="246.58241" +       sodipodi:rx="225.51656" +       sodipodi:ry="225.51656" +       d="m 453.05342,246.58241 a 225.51656,225.51656 0 1 1 -451.0331106,0 225.51656,225.51656 0 1 1 451.0331106,0 z" +       transform="translate(28.463135,549.77977)" /> +    <path +       sodipodi:type="star" +       style="color:#000000;fill:#ffffff;fill-opacity:0.16256157;fill-rule:nonzero;stroke:none;stroke-width:1.76498926;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" +       id="path7021" +       sodipodi:sides="25" +       sodipodi:cx="237.45834" +       sodipodi:cy="671.60583" +       sodipodi:r1="60.973591" +       sodipodi:r2="37.364418" +       sodipodi:arg1="0.87061839" +       sodipodi:arg2="0.99902889" +       inkscape:flatsided="false" +       inkscape:rounded="0" +       inkscape:randomized="0" +       d="m 276.74691,718.23402 -19.06996,-15.20673 6.23968,23.51249 -14.68908,-19.47149 0.19632,24.32554 -9.38523,-22.51278 -5.85936,23.61014 -3.49167,-24.13951 -11.54688,21.41122 2.62127,-24.24948 -16.50886,17.86696 8.56952,-22.83575 -20.43354,13.20005 13.97931,-19.98717 -23.0743,7.70373 18.51073,-15.88272 -24.26522,1.72335 21.87906,-10.7803 -23.93146,-4.3653 23.87263,-5.00052 -22.094,-10.17967 24.36621,1.09346 -18.8683,-15.35441 23.32877,7.11874 -14.45703,-19.56438 20.8255,12.69672 -9.13738,-22.54504 17.01368,17.47692 -3.24358,-24.10913 12.13283,21.15898 2.85402,-24.15834 6.48963,23.51155 8.77228,-22.6896 0.43867,24.38679 14.13936,-19.79518 -5.63987,23.72973 18.61801,-15.65696 -11.36402,21.58163 21.92682,-10.53495 -16.37414,18.07749 23.85789,-4.75101 -20.35541,13.43747 24.28988,1.33147 -23.05767,7.95313 23.19564,7.33029 -24.31112,1.96905 20.64393,12.86851 -24.03703,-4.13873 16.7951,17.59816 -22.2526,-9.98648 z" +       transform="matrix(2.3346891,0,0,2.3346891,-297.98143,-788.87687)" +       inkscape:transform-center-x="0.48070196" +       inkscape:transform-center-y="0.080663394" /> +    <rect +       ry="14.623538" +       rx="14.623538" +       y="778.16095" +       x="189.31395" +       height="91.847595" +       width="133.37183" +       id="rect3529" +       style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.12070131;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" /> +    <path +       inkscape:connector-curvature="0" +       style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.12070131;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" +       d="m 254.33815,626.00818 c -27.93766,0 -50.4195,22.48185 -50.4195,50.41951 l 0,93.04182 c 0,27.93768 22.48184,50.44562 50.4195,50.44562 l 3.3229,0 c 27.93773,0 50.4195,-22.50794 50.4195,-50.44562 l 0,-93.04182 c 0,-27.93766 -22.48177,-50.41951 -50.4195,-50.41951 l -3.3229,0 z m 1.67456,16.74544 c 20.12817,0 35.95034,15.82142 35.95034,35.47939 l 0,74.46486 c 0,19.65806 -15.82217,35.47941 -35.95034,35.47941 -20.1282,0 -35.97658,-15.82135 -35.97658,-35.47941 l 0,-74.46486 c 0,-19.65797 15.84838,-35.47939 35.97658,-35.47939 z" +       id="path3531" /> +    <rect +       ry="3.9737797" +       rx="3.9737797" +       y="706.40234" +       x="171.49129" +       height="124.14557" +       width="169.01746" +       id="rect3533" +       style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.12070131;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" /> +    <path +       sodipodi:nodetypes="cscc" +       inkscape:connector-curvature="0" +       id="path3535" +       d="m 199.31408,796.34234 c 12.41682,10.27683 33.179,17.03328 56.69903,17.03328 23.51804,0 44.25559,-6.75787 56.67285,-17.03328 z" +       style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.12070131;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" /> +    <path +       transform="matrix(0.90538746,0,0,0.90538746,-76.624431,405.57604)" +       d="m 316.87315,407.66223 a 9.388834,9.388834 0 1 1 -18.77767,0 9.388834,9.388834 0 1 1 18.77767,0 z" +       sodipodi:ry="9.388834" +       sodipodi:rx="9.388834" +       sodipodi:cy="407.66223" +       sodipodi:cx="307.48431" +       id="path3537" +       style="color:#000000;fill:#4b4f2f;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.55131245;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" +       sodipodi:type="arc" /> +    <path +       transform="matrix(0.90538746,0,0,0.90538746,36.007648,405.57604)" +       sodipodi:type="arc" +       style="color:#000000;fill:#4b4f2f;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.55131245;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" +       id="path3539" +       sodipodi:cx="307.48431" +       sodipodi:cy="407.66223" +       sodipodi:rx="9.388834" +       sodipodi:ry="9.388834" +       d="m 316.87315,407.66223 a 9.388834,9.388834 0 1 1 -18.77767,0 9.388834,9.388834 0 1 1 18.77767,0 z" /> +    <g +       id="g10754" +       transform="matrix(0.83727181,0,0,0.83727181,829.92363,-416.95697)" +       style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"> +      <g +         id="g5737-8" +         transform="translate(-898.12108,631.20806)" +         style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"> +        <rect +           style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" +           id="rect5739-5" +           width="29.344997" +           height="96.371902" +           x="153.20558" +           y="918.29181" +           rx="0.84849852" +           ry="0.95320696" /> +        <path +           sodipodi:nodetypes="cscc" +           inkscape:connector-curvature="0" +           id="path5741-6" +           d="m 192.11794,916.83294 c -7.43536,-12.87842 -23.7891,-17.26042 -36.66754,-9.82505 -12.87845,7.43537 -17.26039,23.78913 -9.82503,36.66755 z" +           style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" /> +        <rect +           ry="3" +           rx="3" +           y="1010.0776" +           x="133.00574" +           height="11.311689" +           width="69.74469" +           id="rect5743-2" +           style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" /> +        <path +           style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" +           d="m 145.00574,916.83294 c 7.43536,-12.87842 23.7891,-17.26042 36.66754,-9.82505 12.87845,7.43537 17.26039,23.78913 9.82503,36.66755 z" +           id="path5745-2" +           inkscape:connector-curvature="0" +           sodipodi:nodetypes="cscc" /> +        <rect +           ry="7.1440544" +           rx="7.1440544" +           y="968.78937" +           x="145.20558" +           height="14.288109" +           width="45.344997" +           id="rect5747-2" +           style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" /> +      </g> +      <g +         id="g5749-5" +         transform="matrix(-1,0,0,1,-472.81628,631.20806)" +         style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"> +        <rect +           ry="0.95320696" +           rx="0.84849852" +           y="918.29181" +           x="153.20558" +           height="96.371902" +           width="29.344997" +           id="rect5751-5" +           style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" /> +        <path +           style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" +           d="m 192.11794,916.83294 c -7.43536,-12.87842 -23.7891,-17.26042 -36.66754,-9.82505 -12.87845,7.43537 -17.26039,23.78913 -9.82503,36.66755 z" +           id="path5753-0" +           inkscape:connector-curvature="0" +           sodipodi:nodetypes="cscc" /> +        <rect +           style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" +           id="rect5755-0" +           width="69.74469" +           height="11.311689" +           x="133.00574" +           y="1010.0776" +           rx="3" +           ry="3" /> +        <path +           sodipodi:nodetypes="cscc" +           inkscape:connector-curvature="0" +           id="path5757-1" +           d="m 145.00574,916.83294 c 7.43536,-12.87842 23.7891,-17.26042 36.66754,-9.82505 12.87845,7.43537 17.26039,23.78913 9.82503,36.66755 z" +           style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" /> +        <rect +           style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" +           id="rect5759-7" +           width="45.344997" +           height="14.288109" +           x="145.20558" +           y="968.78937" +           rx="7.1440544" +           ry="7.1440544" /> +      </g> +    </g> +    <g +       transform="matrix(0.83727181,0,0,0.83727181,84.650265,61.513651)" +       id="g5697-5" +       style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"> +      <path +         sodipodi:nodetypes="cccccc" +         inkscape:connector-curvature="0" +         id="path5699-6" +         d="m 326.98547,847.70808 57.45819,0 0,-73.83379 -10,0 0,61.83379 -47.45819,0" +         style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" /> +      <rect +         style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" +         id="rect5703-1" +         width="9.4107542" +         height="42.249756" +         x="305.50339" +         y="821.53503" +         rx="4.7053771" +         ry="4.7053771" /> +      <path +         inkscape:connector-curvature="0" +         style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" +         d="m 390.56742,727.10519 c -6.09957,0 -11,4.90043 -11,11 l 0,10.15625 -10.15625,0 c -6.09957,0 -11,4.90043 -11,11 0,6.09957 4.90043,11 11,11 l 21.15625,0 c 6.09957,0 11,-4.90043 11,-11 l 0,-21.15625 c 0,-6.09957 -4.90043,-11 -11,-11 z" +         id="path5705-8" /> +      <rect +         ry="2.1935852" +         rx="4.0765176" +         y="771.6814" +         x="369.11923" +         height="12.20938" +         width="21.740107" +         id="rect5707-1" +         style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" /> +      <rect +         ry="2.9156427" +         rx="7.0489321" +         y="829.57007" +         x="318.22556" +         height="26.179665" +         width="14.097864" +         id="rect5709-1" +         style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" /> +      <g +         id="g5711-2" +         transform="translate(-34.057657,-0.65450616)" +         style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"> +        <path +           inkscape:connector-curvature="0" +           id="path5713-1" +           d="m 404.77705,825.97291 -9.16957,15.86559 9.16957,15.84038 18.38979,0 9.19491,-15.84038 -9.19491,-15.86559 -18.38979,0 z" +           style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" /> +        <path +           inkscape:connector-curvature="0" +           id="path5715-3" +           d="m 413.97195,836.00644 c 3.23493,0 5.85942,2.61224 5.85942,5.83206 0,3.21983 -2.62449,5.83207 -5.85942,5.83207 -3.23494,0 -5.85943,-2.61224 -5.85943,-5.83207 0,-3.21982 2.62449,-5.83206 5.85943,-5.83206 z" +           style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" /> +      </g> +    </g> +    <g +       transform="matrix(-0.83727181,0,0,-0.83727181,427.34978,1471.4837)" +       id="g10957" +       style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"> +      <path +         sodipodi:nodetypes="cccccc" +         inkscape:connector-curvature="0" +         id="path10959" +         d="m 326.98547,847.70808 57.45819,0 0,-73.83379 -10,0 0,61.83379 -47.45819,0" +         style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" /> +      <rect +         style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" +         id="rect10961" +         width="9.4107542" +         height="42.249756" +         x="305.50339" +         y="821.53503" +         rx="4.7053771" +         ry="4.7053771" /> +      <path +         inkscape:connector-curvature="0" +         style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" +         d="m 390.56742,727.10519 c -6.09957,0 -11,4.90043 -11,11 l 0,10.15625 -10.15625,0 c -6.09957,0 -11,4.90043 -11,11 0,6.09957 4.90043,11 11,11 l 21.15625,0 c 6.09957,0 11,-4.90043 11,-11 l 0,-21.15625 c 0,-6.09957 -4.90043,-11 -11,-11 z" +         id="path10963" /> +      <rect +         ry="2.1935852" +         rx="4.0765176" +         y="771.6814" +         x="369.11923" +         height="12.20938" +         width="21.740107" +         id="rect10965" +         style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" /> +      <rect +         ry="2.9156427" +         rx="7.0489321" +         y="829.57007" +         x="318.22556" +         height="26.179665" +         width="14.097864" +         id="rect10967" +         style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" /> +      <g +         id="g10969" +         transform="translate(-34.057657,-0.65450616)" +         style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"> +        <path +           inkscape:connector-curvature="0" +           id="path10971" +           d="m 404.77705,825.97291 -9.16957,15.86559 9.16957,15.84038 18.38979,0 9.19491,-15.84038 -9.19491,-15.86559 -18.38979,0 z" +           style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" /> +        <path +           inkscape:connector-curvature="0" +           id="path10973" +           d="m 413.97195,836.00644 c 3.23493,0 5.85942,2.61224 5.85942,5.83206 0,3.21983 -2.62449,5.83207 -5.85942,5.83207 -3.23494,0 -5.85943,-2.61224 -5.85943,-5.83207 0,-3.21982 2.62449,-5.83206 5.85943,-5.83206 z" +           style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.92158127;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" /> +      </g> +    </g> +    <rect +       ry="12.863822" +       rx="12.863822" +       y="836.96265" +       x="214.05772" +       height="25.727644" +       width="83.8843" +       id="rect5685-1" +       style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#4b4f2f;stroke-width:4.12070131;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" /> +  </g> +</svg> diff --git a/build.gradle b/build.gradle index ceb963cd8..698b4cd37 100644 --- a/build.gradle +++ b/build.gradle @@ -1,12 +1,16 @@  buildscript {      repositories {          mavenCentral() +        // need this for com.novoda:gradle-android-test-plugin:0.9.9-SNAPSHOT below (0.9.3 in repos doesn't work!) +        // run ./install-custom-gradle-test-plugin.sh to pull the thing into the local repository +        mavenLocal()      }      dependencies {          // NOTE: Always use fixed version codes not dynamic ones, e.g. 0.7.3 instead of 0.7.+, see README for more information          classpath 'com.android.tools.build:gradle:0.12.0'          classpath 'org.robolectric:robolectric-gradle-plugin:0.11.0' +        classpath 'com.novoda:gradle-android-test-plugin:0.9.9-SNAPSHOT'      }  } @@ -19,3 +23,9 @@ allprojects {  task wrapper(type: Wrapper) {      gradleVersion = '1.12'  } + +subprojects { +    tasks.withType(Test) { +        maxParallelForks = 1 +    } +} diff --git a/install-custom-gradle-test-plugin.sh b/install-custom-gradle-test-plugin.sh new file mode 100755 index 000000000..85c13d959 --- /dev/null +++ b/install-custom-gradle-test-plugin.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +mkdir temp +cd temp + +  git clone https://github.com/nenick/gradle-android-test-plugin.git +  cd gradle-android-test-plugin + +    echo "rootProject.name = 'gradle-android-test-plugin-parent'" > settings.gradle +    echo "include ':gradle-android-test-plugin'" >> settings.gradle + +    ./gradlew :gradle-android-test-plugin:install + +  cd .. +cd ..
\ No newline at end of file diff --git a/settings.gradle b/settings.gradle index d8802320c..86088e04a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,5 @@  include ':OpenKeychain' +include ':OpenKeychain-Test'  include ':extern:openpgp-api-lib'  include ':extern:openkeychain-api-lib'  include ':extern:html-textview' | 
