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.
This commit is contained in:
Joe Steele 2013-12-02 14:04:40 -05:00
parent eb13691918
commit 76605f7d86
4 changed files with 170 additions and 132 deletions

View File

@ -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,

View File

@ -0,0 +1,126 @@
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;
}
}

View File

@ -1,24 +1,15 @@
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,22 +79,16 @@ 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)) {
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);
if (foundInGlobalKeyStore
&& DomainNameChecker.match(certificate, mHost)
|| keyStore.isValidCertificate(certificate, mHost, mPort)) {
return;
}
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() {
@ -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;
}
}

View File

@ -1,17 +1,11 @@
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();
@Override
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;
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));
DummyApplication(Context context) {
mContext = context;
}
// reload store from same file
mKeyStore.setKeyStoreFile(mKeyStoreFile);
assertTrue(mKeyStore.isValidCertificate(mCert1, MATCHING_HOST, PORT1));
assertTrue(mKeyStore.isValidCertificate(mCert2, NOT_MATCHING_HOST, PORT2));
public File getDir(String name, int mode) {
return mContext.getDir(name, mode);
}
// 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));
}
}