k-9/k9mail/src/main/java/com/fsck/k9/activity/setup/AccountSetupCheckSettings.java

509 lines
20 KiB
Java
Raw Normal View History

package com.fsck.k9.activity.setup;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.DialogFragment;
import android.app.FragmentTransaction;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.ProgressBar;
import android.widget.TextView;
2014-05-25 16:45:14 -04:00
import com.fsck.k9.*;
import com.fsck.k9.activity.K9Activity;
import com.fsck.k9.controller.MessagingController;
2014-05-25 16:45:14 -04:00
import com.fsck.k9.fragment.ConfirmationDialogFragment;
import com.fsck.k9.fragment.ConfirmationDialogFragment.ConfirmationDialogFragmentListener;
import com.fsck.k9.mail.AuthenticationFailedException;
import com.fsck.k9.mail.CertificateValidationException;
2015-02-22 11:58:46 -05:00
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Store;
import com.fsck.k9.mail.Transport;
2014-12-18 03:33:09 -05:00
import com.fsck.k9.mail.store.webdav.WebDavStore;
import com.fsck.k9.mail.filter.Hex;
import java.security.cert.CertificateException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.security.NoSuchAlgorithmException;
import java.security.MessageDigest;
import java.util.Collection;
import java.util.List;
2014-05-25 16:45:14 -04:00
import java.util.Locale;
/**
* Checks the given settings to make sure that they can be used to send and
* receive mail.
2014-05-25 16:45:14 -04:00
*
* XXX NOTE: The manifest for this app has it ignore config changes, because
* it doesn't correctly deal with restarting while its thread is running.
*/
2014-05-25 16:45:14 -04:00
public class AccountSetupCheckSettings extends K9Activity implements OnClickListener,
ConfirmationDialogFragmentListener{
public static final int ACTIVITY_REQUEST_CODE = 1;
private static final String EXTRA_ACCOUNT = "account";
Fix inadequate certificate validation Proper host name validation was not being performed for certificates kept in the local keystore. If an attacker could convince a user to accept and store an attacker's certificate, then that certificate could be used for MITM attacks, giving the attacker access to all connections to all servers in all accounts in K-9. This commit changes how the certificates are stored. Previously, an entire certificate chain was stored for a server (and any of those certificates in the chain were available for validating signatures on certificates received when connecting). Now just the single certificate for the server is stored. This commit changes how locally stored certificates are retrieved. They can only be retrieved using the host:port that the user configured for the server. This also fixes issue 1326. Users can now use different certificates for different servers on the same host (listening to different ports). The above changes mean that users might have to re-accept certificates that they had previously accepted and are still using (but only if the certificate's Subject doesn't match the host that they are connecting to). This commit modifies AccountSetupBasics so that it now calls AccountSetupCheckSettings twice -- once for checking the incoming settings and once for the outgoing settings. Otherwise, an exception could occur while checking incoming settings, the user could say continue (or the user could accept a certificate key), and the outgoing settings would not be checked. This also helps with determining if a certificate exception was for the incoming or outgoing server, which is needed if the user decides to add the certificate to the keystore.
2013-11-23 13:26:57 -05:00
private static final String EXTRA_CHECK_DIRECTION ="checkDirection";
Fix inadequate certificate validation Proper host name validation was not being performed for certificates kept in the local keystore. If an attacker could convince a user to accept and store an attacker's certificate, then that certificate could be used for MITM attacks, giving the attacker access to all connections to all servers in all accounts in K-9. This commit changes how the certificates are stored. Previously, an entire certificate chain was stored for a server (and any of those certificates in the chain were available for validating signatures on certificates received when connecting). Now just the single certificate for the server is stored. This commit changes how locally stored certificates are retrieved. They can only be retrieved using the host:port that the user configured for the server. This also fixes issue 1326. Users can now use different certificates for different servers on the same host (listening to different ports). The above changes mean that users might have to re-accept certificates that they had previously accepted and are still using (but only if the certificate's Subject doesn't match the host that they are connecting to). This commit modifies AccountSetupBasics so that it now calls AccountSetupCheckSettings twice -- once for checking the incoming settings and once for the outgoing settings. Otherwise, an exception could occur while checking incoming settings, the user could say continue (or the user could accept a certificate key), and the outgoing settings would not be checked. This also helps with determining if a certificate exception was for the incoming or outgoing server, which is needed if the user decides to add the certificate to the keystore.
2013-11-23 13:26:57 -05:00
public enum CheckDirection {
INCOMING,
OUTGOING
}
private Handler mHandler = new Handler();
private ProgressBar mProgressBar;
private TextView mMessageView;
private Account mAccount;
Fix inadequate certificate validation Proper host name validation was not being performed for certificates kept in the local keystore. If an attacker could convince a user to accept and store an attacker's certificate, then that certificate could be used for MITM attacks, giving the attacker access to all connections to all servers in all accounts in K-9. This commit changes how the certificates are stored. Previously, an entire certificate chain was stored for a server (and any of those certificates in the chain were available for validating signatures on certificates received when connecting). Now just the single certificate for the server is stored. This commit changes how locally stored certificates are retrieved. They can only be retrieved using the host:port that the user configured for the server. This also fixes issue 1326. Users can now use different certificates for different servers on the same host (listening to different ports). The above changes mean that users might have to re-accept certificates that they had previously accepted and are still using (but only if the certificate's Subject doesn't match the host that they are connecting to). This commit modifies AccountSetupBasics so that it now calls AccountSetupCheckSettings twice -- once for checking the incoming settings and once for the outgoing settings. Otherwise, an exception could occur while checking incoming settings, the user could say continue (or the user could accept a certificate key), and the outgoing settings would not be checked. This also helps with determining if a certificate exception was for the incoming or outgoing server, which is needed if the user decides to add the certificate to the keystore.
2013-11-23 13:26:57 -05:00
private CheckDirection mDirection;
private boolean mCanceled;
private boolean mDestroyed;
2014-05-25 16:45:14 -04:00
public static void actionCheckSettings(Activity context, Account account,
CheckDirection direction) {
Intent i = new Intent(context, AccountSetupCheckSettings.class);
i.putExtra(EXTRA_ACCOUNT, account.getUuid());
Fix inadequate certificate validation Proper host name validation was not being performed for certificates kept in the local keystore. If an attacker could convince a user to accept and store an attacker's certificate, then that certificate could be used for MITM attacks, giving the attacker access to all connections to all servers in all accounts in K-9. This commit changes how the certificates are stored. Previously, an entire certificate chain was stored for a server (and any of those certificates in the chain were available for validating signatures on certificates received when connecting). Now just the single certificate for the server is stored. This commit changes how locally stored certificates are retrieved. They can only be retrieved using the host:port that the user configured for the server. This also fixes issue 1326. Users can now use different certificates for different servers on the same host (listening to different ports). The above changes mean that users might have to re-accept certificates that they had previously accepted and are still using (but only if the certificate's Subject doesn't match the host that they are connecting to). This commit modifies AccountSetupBasics so that it now calls AccountSetupCheckSettings twice -- once for checking the incoming settings and once for the outgoing settings. Otherwise, an exception could occur while checking incoming settings, the user could say continue (or the user could accept a certificate key), and the outgoing settings would not be checked. This also helps with determining if a certificate exception was for the incoming or outgoing server, which is needed if the user decides to add the certificate to the keystore.
2013-11-23 13:26:57 -05:00
i.putExtra(EXTRA_CHECK_DIRECTION, direction);
context.startActivityForResult(i, ACTIVITY_REQUEST_CODE);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.account_setup_check_settings);
mMessageView = (TextView)findViewById(R.id.message);
mProgressBar = (ProgressBar)findViewById(R.id.progress);
findViewById(R.id.cancel).setOnClickListener(this);
setMessage(R.string.account_setup_check_settings_retr_info_msg);
mProgressBar.setIndeterminate(true);
String accountUuid = getIntent().getStringExtra(EXTRA_ACCOUNT);
mAccount = Preferences.getPreferences(this).getAccount(accountUuid);
Fix inadequate certificate validation Proper host name validation was not being performed for certificates kept in the local keystore. If an attacker could convince a user to accept and store an attacker's certificate, then that certificate could be used for MITM attacks, giving the attacker access to all connections to all servers in all accounts in K-9. This commit changes how the certificates are stored. Previously, an entire certificate chain was stored for a server (and any of those certificates in the chain were available for validating signatures on certificates received when connecting). Now just the single certificate for the server is stored. This commit changes how locally stored certificates are retrieved. They can only be retrieved using the host:port that the user configured for the server. This also fixes issue 1326. Users can now use different certificates for different servers on the same host (listening to different ports). The above changes mean that users might have to re-accept certificates that they had previously accepted and are still using (but only if the certificate's Subject doesn't match the host that they are connecting to). This commit modifies AccountSetupBasics so that it now calls AccountSetupCheckSettings twice -- once for checking the incoming settings and once for the outgoing settings. Otherwise, an exception could occur while checking incoming settings, the user could say continue (or the user could accept a certificate key), and the outgoing settings would not be checked. This also helps with determining if a certificate exception was for the incoming or outgoing server, which is needed if the user decides to add the certificate to the keystore.
2013-11-23 13:26:57 -05:00
mDirection = (CheckDirection) getIntent().getSerializableExtra(EXTRA_CHECK_DIRECTION);
new CheckAccountTask(mAccount).execute(mDirection);
}
2014-05-25 16:45:14 -04:00
private void handleCertificateValidationException(CertificateValidationException cve) {
Log.e(K9.LOG_TAG, "Error while testing settings", cve);
X509Certificate[] chain = cve.getCertChain();
// Avoid NullPointerException in acceptKeyDialog()
if (chain != null) {
acceptKeyDialog(
R.string.account_setup_failed_dlg_certificate_message_fmt,
cve);
} else {
showErrorDialog(
R.string.account_setup_failed_dlg_server_message_fmt,
errorMessageForCertificateException(cve));
2014-05-25 16:45:14 -04:00
}
}
@Override
public void onDestroy() {
super.onDestroy();
mDestroyed = true;
mCanceled = true;
}
private void setMessage(final int resId) {
mMessageView.setText(getString(resId));
}
2014-05-25 16:45:14 -04:00
private void acceptKeyDialog(final int msgResId, final CertificateValidationException ex) {
mHandler.post(new Runnable() {
public void run() {
if (mDestroyed) {
return;
}
String exMessage = "Unknown Error";
if (ex != null) {
if (ex.getCause() != null) {
if (ex.getCause().getCause() != null) {
exMessage = ex.getCause().getCause().getMessage();
} else {
exMessage = ex.getCause().getMessage();
}
} else {
exMessage = ex.getMessage();
}
}
mProgressBar.setIndeterminate(false);
StringBuilder chainInfo = new StringBuilder(100);
MessageDigest sha1 = null;
try {
sha1 = MessageDigest.getInstance("SHA-1");
} catch (NoSuchAlgorithmException e) {
Log.e(K9.LOG_TAG, "Error while initializing MessageDigest", e);
}
final X509Certificate[] chain = ex.getCertChain();
// We already know chain != null (tested before calling this method)
for (int i = 0; i < chain.length; i++) {
// display certificate chain information
2011-06-01 16:03:56 -04:00
//TODO: localize this strings
chainInfo.append("Certificate chain[").append(i).append("]:\n");
chainInfo.append("Subject: ").append(chain[i].getSubjectDN().toString()).append("\n");
// display SubjectAltNames too
// (the user may be mislead into mistrusting a certificate
// by a subjectDN not matching the server even though a
// SubjectAltName matches)
try {
2011-06-01 16:03:56 -04:00
final Collection < List<? >> subjectAlternativeNames = chain[i].getSubjectAlternativeNames();
if (subjectAlternativeNames != null) {
// The list of SubjectAltNames may be very long
//TODO: localize this string
StringBuilder altNamesText = new StringBuilder();
altNamesText.append("Subject has ").append(subjectAlternativeNames.size()).append(" alternative names\n");
2011-06-01 16:03:56 -04:00
// we need these for matching
String storeURIHost = (Uri.parse(mAccount.getStoreUri())).getHost();
String transportURIHost = (Uri.parse(mAccount.getTransportUri())).getHost();
for (List<?> subjectAlternativeName : subjectAlternativeNames) {
Integer type = (Integer)subjectAlternativeName.get(0);
Object value = subjectAlternativeName.get(1);
String name;
2011-06-01 16:03:56 -04:00
switch (type.intValue()) {
case 0:
Log.w(K9.LOG_TAG, "SubjectAltName of type OtherName not supported.");
continue;
case 1: // RFC822Name
name = (String)value;
break;
case 2: // DNSName
name = (String)value;
break;
case 3:
Log.w(K9.LOG_TAG, "unsupported SubjectAltName of type x400Address");
continue;
case 4:
Log.w(K9.LOG_TAG, "unsupported SubjectAltName of type directoryName");
continue;
case 5:
Log.w(K9.LOG_TAG, "unsupported SubjectAltName of type ediPartyName");
continue;
case 6: // Uri
name = (String)value;
break;
case 7: // ip-address
name = (String)value;
break;
default:
Log.w(K9.LOG_TAG, "unsupported SubjectAltName of unknown type");
continue;
}
// if some of the SubjectAltNames match the store or transport -host,
// display them
if (name.equalsIgnoreCase(storeURIHost) || name.equalsIgnoreCase(transportURIHost)) {
//TODO: localize this string
altNamesText.append("Subject(alt): ").append(name).append(",...\n");
} else if (name.startsWith("*.") && (
storeURIHost.endsWith(name.substring(2)) ||
transportURIHost.endsWith(name.substring(2)))) {
//TODO: localize this string
altNamesText.append("Subject(alt): ").append(name).append(",...\n");
2011-06-01 16:03:56 -04:00
}
}
chainInfo.append(altNamesText);
}
} catch (Exception e1) {
// don't fail just because of subjectAltNames
Log.w(K9.LOG_TAG, "cannot display SubjectAltNames in dialog", e1);
}
chainInfo.append("Issuer: ").append(chain[i].getIssuerDN().toString()).append("\n");
if (sha1 != null) {
sha1.reset();
try {
char[] sha1sum = Hex.encodeHex(sha1.digest(chain[i].getEncoded()));
chainInfo.append("Fingerprint (SHA-1): ").append(new String(sha1sum)).append("\n");
} catch (CertificateEncodingException e) {
Log.e(K9.LOG_TAG, "Error while encoding certificate", e);
}
}
}
2014-05-25 16:45:14 -04:00
// TODO: refactor with DialogFragment.
// This is difficult because we need to pass through chain[0] for onClick()
new AlertDialog.Builder(AccountSetupCheckSettings.this)
.setTitle(getString(R.string.account_setup_failed_dlg_invalid_certificate_title))
//.setMessage(getString(R.string.account_setup_failed_dlg_invalid_certificate)
.setMessage(getString(msgResId, exMessage)
+ " " + chainInfo.toString()
)
.setCancelable(true)
.setPositiveButton(
getString(R.string.account_setup_failed_dlg_invalid_certificate_accept),
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
2014-05-25 16:45:14 -04:00
acceptCertificate(chain[0]);
}
})
.setNegativeButton(
getString(R.string.account_setup_failed_dlg_invalid_certificate_reject),
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
finish();
}
})
.show();
}
});
}
2014-05-25 16:45:14 -04:00
/**
* Permanently accepts a certificate for the INCOMING or OUTGOING direction
* by adding it to the local key store.
*
* @param certificate
*/
private void acceptCertificate(X509Certificate certificate) {
try {
mAccount.addCertificate(mDirection, certificate);
} catch (CertificateException e) {
showErrorDialog(
R.string.account_setup_failed_dlg_certificate_message_fmt,
e.getMessage() == null ? "" : e.getMessage());
}
AccountSetupCheckSettings.actionCheckSettings(AccountSetupCheckSettings.this, mAccount,
mDirection);
}
@Override
public void onActivityResult(int reqCode, int resCode, Intent data) {
setResult(resCode);
finish();
}
private void onCancel() {
mCanceled = true;
setMessage(R.string.account_setup_check_settings_canceling_msg);
}
public void onClick(View v) {
switch (v.getId()) {
case R.id.cancel:
onCancel();
break;
}
}
2014-05-25 16:45:14 -04:00
private void showErrorDialog(final int msgResId, final Object... args) {
mHandler.post(new Runnable() {
public void run() {
showDialogFragment(R.id.dialog_account_setup_error, getString(msgResId, args));
}
});
}
private void showDialogFragment(int dialogId, String customMessage) {
if (mDestroyed) {
return;
}
mProgressBar.setIndeterminate(false);
DialogFragment fragment;
switch (dialogId) {
case R.id.dialog_account_setup_error: {
fragment = ConfirmationDialogFragment.newInstance(dialogId,
getString(R.string.account_setup_failed_dlg_title),
customMessage,
getString(R.string.account_setup_failed_dlg_edit_details_action),
getString(R.string.account_setup_failed_dlg_continue_action)
);
break;
}
default: {
throw new RuntimeException("Called showDialog(int) with unknown dialog id.");
}
}
FragmentTransaction ta = getFragmentManager().beginTransaction();
2014-05-25 16:45:14 -04:00
ta.add(fragment, getDialogTag(dialogId));
ta.commitAllowingStateLoss();
// TODO: commitAllowingStateLoss() is used to prevent https://code.google.com/p/android/issues/detail?id=23761
// but is a bad...
//fragment.show(ta, getDialogTag(dialogId));
}
private String getDialogTag(int dialogId) {
return String.format(Locale.US, "dialog-%d", dialogId);
}
@Override
public void doPositiveClick(int dialogId) {
switch (dialogId) {
case R.id.dialog_account_setup_error: {
finish();
break;
}
}
}
@Override
public void doNegativeClick(int dialogId) {
switch (dialogId) {
case R.id.dialog_account_setup_error: {
2014-05-25 16:45:14 -04:00
mCanceled = false;
setResult(RESULT_OK);
finish();
break;
}
}
}
@Override
public void dialogCancelled(int dialogId) {
// nothing to do here...
}
private String errorMessageForCertificateException(CertificateValidationException e) {
switch (e.getReason()) {
case Expired: return getString(R.string.client_certificate_expired, e.getAlias(), e.getMessage());
case MissingCapability: return getString(R.string.auth_external_error);
case RetrievalFailure: return getString(R.string.client_certificate_retrieval_failure, e.getAlias());
case UseMessage: return e.getMessage();
case Unknown:
default: return "";
}
}
2015-02-23 12:28:42 -05:00
/**
* FIXME: Don't use an AsyncTask to perform network operations.
* See also discussion in https://github.com/k9mail/k-9/pull/560
*/
2015-02-22 11:58:46 -05:00
private class CheckAccountTask extends AsyncTask<CheckDirection, Integer, Void> {
private final Account account;
2015-01-13 03:34:13 -05:00
2015-02-22 11:58:46 -05:00
private CheckAccountTask(Account account) {
this.account = account;
}
2015-01-13 03:34:13 -05:00
@Override
protected Void doInBackground(CheckDirection... params) {
final CheckDirection direction = params[0];
try {
2015-02-22 11:58:46 -05:00
/*
* This task could be interrupted at any point, but network operations can block,
* so relying on InterruptedException is not enough. Instead, check after
* each potentially long-running operation.
*/
if (cancelled()) {
return null;
}
2015-02-22 11:58:46 -05:00
2015-02-23 12:28:42 -05:00
clearCertificateErrorNotifications(direction);
checkServerSettings(direction);
2015-02-22 11:58:46 -05:00
if (cancelled()) {
return null;
}
2015-02-22 11:58:46 -05:00
2015-02-23 12:28:42 -05:00
setResult(RESULT_OK);
finish();
} catch (AuthenticationFailedException afe) {
Log.e(K9.LOG_TAG, "Error while testing settings", afe);
showErrorDialog(
R.string.account_setup_failed_dlg_auth_message_fmt,
afe.getMessage() == null ? "" : afe.getMessage());
} catch (CertificateValidationException cve) {
handleCertificateValidationException(cve);
} catch (Throwable t) {
Log.e(K9.LOG_TAG, "Error while testing settings", t);
showErrorDialog(
R.string.account_setup_failed_dlg_server_message_fmt,
(t.getMessage() == null ? "" : t.getMessage()));
}
return null;
}
2015-02-23 12:28:42 -05:00
private void clearCertificateErrorNotifications(CheckDirection direction) {
final MessagingController ctrl = MessagingController.getInstance(getApplication());
ctrl.clearCertificateErrorNotifications(AccountSetupCheckSettings.this,
account, direction);
}
2015-02-22 11:58:46 -05:00
private boolean cancelled() {
if (mDestroyed) {
return true;
}
if (mCanceled) {
finish();
return true;
}
return false;
}
2015-02-23 12:28:42 -05:00
private void checkServerSettings(CheckDirection direction) throws MessagingException {
switch (direction) {
case INCOMING: {
checkIncoming();
break;
}
case OUTGOING: {
checkOutgoing();
break;
}
}
}
2015-02-22 11:58:46 -05:00
private void checkOutgoing() throws MessagingException {
if (!(account.getRemoteStore() instanceof WebDavStore)) {
publishProgress(R.string.account_setup_check_settings_check_outgoing_msg);
}
Transport transport = Transport.getInstance(K9.app, account);
transport.close();
transport.open();
transport.close();
}
private void checkIncoming() throws MessagingException {
Store store = account.getRemoteStore();
if (store instanceof WebDavStore) {
publishProgress(R.string.account_setup_check_settings_authenticate);
} else {
publishProgress(R.string.account_setup_check_settings_check_incoming_msg);
}
store.checkSettings();
if (store instanceof WebDavStore) {
publishProgress(R.string.account_setup_check_settings_fetch);
}
MessagingController.getInstance(getApplication()).listFoldersSynchronous(account, true, null);
MessagingController.getInstance(getApplication())
.synchronizeMailbox(account, account.getInboxFolderName(), null, null);
}
@Override
protected void onProgressUpdate(Integer... values) {
setMessage(values[0]);
}
}
}