From c5c195d243cbe26877d6dcb56cef41cc4ef671ea Mon Sep 17 00:00:00 2001 From: cketti Date: Fri, 29 Nov 2013 10:49:52 +0100 Subject: [PATCH] Add unit tests for TrustManagerFactory --- build.gradle | 2 + src/com/fsck/k9/K9.java | 40 ++++-- .../k9/mail/store/TrustManagerFactory.java | 49 ++++--- src/com/fsck/k9/provider/MessageProvider.java | 3 +- tests/assets/cert1.der | Bin 0 -> 525 bytes tests/assets/cert2.der | Bin 0 -> 525 bytes .../mail/store/TrustManagerFactoryTest.java | 129 ++++++++++++++++++ 7 files changed, 191 insertions(+), 32 deletions(-) create mode 100644 tests/assets/cert1.der create mode 100644 tests/assets/cert2.der create mode 100644 tests/src/com/fsck/k9/mail/store/TrustManagerFactoryTest.java diff --git a/build.gradle b/build.gradle index 017b5bd0d..2a75a8d7b 100644 --- a/build.gradle +++ b/build.gradle @@ -27,11 +27,13 @@ android { manifest.srcFile 'AndroidManifest.xml' java.srcDirs = ['src'] res.srcDirs = ['res'] + assets.srcDirs = ['assets'] } instrumentTest { manifest.srcFile 'tests/AndroidManifest.xml' java.srcDirs = ['tests/src'] + assets.srcDirs = ['tests/assets'] } } } diff --git a/src/com/fsck/k9/K9.java b/src/com/fsck/k9/K9.java index 5b3e20bbc..a4bed493b 100644 --- a/src/com/fsck/k9/K9.java +++ b/src/com/fsck/k9/K9.java @@ -59,7 +59,7 @@ public class K9 extends Application { * The application instance. Never null. * @throws Exception */ - void initializeComponent(K9 application); + void initializeComponent(Application application); } public static Application app = null; @@ -91,6 +91,15 @@ public class K9 extends Application { */ private static List observers = new ArrayList(); + /** + * This will be {@code true} once the initialization is complete and {@link #notifyObservers()} + * was called. + * Afterwards calls to {@link #registerApplicationAware(com.fsck.k9.K9.ApplicationAware)} will + * immediately call {@link com.fsck.k9.K9.ApplicationAware#initializeComponent(K9)} for the + * supplied argument. + */ + private static boolean sInitialized = false; + public enum BACKGROUND_OPS { WHEN_CHECKED, ALWAYS, NEVER, WHEN_CHECKED_AUTO_SYNC } @@ -829,15 +838,20 @@ public class K9 extends Application { * component that the application is available and ready */ protected void notifyObservers() { - for (final ApplicationAware aware : observers) { - if (K9.DEBUG) { - Log.v(K9.LOG_TAG, "Initializing observer: " + aware); - } - try { - aware.initializeComponent(this); - } catch (Exception e) { - Log.w(K9.LOG_TAG, "Failure when notifying " + aware, e); + synchronized (observers) { + for (final ApplicationAware aware : observers) { + if (K9.DEBUG) { + Log.v(K9.LOG_TAG, "Initializing observer: " + aware); + } + try { + aware.initializeComponent(this); + } catch (Exception e) { + Log.w(K9.LOG_TAG, "Failure when notifying " + aware, e); + } } + + sInitialized = true; + observers.clear(); } } @@ -848,8 +862,12 @@ public class K9 extends Application { * Never null. */ public static void registerApplicationAware(final ApplicationAware component) { - if (!observers.contains(component)) { - observers.add(component); + synchronized (observers) { + if (sInitialized) { + component.initializeComponent(K9.app); + } else if (!observers.contains(component)) { + observers.add(component); + } } } diff --git a/src/com/fsck/k9/mail/store/TrustManagerFactory.java b/src/com/fsck/k9/mail/store/TrustManagerFactory.java index 37c9f32dc..ff572ebff 100644 --- a/src/com/fsck/k9/mail/store/TrustManagerFactory.java +++ b/src/com/fsck/k9/mail/store/TrustManagerFactory.java @@ -1,7 +1,6 @@ package com.fsck.k9.mail.store; -import android.app.Application; import android.content.Context; import android.util.Log; import com.fsck.k9.K9; @@ -13,6 +12,7 @@ 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; @@ -114,26 +114,9 @@ public final class TrustManagerFactory { } static { - java.io.InputStream fis = null; try { javax.net.ssl.TrustManagerFactory tmf = javax.net.ssl.TrustManagerFactory.getInstance("X509"); - Application app = K9.app; - keyStoreFile = new File(app.getDir("KeyStore", Context.MODE_PRIVATE) + File.separator + "KeyStore.bks"); - keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); - try { - fis = new java.io.FileInputStream(keyStoreFile); - } catch (FileNotFoundException e1) { - 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; - } + loadKeyStore(); tmf.init(keyStore); TrustManager[] tms = tmf.getTrustManagers(); if (tms != null) { @@ -160,10 +143,36 @@ public final class TrustManagerFactory { 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); + } + 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); } - unsecureTrustManager = new SimpleX509TrustManager(); } private TrustManagerFactory() { diff --git a/src/com/fsck/k9/provider/MessageProvider.java b/src/com/fsck/k9/provider/MessageProvider.java index 80c5b0be7..989ee5e16 100644 --- a/src/com/fsck/k9/provider/MessageProvider.java +++ b/src/com/fsck/k9/provider/MessageProvider.java @@ -1,5 +1,6 @@ package com.fsck.k9.provider; +import android.app.Application; import android.content.ContentProvider; import android.content.ContentResolver; import android.content.ContentValues; @@ -981,7 +982,7 @@ public class MessageProvider extends ContentProvider { K9.registerApplicationAware(new K9.ApplicationAware() { @Override - public void initializeComponent(final K9 application) { + public void initializeComponent(final Application application) { Log.v(K9.LOG_TAG, "Registering content resolver notifier"); MessagingController.getInstance(application).addListener(new MessagingListener() { diff --git a/tests/assets/cert1.der b/tests/assets/cert1.der new file mode 100644 index 0000000000000000000000000000000000000000..cdfe84df76c6d6ba28f6b20eaf02109c62b6296f GIT binary patch literal 525 zcmXqLV&XJtVk~0fWccsya`o+9rl$tHY@Awc9&O)w85vnw84NrPxeYkkm_u3EgqfT@ z422B@K^zWYuHgLKRNdf`#FA7)9s@3rAeS(+x2~n3xPd5$$1Kd3ZK;=9k(gVMld6}T zpKBl|&TD9BU}<1xXlZJ0Y#JrbYXssNL%Gzns&PKDKNwjVm>YW;3>rI`8XFl_zKK5W zx*)z#xeg?t)9a0wozyhG1}0yc5hK~^#hbTnVNsuhIIl8y z)S}-754SI~dQ^V)tnm!rlUwcWbXQh1@G>zoGB6@L4;akMKzB92X}mXoM@UAmazmld z!mOrQ$uq)L|I5CWJ&>3a!|+XQ{tD~6GuQn5_n)KI=-*|VYYoBE)<^2@I(cHxp?4p* zE?e4eaINcIMnhZgHbv4cix_Y#?V>qC-^;Y=vwt15xH~r Wo1*frEg_=ftN!0ke|tOR$U*>OZo6dw literal 0 HcmV?d00001 diff --git a/tests/assets/cert2.der b/tests/assets/cert2.der new file mode 100644 index 0000000000000000000000000000000000000000..aecc3459dbb976895eb9d77320580d9eb7796df9 GIT binary patch literal 525 zcmXqLV&XJtVk~0fWH_^4Yt<#Tno9<}Y@Awc9&O)w85vnw84NrPxeYkkm_u3EgqfT@ z422B@K^zWYuHgLKRNdf`#FA7)9s@3rAeS(+x2~n3xPd5$$1Kd3ZK;=9k(gVMld6}T zpKBl|&TD9BU}<1xWME)mY8oZZYXssNL%Gzns&PKDKNwjVm>YW;3>rI`8XFnT>6_#g z=)T=q^xy18x|E0T+{iC?w=G@wjei0|{=SWS?pS3$)AHc{(Z7-V!?ItSmMXii+L<9B z_u^jLg-`%*eoq>^xvFGXvcfa7QL8E&a-rwP(e$ zwN9CO1!WXnl)k5Trl%y-ncL;LxPa@FO)Uji#b05*e%`o8hi0wqh^ce&Nw zKg3wG{=W-8lT{;8+QGmGO literal 0 HcmV?d00001 diff --git a/tests/src/com/fsck/k9/mail/store/TrustManagerFactoryTest.java b/tests/src/com/fsck/k9/mail/store/TrustManagerFactoryTest.java new file mode 100644 index 000000000..b68c33328 --- /dev/null +++ b/tests/src/com/fsck/k9/mail/store/TrustManagerFactoryTest.java @@ -0,0 +1,129 @@ +package com.fsck.k9.mail.store; + +import javax.net.ssl.X509TrustManager; +import com.fsck.k9.K9; +import java.io.File; +import java.lang.reflect.Method; +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.content.res.AssetManager; +import android.test.AndroidTestCase; + +/** + * Test the functionality of {@link TrustManagerFactory}. + */ +public class TrustManagerFactoryTest extends AndroidTestCase { + public static final String MATCHING_HOST = "k9.example.com"; + public static final String NOT_MATCHING_HOST = "bla.example.com"; + public static final int PORT1 = 993; + public static final int PORT2 = 465; + + + private Context mTestContext; + private X509Certificate mCert1; + private X509Certificate mCert2; + + + @Override + public void setUp() throws Exception { + waitForAppInitialization(); + + // Hack to make sure TrustManagerFactory.loadKeyStore() can create the key store file + K9.app = new DummyApplication(getContext()); + + // Source: https://kmansoft.wordpress.com/2011/04/18/accessing-resources-in-an-androidtestcase/ + Method m = AndroidTestCase.class.getMethod("getTestContext", new Class[] {}); + mTestContext = (Context) m.invoke(this, (Object[]) null); + + // 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(); + + // Load certificates + AssetManager assets = mTestContext.getAssets(); + + CertificateFactory certFactory = CertificateFactory.getInstance("X509"); + mCert1 = (X509Certificate) certFactory.generateCertificate(assets.open("cert1.der")); + mCert2 = (X509Certificate) certFactory.generateCertificate(assets.open("cert2.der")); + } + + 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(); + } + + /** + * Checks if TrustManagerFactory supports a host with different certificates for different + * services (e.g. SMTP and IMAP). + * + *

+ * This test is to make sure entries in the keystore file aren't overwritten. + * See Issue 1326. + *

+ * + * @throws Exception + * if anything goes wrong + */ + public void testDifferentCertificatesOnSameServer() throws Exception { + TrustManagerFactory.addCertificate(NOT_MATCHING_HOST, PORT1, mCert1); + TrustManagerFactory.addCertificate(NOT_MATCHING_HOST, PORT2, mCert2); + + X509TrustManager trustManager1 = TrustManagerFactory.get(NOT_MATCHING_HOST, PORT1, true); + X509TrustManager trustManager2 = TrustManagerFactory.get(NOT_MATCHING_HOST, PORT2, true); + trustManager2.checkServerTrusted(new X509Certificate[] { mCert2 }, "authType"); + trustManager1.checkServerTrusted(new X509Certificate[] { mCert1 }, "authType"); + } + + public void testSelfSignedCertificateMatchingHost() throws Exception { + TrustManagerFactory.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); + 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); + X509TrustManager trustManager = TrustManagerFactory.get(MATCHING_HOST, PORT1, true); + boolean certificateValid; + try { + trustManager.checkServerTrusted(new X509Certificate[] { mCert2 }, "authType"); + certificateValid = true; + } catch (CertificateException e) { + certificateValid = false; + } + 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); + } + } +}