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/Account.java b/src/com/fsck/k9/Account.java index 25a766f65..67a83e7ef 100644 --- a/src/com/fsck/k9/Account.java +++ b/src/com/fsck/k9/Account.java @@ -1,6 +1,8 @@ package com.fsck.k9; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; @@ -22,6 +24,7 @@ import android.net.ConnectivityManager; import android.net.Uri; import android.util.Log; +import com.fsck.k9.activity.setup.AccountSetupCheckSettings.CheckDirection; import com.fsck.k9.crypto.Apg; import com.fsck.k9.crypto.CryptoProvider; import com.fsck.k9.helper.Utility; @@ -40,6 +43,7 @@ import com.fsck.k9.search.SqlQueryBuilder; import com.fsck.k9.search.SearchSpecification.Attribute; import com.fsck.k9.search.SearchSpecification.SearchCondition; import com.fsck.k9.search.SearchSpecification.Searchfield; +import com.fsck.k9.security.LocalKeyStore; import com.fsck.k9.view.ColorChip; import com.larswerkman.colorpicker.ColorPicker; @@ -1865,4 +1869,57 @@ public class Account implements BaseAccount { search.and(Searchfield.FOLDER, folderName, Attribute.NOT_EQUALS); } } + + /** + * Add a new certificate for the incoming or outgoing server to the local key store. + */ + public void addCertificate(CheckDirection direction, + X509Certificate certificate) throws CertificateException { + Uri uri; + if (direction.equals(CheckDirection.INCOMING)) { + uri = Uri.parse(getStoreUri()); + } else { + uri = Uri.parse(getTransportUri()); + } + LocalKeyStore localKeyStore = LocalKeyStore.getInstance(); + localKeyStore.addCertificate(uri.getHost(), uri.getPort(), certificate); + } + + /** + * Examine the existing settings for an account. If the old host/port is different from the + * new host/port, then try and delete any (possibly non-existent) certificate stored for the + * old host/port. + */ + public void deleteCertificate(String newHost, int newPort, + CheckDirection direction) { + Uri uri; + if (direction.equals(CheckDirection.INCOMING)) { + uri = Uri.parse(getStoreUri()); + } else { + uri = Uri.parse(getTransportUri()); + } + String oldHost = uri.getHost(); + int oldPort = uri.getPort(); + if (oldPort == -1) { + // This occurs when a new account is created + return; + } + if (!newHost.equals(oldHost) || newPort != oldPort) { + LocalKeyStore localKeyStore = LocalKeyStore.getInstance(); + localKeyStore.deleteCertificate(oldHost, oldPort); + } + } + + /** + * Examine the settings for the account and attempt to delete (possibly non-existent) + * certificates for the incoming and outgoing servers. + */ + public void deleteCertificates() { + LocalKeyStore localKeyStore = LocalKeyStore.getInstance(); + + Uri uri = Uri.parse(getStoreUri()); + localKeyStore.deleteCertificate(uri.getHost(), uri.getPort()); + uri = Uri.parse(getTransportUri()); + localKeyStore.deleteCertificate(uri.getHost(), uri.getPort()); + } } diff --git a/src/com/fsck/k9/K9.java b/src/com/fsck/k9/K9.java index 5b3e20bbc..8ad98b96c 100644 --- a/src/com/fsck/k9/K9.java +++ b/src/com/fsck/k9/K9.java @@ -39,6 +39,7 @@ import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.internet.BinaryTempFileBody; import com.fsck.k9.mail.store.LocalStore; import com.fsck.k9.provider.UnreadWidgetProvider; +import com.fsck.k9.security.LocalKeyStore; import com.fsck.k9.service.BootReceiver; import com.fsck.k9.service.MailService; import com.fsck.k9.service.ShutdownReceiver; @@ -59,7 +60,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 +92,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 } @@ -581,6 +591,8 @@ public class K9 extends Application { */ BinaryTempFileBody.setTempDirectory(getCacheDir()); + LocalKeyStore.setKeyStoreLocation(getDir("KeyStore", MODE_PRIVATE).toString()); + /* * Enable background sync of messages */ @@ -829,15 +841,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 +865,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/Preferences.java b/src/com/fsck/k9/Preferences.java index 99d066d08..51b83d873 100644 --- a/src/com/fsck/k9/Preferences.java +++ b/src/com/fsck/k9/Preferences.java @@ -127,6 +127,7 @@ public class Preferences { Store.removeAccount(account); + account.deleteCertificates(); account.delete(this); if (newAccount == account) { diff --git a/src/com/fsck/k9/activity/setup/AccountSetupBasics.java b/src/com/fsck/k9/activity/setup/AccountSetupBasics.java index 435a35d24..94ba4cf5b 100644 --- a/src/com/fsck/k9/activity/setup/AccountSetupBasics.java +++ b/src/com/fsck/k9/activity/setup/AccountSetupBasics.java @@ -17,6 +17,7 @@ import android.widget.Button; import android.widget.EditText; import com.fsck.k9.*; import com.fsck.k9.activity.K9Activity; +import com.fsck.k9.activity.setup.AccountSetupCheckSettings.CheckDirection; import com.fsck.k9.helper.Utility; import java.io.Serializable; import java.io.UnsupportedEncodingException; @@ -36,7 +37,9 @@ public class AccountSetupBasics extends K9Activity private final static String EXTRA_ACCOUNT = "com.fsck.k9.AccountSetupBasics.account"; private final static int DIALOG_NOTE = 1; private final static String STATE_KEY_PROVIDER = - "com.fsck.k9.AccountSetupBasics.provider"; + "com.fsck.k9.AccountSetupBasics.provider"; + private final static String STATE_KEY_CHECKED_INCOMING = + "com.fsck.k9.AccountSetupBasics.checkedIncoming"; private EditText mEmailView; private EditText mPasswordView; @@ -46,6 +49,7 @@ public class AccountSetupBasics extends K9Activity private Provider mProvider; private EmailAddressValidator mEmailValidator = new EmailAddressValidator(); + private boolean mCheckedIncoming = false; public static void actionNewAccount(Context context) { Intent i = new Intent(context, AccountSetupBasics.class); @@ -66,15 +70,6 @@ public class AccountSetupBasics extends K9Activity mEmailView.addTextChangedListener(this); mPasswordView.addTextChangedListener(this); - - if (savedInstanceState != null && savedInstanceState.containsKey(EXTRA_ACCOUNT)) { - String accountUuid = savedInstanceState.getString(EXTRA_ACCOUNT); - mAccount = Preferences.getPreferences(this).getAccount(accountUuid); - } - - if (savedInstanceState != null && savedInstanceState.containsKey(STATE_KEY_PROVIDER)) { - mProvider = (Provider)savedInstanceState.getSerializable(STATE_KEY_PROVIDER); - } } @Override @@ -92,6 +87,23 @@ public class AccountSetupBasics extends K9Activity if (mProvider != null) { outState.putSerializable(STATE_KEY_PROVIDER, mProvider); } + outState.putBoolean(STATE_KEY_CHECKED_INCOMING, mCheckedIncoming); + } + + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + + if (savedInstanceState.containsKey(EXTRA_ACCOUNT)) { + String accountUuid = savedInstanceState.getString(EXTRA_ACCOUNT); + mAccount = Preferences.getPreferences(this).getAccount(accountUuid); + } + + if (savedInstanceState.containsKey(STATE_KEY_PROVIDER)) { + mProvider = (Provider) savedInstanceState.getSerializable(STATE_KEY_PROVIDER); + } + + mCheckedIncoming = savedInstanceState.getBoolean(STATE_KEY_CHECKED_INCOMING); } public void afterTextChanged(Editable s) { @@ -229,7 +241,8 @@ public class AccountSetupBasics extends K9Activity } else if (incomingUri.toString().startsWith("pop3")) { mAccount.setDeletePolicy(Account.DELETE_POLICY_NEVER); } - AccountSetupCheckSettings.actionCheckSettings(this, mAccount, true, true); + // Check incoming here. Then check outgoing in onActivityResult() + AccountSetupCheckSettings.actionCheckSettings(this, mAccount, CheckDirection.INCOMING); } catch (UnsupportedEncodingException enc) { // This really shouldn't happen since the encoding is hardcoded to UTF-8 Log.e(K9.LOG_TAG, "Couldn't urlencode username or password.", enc); @@ -266,11 +279,18 @@ public class AccountSetupBasics extends K9Activity @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode == RESULT_OK) { - mAccount.setDescription(mAccount.getEmail()); - mAccount.save(Preferences.getPreferences(this)); - K9.setServicesEnabled(this); - AccountSetupNames.actionSetNames(this, mAccount); - finish(); + if (!mCheckedIncoming) { + //We've successfully checked incoming. Now check outgoing. + mCheckedIncoming = true; + AccountSetupCheckSettings.actionCheckSettings(this, mAccount, CheckDirection.OUTGOING); + } else { + //We've successfully checked outgoing as well. + mAccount.setDescription(mAccount.getEmail()); + mAccount.save(Preferences.getPreferences(this)); + K9.setServicesEnabled(this); + AccountSetupNames.actionSetNames(this, mAccount); + finish(); + } } } diff --git a/src/com/fsck/k9/activity/setup/AccountSetupCheckSettings.java b/src/com/fsck/k9/activity/setup/AccountSetupCheckSettings.java index 5941f7369..ea2d1c166 100644 --- a/src/com/fsck/k9/activity/setup/AccountSetupCheckSettings.java +++ b/src/com/fsck/k9/activity/setup/AccountSetupCheckSettings.java @@ -22,7 +22,6 @@ 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.WebDavStore; import com.fsck.k9.mail.filter.Hex; @@ -47,9 +46,12 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList private static final String EXTRA_ACCOUNT = "account"; - private static final String EXTRA_CHECK_INCOMING = "checkIncoming"; + private static final String EXTRA_CHECK_DIRECTION ="checkDirection"; - private static final String EXTRA_CHECK_OUTGOING = "checkOutgoing"; + public enum CheckDirection { + INCOMING, + OUTGOING + } private Handler mHandler = new Handler(); @@ -59,20 +61,16 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList private Account mAccount; - private boolean mCheckIncoming; - - private boolean mCheckOutgoing; + private CheckDirection mDirection; private boolean mCanceled; private boolean mDestroyed; - public static void actionCheckSettings(Activity context, Account account, - boolean checkIncoming, boolean checkOutgoing) { + public static void actionCheckSettings(Activity context, Account account, CheckDirection direction) { Intent i = new Intent(context, AccountSetupCheckSettings.class); i.putExtra(EXTRA_ACCOUNT, account.getUuid()); - i.putExtra(EXTRA_CHECK_INCOMING, checkIncoming); - i.putExtra(EXTRA_CHECK_OUTGOING, checkOutgoing); + i.putExtra(EXTRA_CHECK_DIRECTION, direction); context.startActivityForResult(i, ACTIVITY_REQUEST_CODE); } @@ -89,8 +87,7 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList String accountUuid = getIntent().getStringExtra(EXTRA_ACCOUNT); mAccount = Preferences.getPreferences(this).getAccount(accountUuid); - mCheckIncoming = getIntent().getBooleanExtra(EXTRA_CHECK_INCOMING, false); - mCheckOutgoing = getIntent().getBooleanExtra(EXTRA_CHECK_OUTGOING, false); + mDirection = (CheckDirection) getIntent().getSerializableExtra(EXTRA_CHECK_DIRECTION); new Thread() { @Override @@ -108,9 +105,9 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList final MessagingController ctrl = MessagingController.getInstance(getApplication()); ctrl.clearCertificateErrorNotifications(AccountSetupCheckSettings.this, - mAccount, mCheckIncoming, mCheckOutgoing); + mAccount, mDirection); - if (mCheckIncoming) { + if (mDirection.equals(CheckDirection.INCOMING)) { store = mAccount.getRemoteStore(); if (store instanceof WebDavStore) { @@ -133,7 +130,7 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList finish(); return; } - if (mCheckOutgoing) { + if (mDirection.equals(CheckDirection.OUTGOING)) { if (!(mAccount.getRemoteStore() instanceof WebDavStore)) { setMessage(R.string.account_setup_check_settings_check_outgoing_msg); } @@ -366,21 +363,14 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { try { - String alias = mAccount.getUuid(); - if (mCheckIncoming) { - alias = alias + ".incoming"; - } - if (mCheckOutgoing) { - alias = alias + ".outgoing"; - } - TrustManagerFactory.addCertificateChain(alias, chain); + mAccount.addCertificate(mDirection, chain[0]); } catch (CertificateException e) { showErrorDialog( R.string.account_setup_failed_dlg_certificate_message_fmt, e.getMessage() == null ? "" : e.getMessage()); } AccountSetupCheckSettings.actionCheckSettings(AccountSetupCheckSettings.this, mAccount, - mCheckIncoming, mCheckOutgoing); + mDirection); } }) .setNegativeButton( diff --git a/src/com/fsck/k9/activity/setup/AccountSetupIncoming.java b/src/com/fsck/k9/activity/setup/AccountSetupIncoming.java index c76b6289f..e58fc8846 100644 --- a/src/com/fsck/k9/activity/setup/AccountSetupIncoming.java +++ b/src/com/fsck/k9/activity/setup/AccountSetupIncoming.java @@ -16,6 +16,7 @@ import android.widget.CompoundButton.OnCheckedChangeListener; import com.fsck.k9.*; import com.fsck.k9.activity.K9Activity; +import com.fsck.k9.activity.setup.AccountSetupCheckSettings.CheckDirection; import com.fsck.k9.helper.Utility; import com.fsck.k9.mail.ConnectionSecurity; import com.fsck.k9.mail.ServerSettings; @@ -427,6 +428,7 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener mWebdavMailboxPathView.getText().toString()); } + mAccount.deleteCertificate(host, port, CheckDirection.INCOMING); ServerSettings settings = new ServerSettings(mStoreType, host, port, connectionSecurity, authType, username, password, extra); @@ -437,7 +439,7 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener mAccount.setCompression(Account.TYPE_OTHER, mCompressionOther.isChecked()); mAccount.setSubscribedFoldersOnly(mSubscribedFoldersOnly.isChecked()); - AccountSetupCheckSettings.actionCheckSettings(this, mAccount, true, false); + AccountSetupCheckSettings.actionCheckSettings(this, mAccount, CheckDirection.INCOMING); } catch (Exception e) { failure(e); } diff --git a/src/com/fsck/k9/activity/setup/AccountSetupOutgoing.java b/src/com/fsck/k9/activity/setup/AccountSetupOutgoing.java index 6f71545fe..6d1af8bc0 100644 --- a/src/com/fsck/k9/activity/setup/AccountSetupOutgoing.java +++ b/src/com/fsck/k9/activity/setup/AccountSetupOutgoing.java @@ -15,6 +15,7 @@ import android.widget.*; import android.widget.CompoundButton.OnCheckedChangeListener; import com.fsck.k9.*; import com.fsck.k9.activity.K9Activity; +import com.fsck.k9.activity.setup.AccountSetupCheckSettings.CheckDirection; import com.fsck.k9.helper.Utility; import com.fsck.k9.mail.transport.SmtpTransport; @@ -95,7 +96,7 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener, try { if (new URI(mAccount.getStoreUri()).getScheme().startsWith("webdav")) { mAccount.setTransportUri(mAccount.getStoreUri()); - AccountSetupCheckSettings.actionCheckSettings(this, mAccount, false, true); + AccountSetupCheckSettings.actionCheckSettings(this, mAccount, CheckDirection.OUTGOING); } } catch (URISyntaxException e) { // TODO Auto-generated catch block @@ -308,10 +309,12 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener, if (mRequireLoginView.isChecked()) { userInfo = usernameEnc + ":" + passwordEnc + ":" + authType; } - uri = new URI(smtpSchemes[securityType], userInfo, mServerView.getText().toString(), - Integer.parseInt(mPortView.getText().toString()), null, null, null); + String newHost = mServerView.getText().toString(); + int newPort = Integer.parseInt(mPortView.getText().toString()); + uri = new URI(smtpSchemes[securityType], userInfo, newHost, newPort, null, null, null); + mAccount.deleteCertificate(newHost, newPort, CheckDirection.OUTGOING); mAccount.setTransportUri(uri.toString()); - AccountSetupCheckSettings.actionCheckSettings(this, mAccount, false, true); + AccountSetupCheckSettings.actionCheckSettings(this, mAccount, CheckDirection.OUTGOING); } catch (UnsupportedEncodingException enc) { // This really shouldn't happen since the encoding is hardcoded to UTF-8 Log.e(K9.LOG_TAG, "Couldn't urlencode username or password.", enc); diff --git a/src/com/fsck/k9/controller/MessagingController.java b/src/com/fsck/k9/controller/MessagingController.java index 33c457353..7de35d667 100644 --- a/src/com/fsck/k9/controller/MessagingController.java +++ b/src/com/fsck/k9/controller/MessagingController.java @@ -53,6 +53,7 @@ import com.fsck.k9.activity.FolderList; import com.fsck.k9.activity.MessageList; import com.fsck.k9.activity.MessageReference; import com.fsck.k9.activity.NotificationDeleteConfirmation; +import com.fsck.k9.activity.setup.AccountSetupCheckSettings.CheckDirection; import com.fsck.k9.activity.setup.AccountSetupIncoming; import com.fsck.k9.activity.setup.AccountSetupOutgoing; import com.fsck.k9.cache.EmailProviderCache; @@ -2671,14 +2672,13 @@ public class MessagingController implements Runnable { } public void clearCertificateErrorNotifications(Context context, - final Account account, boolean incoming, boolean outgoing) { + final Account account, CheckDirection direction) { final NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - if (incoming) { + if (direction.equals(CheckDirection.INCOMING)) { nm.cancel(null, K9.CERTIFICATE_EXCEPTION_NOTIFICATION_INCOMING + account.getAccountNumber()); - } - if (outgoing) { + } else { nm.cancel(null, K9.CERTIFICATE_EXCEPTION_NOTIFICATION_OUTGOING + account.getAccountNumber()); } } diff --git a/src/com/fsck/k9/mail/store/ImapStore.java b/src/com/fsck/k9/mail/store/ImapStore.java index a71a95aaf..eda66afb9 100644 --- a/src/com/fsck/k9/mail/store/ImapStore.java +++ b/src/com/fsck/k9/mail/store/ImapStore.java @@ -96,6 +96,8 @@ import com.fsck.k9.mail.store.ImapResponseParser.ImapList; import com.fsck.k9.mail.store.ImapResponseParser.ImapResponse; import com.fsck.k9.mail.store.imap.ImapUtility; import com.fsck.k9.mail.transport.imap.ImapSettings; +import com.fsck.k9.net.ssl.TrustManagerFactory; +import com.fsck.k9.net.ssl.TrustedSocketFactory; import com.jcraft.jzlib.JZlib; import com.jcraft.jzlib.ZOutputStream; @@ -2446,9 +2448,12 @@ public class ImapStore extends Store { connectionSecurity == CONNECTION_SECURITY_SSL_OPTIONAL) { SSLContext sslContext = SSLContext.getInstance("TLS"); boolean secure = connectionSecurity == CONNECTION_SECURITY_SSL_REQUIRED; - sslContext.init(null, new TrustManager[] { - TrustManagerFactory.get(mSettings.getHost(), secure) - }, new SecureRandom()); + sslContext + .init(null, + new TrustManager[] { TrustManagerFactory.get( + mSettings.getHost(), + mSettings.getPort(), secure) }, + new SecureRandom()); mSocket = TrustedSocketFactory.createSocket(sslContext); } else { mSocket = new Socket(); @@ -2501,9 +2506,11 @@ public class ImapStore extends Store { SSLContext sslContext = SSLContext.getInstance("TLS"); boolean secure = mSettings.getConnectionSecurity() == CONNECTION_SECURITY_TLS_REQUIRED; - sslContext.init(null, new TrustManager[] { - TrustManagerFactory.get(mSettings.getHost(), secure) - }, new SecureRandom()); + sslContext.init(null, + new TrustManager[] { TrustManagerFactory.get( + mSettings.getHost(), + mSettings.getPort(), secure) }, + new SecureRandom()); mSocket = TrustedSocketFactory.createSocket(sslContext, mSocket, mSettings.getHost(), mSettings.getPort(), true); mSocket.setSoTimeout(Store.SOCKET_READ_TIMEOUT); diff --git a/src/com/fsck/k9/mail/store/Pop3Store.java b/src/com/fsck/k9/mail/store/Pop3Store.java index 26e37d1dd..80711a48f 100644 --- a/src/com/fsck/k9/mail/store/Pop3Store.java +++ b/src/com/fsck/k9/mail/store/Pop3Store.java @@ -10,6 +10,8 @@ import com.fsck.k9.helper.Utility; import com.fsck.k9.mail.*; import com.fsck.k9.mail.internet.MimeMessage; +import com.fsck.k9.net.ssl.TrustManagerFactory; +import com.fsck.k9.net.ssl.TrustedSocketFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLException; @@ -327,9 +329,9 @@ public class Pop3Store extends Store { mConnectionSecurity == CONNECTION_SECURITY_SSL_OPTIONAL) { SSLContext sslContext = SSLContext.getInstance("TLS"); final boolean secure = mConnectionSecurity == CONNECTION_SECURITY_SSL_REQUIRED; - sslContext.init(null, new TrustManager[] { - TrustManagerFactory.get(mHost, secure) - }, new SecureRandom()); + sslContext.init(null, + new TrustManager[] { TrustManagerFactory.get(mHost, + mPort, secure) }, new SecureRandom()); mSocket = TrustedSocketFactory.createSocket(sslContext); } else { mSocket = new Socket(); @@ -356,9 +358,10 @@ public class Pop3Store extends Store { SSLContext sslContext = SSLContext.getInstance("TLS"); boolean secure = mConnectionSecurity == CONNECTION_SECURITY_TLS_REQUIRED; - sslContext.init(null, new TrustManager[] { - TrustManagerFactory.get(mHost, secure) - }, new SecureRandom()); + sslContext.init(null, + new TrustManager[] { TrustManagerFactory.get( + mHost, mPort, secure) }, + new SecureRandom()); mSocket = TrustedSocketFactory.createSocket(sslContext, mSocket, mHost, mPort, true); mSocket.setSoTimeout(Store.SOCKET_READ_TIMEOUT); diff --git a/src/com/fsck/k9/mail/store/TrustManagerFactory.java b/src/com/fsck/k9/mail/store/TrustManagerFactory.java deleted file mode 100644 index 7b508c3bb..000000000 --- a/src/com/fsck/k9/mail/store/TrustManagerFactory.java +++ /dev/null @@ -1,214 +0,0 @@ - -package com.fsck.k9.mail.store; - -import android.app.Application; -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.FileNotFoundException; -import java.io.IOException; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import java.util.HashMap; -import java.util.Map; - -public final class TrustManagerFactory { - private static final String LOG_TAG = "TrustManagerFactory"; - - private static X509TrustManager defaultTrustManager; - private static X509TrustManager unsecureTrustManager; - private static X509TrustManager localTrustManager; - - private static File keyStoreFile; - private static KeyStore keyStore; - - - private static class SimpleX509TrustManager implements X509TrustManager { - public void checkClientTrusted(X509Certificate[] chain, String authType) - throws CertificateException { - } - - public void checkServerTrusted(X509Certificate[] chain, String authType) - throws CertificateException { - } - - public X509Certificate[] getAcceptedIssuers() { - return null; - } - } - - private static class SecureX509TrustManager implements X509TrustManager { - private static final Map mTrustManager = - new HashMap(); - - private final String mHost; - - private SecureX509TrustManager(String host) { - mHost = host; - } - - public synchronized static X509TrustManager getInstance(String host) { - SecureX509TrustManager trustManager; - if (mTrustManager.containsKey(host)) { - trustManager = mTrustManager.get(host); - } else { - trustManager = new SecureX509TrustManager(host); - mTrustManager.put(host, trustManager); - } - - return trustManager; - } - - public void checkClientTrusted(X509Certificate[] chain, String authType) - throws CertificateException { - defaultTrustManager.checkClientTrusted(chain, authType); - } - - public void checkServerTrusted(X509Certificate[] chain, String authType) - throws CertificateException { - try { - defaultTrustManager.checkServerTrusted(chain, authType); - } catch (CertificateException e) { - try { - localTrustManager.checkServerTrusted( - new X509Certificate[] { chain[0] }, authType); - } catch (CertificateException ce) { - throw new CertificateChainException(ce, chain); - } - } - if (!DomainNameChecker.match(chain[0], mHost)) { - try { - String dn = chain[0].getSubjectDN().toString(); - if ((dn != null) && (dn.equalsIgnoreCase(keyStore.getCertificateAlias(chain[0])))) { - return; - } - } catch (KeyStoreException e) { - throw new CertificateException("Certificate cannot be verified; KeyStore Exception: " + e); - } - throw new CertificateChainException( - "Certificate domain name does not match " + mHost, - chain); - } - } - - public X509Certificate[] getAcceptedIssuers() { - return defaultTrustManager.getAcceptedIssuers(); - } - - } - - 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; - } - tmf.init(keyStore); - TrustManager[] tms = tmf.getTrustManagers(); - if (tms != null) { - for (TrustManager tm : tms) { - if (tm instanceof X509TrustManager) { - localTrustManager = (X509TrustManager)tm; - break; - } - } - } - tmf = javax.net.ssl.TrustManagerFactory.getInstance("X509"); - tmf.init((KeyStore)null); - tms = tmf.getTrustManagers(); - if (tms != null) { - for (TrustManager tm : tms) { - if (tm instanceof X509TrustManager) { - defaultTrustManager = (X509TrustManager) tm; - break; - } - } - } - - } 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); - } finally { - IOUtils.closeQuietly(fis); - } - unsecureTrustManager = new SimpleX509TrustManager(); - } - - private TrustManagerFactory() { - } - - public static X509TrustManager get(String host, boolean secure) { - return secure ? SecureX509TrustManager.getInstance(host) : - unsecureTrustManager; - } - - public static KeyStore getKeyStore() { - return keyStore; - } - - public static void addCertificateChain(String alias, X509Certificate[] chain) throws CertificateException { - try { - javax.net.ssl.TrustManagerFactory tmf = javax.net.ssl.TrustManagerFactory.getInstance("X509"); - for (X509Certificate element : chain) { - keyStore.setCertificateEntry - (element.getSubjectDN().toString(), element); - } - - tmf.init(keyStore); - TrustManager[] tms = tmf.getTrustManagers(); - if (tms != null) { - for (TrustManager tm : tms) { - if (tm instanceof X509TrustManager) { - localTrustManager = (X509TrustManager) tm; - break; - } - } - } - 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); - } - } -} diff --git a/src/com/fsck/k9/mail/store/WebDavSocketFactory.java b/src/com/fsck/k9/mail/store/WebDavSocketFactory.java index 2d4f959ed..9563f510e 100644 --- a/src/com/fsck/k9/mail/store/WebDavSocketFactory.java +++ b/src/com/fsck/k9/mail/store/WebDavSocketFactory.java @@ -4,6 +4,8 @@ import org.apache.http.conn.ConnectTimeoutException; import org.apache.http.conn.scheme.LayeredSocketFactory; import org.apache.http.params.HttpParams; +import com.fsck.k9.net.ssl.TrustManagerFactory; + import java.io.IOException; import java.net.InetAddress; import java.net.Socket; @@ -26,10 +28,10 @@ public class WebDavSocketFactory implements LayeredSocketFactory { private SSLSocketFactory mSocketFactory; private org.apache.http.conn.ssl.SSLSocketFactory mSchemeSocketFactory; - public WebDavSocketFactory(String host, boolean secure) throws NoSuchAlgorithmException, KeyManagementException { + public WebDavSocketFactory(String host, int port, boolean secure) throws NoSuchAlgorithmException, KeyManagementException { SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, new TrustManager[] { - TrustManagerFactory.get(host, secure) + TrustManagerFactory.get(host, port, secure) }, new SecureRandom()); mSocketFactory = sslContext.getSocketFactory(); mSchemeSocketFactory = org.apache.http.conn.ssl.SSLSocketFactory.getSocketFactory(); diff --git a/src/com/fsck/k9/mail/store/WebDavStore.java b/src/com/fsck/k9/mail/store/WebDavStore.java index b3f8d5f24..dd8b399ed 100644 --- a/src/com/fsck/k9/mail/store/WebDavStore.java +++ b/src/com/fsck/k9/mail/store/WebDavStore.java @@ -1079,7 +1079,7 @@ public class WebDavStore extends Store { SchemeRegistry reg = mHttpClient.getConnectionManager().getSchemeRegistry(); try { - Scheme s = new Scheme("https", new WebDavSocketFactory(mHost, mSecure), 443); + Scheme s = new Scheme("https", new WebDavSocketFactory(mHost, 443, mSecure), 443); reg.register(s); } catch (NoSuchAlgorithmException nsa) { Log.e(K9.LOG_TAG, "NoSuchAlgorithmException in getHttpClient: " + nsa); diff --git a/src/com/fsck/k9/mail/transport/SmtpTransport.java b/src/com/fsck/k9/mail/transport/SmtpTransport.java index daf147326..6dfa641db 100644 --- a/src/com/fsck/k9/mail/transport/SmtpTransport.java +++ b/src/com/fsck/k9/mail/transport/SmtpTransport.java @@ -12,9 +12,9 @@ import com.fsck.k9.mail.filter.LineWrapOutputStream; import com.fsck.k9.mail.filter.PeekableInputStream; import com.fsck.k9.mail.filter.SmtpDataStuffing; import com.fsck.k9.mail.internet.MimeUtility; -import com.fsck.k9.mail.store.TrustManagerFactory; import com.fsck.k9.mail.store.LocalStore.LocalMessage; -import com.fsck.k9.mail.store.TrustedSocketFactory; +import com.fsck.k9.net.ssl.TrustManagerFactory; +import com.fsck.k9.net.ssl.TrustedSocketFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLException; @@ -242,9 +242,10 @@ public class SmtpTransport extends Transport { mConnectionSecurity == CONNECTION_SECURITY_SSL_OPTIONAL) { SSLContext sslContext = SSLContext.getInstance("TLS"); boolean secure = mConnectionSecurity == CONNECTION_SECURITY_SSL_REQUIRED; - sslContext.init(null, new TrustManager[] { - TrustManagerFactory.get(mHost, secure) - }, new SecureRandom()); + sslContext.init(null, + new TrustManager[] { TrustManagerFactory.get( + mHost, mPort, secure) }, + new SecureRandom()); mSocket = TrustedSocketFactory.createSocket(sslContext); mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT); } else { @@ -301,9 +302,9 @@ public class SmtpTransport extends Transport { SSLContext sslContext = SSLContext.getInstance("TLS"); boolean secure = mConnectionSecurity == CONNECTION_SECURITY_TLS_REQUIRED; - sslContext.init(null, new TrustManager[] { - TrustManagerFactory.get(mHost, secure) - }, new SecureRandom()); + sslContext.init(null, + new TrustManager[] { TrustManagerFactory.get(mHost, + mPort, secure) }, new SecureRandom()); mSocket = TrustedSocketFactory.createSocket(sslContext, mSocket, mHost, mPort, true); mIn = new PeekableInputStream(new BufferedInputStream(mSocket.getInputStream(), diff --git a/src/com/fsck/k9/net/ssl/TrustManagerFactory.java b/src/com/fsck/k9/net/ssl/TrustManagerFactory.java new file mode 100644 index 000000000..0292202fc --- /dev/null +++ b/src/com/fsck/k9/net/ssl/TrustManagerFactory.java @@ -0,0 +1,133 @@ + +package com.fsck.k9.net.ssl; + +import android.util.Log; + +import com.fsck.k9.helper.DomainNameChecker; +import com.fsck.k9.mail.CertificateChainException; +import com.fsck.k9.security.LocalKeyStore; + +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.HashMap; +import java.util.Map; + +public final class TrustManagerFactory { + private static final String LOG_TAG = "TrustManagerFactory"; + + private static X509TrustManager defaultTrustManager; + private static X509TrustManager unsecureTrustManager; + + private static LocalKeyStore keyStore; + + private static class SimpleX509TrustManager implements X509TrustManager { + public void checkClientTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + } + + public void checkServerTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + } + + public X509Certificate[] getAcceptedIssuers() { + return null; + } + } + + private static class SecureX509TrustManager implements X509TrustManager { + private static final Map mTrustManager = + new HashMap(); + + private final String mHost; + private final int mPort; + + private SecureX509TrustManager(String host, int port) { + mHost = host; + mPort = port; + } + + public synchronized static X509TrustManager getInstance(String host, int port) { + String key = host + ":" + port; + SecureX509TrustManager trustManager; + if (mTrustManager.containsKey(key)) { + trustManager = mTrustManager.get(key); + } else { + trustManager = new SecureX509TrustManager(host, port); + mTrustManager.put(key, trustManager); + } + + return trustManager; + } + + public void checkClientTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + defaultTrustManager.checkClientTrusted(chain, authType); + } + + public void checkServerTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + boolean foundInGlobalKeyStore = false; + try { + defaultTrustManager.checkServerTrusted(chain, authType); + foundInGlobalKeyStore = true; + } catch (CertificateException e) { /* ignore */ } + + X509Certificate certificate = chain[0]; + + // 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) + || 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() { + return defaultTrustManager.getAcceptedIssuers(); + } + + } + + static { + try { + keyStore = LocalKeyStore.getInstance(); + + javax.net.ssl.TrustManagerFactory tmf = javax.net.ssl.TrustManagerFactory.getInstance("X509"); + tmf.init((KeyStore) null); + + TrustManager[] tms = tmf.getTrustManagers(); + if (tms != null) { + for (TrustManager tm : tms) { + if (tm instanceof X509TrustManager) { + defaultTrustManager = (X509TrustManager) tm; + break; + } + } + } + } 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); + } + unsecureTrustManager = new SimpleX509TrustManager(); + } + + private TrustManagerFactory() { + } + + public static X509TrustManager get(String host, int port, boolean secure) { + return secure ? SecureX509TrustManager.getInstance(host, port) : + unsecureTrustManager; + } +} diff --git a/src/com/fsck/k9/mail/store/TrustedSocketFactory.java b/src/com/fsck/k9/net/ssl/TrustedSocketFactory.java similarity index 99% rename from src/com/fsck/k9/mail/store/TrustedSocketFactory.java rename to src/com/fsck/k9/net/ssl/TrustedSocketFactory.java index 2dd319251..acb9c7574 100644 --- a/src/com/fsck/k9/mail/store/TrustedSocketFactory.java +++ b/src/com/fsck/k9/net/ssl/TrustedSocketFactory.java @@ -1,4 +1,4 @@ -package com.fsck.k9.mail.store; +package com.fsck.k9.net.ssl; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocket; 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/src/com/fsck/k9/security/LocalKeyStore.java b/src/com/fsck/k9/security/LocalKeyStore.java new file mode 100644 index 000000000..f7b34939c --- /dev/null +++ b/src/com/fsck/k9/security/LocalKeyStore.java @@ -0,0 +1,181 @@ +package com.fsck.k9.security; + +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.util.Log; + +import com.fsck.k9.K9; + +public class LocalKeyStore { + private static final int KEY_STORE_FILE_VERSION = 1; + + private static String sKeyStoreLocation; + + public static void setKeyStoreLocation(String directory) { + sKeyStoreLocation = directory; + } + + private static class LocalKeyStoreHolder { + static final LocalKeyStore INSTANCE = new LocalKeyStore(); + } + + public static LocalKeyStore getInstance() { + return LocalKeyStoreHolder.INSTANCE; + } + + + private File mKeyStoreFile; + private KeyStore mKeyStore; + + + private LocalKeyStore() { + if (sKeyStoreLocation == null) { + Log.e(K9.LOG_TAG, "Local key store location has not been initialized"); + } else { + upgradeKeyStoreFile(); + 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(getKeyStoreFilePath(KEY_STORE_FILE_VERSION)); + } + 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"); + } + try { + mKeyStore.setCertificateEntry(getCertKey(host, port), certificate); + } catch (KeyStoreException e) { + throw new CertificateException( + "Failed to add certificate to local key store", e); + } + writeCertificateFile(); + } + + private void writeCertificateFile() throws CertificateException { + java.io.OutputStream keyStoreStream = null; + try { + 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; + } + + public synchronized void deleteCertificate(String oldHost, int oldPort) { + if (mKeyStore == null) { + return; + } + try { + mKeyStore.deleteEntry(getCertKey(oldHost, oldPort)); + writeCertificateFile(); + } catch (KeyStoreException e) { + // Ignore: most likely there was no cert. found + } catch (CertificateException e) { + Log.e(K9.LOG_TAG, "Error updating the local key store file", e); + } + } + + private void upgradeKeyStoreFile() { + if (KEY_STORE_FILE_VERSION > 0) { + // Blow away version "0" because certificate aliases have changed. + new File(getKeyStoreFilePath(0)).delete(); + } + } + + private String getKeyStoreFilePath(int version) { + if (version < 1) { + return sKeyStoreLocation + File.separator + "KeyStore.bks"; + } else { + return sKeyStoreLocation + File.separator + "KeyStore_v" + version + ".bks"; + } + } +} diff --git a/tests/src/com/fsck/k9/net/ssl/TrustManagerFactoryTest.java b/tests/src/com/fsck/k9/net/ssl/TrustManagerFactoryTest.java new file mode 100644 index 000000000..01c91ef93 --- /dev/null +++ b/tests/src/com/fsck/k9/net/ssl/TrustManagerFactoryTest.java @@ -0,0 +1,335 @@ +package com.fsck.k9.net.ssl; + +import javax.net.ssl.X509TrustManager; + +import com.fsck.k9.security.LocalKeyStore; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +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 static final String K9_EXAMPLE_COM_CERT1 = + "-----BEGIN CERTIFICATE-----\n" + + "MIICCTCCAXICCQD/R0TV7d0C5TANBgkqhkiG9w0BAQUFADBJMQswCQYDVQQGEwJD\n" + + "SDETMBEGA1UECBMKU29tZS1TdGF0ZTEMMAoGA1UEChMDSy05MRcwFQYDVQQDEw5r\n" + + "OS5leGFtcGxlLmNvbTAeFw0xMTA5MDYxOTU3MzVaFw0yMTA5MDMxOTU3MzVaMEkx\n" + + "CzAJBgNVBAYTAkNIMRMwEQYDVQQIEwpTb21lLVN0YXRlMQwwCgYDVQQKEwNLLTkx\n" + + "FzAVBgNVBAMTDms5LmV4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB\n" + + "iQKBgQCp7FvHRaQaOIu3iyB5GB0PtPCxy/bLlBxBb8p9QsMimX2Yz3SNjWVUzU5N\n" + + "ggpXmmeGopLAnvZlhWYSx0yIGWwPB44kGK5eaYDRWav+K+XXgdNCJij1UWPSmFwZ\n" + + "hUoNbrahco5AFw0jC1qi+3Dht6Y64nfNzTOYTcm1Pz4tqXiADQIDAQABMA0GCSqG\n" + + "SIb3DQEBBQUAA4GBAIPsgd6fuFRojSOAcUyhaoKaY5hXJf8d7R3AYWxcAPYmn6g7\n" + + "3Zms+f7/CH0y/tM81oBTlq9ZLbrJyLzC7vG1pqWHMNaK7miAho22IRuk+HwvL6OA\n" + + "uH3x3W1/mH4ci268cIFVmofID0nYLTqOxBTczfYhI7q0VBUXqv/bZ+3bVMSh\n" + + "-----END CERTIFICATE-----\n"; + + private static final String K9_EXAMPLE_COM_CERT2 = + "-----BEGIN CERTIFICATE-----\n" + + "MIICCTCCAXICCQDMryqq0gZ80jANBgkqhkiG9w0BAQUFADBJMQswCQYDVQQGEwJD\n" + + "SDETMBEGA1UECBMKU29tZS1TdGF0ZTEMMAoGA1UEChMDSy05MRcwFQYDVQQDEw5r\n" + + "OS5leGFtcGxlLmNvbTAeFw0xMTA5MDYyMDAwNTVaFw0yMTA5MDMyMDAwNTVaMEkx\n" + + "CzAJBgNVBAYTAkNIMRMwEQYDVQQIEwpTb21lLVN0YXRlMQwwCgYDVQQKEwNLLTkx\n" + + "FzAVBgNVBAMTDms5LmV4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB\n" + + "iQKBgQDOLzRucC3tuXL/NthnGkgTnVn03balrvYPkABvvrG83Dpp5ipIC/iPsQvw\n" + + "pvqypSNHqrloEB7o3obQ8tiRDtbOsNQ7gKJ+YoD1drDNClV0pBvr7mvRgA2AcDpw\n" + + "CTLKwVIyKmE+rm3vl8CWFd9CqHcYQ3Mc1KXXasN4DEAzZ/sHRwIDAQABMA0GCSqG\n" + + "SIb3DQEBBQUAA4GBAFDcHFpmZ9SUrc0WayrKNUpSaHLRG94uzIx0VUMLROcXEEWU\n" + + "soRw1RfoSBkcy2SEjB4CAvex6qAiOT3ubXuL+BYFav/uU8JPWZ9ovSAYqBZ9aUJo\n" + + "G6A2hvA1lpvP97qQ/NFaGQ38XqSykZamZwSx3PlZUM/i9S9n/3MfuuXWqtLC\n" + + "-----END CERTIFICATE-----\n"; + + private static final String CA_CERT = + "-----BEGIN CERTIFICATE-----\n" + + "MIIDbTCCAlWgAwIBAgIJANCdQ+Cwnyg+MA0GCSqGSIb3DQEBBQUAME0xCzAJBgNV\n" + + "BAYTAkNIMRMwEQYDVQQIDApTb21lLVN0YXRlMQwwCgYDVQQKDANLLTkxGzAZBgNV\n" + + "BAMMEnRlc3QtY2EuazltYWlsLm9yZzAeFw0xMzEyMDIxMjUwNThaFw0yMzExMzAx\n" + + "MjUwNThaME0xCzAJBgNVBAYTAkNIMRMwEQYDVQQIDApTb21lLVN0YXRlMQwwCgYD\n" + + "VQQKDANLLTkxGzAZBgNVBAMMEnRlc3QtY2EuazltYWlsLm9yZzCCASIwDQYJKoZI\n" + + "hvcNAQEBBQADggEPADCCAQoCggEBAJ+YLg9enfFk5eba6B3LtQzUE7GiR2tIpQSi\n" + + "zHMtHzn8KUnRDiGwC8VnSuWCOX7hXyQ0P6i2+DVRVBYOAeDCNMZHOq1hRqI66B33\n" + + "QqLfkBnJAIDeLqfqlgigHs1+//7eagVA6Z38ZFre3PFuKnK9NCwS+gz7PKw/poIG\n" + + "/FZP+ltMlkwvPww4S8SMlY6RXXH09+S/uM8aG6DUBT298eoAXTbSEIeaNhwBHZPe\n" + + "rXqqzd8QDAIE9BFXSkh/BQiVEFDPSBMSdmUzUAsT2aM8osntnKWY5/G7B60wutvA\n" + + "jYCULgtR6lR6jIDbG3ECHVDsTWR+Pgl+h1zeyERhN5iG1ffOtLUCAwEAAaNQME4w\n" + + "HQYDVR0OBBYEFBlUYiTGlOu9zIPx8Q13xcnDL5QpMB8GA1UdIwQYMBaAFBlUYiTG\n" + + "lOu9zIPx8Q13xcnDL5QpMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEB\n" + + "AJ6oC6O6I6p0vgA4+7dfyxKX745zl/fK6IVHV/GO75mLjVdyw00USbHGHAmZM5C6\n" + + "eCKVV83m/Re5lHf8ZBjc+3rWdGCEjwyUwvDeUvzpcKF3wPxYDUOOqSI+np1cxj6q\n" + + "6+XI5QXwyUObWtWyw1GOpLuFPbxny/TlRWvk8AfOaLANg3UhvITNZMdMHoQ2sJ3u\n" + + "MrQ+CHe/Tal2MkwiCrYT91f3YWVaswiEAxpqxnwuSXnYyaJpqMCcA1txBDgX84FP\n" + + "dSIM4ut+QltV2Tlx0lpH43dvttAwkPB+iL7ZF6zUki/Nq5aKyNoHOL88TACe18Lq\n" + + "zOztD2HZfxhIz3uH2gXmqUo=\n" + + "-----END CERTIFICATE-----\n"; + + private static final String CERT3 = + "-----BEGIN CERTIFICATE-----\n" + + "MIIDjDCCAnSgAwIBAgIBATANBgkqhkiG9w0BAQUFADBNMQswCQYDVQQGEwJDSDET\n" + + "MBEGA1UECAwKU29tZS1TdGF0ZTEMMAoGA1UECgwDSy05MRswGQYDVQQDDBJ0ZXN0\n" + + "LWNhLms5bWFpbC5vcmcwHhcNMTMxMjAyMTMxNzEyWhcNMjMxMTMwMTMxNzEyWjBJ\n" + + "MQswCQYDVQQGEwJDSDETMBEGA1UECAwKU29tZS1TdGF0ZTEMMAoGA1UECgwDSy05\n" + + "MRcwFQYDVQQDDA5rOS5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEP\n" + + "ADCCAQoCggEBAL9OvWtLcp6bd40Hai6A6cCmJRwn3mwcTB8E41iEQgQexqx/f9RR\n" + + "BuQi2s80k/vXq8QU2GbwGiPkBBXMUHuiT27Lsoj8kMOnH5BXeKLaWDiMpvNqfent\n" + + "UzBXSIOK6Yu9UtlU0MzAuYxXaunrXoS5Dejrbz743P9yW8hx7pANNU0Qfck+ekR7\n" + + "Q4PWNgfbFHrnvcobzuFzJeWg8x9iTTsVGIaX9AVMjMUlIKvhhOWTlcTJHKzU67sp\n" + + "OLzwH9IJ3hqwdmsgZu5D/2AZlYlpFk6AlnoxNhfy9m+T41P8+iWDYCJoxvf3d6gl\n" + + "TlZ1FL0PzPReXeAgugyJ1qx5gJ9Vhf/rBaUCAwEAAaN7MHkwCQYDVR0TBAIwADAs\n" + + "BglghkgBhvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYD\n" + + "VR0OBBYEFPm9hbTbfmcnjjfOzrec/TrvsS5ZMB8GA1UdIwQYMBaAFBlUYiTGlOu9\n" + + "zIPx8Q13xcnDL5QpMA0GCSqGSIb3DQEBBQUAA4IBAQAgvYQoCEklJNXBwLuWpSMx\n" + + "CQrVxLI1XsYRzqMs0kUgM59OhwAPwdSR+UEuyXQ8QGKwSt1d//DkdhzQDATXSBYc\n" + + "VHr16ocYPGNd/VNo7BoUCvykp3cCH3WxYYpAugXbLU8RBJzQwCM75SLQtFe20qfI\n" + + "LErbrmKONtMk3Rfg6XtLLcaOVh1A3q13CKqDvwtZT4oo56EJOvkBkzlCvTuxJb6s\n" + + "FD9pwROFpIN8O54C333tZzj4TDP4g9zb3sofAJ4U0osfQAXekZJdZETFGJsU6TIM\n" + + "Dcf5/G8bZe2DnavBQfML1wI5d7NUWE8CWb95SsIvFXI0qZE0oIR+axBVl9u97uaO\n" + + "-----END CERTIFICATE-----\n"; + + private static final String DIGI_CERT = + "-----BEGIN CERTIFICATE-----\n" + + "MIIG5jCCBc6gAwIBAgIQAze5KDR8YKauxa2xIX84YDANBgkqhkiG9w0BAQUFADBs\n" + + "MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\n" + + "d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j\n" + + "ZSBFViBSb290IENBMB4XDTA3MTEwOTEyMDAwMFoXDTIxMTExMDAwMDAwMFowaTEL\n" + + "MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3\n" + + "LmRpZ2ljZXJ0LmNvbTEoMCYGA1UEAxMfRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug\n" + + "RVYgQ0EtMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPOWYth1bhn/\n" + + "PzR8SU8xfg0ETpmB4rOFVZEwscCvcLssqOcYqj9495BoUoYBiJfiOwZlkKq9ZXbC\n" + + "7L4QWzd4g2B1Rca9dKq2n6Q6AVAXxDlpufFP74LByvNK28yeUE9NQKM6kOeGZrzw\n" + + "PnYoTNF1gJ5qNRQ1A57bDIzCKK1Qss72kaPDpQpYSfZ1RGy6+c7pqzoC4E3zrOJ6\n" + + "4GAiBTyC01Li85xH+DvYskuTVkq/cKs+6WjIHY9YHSpNXic9rQpZL1oRIEDZaARo\n" + + "LfTAhAsKG3jf7RpY3PtBWm1r8u0c7lwytlzs16YDMqbo3rcoJ1mIgP97rYlY1R4U\n" + + "pPKwcNSgPqcCAwEAAaOCA4UwggOBMA4GA1UdDwEB/wQEAwIBhjA7BgNVHSUENDAy\n" + + "BggrBgEFBQcDAQYIKwYBBQUHAwIGCCsGAQUFBwMDBggrBgEFBQcDBAYIKwYBBQUH\n" + + "AwgwggHEBgNVHSAEggG7MIIBtzCCAbMGCWCGSAGG/WwCATCCAaQwOgYIKwYBBQUH\n" + + "AgEWLmh0dHA6Ly93d3cuZGlnaWNlcnQuY29tL3NzbC1jcHMtcmVwb3NpdG9yeS5o\n" + + "dG0wggFkBggrBgEFBQcCAjCCAVYeggFSAEEAbgB5ACAAdQBzAGUAIABvAGYAIAB0\n" + + "AGgAaQBzACAAQwBlAHIAdABpAGYAaQBjAGEAdABlACAAYwBvAG4AcwB0AGkAdAB1\n" + + "AHQAZQBzACAAYQBjAGMAZQBwAHQAYQBuAGMAZQAgAG8AZgAgAHQAaABlACAARABp\n" + + "AGcAaQBDAGUAcgB0ACAARQBWACAAQwBQAFMAIABhAG4AZAAgAHQAaABlACAAUgBl\n" + + "AGwAeQBpAG4AZwAgAFAAYQByAHQAeQAgAEEAZwByAGUAZQBtAGUAbgB0ACAAdwBo\n" + + "AGkAYwBoACAAbABpAG0AaQB0ACAAbABpAGEAYgBpAGwAaQB0AHkAIABhAG4AZAAg\n" + + "AGEAcgBlACAAaQBuAGMAbwByAHAAbwByAGEAdABlAGQAIABoAGUAcgBlAGkAbgAg\n" + + "AGIAeQAgAHIAZQBmAGUAcgBlAG4AYwBlAC4wEgYDVR0TAQH/BAgwBgEB/wIBADCB\n" + + "gwYIKwYBBQUHAQEEdzB1MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2Vy\n" + + "dC5jb20wTQYIKwYBBQUHMAKGQWh0dHA6Ly93d3cuZGlnaWNlcnQuY29tL0NBQ2Vy\n" + + "dHMvRGlnaUNlcnRIaWdoQXNzdXJhbmNlRVZSb290Q0EuY3J0MIGPBgNVHR8EgYcw\n" + + "gYQwQKA+oDyGOmh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEhpZ2hB\n" + + "c3N1cmFuY2VFVlJvb3RDQS5jcmwwQKA+oDyGOmh0dHA6Ly9jcmw0LmRpZ2ljZXJ0\n" + + "LmNvbS9EaWdpQ2VydEhpZ2hBc3N1cmFuY2VFVlJvb3RDQS5jcmwwHQYDVR0OBBYE\n" + + "FExYyyXwQU9S9CjIgUObpqig5pLlMB8GA1UdIwQYMBaAFLE+w2kD+L9HAdSYJhoI\n" + + "Au9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQBMeheHKF0XvLIyc7/NLvVYMR3wsXFU\n" + + "nNabZ5PbLwM+Fm8eA8lThKNWYB54lBuiqG+jpItSkdfdXJW777UWSemlQk808kf/\n" + + "roF/E1S3IMRwFcuBCoHLdFfcnN8kpCkMGPAc5K4HM+zxST5Vz25PDVR708noFUjU\n" + + "xbvcNRx3RQdIRYW9135TuMAW2ZXNi419yWBP0aKb49Aw1rRzNubS+QOy46T15bg+\n" + + "BEkAui6mSnKDcp33C4ypieez12Qf1uNgywPE3IjpnSUBAHHLA7QpYCWP+UbRe3Gu\n" + + "zVMSW4SOwg/H7ZMZ2cn6j1g0djIvruFQFGHUqFijyDATI+/GJYw2jxyA\n" + + "-----END CERTIFICATE-----\n"; + + private static final String GITHUB_CERT = + "-----BEGIN CERTIFICATE-----\n" + + "MIIHOjCCBiKgAwIBAgIQBH++LkveAITSyvjj7P5wWDANBgkqhkiG9w0BAQUFADBp\n" + + "MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\n" + + "d3cuZGlnaWNlcnQuY29tMSgwJgYDVQQDEx9EaWdpQ2VydCBIaWdoIEFzc3VyYW5j\n" + + "ZSBFViBDQS0xMB4XDTEzMDYxMDAwMDAwMFoXDTE1MDkwMjEyMDAwMFowgfAxHTAb\n" + + "BgNVBA8MFFByaXZhdGUgT3JnYW5pemF0aW9uMRMwEQYLKwYBBAGCNzwCAQMTAlVT\n" + + "MRkwFwYLKwYBBAGCNzwCAQITCERlbGF3YXJlMRAwDgYDVQQFEwc1MTU3NTUwMRcw\n" + + "FQYDVQQJEw41NDggNHRoIFN0cmVldDEOMAwGA1UEERMFOTQxMDcxCzAJBgNVBAYT\n" + + "AlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1TYW4gRnJhbmNpc2Nv\n" + + "MRUwEwYDVQQKEwxHaXRIdWIsIEluYy4xEzARBgNVBAMTCmdpdGh1Yi5jb20wggEi\n" + + "MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDt04nDXXByCfMzTxpydNm2WpVQ\n" + + "u2hhn/f7Hxnh2gQxrxV8Gn/5c68d5UMrVgkARWlK6MRb38J3UlEZW9Er2TllNqAy\n" + + "GRxBc/sysj2fmOyCWws3ZDkstxCDcs3w6iRL+tmULsOFFTmpOvaI2vQniaaVT4Si\n" + + "N058JXg6yYNtAheVeH1HqFWD7hPIGRqzPPFf/jsC4YX7EWarCV2fTEPwxyReKXIo\n" + + "ztR1aE8kcimuOSj8341PTYNzdAxvEZun3WLe/+LrF+b/DL/ALTE71lmi8t2HSkh7\n" + + "bTMRFE00nzI49sgZnfG2PcVG71ELisYz7UhhxB0XG718tmfpOc+lUoAK9OrNAgMB\n" + + "AAGjggNUMIIDUDAfBgNVHSMEGDAWgBRMWMsl8EFPUvQoyIFDm6aooOaS5TAdBgNV\n" + + "HQ4EFgQUh9GPGW7kh29TjHeRB1Dfo79VRyAwJQYDVR0RBB4wHIIKZ2l0aHViLmNv\n" + + "bYIOd3d3LmdpdGh1Yi5jb20wDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsG\n" + + "AQUFBwMBBggrBgEFBQcDAjBjBgNVHR8EXDBaMCugKaAnhiVodHRwOi8vY3JsMy5k\n" + + "aWdpY2VydC5jb20vZXZjYTEtZzIuY3JsMCugKaAnhiVodHRwOi8vY3JsNC5kaWdp\n" + + "Y2VydC5jb20vZXZjYTEtZzIuY3JsMIIBxAYDVR0gBIIBuzCCAbcwggGzBglghkgB\n" + + "hv1sAgEwggGkMDoGCCsGAQUFBwIBFi5odHRwOi8vd3d3LmRpZ2ljZXJ0LmNvbS9z\n" + + "c2wtY3BzLXJlcG9zaXRvcnkuaHRtMIIBZAYIKwYBBQUHAgIwggFWHoIBUgBBAG4A\n" + + "eQAgAHUAcwBlACAAbwBmACAAdABoAGkAcwAgAEMAZQByAHQAaQBmAGkAYwBhAHQA\n" + + "ZQAgAGMAbwBuAHMAdABpAHQAdQB0AGUAcwAgAGEAYwBjAGUAcAB0AGEAbgBjAGUA\n" + + "IABvAGYAIAB0AGgAZQAgAEQAaQBnAGkAQwBlAHIAdAAgAEMAUAAvAEMAUABTACAA\n" + + "YQBuAGQAIAB0AGgAZQAgAFIAZQBsAHkAaQBuAGcAIABQAGEAcgB0AHkAIABBAGcA\n" + + "cgBlAGUAbQBlAG4AdAAgAHcAaABpAGMAaAAgAGwAaQBtAGkAdAAgAGwAaQBhAGIA\n" + + "aQBsAGkAdAB5ACAAYQBuAGQAIABhAHIAZQAgAGkAbgBjAG8AcgBwAG8AcgBhAHQA\n" + + "ZQBkACAAaABlAHIAZQBpAG4AIABiAHkAIAByAGUAZgBlAHIAZQBuAGMAZQAuMH0G\n" + + "CCsGAQUFBwEBBHEwbzAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQu\n" + + "Y29tMEcGCCsGAQUFBzAChjtodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGln\n" + + "aUNlcnRIaWdoQXNzdXJhbmNlRVZDQS0xLmNydDAMBgNVHRMBAf8EAjAAMA0GCSqG\n" + + "SIb3DQEBBQUAA4IBAQBfFW1nwzrVo94WnEUzJtU9yRZ0NMqHSBsUkG31q0eGufW4\n" + + "4wFFZWjuqRJ1n3Ym7xF8fTjP3fdKGQnxIHKSsE0nuuh/XbQX5DpBJknHdGFoLwY8\n" + + "xZ9JPI57vgvzLo8+fwHyZp3Vm/o5IYLEQViSo+nlOSUQ8YAVqu6KcsP/e612UiqS\n" + + "+UMBmgdx9KPDDzZy4MJZC2hbfUoXj9A54mJN8cuEOPyw3c3yKOcq/h48KzVguQXi\n" + + "SdJbwfqNIbQ9oJM+YzDjzS62+TCtNSNWzWbwABZCmuQxK0oEOSbTmbhxUF7rND3/\n" + + "+mx9u8cY//7uAxLWYS5gIZlCbxcf0lkiKSHJB319\n" + + "-----END CERTIFICATE-----\n"; + + private File mKeyStoreFile; + private LocalKeyStore mKeyStore; + private X509Certificate mCert1; + private X509Certificate mCert2; + private X509Certificate mCaCert; + private X509Certificate mCert3; + private X509Certificate mDigiCert; + private X509Certificate mGithubCert; + + + public TrustManagerFactoryTest() throws CertificateException { + mCert1 = loadCert(K9_EXAMPLE_COM_CERT1); + mCert2 = loadCert(K9_EXAMPLE_COM_CERT2); + mCaCert = loadCert(CA_CERT); + mCert3 = loadCert(CERT3); + mDigiCert = loadCert(DIGI_CERT); + mGithubCert = loadCert(GITHUB_CERT); + } + + private X509Certificate loadCert(String encodedCert) throws CertificateException { + CertificateFactory certFactory = CertificateFactory.getInstance("X509"); + return (X509Certificate) certFactory.generateCertificate( + new ByteArrayInputStream(encodedCert.getBytes())); + } + + @Override + public void setUp() throws Exception { + mKeyStoreFile = File.createTempFile("localKeyStore", null, getContext().getCacheDir()); + mKeyStore = LocalKeyStore.getInstance(); + mKeyStore.setKeyStoreFile(mKeyStoreFile); + } + + @Override + protected void tearDown() { + mKeyStoreFile.delete(); + } + + /** + * 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 { + 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); + trustManager2.checkServerTrusted(new X509Certificate[] { mCert2 }, "authType"); + trustManager1.checkServerTrusted(new X509Certificate[] { mCert1 }, "authType"); + } + + public void testSelfSignedCertificateMatchingHost() throws Exception { + 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 { + 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 { + mKeyStore.addCertificate(MATCHING_HOST, PORT1, mCert1); + X509TrustManager trustManager = TrustManagerFactory.get(MATCHING_HOST, PORT1, true); + assertCertificateRejection(trustManager, new X509Certificate[] { mCert2 }); + } + + public void testCertificateOfOtherHost() throws Exception { + mKeyStore.addCertificate(MATCHING_HOST, PORT1, mCert1); + mKeyStore.addCertificate(MATCHING_HOST, PORT2, mCert2); + + X509TrustManager trustManager = TrustManagerFactory.get(MATCHING_HOST, PORT1, true); + assertCertificateRejection(trustManager, new X509Certificate[] { mCert2 }); + } + + public void testUntrustedCertificateChain() throws Exception { + X509TrustManager trustManager = TrustManagerFactory.get(MATCHING_HOST, PORT1, true); + assertCertificateRejection(trustManager, new X509Certificate[] { mCert3, mCaCert }); + } + + public void testLocallyTrustedCertificateChain() throws Exception { + mKeyStore.addCertificate(MATCHING_HOST, PORT1, mCert3); + + X509TrustManager trustManager = TrustManagerFactory.get(MATCHING_HOST, PORT1, true); + trustManager.checkServerTrusted(new X509Certificate[] { mCert3, mCaCert }, "authType"); + } + + public void testLocallyTrustedCertificateChainNotMatchingHost() throws Exception { + mKeyStore.addCertificate(NOT_MATCHING_HOST, PORT1, mCert3); + + X509TrustManager trustManager = TrustManagerFactory.get(NOT_MATCHING_HOST, PORT1, true); + trustManager.checkServerTrusted(new X509Certificate[] { mCert3, mCaCert }, "authType"); + } + + public void testGloballyTrustedCertificateChain() throws Exception { + X509TrustManager trustManager = TrustManagerFactory.get("github.com", PORT1, true); + X509Certificate[] certificates = new X509Certificate[] { mGithubCert, mDigiCert }; + trustManager.checkServerTrusted(certificates, "authType"); + } + + public void testGloballyTrustedCertificateNotMatchingHost() throws Exception { + X509TrustManager trustManager = TrustManagerFactory.get(NOT_MATCHING_HOST, PORT1, true); + assertCertificateRejection(trustManager, new X509Certificate[] { mGithubCert, mDigiCert}); + } + + public void testGloballyTrustedCertificateNotMatchingHostOverride() throws Exception { + mKeyStore.addCertificate(MATCHING_HOST, PORT1, mGithubCert); + + X509TrustManager trustManager = TrustManagerFactory.get(MATCHING_HOST, PORT1, true); + X509Certificate[] certificates = new X509Certificate[] { mGithubCert, mDigiCert }; + trustManager.checkServerTrusted(certificates, "authType"); + } + + private void assertCertificateRejection(X509TrustManager trustManager, + X509Certificate[] certificates) { + boolean certificateValid; + try { + trustManager.checkServerTrusted(certificates, "authType"); + certificateValid = true; + } catch (CertificateException e) { + certificateValid = false; + } + assertFalse("The certificate should have been rejected but wasn't", certificateValid); + } + + 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)); + } +}