Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 76605f7d authored by Joe Steele's avatar Joe Steele
Browse files

Extract code into new LocalKeyStore class

Also, implement the ability to configure an alternate key store
file location. This permits the running of unit tests without
clobbering the live key store file.

Also, add a test to confirm that the key store file is being written
out and reread correctly.
parent eb136919
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -22,7 +22,7 @@ import com.fsck.k9.mail.AuthenticationFailedException;
import com.fsck.k9.mail.CertificateValidationException;
import com.fsck.k9.mail.Store;
import com.fsck.k9.mail.Transport;
import com.fsck.k9.mail.store.TrustManagerFactory;
import com.fsck.k9.mail.store.LocalKeyStore;
import com.fsck.k9.mail.store.WebDavStore;
import com.fsck.k9.mail.filter.Hex;

@@ -370,7 +370,7 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
                            } else {
                                uri = Uri.parse(mAccount.getTransportUri());
                            }
                            TrustManagerFactory.addCertificate(uri.getHost(), uri.getPort(), chain[0]);
                            LocalKeyStore.getInstance().addCertificate(uri.getHost(), uri.getPort(), chain[0]);
                        } catch (CertificateException e) {
                            showErrorDialog(
                                R.string.account_setup_failed_dlg_certificate_message_fmt,
+126 −0
Original line number Diff line number Diff line
package com.fsck.k9.mail.store;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

import org.apache.commons.io.IOUtils;

import android.content.Context;
import android.util.Log;

import com.fsck.k9.K9;

public class LocalKeyStore {
    private static final LocalKeyStore sInstance = new LocalKeyStore();
    private File mKeyStoreFile;
    private KeyStore mKeyStore;

    public static LocalKeyStore getInstance() {
        return sInstance;
    }

    private LocalKeyStore() {
        setKeyStoreFile(null);
    }

    /**
     * Reinitialize the local key store with certificates contained in
     * {@code file}
     *
     * @param file
     *            {@link File} containing locally saved certificates. May be 0
     *            length, in which case it is deleted and recreated. May be
     *            {@code null}, in which case a default file location is used.
     */
    public synchronized void setKeyStoreFile(File file) {
        if (file == null) {
            file = new File(K9.app.getDir("KeyStore", Context.MODE_PRIVATE)
                    + File.separator + "KeyStore.bks");
        }
        if (file.length() == 0) {
            // The file may be empty (e.g., if it was created with
            // File.createTempFile)
            // We can't pass an empty file to Keystore.load. Instead, we let it
            // be created anew.
            file.delete();
        }

        FileInputStream fis = null;
        try {
            fis = new FileInputStream(file);
        } catch (FileNotFoundException e) {
            // If the file doesn't exist, that's fine, too
        }

        try {
            KeyStore store = KeyStore.getInstance(KeyStore.getDefaultType());
            store.load(fis, "".toCharArray());
            mKeyStore = store;
            mKeyStoreFile = file;
        } catch (Exception e) {
            Log.e(K9.LOG_TAG, "Failed to initialize local key store", e);
            // Use of the local key store is effectively disabled.
            mKeyStore = null;
            mKeyStoreFile = null;
        } finally {
            IOUtils.closeQuietly(fis);
        }
    }

    public synchronized void addCertificate(String host, int port,
            X509Certificate certificate) throws CertificateException {
        if (mKeyStore == null) {
            throw new CertificateException(
                    "Certificate not added because key store not initialized");
        }
        java.io.OutputStream keyStoreStream = null;
        try {
            mKeyStore.setCertificateEntry(getCertKey(host, port), certificate);
            keyStoreStream = new java.io.FileOutputStream(mKeyStoreFile);
            mKeyStore.store(keyStoreStream, "".toCharArray());
        } catch (FileNotFoundException e) {
            throw new CertificateException("Unable to write KeyStore: "
                    + e.getMessage());
        } catch (CertificateException e) {
            throw new CertificateException("Unable to write KeyStore: "
                    + e.getMessage());
        } catch (IOException e) {
            throw new CertificateException("Unable to write KeyStore: "
                    + e.getMessage());
        } catch (NoSuchAlgorithmException e) {
            throw new CertificateException("Unable to write KeyStore: "
                    + e.getMessage());
        } catch (KeyStoreException e) {
            throw new CertificateException("Unable to write KeyStore: "
                    + e.getMessage());
        } finally {
            IOUtils.closeQuietly(keyStoreStream);
        }
    }

    public synchronized boolean isValidCertificate(Certificate certificate, String host,
            int port) {
        if (mKeyStore == null) {
            return false;
        }
        Certificate storedCert = null;
        try {
            storedCert = mKeyStore.getCertificate(getCertKey(host, port));
            return (storedCert != null && storedCert.equals(certificate));
        } catch (KeyStoreException e) {
            return false;
        }
    }

    private static String getCertKey(String host, int port) {
        return host + ":" + port;
    }
}
+12 −86
Original line number Diff line number Diff line

package com.fsck.k9.mail.store;

import android.content.Context;
import android.util.Log;
import com.fsck.k9.K9;
import com.fsck.k9.helper.DomainNameChecker;
import com.fsck.k9.mail.CertificateChainException;

import org.apache.commons.io.IOUtils;

import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.HashMap;
@@ -30,9 +21,7 @@ public final class TrustManagerFactory {
    private static X509TrustManager defaultTrustManager;
    private static X509TrustManager unsecureTrustManager;

    private static File keyStoreFile;
    private static KeyStore keyStore;

    private static LocalKeyStore keyStore;

    private static class SimpleX509TrustManager implements X509TrustManager {
        public void checkClientTrusted(X509Certificate[] chain, String authType)
@@ -61,7 +50,7 @@ public final class TrustManagerFactory {
        }

        public synchronized static X509TrustManager getInstance(String host, int port) {
            String key = getCertKey(host, port);
            String key = host + ":" + port;
            SecureX509TrustManager trustManager;
            if (mTrustManager.containsKey(key)) {
                trustManager = mTrustManager.get(key);
@@ -90,23 +79,17 @@ public final class TrustManagerFactory {

            // Check the local key store if we couldn't verify the certificate using the global
            // key store or if the host name doesn't match the certificate name
            if (!foundInGlobalKeyStore || !DomainNameChecker.match(certificate, mHost)) {
                try {
                    Certificate storedCert = keyStore.getCertificate(getCertKey(mHost, mPort));
                    if (storedCert != null && storedCert.equals(certificate)) {
            if (foundInGlobalKeyStore
                    && DomainNameChecker.match(certificate, mHost)
                    || keyStore.isValidCertificate(certificate, mHost, mPort)) {
                return;
            }
                } catch (KeyStoreException e) {
                    throw new CertificateException("Certificate cannot be verified", e);
                }

            String message = (foundInGlobalKeyStore) ?
                    "Certificate domain name does not match " + mHost :
                    "Couldn't find certificate in local key store";

            throw new CertificateChainException(message, chain);
        }
        }

        public X509Certificate[] getAcceptedIssuers() {
            return defaultTrustManager.getAcceptedIssuers();
@@ -116,7 +99,7 @@ public final class TrustManagerFactory {

    static {
        try {
            loadKeyStore();
            keyStore = LocalKeyStore.getInstance();

            javax.net.ssl.TrustManagerFactory tmf = javax.net.ssl.TrustManagerFactory.getInstance("X509");
            tmf.init((KeyStore) null);
@@ -138,34 +121,6 @@ public final class TrustManagerFactory {
        unsecureTrustManager = new SimpleX509TrustManager();
    }

    static void loadKeyStore() throws KeyStoreException, NoSuchAlgorithmException {
        Context context = K9.app;

        keyStoreFile = new File(context.getDir("KeyStore", Context.MODE_PRIVATE) +
                File.separator + "KeyStore.bks");
        keyStore = KeyStore.getInstance(KeyStore.getDefaultType());

        FileInputStream fis;
        try {
            fis = new FileInputStream(keyStoreFile);
        } catch (FileNotFoundException e) {
            // If the file doesn't exist, that's fine, too
            fis = null;
        }

        try {
            keyStore.load(fis, "".toCharArray());
        } catch (IOException e) {
            Log.e(LOG_TAG, "KeyStore IOException while initializing TrustManagerFactory ", e);
            keyStore = null;
        } catch (CertificateException e) {
            Log.e(LOG_TAG, "KeyStore CertificateException while initializing TrustManagerFactory ", e);
            keyStore = null;
        } finally {
            IOUtils.closeQuietly(fis);
        }
    }

    private TrustManagerFactory() {
    }

@@ -173,33 +128,4 @@ public final class TrustManagerFactory {
        return secure ? SecureX509TrustManager.getInstance(host, port) :
               unsecureTrustManager;
    }

    public static void addCertificate(String host, int port, X509Certificate certificate) throws CertificateException {
        try {
            keyStore.setCertificateEntry(getCertKey(host, port), certificate);

            java.io.OutputStream keyStoreStream = null;
            try {
                keyStoreStream = new java.io.FileOutputStream(keyStoreFile);
                keyStore.store(keyStoreStream, "".toCharArray());
            } catch (FileNotFoundException e) {
                throw new CertificateException("Unable to write KeyStore: " + e.getMessage());
            } catch (CertificateException e) {
                throw new CertificateException("Unable to write KeyStore: " + e.getMessage());
            } catch (IOException e) {
                throw new CertificateException("Unable to write KeyStore: " + e.getMessage());
            } finally {
                IOUtils.closeQuietly(keyStoreStream);
            }

        } catch (NoSuchAlgorithmException e) {
            Log.e(LOG_TAG, "Unable to get X509 Trust Manager ", e);
        } catch (KeyStoreException e) {
            Log.e(LOG_TAG, "Key Store exception while initializing TrustManagerFactory ", e);
        }
    }

    private static String getCertKey(String host, int port) {
        return host + ":" + port;
    }
}
+32 −46
Original line number Diff line number Diff line
package com.fsck.k9.mail.store;

import javax.net.ssl.X509TrustManager;
import com.fsck.k9.K9;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.concurrent.CountDownLatch;

import android.app.Application;
import android.content.Context;
import android.test.AndroidTestCase;

/**
@@ -56,6 +50,8 @@ public class TrustManagerFactoryTest extends AndroidTestCase {

    private X509Certificate mCert1;
    private X509Certificate mCert2;
    private File mKeyStoreFile;
    private LocalKeyStore mKeyStore;


    public TrustManagerFactoryTest() throws CertificateException {
@@ -68,31 +64,15 @@ public class TrustManagerFactoryTest extends AndroidTestCase {

    @Override
    public void setUp() throws Exception {
        waitForAppInitialization();

        // Hack to make sure TrustManagerFactory.loadKeyStore() can create the key store file
        K9.app = new DummyApplication(getContext());

        // Delete the key store file to make sure we start without any stored certificates
        File keyStoreDir = getContext().getDir("KeyStore", Context.MODE_PRIVATE);
        new File(keyStoreDir + File.separator + "KeyStore.bks").delete();

        // Load the empty key store file
        TrustManagerFactory.loadKeyStore();

        mKeyStoreFile = File.createTempFile("localKeyStore", null, getContext()
                .getCacheDir());
        mKeyStore = LocalKeyStore.getInstance();
        mKeyStore.setKeyStoreFile(mKeyStoreFile);
    }

    private void waitForAppInitialization() throws InterruptedException {
        final CountDownLatch latch = new CountDownLatch(1);

        K9.registerApplicationAware(new K9.ApplicationAware() {
    @Override
            public void initializeComponent(Application application) {
                latch.countDown();
            }
        });

        latch.await();
    protected void tearDown() {
        mKeyStoreFile.delete();
    }

    /**
@@ -108,8 +88,8 @@ public class TrustManagerFactoryTest extends AndroidTestCase {
     *         if anything goes wrong
     */
    public void testDifferentCertificatesOnSameServer() throws Exception {
        TrustManagerFactory.addCertificate(NOT_MATCHING_HOST, PORT1, mCert1);
        TrustManagerFactory.addCertificate(NOT_MATCHING_HOST, PORT2, mCert2);
        mKeyStore.addCertificate(NOT_MATCHING_HOST, PORT1, mCert1);
        mKeyStore.addCertificate(NOT_MATCHING_HOST, PORT2, mCert2);

        X509TrustManager trustManager1 = TrustManagerFactory.get(NOT_MATCHING_HOST, PORT1, true);
        X509TrustManager trustManager2 = TrustManagerFactory.get(NOT_MATCHING_HOST, PORT2, true);
@@ -118,19 +98,19 @@ public class TrustManagerFactoryTest extends AndroidTestCase {
    }

    public void testSelfSignedCertificateMatchingHost() throws Exception {
        TrustManagerFactory.addCertificate(MATCHING_HOST, PORT1, mCert1);
        mKeyStore.addCertificate(MATCHING_HOST, PORT1, mCert1);
        X509TrustManager trustManager = TrustManagerFactory.get(MATCHING_HOST, PORT1, true);
        trustManager.checkServerTrusted(new X509Certificate[] { mCert1 }, "authType");
    }

    public void testSelfSignedCertificateNotMatchingHost() throws Exception {
        TrustManagerFactory.addCertificate(NOT_MATCHING_HOST, PORT1, mCert1);
        mKeyStore.addCertificate(NOT_MATCHING_HOST, PORT1, mCert1);
        X509TrustManager trustManager = TrustManagerFactory.get(NOT_MATCHING_HOST, PORT1, true);
        trustManager.checkServerTrusted(new X509Certificate[] { mCert1 }, "authType");
    }

    public void testWrongCertificate() throws Exception {
        TrustManagerFactory.addCertificate(MATCHING_HOST, PORT1, mCert1);
        mKeyStore.addCertificate(MATCHING_HOST, PORT1, mCert1);
        X509TrustManager trustManager = TrustManagerFactory.get(MATCHING_HOST, PORT1, true);
        boolean certificateValid;
        try {
@@ -143,8 +123,8 @@ public class TrustManagerFactoryTest extends AndroidTestCase {
    }

    public void testCertificateOfOtherHost() throws Exception {
        TrustManagerFactory.addCertificate(MATCHING_HOST, PORT1, mCert1);
        TrustManagerFactory.addCertificate(MATCHING_HOST, PORT2, mCert2);
        mKeyStore.addCertificate(MATCHING_HOST, PORT1, mCert1);
        mKeyStore.addCertificate(MATCHING_HOST, PORT2, mCert2);

        X509TrustManager trustManager = TrustManagerFactory.get(MATCHING_HOST, PORT1, true);
        boolean certificateValid;
@@ -157,15 +137,21 @@ public class TrustManagerFactoryTest extends AndroidTestCase {
        assertFalse("The certificate should have been rejected but wasn't", certificateValid);
    }

    private static class DummyApplication extends Application {
        private final Context mContext;

        DummyApplication(Context context) {
            mContext = context;
        }

        public File getDir(String name, int mode) {
            return mContext.getDir(name, mode);
        }
    public void testKeyStoreLoading() throws Exception {
        mKeyStore.addCertificate(MATCHING_HOST, PORT1, mCert1);
        mKeyStore.addCertificate(NOT_MATCHING_HOST, PORT2, mCert2);
        assertTrue(mKeyStore.isValidCertificate(mCert1, MATCHING_HOST, PORT1));
        assertTrue(mKeyStore.isValidCertificate(mCert2, NOT_MATCHING_HOST, PORT2));

        // reload store from same file
        mKeyStore.setKeyStoreFile(mKeyStoreFile);
        assertTrue(mKeyStore.isValidCertificate(mCert1, MATCHING_HOST, PORT1));
        assertTrue(mKeyStore.isValidCertificate(mCert2, NOT_MATCHING_HOST, PORT2));

        // reload store from empty file
        mKeyStoreFile.delete();
        mKeyStore.setKeyStoreFile(mKeyStoreFile);
        assertFalse(mKeyStore.isValidCertificate(mCert1, MATCHING_HOST, PORT1));
        assertFalse(mKeyStore.isValidCertificate(mCert2, NOT_MATCHING_HOST, PORT2));
    }
}