1
0
mirror of https://github.com/moparisthebest/k-9 synced 2024-11-30 13:12:25 -05:00

Client Certificate Authentication

This commit is contained in:
Dominik Schürmann 2014-05-25 22:45:14 +02:00
parent 2fdcb77c5c
commit aad171ff7e
28 changed files with 1327 additions and 273 deletions

View File

@ -34,6 +34,17 @@
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:nextFocusDown="@+id/next" android:nextFocusDown="@+id/next"
/> />
<com.fsck.k9.view.ClientCertificateSpinner
android:id="@+id/account_client_certificate_spinner"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:visibility="gone"
/>
<CheckBox
android:id="@+id/account_client_certificate"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/account_setup_basics_client_certificate" />
<View <View
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="0dip" android:layout_height="0dip"

View File

@ -30,6 +30,7 @@
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:contentDescription="@string/account_setup_incoming_username_label" /> android:contentDescription="@string/account_setup_incoming_username_label" />
<TextView <TextView
android:id="@+id/account_password_label"
android:text="@string/account_setup_incoming_password_label" android:text="@string/account_setup_incoming_password_label"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_width="fill_parent" android:layout_width="fill_parent"
@ -41,6 +42,19 @@
android:singleLine="true" android:singleLine="true"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_width="fill_parent" /> android:layout_width="fill_parent" />
<TextView
android:id="@+id/account_client_certificate_label"
android:text="@string/account_setup_incoming_client_certificate_label"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorPrimary"
android:visibility="gone" />
<com.fsck.k9.view.ClientCertificateSpinner
android:id="@+id/account_client_certificate_spinner"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:visibility="gone" />
<!-- This text may be changed in code if the server is IMAP, etc. --> <!-- This text may be changed in code if the server is IMAP, etc. -->
<TextView <TextView
android:id="@+id/account_server_label" android:id="@+id/account_server_label"

View File

@ -89,6 +89,7 @@
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:contentDescription="@string/account_setup_outgoing_username_label" /> android:contentDescription="@string/account_setup_outgoing_username_label" />
<TextView <TextView
android:id="@+id/account_password_label"
android:text="@string/account_setup_outgoing_password_label" android:text="@string/account_setup_outgoing_password_label"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_width="fill_parent" android:layout_width="fill_parent"
@ -101,6 +102,19 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:contentDescription="@string/account_setup_outgoing_password_label" /> android:contentDescription="@string/account_setup_outgoing_password_label" />
<TextView
android:id="@+id/account_client_certificate_label"
android:text="@string/account_setup_incoming_client_certificate_label"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorPrimary"
android:visibility="gone" />
<com.fsck.k9.view.ClientCertificateSpinner
android:id="@+id/account_client_certificate_spinner"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:visibility="gone" />
</LinearLayout> </LinearLayout>
<View <View
android:layout_width="fill_parent" android:layout_width="fill_parent"

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android" >
<Button
style="?android:attr/spinnerStyle"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1" />
<ImageButton
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="@string/client_certificate_spinner_delete"
android:padding="8dp"
android:src="@drawable/ic_action_cancel_light" />
</merge>

View File

@ -4,5 +4,7 @@
<item type="id" name="dialog_confirm_delete"/> <item type="id" name="dialog_confirm_delete"/>
<item type="id" name="dialog_confirm_spam"/> <item type="id" name="dialog_confirm_spam"/>
<item type="id" name="dialog_attachment_progress"/> <item type="id" name="dialog_attachment_progress"/>
<item type="id" name="dialog_client_certificate_not_supported"/>
<item type="id" name="dialog_account_setup_error"/>
</resources> </resources>

View File

@ -373,10 +373,12 @@ Please submit bug reports, contribute new features and ask questions at
<string name="account_setup_auth_type_normal_password">Normal password</string> <string name="account_setup_auth_type_normal_password">Normal password</string>
<string name="account_setup_auth_type_insecure_password">Password, transmitted insecurely</string> <string name="account_setup_auth_type_insecure_password">Password, transmitted insecurely</string>
<string name="account_setup_auth_type_encrypted_password">Encrypted password</string> <string name="account_setup_auth_type_encrypted_password">Encrypted password</string>
<string name="account_setup_auth_type_tls_client_certificate">Client certificate</string>
<string name="account_setup_incoming_title">Incoming server settings</string> <string name="account_setup_incoming_title">Incoming server settings</string>
<string name="account_setup_incoming_username_label">Username</string> <string name="account_setup_incoming_username_label">Username</string>
<string name="account_setup_incoming_password_label">Password</string> <string name="account_setup_incoming_password_label">Password</string>
<string name="account_setup_incoming_client_certificate_label">Client certificate</string>
<string name="account_setup_incoming_pop_server_label">POP3 server</string> <string name="account_setup_incoming_pop_server_label">POP3 server</string>
<string name="account_setup_incoming_imap_server_label">IMAP server</string> <string name="account_setup_incoming_imap_server_label">IMAP server</string>
<string name="account_setup_incoming_webdav_server_label">Exchange server</string> <string name="account_setup_incoming_webdav_server_label">Exchange server</string>
@ -1116,4 +1118,13 @@ Please submit bug reports, contribute new features and ask questions at
<string name="openpgp_error">OpenPGP Error:</string> <string name="openpgp_error">OpenPGP Error:</string>
<string name="openpgp_user_id">User Id</string> <string name="openpgp_user_id">User Id</string>
<!-- === Client certificates specific ================================================================== -->
<string name="account_setup_basics_client_certificate">Use client certificate</string>
<string name="dialog_client_certificate_title">Client Certificate Authentication</string>
<string name="dialog_client_certificate_not_supported">Client certificates are not supported on Android versions below 4.0.</string>
<string name="dialog_client_certificate_required">This server requires a valid client certificate to be selected.</string>
<string name="client_certificate_spinner_empty">No client certificate</string>
<string name="client_certificate_spinner_delete">Remove client certificate selection</string>
</resources> </resources>

View File

@ -1305,6 +1305,10 @@ public class Account implements BaseAccount {
return Store.getRemoteInstance(this); return Store.getRemoteInstance(this);
} }
public Store getRemoteStore(boolean reload) throws MessagingException {
return Store.getRemoteInstance(this, reload);
}
// It'd be great if this actually went into the store implementation // It'd be great if this actually went into the store implementation
// to get this, but that's expensive and not easily accessible // to get this, but that's expensive and not easily accessible
// during initialization // during initialization

View File

@ -2,6 +2,7 @@
package com.fsck.k9; package com.fsck.k9;
import java.io.File; import java.io.File;
import java.security.PrivateKey;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@ -1414,4 +1415,14 @@ public class K9 extends Application {
editor.commit(); editor.commit();
} }
} }
/**
* Holding a reference to PrivateKey selected for client certificate
* authentication. We need to keep reference to this key so it won't get
* garbage collected. If it will then the whole app will crash
* on Android <= 4.2 with "Fatal signal 11 code=1".
*
* see https://code.google.com/p/android/issues/detail?id=62319
*/
public static ArrayList<PrivateKey> sClientCertificateReferenceWorkaround = new ArrayList<PrivateKey>();
} }

View File

@ -112,6 +112,7 @@ public class AccountSetupAccountType extends K9Activity implements OnClickListen
break; break;
} }
} }
private void failure(Exception use) { private void failure(Exception use) {
Log.e(K9.LOG_TAG, "Failure", use); Log.e(K9.LOG_TAG, "Failure", use);
String toastText = getString(R.string.account_setup_bad_uri, use.getMessage()); String toastText = getString(R.string.account_setup_bad_uri, use.getMessage());

View File

@ -14,11 +14,25 @@ import android.util.Log;
import android.view.View; import android.view.View;
import android.view.View.OnClickListener; import android.view.View.OnClickListener;
import android.widget.Button; import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.EditText; import android.widget.EditText;
import com.fsck.k9.*; import com.fsck.k9.*;
import com.fsck.k9.activity.K9Activity; import com.fsck.k9.activity.K9Activity;
import com.fsck.k9.activity.setup.AccountSetupCheckSettings.CheckDirection; import com.fsck.k9.activity.setup.AccountSetupCheckSettings.CheckDirection;
import com.fsck.k9.helper.Utility; import com.fsck.k9.helper.Utility;
import com.fsck.k9.mail.AuthType;
import com.fsck.k9.mail.ConnectionSecurity;
import com.fsck.k9.mail.ServerSettings;
import com.fsck.k9.mail.Store;
import com.fsck.k9.mail.Transport;
import com.fsck.k9.mail.store.ImapStore;
import com.fsck.k9.mail.transport.SmtpTransport;
import com.fsck.k9.view.ClientCertificateSpinner;
import com.fsck.k9.view.ClientCertificateSpinner.OnClientCertificateChangedListener;
import java.io.Serializable; import java.io.Serializable;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.net.URI; import java.net.URI;
@ -34,7 +48,7 @@ import java.util.Locale;
* AccountSetupAccountType activity. * AccountSetupAccountType activity.
*/ */
public class AccountSetupBasics extends K9Activity public class AccountSetupBasics extends K9Activity
implements OnClickListener, TextWatcher { implements OnClickListener, TextWatcher, OnCheckedChangeListener, OnClientCertificateChangedListener {
private final static String EXTRA_ACCOUNT = "com.fsck.k9.AccountSetupBasics.account"; private final static String EXTRA_ACCOUNT = "com.fsck.k9.AccountSetupBasics.account";
private final static int DIALOG_NOTE = 1; private final static int DIALOG_NOTE = 1;
private final static String STATE_KEY_PROVIDER = private final static String STATE_KEY_PROVIDER =
@ -44,6 +58,8 @@ public class AccountSetupBasics extends K9Activity
private EditText mEmailView; private EditText mEmailView;
private EditText mPasswordView; private EditText mPasswordView;
private CheckBox mClientCertificateCheckBox;
private ClientCertificateSpinner mClientCertificateSpinner;
private Button mNextButton; private Button mNextButton;
private Button mManualSetupButton; private Button mManualSetupButton;
private Account mAccount; private Account mAccount;
@ -63,6 +79,8 @@ public class AccountSetupBasics extends K9Activity
setContentView(R.layout.account_setup_basics); setContentView(R.layout.account_setup_basics);
mEmailView = (EditText)findViewById(R.id.account_email); mEmailView = (EditText)findViewById(R.id.account_email);
mPasswordView = (EditText)findViewById(R.id.account_password); mPasswordView = (EditText)findViewById(R.id.account_password);
mClientCertificateCheckBox = (CheckBox)findViewById(R.id.account_client_certificate);
mClientCertificateSpinner = (ClientCertificateSpinner)findViewById(R.id.account_client_certificate_spinner);
mNextButton = (Button)findViewById(R.id.next); mNextButton = (Button)findViewById(R.id.next);
mManualSetupButton = (Button)findViewById(R.id.manual_setup); mManualSetupButton = (Button)findViewById(R.id.manual_setup);
@ -71,6 +89,8 @@ public class AccountSetupBasics extends K9Activity
mEmailView.addTextChangedListener(this); mEmailView.addTextChangedListener(this);
mPasswordView.addTextChangedListener(this); mPasswordView.addTextChangedListener(this);
mClientCertificateCheckBox.setOnCheckedChangeListener(this);
mClientCertificateSpinner.setOnClientCertificateChangedListener(this);
} }
@Override @Override
@ -117,10 +137,46 @@ public class AccountSetupBasics extends K9Activity
public void onTextChanged(CharSequence s, int start, int before, int count) { public void onTextChanged(CharSequence s, int start, int before, int count) {
} }
@Override
public void onClientCertificateChanged(String alias) {
validateFields();
}
/**
* Called when checking the client certificate CheckBox
*/
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
// clear password field
mPasswordView.removeTextChangedListener(this);
mPasswordView.setText("");
mPasswordView.addTextChangedListener(this);
// hide password fields, show client certificate spinner
mPasswordView.setVisibility(View.GONE);
mClientCertificateSpinner.setVisibility(View.VISIBLE);
} else {
// clear client certificate spinner
mClientCertificateSpinner.setOnClientCertificateChangedListener(null);
mClientCertificateSpinner.setAlias(null);
mClientCertificateSpinner.setOnClientCertificateChangedListener(this);
// show password fields, hide client certificate spinner
mPasswordView.setVisibility(View.VISIBLE);
mClientCertificateSpinner.setVisibility(View.GONE);
}
validateFields();
}
private void validateFields() { private void validateFields() {
boolean clientCertificateChecked = mClientCertificateCheckBox.isChecked();
String clientCertificateAlias = mClientCertificateSpinner.getAlias();
String email = mEmailView.getText().toString(); String email = mEmailView.getText().toString();
boolean valid = Utility.requiredFieldValid(mEmailView) boolean valid = Utility.requiredFieldValid(mEmailView)
&& Utility.requiredFieldValid(mPasswordView) && (Utility.requiredFieldValid(mPasswordView)
|| (clientCertificateChecked && clientCertificateAlias != null))
&& mEmailValidator.isValidAddressOnly(email); && mEmailValidator.isValidAddressOnly(email);
mNextButton.setEnabled(valid); mNextButton.setEnabled(valid);
@ -297,33 +353,38 @@ public class AccountSetupBasics extends K9Activity
private void onManualSetup() { private void onManualSetup() {
String email = mEmailView.getText().toString(); String email = mEmailView.getText().toString();
String password = mPasswordView.getText().toString();
String[] emailParts = splitEmail(email); String[] emailParts = splitEmail(email);
String user = emailParts[0]; String user = emailParts[0];
String domain = emailParts[1]; String domain = emailParts[1];
String password = null;
String clientCertificateAlias = null;
AuthType authenticationType = null;
if (mClientCertificateCheckBox.isChecked()) {
authenticationType = AuthType.EXTERNAL;
clientCertificateAlias = mClientCertificateSpinner.getAlias();
} else {
authenticationType = AuthType.PLAIN;
password = mPasswordView.getText().toString();
}
if (mAccount == null) { if (mAccount == null) {
mAccount = Preferences.getPreferences(this).newAccount(); mAccount = Preferences.getPreferences(this).newAccount();
} }
mAccount.setName(getOwnerName()); mAccount.setName(getOwnerName());
mAccount.setEmail(email); mAccount.setEmail(email);
try {
String userEnc = URLEncoder.encode(user, "UTF-8");
String passwordEnc = URLEncoder.encode(password, "UTF-8");
URI uri = new URI("placeholder", userEnc + ":" + passwordEnc, "mail." + domain, -1, null, // set default uris
null, null); // NOTE: they will be changed again in AccountSetupAccountType!
mAccount.setStoreUri(uri.toString()); ServerSettings storeServer = new ServerSettings(ImapStore.STORE_TYPE, "mail." + domain, -1,
mAccount.setTransportUri(uri.toString()); ConnectionSecurity.SSL_TLS_REQUIRED, authenticationType, user, password, clientCertificateAlias);
} catch (UnsupportedEncodingException enc) { ServerSettings transportServer = new ServerSettings(SmtpTransport.TRANSPORT_TYPE, "mail." + domain, -1,
// This really shouldn't happen since the encoding is hardcoded to UTF-8 ConnectionSecurity.SSL_TLS_REQUIRED, authenticationType, user, password, clientCertificateAlias);
Log.e(K9.LOG_TAG, "Couldn't urlencode username or password.", enc); String storeUri = Store.createStoreUri(storeServer);
} catch (URISyntaxException use) { String transportUri = Transport.createTransportUri(transportServer);
/* mAccount.setStoreUri(storeUri);
* If we can't set up the URL we just continue. It's only for mAccount.setTransportUri(transportUri);
* convenience.
*/
}
mAccount.setDraftsFolderName(getString(R.string.special_mailbox_name_drafts)); mAccount.setDraftsFolderName(getString(R.string.special_mailbox_name_drafts));
mAccount.setTrashFolderName(getString(R.string.special_mailbox_name_trash)); mAccount.setTrashFolderName(getString(R.string.special_mailbox_name_trash));
mAccount.setSentFolderName(getString(R.string.special_mailbox_name_sent)); mAccount.setSentFolderName(getString(R.string.special_mailbox_name_sent));
@ -430,4 +491,5 @@ public class AccountSetupBasics extends K9Activity
public String note; public String note;
} }
} }

View File

@ -9,21 +9,30 @@ import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.Process; import android.os.Process;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.FragmentTransaction;
import android.util.Log; import android.util.Log;
import android.view.View; import android.view.View;
import android.view.View.OnClickListener; import android.view.View.OnClickListener;
import android.widget.Button; import android.widget.Button;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.TextView; import android.widget.TextView;
import com.fsck.k9.*; import com.fsck.k9.*;
import com.fsck.k9.activity.K9Activity; import com.fsck.k9.activity.K9Activity;
import com.fsck.k9.controller.MessagingController; import com.fsck.k9.controller.MessagingController;
import com.fsck.k9.fragment.ConfirmationDialogFragment;
import com.fsck.k9.fragment.ConfirmationDialogFragment.ConfirmationDialogFragmentListener;
import com.fsck.k9.mail.AuthenticationFailedException; import com.fsck.k9.mail.AuthenticationFailedException;
import com.fsck.k9.mail.CertificateValidationException; import com.fsck.k9.mail.CertificateValidationException;
import com.fsck.k9.mail.ClientCertificateRequiredException;
import com.fsck.k9.mail.ServerSettings;
import com.fsck.k9.mail.Store; import com.fsck.k9.mail.Store;
import com.fsck.k9.mail.Transport; import com.fsck.k9.mail.Transport;
import com.fsck.k9.mail.store.WebDavStore; import com.fsck.k9.mail.store.WebDavStore;
import com.fsck.k9.mail.filter.Hex; import com.fsck.k9.mail.filter.Hex;
import com.fsck.k9.net.ssl.SslHelper;
import com.fsck.k9.security.KeyChainKeyManager;
import java.security.cert.CertificateException; import java.security.cert.CertificateException;
import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateEncodingException;
@ -32,6 +41,7 @@ import java.security.NoSuchAlgorithmException;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Locale;
/** /**
* Checks the given settings to make sure that they can be used to send and * Checks the given settings to make sure that they can be used to send and
@ -40,7 +50,8 @@ import java.util.List;
* XXX NOTE: The manifest for this app has it ignore config changes, because * 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. * it doesn't correctly deal with restarting while its thread is running.
*/ */
public class AccountSetupCheckSettings extends K9Activity implements OnClickListener { public class AccountSetupCheckSettings extends K9Activity implements OnClickListener,
ConfirmationDialogFragmentListener{
public static final int ACTIVITY_REQUEST_CODE = 1; public static final int ACTIVITY_REQUEST_CODE = 1;
@ -61,13 +72,16 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
private Account mAccount; private Account mAccount;
private boolean mIsClientCertSet;
private CheckDirection mDirection; private CheckDirection mDirection;
private boolean mCanceled; private boolean mCanceled;
private boolean mDestroyed; private boolean mDestroyed;
public static void actionCheckSettings(Activity context, Account account, CheckDirection direction) { public static void actionCheckSettings(Activity context, Account account,
CheckDirection direction) {
Intent i = new Intent(context, AccountSetupCheckSettings.class); Intent i = new Intent(context, AccountSetupCheckSettings.class);
i.putExtra(EXTRA_ACCOUNT, account.getUuid()); i.putExtra(EXTRA_ACCOUNT, account.getUuid());
i.putExtra(EXTRA_CHECK_DIRECTION, direction); i.putExtra(EXTRA_CHECK_DIRECTION, direction);
@ -108,7 +122,9 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
mAccount, mDirection); mAccount, mDirection);
if (mDirection.equals(CheckDirection.INCOMING)) { if (mDirection.equals(CheckDirection.INCOMING)) {
store = mAccount.getRemoteStore(); // refresh URI that stores settings in order to include
// client certificate set by user
store = mAccount.getRemoteStore(mIsClientCertSet);
if (store instanceof WebDavStore) { if (store instanceof WebDavStore) {
setMessage(R.string.account_setup_check_settings_authenticate); setMessage(R.string.account_setup_check_settings_authenticate);
@ -154,6 +170,22 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
R.string.account_setup_failed_dlg_auth_message_fmt, R.string.account_setup_failed_dlg_auth_message_fmt,
afe.getMessage() == null ? "" : afe.getMessage()); afe.getMessage() == null ? "" : afe.getMessage());
} catch (final CertificateValidationException cve) { } catch (final CertificateValidationException cve) {
handleCertificateValidationException(cve);
} catch (final ClientCertificateRequiredException ccr) {
handleClientCertificateRequiredException(ccr);
} catch (final 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()));
}
}
}
.start();
}
private void handleCertificateValidationException(CertificateValidationException cve) {
Log.e(K9.LOG_TAG, "Error while testing settings", cve); Log.e(K9.LOG_TAG, "Error while testing settings", cve);
X509Certificate[] chain = cve.getCertChain(); X509Certificate[] chain = cve.getCertChain();
@ -167,17 +199,85 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
R.string.account_setup_failed_dlg_server_message_fmt, R.string.account_setup_failed_dlg_server_message_fmt,
(cve.getMessage() == null ? "" : cve.getMessage())); (cve.getMessage() == null ? "" : cve.getMessage()));
} }
} catch (final 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()));
}
} }
private void handleClientCertificateRequiredException(ClientCertificateRequiredException ccr) {
if (K9.DEBUG)
Log.d(K9.LOG_TAG, "Client certificate alias required: " + ccr.getMessage());
/*
* If the KeyChain API is not available on this Android
* version, inform user and abort
*/
if (!SslHelper.isClientCertificateSupportAvailable()) {
mHandler.post(new Runnable() {
public void run() {
showDialogFragment(R.id.dialog_client_certificate_not_supported);
}
});
// abort
return;
}
String alias = null;
if (CheckDirection.INCOMING.equals(mDirection)) {
ServerSettings storeSettings = Store.decodeStoreUri(mAccount.getStoreUri());
alias = storeSettings.clientCertificateAlias;
} else if (CheckDirection.OUTGOING.equals(mDirection)) {
ServerSettings transportSettings = Transport.decodeTransportUri(mAccount.getTransportUri());
alias = transportSettings.clientCertificateAlias;
}
if (K9.DEBUG)
Log.d(K9.LOG_TAG, "Client certificate alias is: " + alias);
alias = KeyChainKeyManager.interactivelyChooseClientCertificateAlias(
AccountSetupCheckSettings.this,
ccr.getKeyTypes(),
ccr.getIssuers(),
ccr.getHostName(),
ccr.getPort(),
alias);
// Note: KeyChainKeyManager gives back "" on cancel
if (alias != null && alias.equals("")) {
alias = null;
}
// save client certificate alias
if (alias != null) {
if (CheckDirection.INCOMING.equals(mDirection)) {
if (K9.DEBUG)
Log.d(K9.LOG_TAG, "Setting store client certificate alias to: " + alias);
// Set incoming server client certificate alias
String storeUri = mAccount.getStoreUri();
ServerSettings incoming = Store.decodeStoreUri(storeUri);
ServerSettings newIncoming = incoming.newClientCertificateAlias(alias);
String newStoreUri = Store.createStoreUri(newIncoming);
mAccount.setStoreUri(newStoreUri);
} else if (CheckDirection.OUTGOING.equals(mDirection)) {
if (K9.DEBUG)
Log.d(K9.LOG_TAG, "Setting transport client certificate alias to: " + alias);
// Set outgoing server client certificate alias
String transportUri = mAccount.getTransportUri();
ServerSettings outgoing = Transport.decodeTransportUri(transportUri);
ServerSettings newOutgoing = outgoing.newClientCertificateAlias(alias);
String newTransportUri = Transport.createTransportUri(newOutgoing);
mAccount.setTransportUri(newTransportUri);
}
// Save the account settings
mAccount.save(Preferences.getPreferences(AccountSetupCheckSettings.this));
// try again
AccountSetupCheckSettings.actionCheckSettings(AccountSetupCheckSettings.this, mAccount,
mDirection);
} else {
showErrorDialog(R.string.dialog_client_certificate_required);
} }
.start();
} }
@Override @Override
@ -198,41 +298,7 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
}); });
} }
private void showErrorDialog(final int msgResId, final Object... args) { private void acceptKeyDialog(final int msgResId, final CertificateValidationException ex) {
mHandler.post(new Runnable() {
public void run() {
if (mDestroyed) {
return;
}
mProgressBar.setIndeterminate(false);
new AlertDialog.Builder(AccountSetupCheckSettings.this)
.setTitle(getString(R.string.account_setup_failed_dlg_title))
.setMessage(getString(msgResId, args))
.setCancelable(true)
.setNegativeButton(
getString(R.string.account_setup_failed_dlg_continue_action),
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
mCanceled = false;
setResult(RESULT_OK);
finish();
}
})
.setPositiveButton(
getString(R.string.account_setup_failed_dlg_edit_details_action),
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
finish();
}
})
.show();
}
});
}
private void acceptKeyDialog(final int msgResId,
final CertificateValidationException ex) {
mHandler.post(new Runnable() { mHandler.post(new Runnable() {
public void run() { public void run() {
if (mDestroyed) { if (mDestroyed) {
@ -351,6 +417,8 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
} }
} }
// TODO: refactor with DialogFragment.
// This is difficult because we need to pass through chain[0] for onClick()
new AlertDialog.Builder(AccountSetupCheckSettings.this) new AlertDialog.Builder(AccountSetupCheckSettings.this)
.setTitle(getString(R.string.account_setup_failed_dlg_invalid_certificate_title)) .setTitle(getString(R.string.account_setup_failed_dlg_invalid_certificate_title))
//.setMessage(getString(R.string.account_setup_failed_dlg_invalid_certificate) //.setMessage(getString(R.string.account_setup_failed_dlg_invalid_certificate)
@ -362,15 +430,7 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
getString(R.string.account_setup_failed_dlg_invalid_certificate_accept), getString(R.string.account_setup_failed_dlg_invalid_certificate_accept),
new DialogInterface.OnClickListener() { new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) { public void onClick(DialogInterface dialog, int which) {
try { acceptCertificate(chain[0]);
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,
mDirection);
} }
}) })
.setNegativeButton( .setNegativeButton(
@ -385,13 +445,30 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
}); });
} }
/**
* 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 @Override
public void onActivityResult(int reqCode, int resCode, Intent data) { public void onActivityResult(int reqCode, int resCode, Intent data) {
setResult(resCode); setResult(resCode);
finish(); finish();
} }
private void onCancel() { private void onCancel() {
mCanceled = true; mCanceled = true;
setMessage(R.string.account_setup_check_settings_canceling_msg); setMessage(R.string.account_setup_check_settings_canceling_msg);
@ -404,4 +481,87 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
break; break;
} }
} }
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) {
showDialogFragment(dialogId, null);
}
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;
}
case R.id.dialog_client_certificate_not_supported: {
fragment = ConfirmationDialogFragment.newInstance(dialogId,
getString(R.string.dialog_client_certificate_title),
getString(R.string.dialog_client_certificate_not_supported),
getString(android.R.string.ok)
);
break;
}
default: {
throw new RuntimeException("Called showDialog(int) with unknown dialog id.");
}
}
FragmentTransaction ta = getSupportFragmentManager().beginTransaction();
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:
case R.id.dialog_client_certificate_not_supported: {
mCanceled = false;
setResult(RESULT_OK);
finish();
break;
}
}
}
@Override
public void dialogCancelled(int dialogId) {
// nothing to do here...
}
} }

View File

@ -23,17 +23,20 @@ import com.fsck.k9.mail.AuthType;
import com.fsck.k9.mail.ConnectionSecurity; import com.fsck.k9.mail.ConnectionSecurity;
import com.fsck.k9.mail.ServerSettings; import com.fsck.k9.mail.ServerSettings;
import com.fsck.k9.mail.Store; import com.fsck.k9.mail.Store;
import com.fsck.k9.mail.Transport;
import com.fsck.k9.mail.store.ImapStore; import com.fsck.k9.mail.store.ImapStore;
import com.fsck.k9.mail.store.Pop3Store; import com.fsck.k9.mail.store.Pop3Store;
import com.fsck.k9.mail.store.WebDavStore; import com.fsck.k9.mail.store.WebDavStore;
import com.fsck.k9.mail.store.ImapStore.ImapStoreSettings; import com.fsck.k9.mail.store.ImapStore.ImapStoreSettings;
import com.fsck.k9.mail.store.WebDavStore.WebDavStoreSettings; import com.fsck.k9.mail.store.WebDavStore.WebDavStoreSettings;
import com.fsck.k9.mail.transport.SmtpTransport;
import com.fsck.k9.net.ssl.SslHelper;
import com.fsck.k9.service.MailService; import com.fsck.k9.service.MailService;
import com.fsck.k9.view.ClientCertificateSpinner;
import com.fsck.k9.view.ClientCertificateSpinner.OnClientCertificateChangedListener;
import java.io.UnsupportedEncodingException;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@ -51,6 +54,9 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
private String mStoreType; private String mStoreType;
private EditText mUsernameView; private EditText mUsernameView;
private EditText mPasswordView; private EditText mPasswordView;
private ClientCertificateSpinner mClientCertificateSpinner;
private TextView mClientCertificateLabelView;
private TextView mPasswordLabelView;
private EditText mServerView; private EditText mServerView;
private EditText mPortView; private EditText mPortView;
private Spinner mSecurityTypeView; private Spinner mSecurityTypeView;
@ -97,6 +103,9 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
mUsernameView = (EditText)findViewById(R.id.account_username); mUsernameView = (EditText)findViewById(R.id.account_username);
mPasswordView = (EditText)findViewById(R.id.account_password); mPasswordView = (EditText)findViewById(R.id.account_password);
mClientCertificateSpinner = (ClientCertificateSpinner)findViewById(R.id.account_client_certificate_spinner);
mClientCertificateLabelView = (TextView)findViewById(R.id.account_client_certificate_label);
mPasswordLabelView = (TextView)findViewById(R.id.account_password_label);
TextView serverLabelView = (TextView) findViewById(R.id.account_server_label); TextView serverLabelView = (TextView) findViewById(R.id.account_server_label);
mServerView = (EditText)findViewById(R.id.account_server); mServerView = (EditText)findViewById(R.id.account_server);
mPortView = (EditText)findViewById(R.id.account_port); mPortView = (EditText)findViewById(R.id.account_port);
@ -127,30 +136,25 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
} }
}); });
mAuthTypeAdapter = AuthType.getArrayAdapter(this); mAuthTypeView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position,
long id) {
updateViewFromAuthType();
}
@Override
public void onNothingSelected(AdapterView<?> parent) { /* unused */ }
});
mAuthTypeAdapter = AuthType.getArrayAdapter(this, SslHelper.isClientCertificateSupportAvailable());
mAuthTypeView.setAdapter(mAuthTypeAdapter); mAuthTypeView.setAdapter(mAuthTypeAdapter);
/*
* Calls validateFields() which enables or disables the Next button
* based on the fields' validity.
*/
TextWatcher validationTextWatcher = new TextWatcher() {
public void afterTextChanged(Editable s) {
validateFields();
}
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
/* unused */
}
public void onTextChanged(CharSequence s, int start, int before, int count) {
/* unused */
}
};
mUsernameView.addTextChangedListener(validationTextWatcher); mUsernameView.addTextChangedListener(validationTextWatcher);
mPasswordView.addTextChangedListener(validationTextWatcher); mPasswordView.addTextChangedListener(validationTextWatcher);
mServerView.addTextChangedListener(validationTextWatcher); mServerView.addTextChangedListener(validationTextWatcher);
mPortView.addTextChangedListener(validationTextWatcher); mPortView.addTextChangedListener(validationTextWatcher);
mClientCertificateSpinner.setOnClientCertificateChangedListener(clientCertificateChangedListener);
/* /*
* Only allow digits in the port field. * Only allow digits in the port field.
@ -173,6 +177,20 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
try { try {
ServerSettings settings = Store.decodeStoreUri(mAccount.getStoreUri()); ServerSettings settings = Store.decodeStoreUri(mAccount.getStoreUri());
ArrayAdapter<ConnectionSecurity> securityTypesAdapter =
ConnectionSecurity.getArrayAdapter(this, mConnectionSecurityChoices);
mSecurityTypeView.setAdapter(securityTypesAdapter);
// Select currently configured security type
int index = securityTypesAdapter.getPosition(settings.connectionSecurity);
mSecurityTypeView.setSelection(index, false);
updateAuthPlainTextFromSecurityType(settings.connectionSecurity);
// The first item is selected if settings.authenticationType is null or is not in mAuthTypeAdapter
int position = mAuthTypeAdapter.getPosition(settings.authenticationType);
mAuthTypeView.setSelection(position, false);
if (settings.username != null) { if (settings.username != null) {
mUsernameView.setText(settings.username); mUsernameView.setText(settings.username);
} }
@ -181,11 +199,9 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
mPasswordView.setText(settings.password); mPasswordView.setText(settings.password);
} }
updateAuthPlainTextFromSecurityType(settings.connectionSecurity); if (settings.clientCertificateAlias != null) {
mClientCertificateSpinner.setAlias(settings.clientCertificateAlias);
// The first item is selected if settings.authenticationType is null or is not in mAuthTypeAdapter }
int position = mAuthTypeAdapter.getPosition(settings.authenticationType);
mAuthTypeView.setSelection(position, false);
mStoreType = settings.type; mStoreType = settings.type;
if (Pop3Store.STORE_TYPE.equals(settings.type)) { if (Pop3Store.STORE_TYPE.equals(settings.type)) {
@ -256,15 +272,6 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
throw new Exception("Unknown account type: " + mAccount.getStoreUri()); throw new Exception("Unknown account type: " + mAccount.getStoreUri());
} }
ArrayAdapter<ConnectionSecurity> securityTypesAdapter = new ArrayAdapter<ConnectionSecurity>(this,
android.R.layout.simple_spinner_item, mConnectionSecurityChoices);
securityTypesAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
mSecurityTypeView.setAdapter(securityTypesAdapter);
// Select currently configured security type
int index = securityTypesAdapter.getPosition(settings.connectionSecurity);
mSecurityTypeView.setSelection(index, false);
/* /*
* Updates the port when the user changes the security type. This allows * Updates the port when the user changes the security type. This allows
* us to show a reasonable default which the user can change. * us to show a reasonable default which the user can change.
@ -278,6 +285,7 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
@Override @Override
public void onItemSelected(AdapterView<?> parent, View view, int position, public void onItemSelected(AdapterView<?> parent, View view, int position,
long id) { long id) {
// this indirectly triggers validateFields because the port text is watched
updatePortFromSecurityType(); updatePortFromSecurityType();
} }
@ -313,12 +321,55 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
outState.putString(EXTRA_ACCOUNT, mAccount.getUuid()); outState.putString(EXTRA_ACCOUNT, mAccount.getUuid());
} }
/**
* Shows/hides password field and client certificate spinner
*/
private void updateViewFromAuthType() {
AuthType authType = (AuthType) mAuthTypeView.getSelectedItem();
boolean isAuthTypeExternal = AuthType.EXTERNAL.equals(authType);
// if user wants to user external authentication only, then we need to
// hide password field, since it won't be used
if (isAuthTypeExternal) {
// clear password field
mPasswordView.removeTextChangedListener(validationTextWatcher);
mPasswordView.setText("");
mPasswordView.addTextChangedListener(validationTextWatcher);
// hide password fields, show client certificate fields
mPasswordView.setVisibility(View.GONE);
mPasswordLabelView.setVisibility(View.GONE);
mClientCertificateLabelView.setVisibility(View.VISIBLE);
mClientCertificateSpinner.setVisibility(View.VISIBLE);
} else {
// clear client certificate
mClientCertificateSpinner.setOnClientCertificateChangedListener(null);
mClientCertificateSpinner.setAlias(null);
mClientCertificateSpinner.setOnClientCertificateChangedListener(clientCertificateChangedListener);
// show password fields, hide client certificate fields
mPasswordView.setVisibility(View.VISIBLE);
mPasswordLabelView.setVisibility(View.VISIBLE);
mClientCertificateLabelView.setVisibility(View.GONE);
mClientCertificateSpinner.setVisibility(View.GONE);
}
validateFields();
}
private void validateFields() { private void validateFields() {
AuthType authType = (AuthType) mAuthTypeView.getSelectedItem();
boolean isAuthTypeExternal = AuthType.EXTERNAL.equals(authType);
ConnectionSecurity connectionSecurity = (ConnectionSecurity) mSecurityTypeView.getSelectedItem();
boolean hasConnectionSecurity = (!connectionSecurity.equals(ConnectionSecurity.NONE));
mNextButton mNextButton
.setEnabled(Utility.requiredFieldValid(mUsernameView) .setEnabled(Utility.requiredFieldValid(mUsernameView)
&& Utility.requiredFieldValid(mPasswordView) && (Utility.requiredFieldValid(mPasswordView)
|| (isAuthTypeExternal && mClientCertificateSpinner.getAlias() != null))
&& Utility.domainFieldValid(mServerView) && Utility.domainFieldValid(mServerView)
&& Utility.requiredFieldValid(mPortView)); && Utility.requiredFieldValid(mPortView)
&& (!isAuthTypeExternal || hasConnectionSecurity));
Utility.setCompoundDrawablesAlpha(mNextButton, mNextButton.isEnabled() ? 255 : 128); Utility.setCompoundDrawablesAlpha(mNextButton, mNextButton.isEnabled() ? 255 : 128);
} }
@ -377,21 +428,22 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
* password the user just set for incoming. * password the user just set for incoming.
*/ */
try { try {
String usernameEnc = URLEncoder.encode(mUsernameView.getText().toString(), "UTF-8"); String username = mUsernameView.getText().toString();
String passwordEnc = URLEncoder.encode(mPasswordView.getText().toString(), "UTF-8");
String password = null;
String clientCertificateAlias = null;
AuthType authType = (AuthType) mAuthTypeView.getSelectedItem();
if (AuthType.EXTERNAL.equals(authType)) {
clientCertificateAlias = mClientCertificateSpinner.getAlias();
} else {
password = mPasswordView.getText().toString();
}
URI oldUri = new URI(mAccount.getTransportUri()); URI oldUri = new URI(mAccount.getTransportUri());
URI uri = new URI( ServerSettings transportServer = new ServerSettings(SmtpTransport.TRANSPORT_TYPE, oldUri.getHost(), oldUri.getPort(),
oldUri.getScheme(), ConnectionSecurity.SSL_TLS_REQUIRED, authType, username, password, clientCertificateAlias);
usernameEnc + ":" + passwordEnc, String transportUri = Transport.createTransportUri(transportServer);
oldUri.getHost(), mAccount.setTransportUri(transportUri);
oldUri.getPort(),
null,
null,
null);
mAccount.setTransportUri(uri.toString());
} 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);
} catch (URISyntaxException use) { } catch (URISyntaxException use) {
/* /*
* If we can't set up the URL we just continue. It's only for * If we can't set up the URL we just continue. It's only for
@ -411,8 +463,15 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
ConnectionSecurity connectionSecurity = (ConnectionSecurity) mSecurityTypeView.getSelectedItem(); ConnectionSecurity connectionSecurity = (ConnectionSecurity) mSecurityTypeView.getSelectedItem();
String username = mUsernameView.getText().toString(); String username = mUsernameView.getText().toString();
String password = mPasswordView.getText().toString(); String password = null;
String clientCertificateAlias = null;
AuthType authType = (AuthType) mAuthTypeView.getSelectedItem(); AuthType authType = (AuthType) mAuthTypeView.getSelectedItem();
if (authType.equals(AuthType.EXTERNAL)) {
clientCertificateAlias = mClientCertificateSpinner.getAlias();
} else {
password = mPasswordView.getText().toString();
}
String host = mServerView.getText().toString(); String host = mServerView.getText().toString();
int port = Integer.parseInt(mPortView.getText().toString()); int port = Integer.parseInt(mPortView.getText().toString());
@ -435,7 +494,7 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
mAccount.deleteCertificate(host, port, CheckDirection.INCOMING); mAccount.deleteCertificate(host, port, CheckDirection.INCOMING);
ServerSettings settings = new ServerSettings(mStoreType, host, port, ServerSettings settings = new ServerSettings(mStoreType, host, port,
connectionSecurity, authType, username, password, extra); connectionSecurity, authType, username, password, clientCertificateAlias, extra);
mAccount.setStoreUri(Store.createStoreUri(settings)); mAccount.setStoreUri(Store.createStoreUri(settings));
@ -470,4 +529,29 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
Toast toast = Toast.makeText(getApplication(), toastText, Toast.LENGTH_LONG); Toast toast = Toast.makeText(getApplication(), toastText, Toast.LENGTH_LONG);
toast.show(); toast.show();
} }
/*
* Calls validateFields() which enables or disables the Next button
* based on the fields' validity.
*/
TextWatcher validationTextWatcher = new TextWatcher() {
public void afterTextChanged(Editable s) {
validateFields();
}
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
/* unused */
}
public void onTextChanged(CharSequence s, int start, int before, int count) {
/* unused */
}
};
OnClientCertificateChangedListener clientCertificateChangedListener = new OnClientCertificateChangedListener() {
@Override
public void onClientCertificateChanged(String alias) {
validateFields();
}
};
} }

View File

@ -13,6 +13,7 @@ import android.view.View.OnClickListener;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.*; import android.widget.*;
import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.CompoundButton.OnCheckedChangeListener;
import com.fsck.k9.*; import com.fsck.k9.*;
import com.fsck.k9.activity.K9Activity; import com.fsck.k9.activity.K9Activity;
import com.fsck.k9.activity.setup.AccountSetupCheckSettings.CheckDirection; import com.fsck.k9.activity.setup.AccountSetupCheckSettings.CheckDirection;
@ -22,6 +23,9 @@ import com.fsck.k9.mail.ConnectionSecurity;
import com.fsck.k9.mail.ServerSettings; import com.fsck.k9.mail.ServerSettings;
import com.fsck.k9.mail.Transport; import com.fsck.k9.mail.Transport;
import com.fsck.k9.mail.transport.SmtpTransport; import com.fsck.k9.mail.transport.SmtpTransport;
import com.fsck.k9.net.ssl.SslHelper;
import com.fsck.k9.view.ClientCertificateSpinner;
import com.fsck.k9.view.ClientCertificateSpinner.OnClientCertificateChangedListener;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
@ -37,6 +41,9 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
private EditText mUsernameView; private EditText mUsernameView;
private EditText mPasswordView; private EditText mPasswordView;
private ClientCertificateSpinner mClientCertificateSpinner;
private TextView mClientCertificateLabelView;
private TextView mPasswordLabelView;
private EditText mServerView; private EditText mServerView;
private EditText mPortView; private EditText mPortView;
private CheckBox mRequireLoginView; private CheckBox mRequireLoginView;
@ -87,6 +94,9 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
mUsernameView = (EditText)findViewById(R.id.account_username); mUsernameView = (EditText)findViewById(R.id.account_username);
mPasswordView = (EditText)findViewById(R.id.account_password); mPasswordView = (EditText)findViewById(R.id.account_password);
mClientCertificateSpinner = (ClientCertificateSpinner)findViewById(R.id.account_client_certificate_spinner);
mClientCertificateLabelView = (TextView)findViewById(R.id.account_client_certificate_label);
mPasswordLabelView = (TextView)findViewById(R.id.account_password_label);
mServerView = (EditText)findViewById(R.id.account_server); mServerView = (EditText)findViewById(R.id.account_server);
mPortView = (EditText)findViewById(R.id.account_port); mPortView = (EditText)findViewById(R.id.account_port);
mRequireLoginView = (CheckBox)findViewById(R.id.account_require_login); mRequireLoginView = (CheckBox)findViewById(R.id.account_require_login);
@ -98,33 +108,27 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
mNextButton.setOnClickListener(this); mNextButton.setOnClickListener(this);
mRequireLoginView.setOnCheckedChangeListener(this); mRequireLoginView.setOnCheckedChangeListener(this);
ArrayAdapter<ConnectionSecurity> securityTypesAdapter = new ArrayAdapter<ConnectionSecurity>(this, mSecurityTypeView.setAdapter(ConnectionSecurity.getArrayAdapter(this));
android.R.layout.simple_spinner_item, ConnectionSecurity.values());
securityTypesAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
mSecurityTypeView.setAdapter(securityTypesAdapter);
mAuthTypeAdapter = AuthType.getArrayAdapter(this); mAuthTypeAdapter = AuthType.getArrayAdapter(this, SslHelper.isClientCertificateSupportAvailable());
mAuthTypeView.setAdapter(mAuthTypeAdapter); mAuthTypeView.setAdapter(mAuthTypeAdapter);
/*
* Calls validateFields() which enables or disables the Next button
* based on the fields' validity.
*/
TextWatcher validationTextWatcher = new TextWatcher() {
public void afterTextChanged(Editable s) {
validateFields();
}
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
};
mUsernameView.addTextChangedListener(validationTextWatcher); mUsernameView.addTextChangedListener(validationTextWatcher);
mPasswordView.addTextChangedListener(validationTextWatcher); mPasswordView.addTextChangedListener(validationTextWatcher);
mServerView.addTextChangedListener(validationTextWatcher); mServerView.addTextChangedListener(validationTextWatcher);
mPortView.addTextChangedListener(validationTextWatcher); mPortView.addTextChangedListener(validationTextWatcher);
mClientCertificateSpinner.setOnClientCertificateChangedListener(clientCertificateChangedListener);
mAuthTypeView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position,
long id) {
updateViewFromAuthType();
}
@Override
public void onNothingSelected(AdapterView<?> parent) { /* unused */ }
});
/* /*
* Only allow digits in the port field. * Only allow digits in the port field.
@ -147,17 +151,6 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
try { try {
ServerSettings settings = Transport.decodeTransportUri(mAccount.getTransportUri()); ServerSettings settings = Transport.decodeTransportUri(mAccount.getTransportUri());
String username = settings.username;
String password = settings.password;
if (username != null) {
mUsernameView.setText(username);
mRequireLoginView.setChecked(true);
}
if (password != null) {
mPasswordView.setText(password);
}
updateAuthPlainTextFromSecurityType(settings.connectionSecurity); updateAuthPlainTextFromSecurityType(settings.connectionSecurity);
@ -168,6 +161,19 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
// Select currently configured security type // Select currently configured security type
mSecurityTypeView.setSelection(settings.connectionSecurity.ordinal(), false); mSecurityTypeView.setSelection(settings.connectionSecurity.ordinal(), false);
if (settings.username != null) {
mUsernameView.setText(settings.username);
mRequireLoginView.setChecked(true);
}
if (settings.password != null) {
mPasswordView.setText(settings.password);
}
if (settings.clientCertificateAlias != null) {
mClientCertificateSpinner.setAlias(settings.clientCertificateAlias);
}
/* /*
* Updates the port when the user changes the security type. This allows * Updates the port when the user changes the security type. This allows
* us to show a reasonable default which the user can change. * us to show a reasonable default which the user can change.
@ -181,6 +187,7 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
@Override @Override
public void onItemSelected(AdapterView<?> parent, View view, int position, public void onItemSelected(AdapterView<?> parent, View view, int position,
long id) { long id) {
// this indirectly triggers validateFields because the port text is watched
updatePortFromSecurityType(); updatePortFromSecurityType();
} }
@ -214,14 +221,57 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
outState.putString(EXTRA_ACCOUNT, mAccount.getUuid()); outState.putString(EXTRA_ACCOUNT, mAccount.getUuid());
} }
/**
* Shows/hides password field and client certificate spinner
*/
private void updateViewFromAuthType() {
AuthType authType = (AuthType) mAuthTypeView.getSelectedItem();
boolean isAuthTypeExternal = AuthType.EXTERNAL.equals(authType);
// if user wants to user external authentication only, then we need to
// hide password field, since it won't be used
if (isAuthTypeExternal) {
// clear password field
mPasswordView.removeTextChangedListener(validationTextWatcher);
mPasswordView.setText("");
mPasswordView.addTextChangedListener(validationTextWatcher);
// hide password fields, show client certificate fields
mPasswordView.setVisibility(View.GONE);
mPasswordLabelView.setVisibility(View.GONE);
mClientCertificateLabelView.setVisibility(View.VISIBLE);
mClientCertificateSpinner.setVisibility(View.VISIBLE);
} else {
// clear client certificate
mClientCertificateSpinner.setOnClientCertificateChangedListener(null);
mClientCertificateSpinner.setAlias(null);
mClientCertificateSpinner.setOnClientCertificateChangedListener(clientCertificateChangedListener);
// show password fields, hide client certificate fields
mPasswordView.setVisibility(View.VISIBLE);
mPasswordLabelView.setVisibility(View.VISIBLE);
mClientCertificateLabelView.setVisibility(View.GONE);
mClientCertificateSpinner.setVisibility(View.GONE);
}
validateFields();
}
private void validateFields() { private void validateFields() {
AuthType authType = (AuthType) mAuthTypeView.getSelectedItem();
boolean isAuthTypeExternal = AuthType.EXTERNAL.equals(authType);
ConnectionSecurity connectionSecurity = (ConnectionSecurity) mSecurityTypeView.getSelectedItem();
boolean hasConnectionSecurity = (!connectionSecurity.equals(ConnectionSecurity.NONE));
mNextButton mNextButton
.setEnabled( .setEnabled(Utility.domainFieldValid(mServerView)
Utility.domainFieldValid(mServerView) && && Utility.requiredFieldValid(mPortView)
Utility.requiredFieldValid(mPortView) && && (!mRequireLoginView.isChecked()
(!mRequireLoginView.isChecked() || || (Utility.requiredFieldValid(mUsernameView) && Utility.requiredFieldValid(mPasswordView))
(Utility.requiredFieldValid(mUsernameView) && || (Utility.requiredFieldValid(mUsernameView) && isAuthTypeExternal)
Utility.requiredFieldValid(mPasswordView)))); )
&& (!isAuthTypeExternal || hasConnectionSecurity)
);
Utility.setCompoundDrawablesAlpha(mNextButton, mNextButton.isEnabled() ? 255 : 128); Utility.setCompoundDrawablesAlpha(mNextButton, mNextButton.isEnabled() ? 255 : 128);
} }
@ -276,17 +326,23 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
String uri; String uri;
String username = null; String username = null;
String password = null; String password = null;
String clientCertificateAlias = null;
AuthType authType = null; AuthType authType = null;
if (mRequireLoginView.isChecked()) { if (mRequireLoginView.isChecked()) {
username = mUsernameView.getText().toString(); username = mUsernameView.getText().toString();
password = mPasswordView.getText().toString();
authType = (AuthType) mAuthTypeView.getSelectedItem(); authType = (AuthType) mAuthTypeView.getSelectedItem();
if (AuthType.EXTERNAL.equals(authType)) {
clientCertificateAlias = mClientCertificateSpinner.getAlias();
} else {
password = mPasswordView.getText().toString();
}
} }
String newHost = mServerView.getText().toString(); String newHost = mServerView.getText().toString();
int newPort = Integer.parseInt(mPortView.getText().toString()); int newPort = Integer.parseInt(mPortView.getText().toString());
String type = SmtpTransport.TRANSPORT_TYPE; String type = SmtpTransport.TRANSPORT_TYPE;
ServerSettings server = new ServerSettings(type, newHost, newPort, securityType, authType, username, password); ServerSettings server = new ServerSettings(type, newHost, newPort, securityType, authType, username, password, clientCertificateAlias);
uri = Transport.createTransportUri(server); uri = Transport.createTransportUri(server);
mAccount.deleteCertificate(newHost, newPort, CheckDirection.OUTGOING); mAccount.deleteCertificate(newHost, newPort, CheckDirection.OUTGOING);
mAccount.setTransportUri(uri); mAccount.setTransportUri(uri);
@ -312,4 +368,27 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
Toast toast = Toast.makeText(getApplication(), toastText, Toast.LENGTH_LONG); Toast toast = Toast.makeText(getApplication(), toastText, Toast.LENGTH_LONG);
toast.show(); toast.show();
} }
/*
* Calls validateFields() which enables or disables the Next button
* based on the fields' validity.
*/
TextWatcher validationTextWatcher = new TextWatcher() {
public void afterTextChanged(Editable s) {
validateFields();
}
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
};
OnClientCertificateChangedListener clientCertificateChangedListener = new OnClientCertificateChangedListener() {
@Override
public void onClientCertificateChanged(String alias) {
validateFields();
}
};
} }

View File

@ -1,17 +1,21 @@
package com.fsck.k9.fragment; package com.fsck.k9.fragment;
import android.app.Activity;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.app.Dialog; import android.app.Dialog;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.DialogInterface.OnCancelListener; import android.content.DialogInterface.OnCancelListener;
import android.content.DialogInterface.OnClickListener; import android.content.DialogInterface.OnClickListener;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log;
import com.actionbarsherlock.app.SherlockDialogFragment; import com.actionbarsherlock.app.SherlockDialogFragment;
import com.fsck.k9.K9;
public class ConfirmationDialogFragment extends SherlockDialogFragment implements OnClickListener, public class ConfirmationDialogFragment extends SherlockDialogFragment implements OnClickListener,
OnCancelListener { OnCancelListener {
private ConfirmationDialogFragmentListener mListener;
private static final String ARG_DIALOG_ID = "dialog_id"; private static final String ARG_DIALOG_ID = "dialog_id";
private static final String ARG_TITLE = "title"; private static final String ARG_TITLE = "title";
@ -35,6 +39,11 @@ public class ConfirmationDialogFragment extends SherlockDialogFragment implement
return fragment; return fragment;
} }
public static ConfirmationDialogFragment newInstance(int dialogId, String title, String message,
String cancelText) {
return newInstance(dialogId, title, message, null, cancelText);
}
public interface ConfirmationDialogFragmentListener { public interface ConfirmationDialogFragmentListener {
void doPositiveClick(int dialogId); void doPositiveClick(int dialogId);
@ -54,8 +63,14 @@ public class ConfirmationDialogFragment extends SherlockDialogFragment implement
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle(title); builder.setTitle(title);
builder.setMessage(message); builder.setMessage(message);
if (confirmText != null && cancelText != null) {
builder.setPositiveButton(confirmText, this); builder.setPositiveButton(confirmText, this);
builder.setNegativeButton(cancelText, this); builder.setNegativeButton(cancelText, this);
} else if (cancelText != null) {
builder.setNeutralButton(cancelText, this);
} else {
throw new RuntimeException("Set at least cancelText!");
}
return builder.create(); return builder.create();
} }
@ -71,6 +86,10 @@ public class ConfirmationDialogFragment extends SherlockDialogFragment implement
getListener().doNegativeClick(getDialogId()); getListener().doNegativeClick(getDialogId());
break; break;
} }
case DialogInterface.BUTTON_NEUTRAL: {
getListener().doNegativeClick(getDialogId());
break;
}
} }
} }
@ -84,7 +103,23 @@ public class ConfirmationDialogFragment extends SherlockDialogFragment implement
return getArguments().getInt(ARG_DIALOG_ID); return getArguments().getInt(ARG_DIALOG_ID);
} }
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
mListener = (ConfirmationDialogFragmentListener) activity;
} catch (ClassCastException e) {
if (K9.DEBUG)
Log.d(K9.LOG_TAG, activity.toString() + " did not implement ConfirmationDialogFragmentListener");
}
}
private ConfirmationDialogFragmentListener getListener() { private ConfirmationDialogFragmentListener getListener() {
if (mListener != null) {
return mListener;
}
// fallback to getTargetFragment...
try { try {
return (ConfirmationDialogFragmentListener) getTargetFragment(); return (ConfirmationDialogFragmentListener) getTargetFragment();
} catch (ClassCastException e) { } catch (ClassCastException e) {

View File

@ -33,6 +33,8 @@ public enum AuthType {
CRAM_MD5(R.string.account_setup_auth_type_encrypted_password), CRAM_MD5(R.string.account_setup_auth_type_encrypted_password),
EXTERNAL(R.string.account_setup_auth_type_tls_client_certificate),
/* /*
* The following are obsolete authentication settings that were used with * The following are obsolete authentication settings that were used with
* SMTP. They are no longer presented to the user as options, but they may * SMTP. They are no longer presented to the user as options, but they may
@ -43,8 +45,13 @@ public enum AuthType {
LOGIN(0); LOGIN(0);
static public ArrayAdapter<AuthType> getArrayAdapter(Context context) { static public ArrayAdapter<AuthType> getArrayAdapter(Context context, boolean includeExternal) {
AuthType[] authTypes = {PLAIN, CRAM_MD5}; AuthType[] authTypes;
if (includeExternal) {
authTypes = new AuthType[]{PLAIN, CRAM_MD5, EXTERNAL};
} else {
authTypes = new AuthType[]{PLAIN, CRAM_MD5};
}
ArrayAdapter<AuthType> authTypesAdapter = new ArrayAdapter<AuthType>(context, ArrayAdapter<AuthType> authTypesAdapter = new ArrayAdapter<AuthType>(context,
android.R.layout.simple_spinner_item, authTypes); android.R.layout.simple_spinner_item, authTypes);
authTypesAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); authTypesAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);

View File

@ -0,0 +1,48 @@
package com.fsck.k9.mail;
import java.security.Principal;
/**
* This exception is thrown when, during an SSL handshake, a client certificate
* alias is requested but we want the user to select one instead of using the
* previously selected one silently. This must be a RuntimeException because the
* implemented interface of X509ExtendedKeyManager (where it is thrown) does not
* allow anything else.
*/
public class ClientCertificateRequiredException extends RuntimeException {
public static final long serialVersionUID = -1;
String[] mKeyTypes;
Principal[] mIssuers;
String mHostName;
int mPort;
public ClientCertificateRequiredException(String[] keyTypes,
Principal[] issuers,
String hostName,
int port) {
super("interactive client certificate alias choice required");
this.mKeyTypes = keyTypes;
this.mIssuers = issuers;
this.mHostName = hostName;
this.mPort = port;
}
public String[] getKeyTypes() {
return mKeyTypes;
}
public Principal[] getIssuers() {
return mIssuers;
}
public String getHostName() {
return mHostName;
}
public int getPort() {
return mPort;
}
}

View File

@ -1,5 +1,8 @@
package com.fsck.k9.mail; package com.fsck.k9.mail;
import android.content.Context;
import android.widget.ArrayAdapter;
import com.fsck.k9.K9; import com.fsck.k9.K9;
import com.fsck.k9.R; import com.fsck.k9.R;
@ -8,6 +11,17 @@ public enum ConnectionSecurity {
STARTTLS_REQUIRED(R.string.account_setup_incoming_security_tls_label), STARTTLS_REQUIRED(R.string.account_setup_incoming_security_tls_label),
SSL_TLS_REQUIRED(R.string.account_setup_incoming_security_ssl_label); SSL_TLS_REQUIRED(R.string.account_setup_incoming_security_ssl_label);
static public ArrayAdapter<ConnectionSecurity> getArrayAdapter(Context context) {
return getArrayAdapter(context, ConnectionSecurity.values());
}
static public ArrayAdapter<ConnectionSecurity> getArrayAdapter(Context context, ConnectionSecurity[] securityTypes) {
ArrayAdapter<ConnectionSecurity> securityTypesAdapter = new ArrayAdapter<ConnectionSecurity>(context,
android.R.layout.simple_spinner_item, securityTypes);
securityTypesAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
return securityTypesAdapter;
}
private final int mResourceId; private final int mResourceId;
private ConnectionSecurity(int id) { private ConnectionSecurity(int id) {

View File

@ -64,6 +64,14 @@ public class ServerSettings {
*/ */
public final String password; public final String password;
/**
* The alias to retrieve a client certificate using Android 4.0 KeyChain API
* for TLS client certificate authentication with the server.
*
* {@code null} if not applicable for the store or transport.
*/
public final String clientCertificateAlias;
/** /**
* Store- or transport-specific settings as key/value pair. * Store- or transport-specific settings as key/value pair.
* *
@ -89,10 +97,12 @@ public class ServerSettings {
* see {@link ServerSettings#username} * see {@link ServerSettings#username}
* @param password * @param password
* see {@link ServerSettings#password} * see {@link ServerSettings#password}
* @param clientCertificateAlias
* see {@link ServerSettings#clientCertificateAlias}
*/ */
public ServerSettings(String type, String host, int port, public ServerSettings(String type, String host, int port,
ConnectionSecurity connectionSecurity, AuthType authenticationType, String username, ConnectionSecurity connectionSecurity, AuthType authenticationType, String username,
String password) { String password, String clientCertificateAlias) {
this.type = type; this.type = type;
this.host = host; this.host = host;
this.port = port; this.port = port;
@ -100,6 +110,7 @@ public class ServerSettings {
this.authenticationType = authenticationType; this.authenticationType = authenticationType;
this.username = username; this.username = username;
this.password = password; this.password = password;
this.clientCertificateAlias = clientCertificateAlias;
this.extra = null; this.extra = null;
} }
@ -120,12 +131,14 @@ public class ServerSettings {
* see {@link ServerSettings#username} * see {@link ServerSettings#username}
* @param password * @param password
* see {@link ServerSettings#password} * see {@link ServerSettings#password}
* @param clientCertificateAlias
* see {@link ServerSettings#clientCertificateAlias}
* @param extra * @param extra
* see {@link ServerSettings#extra} * see {@link ServerSettings#extra}
*/ */
public ServerSettings(String type, String host, int port, public ServerSettings(String type, String host, int port,
ConnectionSecurity connectionSecurity, AuthType authenticationType, String username, ConnectionSecurity connectionSecurity, AuthType authenticationType, String username,
String password, Map<String, String> extra) { String password, String clientCertificateAlias, Map<String, String> extra) {
this.type = type; this.type = type;
this.host = host; this.host = host;
this.port = port; this.port = port;
@ -133,6 +146,7 @@ public class ServerSettings {
this.authenticationType = authenticationType; this.authenticationType = authenticationType;
this.username = username; this.username = username;
this.password = password; this.password = password;
this.clientCertificateAlias = clientCertificateAlias;
this.extra = (extra != null) ? this.extra = (extra != null) ?
Collections.unmodifiableMap(new HashMap<String, String>(extra)) : null; Collections.unmodifiableMap(new HashMap<String, String>(extra)) : null;
} }
@ -153,6 +167,7 @@ public class ServerSettings {
authenticationType = null; authenticationType = null;
username = null; username = null;
password = null; password = null;
clientCertificateAlias = null;
extra = null; extra = null;
} }
@ -173,6 +188,11 @@ public class ServerSettings {
public ServerSettings newPassword(String newPassword) { public ServerSettings newPassword(String newPassword) {
return new ServerSettings(type, host, port, connectionSecurity, authenticationType, return new ServerSettings(type, host, port, connectionSecurity, authenticationType,
username, newPassword); username, newPassword, clientCertificateAlias);
}
public ServerSettings newClientCertificateAlias(String newAlias) {
return new ServerSettings(type, host, port, connectionSecurity, AuthType.EXTERNAL,
username, password, newAlias);
} }
} }

View File

@ -51,6 +51,16 @@ public abstract class Store {
* Get an instance of a remote mail store. * Get an instance of a remote mail store.
*/ */
public synchronized static Store getRemoteInstance(Account account) throws MessagingException { public synchronized static Store getRemoteInstance(Account account) throws MessagingException {
return getRemoteInstance(account, false);
}
/**
* Get an instance of a remote mail store.
*
* @param reload
* reload store also if already instantiated
*/
public synchronized static Store getRemoteInstance(Account account, boolean reload) throws MessagingException {
String uri = account.getStoreUri(); String uri = account.getStoreUri();
if (uri.startsWith("local")) { if (uri.startsWith("local")) {
@ -58,7 +68,7 @@ public abstract class Store {
} }
Store store = sStores.get(uri); Store store = sStores.get(uri);
if (store == null) { if (store == null || reload) {
if (uri.startsWith("imap")) { if (uri.startsWith("imap")) {
store = new ImapStore(account); store = new ImapStore(account);
} else if (uri.startsWith("pop3")) { } else if (uri.startsWith("pop3")) {

View File

@ -25,7 +25,6 @@ import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder; import java.nio.charset.CharsetDecoder;
import java.nio.charset.CodingErrorAction; import java.nio.charset.CodingErrorAction;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import java.security.Security; import java.security.Security;
import java.security.cert.CertificateException; import java.security.cert.CertificateException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
@ -50,9 +49,7 @@ import java.util.regex.Pattern;
import java.util.zip.Inflater; import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream; import java.util.zip.InflaterInputStream;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException; import javax.net.ssl.SSLException;
import javax.net.ssl.TrustManager;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
@ -100,8 +97,7 @@ import com.fsck.k9.mail.store.ImapResponseParser.ImapList;
import com.fsck.k9.mail.store.ImapResponseParser.ImapResponse; import com.fsck.k9.mail.store.ImapResponseParser.ImapResponse;
import com.fsck.k9.mail.store.imap.ImapUtility; import com.fsck.k9.mail.store.imap.ImapUtility;
import com.fsck.k9.mail.transport.imap.ImapSettings; import com.fsck.k9.mail.transport.imap.ImapSettings;
import com.fsck.k9.net.ssl.TrustManagerFactory; import com.fsck.k9.net.ssl.SslHelper;
import com.fsck.k9.net.ssl.TrustedSocketFactory;
import com.jcraft.jzlib.JZlib; import com.jcraft.jzlib.JZlib;
import com.jcraft.jzlib.ZOutputStream; import com.jcraft.jzlib.ZOutputStream;
@ -126,6 +122,7 @@ public class ImapStore extends Store {
private static final String CAPABILITY_IDLE = "IDLE"; private static final String CAPABILITY_IDLE = "IDLE";
private static final String CAPABILITY_AUTH_CRAM_MD5 = "AUTH=CRAM-MD5"; private static final String CAPABILITY_AUTH_CRAM_MD5 = "AUTH=CRAM-MD5";
private static final String CAPABILITY_AUTH_PLAIN = "AUTH=PLAIN"; private static final String CAPABILITY_AUTH_PLAIN = "AUTH=PLAIN";
private static final String CAPABILITY_AUTH_EXTERNAL = "AUTH=EXTERNAL";
private static final String CAPABILITY_LOGINDISABLED = "LOGINDISABLED"; private static final String CAPABILITY_LOGINDISABLED = "LOGINDISABLED";
private static final String COMMAND_IDLE = "IDLE"; private static final String COMMAND_IDLE = "IDLE";
private static final String CAPABILITY_NAMESPACE = "NAMESPACE"; private static final String CAPABILITY_NAMESPACE = "NAMESPACE";
@ -158,6 +155,7 @@ public class ImapStore extends Store {
AuthType authenticationType = null; AuthType authenticationType = null;
String username = null; String username = null;
String password = null; String password = null;
String clientCertificateAlias = null;
String pathPrefix = null; String pathPrefix = null;
boolean autoDetectNamespace = true; boolean autoDetectNamespace = true;
@ -213,11 +211,16 @@ public class ImapStore extends Store {
authenticationType = AuthType.PLAIN; authenticationType = AuthType.PLAIN;
username = URLDecoder.decode(userInfoParts[0], "UTF-8"); username = URLDecoder.decode(userInfoParts[0], "UTF-8");
password = URLDecoder.decode(userInfoParts[1], "UTF-8"); password = URLDecoder.decode(userInfoParts[1], "UTF-8");
} else { } else if (userInfoParts.length == 3) {
authenticationType = AuthType.valueOf(userInfoParts[0]); authenticationType = AuthType.valueOf(userInfoParts[0]);
username = URLDecoder.decode(userInfoParts[1], "UTF-8"); username = URLDecoder.decode(userInfoParts[1], "UTF-8");
if (AuthType.EXTERNAL.equals(authenticationType)) {
clientCertificateAlias = URLDecoder.decode(userInfoParts[2], "UTF-8");
} else {
password = URLDecoder.decode(userInfoParts[2], "UTF-8"); password = URLDecoder.decode(userInfoParts[2], "UTF-8");
} }
}
} catch (UnsupportedEncodingException enc) { } catch (UnsupportedEncodingException enc) {
// This shouldn't happen since the encoding is hardcoded to UTF-8 // This shouldn't happen since the encoding is hardcoded to UTF-8
throw new IllegalArgumentException("Couldn't urldecode username or password.", enc); throw new IllegalArgumentException("Couldn't urldecode username or password.", enc);
@ -243,7 +246,7 @@ public class ImapStore extends Store {
} }
return new ImapStoreSettings(host, port, connectionSecurity, authenticationType, username, return new ImapStoreSettings(host, port, connectionSecurity, authenticationType, username,
password, autoDetectNamespace, pathPrefix); password, clientCertificateAlias, autoDetectNamespace, pathPrefix);
} }
/** /**
@ -260,10 +263,13 @@ public class ImapStore extends Store {
public static String createUri(ServerSettings server) { public static String createUri(ServerSettings server) {
String userEnc; String userEnc;
String passwordEnc; String passwordEnc;
String clientCertificateAliasEnc;
try { try {
userEnc = URLEncoder.encode(server.username, "UTF-8"); userEnc = URLEncoder.encode(server.username, "UTF-8");
passwordEnc = (server.password != null) ? passwordEnc = (server.password != null) ?
URLEncoder.encode(server.password, "UTF-8") : ""; URLEncoder.encode(server.password, "UTF-8") : "";
clientCertificateAliasEnc = (server.clientCertificateAlias != null) ?
URLEncoder.encode(server.clientCertificateAlias, "UTF-8") : "";
} }
catch (UnsupportedEncodingException e) { catch (UnsupportedEncodingException e) {
throw new IllegalArgumentException("Could not encode username or password", e); throw new IllegalArgumentException("Could not encode username or password", e);
@ -284,8 +290,12 @@ public class ImapStore extends Store {
} }
AuthType authType = server.authenticationType; AuthType authType = server.authenticationType;
String userInfo;
String userInfo = authType.name() + ":" + userEnc + ":" + passwordEnc; if (authType.equals(AuthType.EXTERNAL)) {
userInfo = authType.name() + ":" + userEnc + ":" + clientCertificateAliasEnc;
} else {
userInfo = authType.name() + ":" + userEnc + ":" + passwordEnc;
}
try { try {
Map<String, String> extra = server.getExtra(); Map<String, String> extra = server.getExtra();
String path = null; String path = null;
@ -320,10 +330,10 @@ public class ImapStore extends Store {
public final String pathPrefix; public final String pathPrefix;
protected ImapStoreSettings(String host, int port, ConnectionSecurity connectionSecurity, protected ImapStoreSettings(String host, int port, ConnectionSecurity connectionSecurity,
AuthType authenticationType, String username, String password, AuthType authenticationType, String username, String password, String clientCertificateAlias,
boolean autodetectNamespace, String pathPrefix) { boolean autodetectNamespace, String pathPrefix) {
super(STORE_TYPE, host, port, connectionSecurity, authenticationType, username, super(STORE_TYPE, host, port, connectionSecurity, authenticationType, username,
password); password, clientCertificateAlias);
this.autoDetectNamespace = autodetectNamespace; this.autoDetectNamespace = autodetectNamespace;
this.pathPrefix = pathPrefix; this.pathPrefix = pathPrefix;
} }
@ -339,7 +349,7 @@ public class ImapStore extends Store {
@Override @Override
public ServerSettings newPassword(String newPassword) { public ServerSettings newPassword(String newPassword) {
return new ImapStoreSettings(host, port, connectionSecurity, authenticationType, return new ImapStoreSettings(host, port, connectionSecurity, authenticationType,
username, newPassword, autoDetectNamespace, pathPrefix); username, newPassword, clientCertificateAlias, autoDetectNamespace, pathPrefix);
} }
} }
@ -348,6 +358,7 @@ public class ImapStore extends Store {
private int mPort; private int mPort;
private String mUsername; private String mUsername;
private String mPassword; private String mPassword;
private String mClientCertificateAlias;
private ConnectionSecurity mConnectionSecurity; private ConnectionSecurity mConnectionSecurity;
private AuthType mAuthType; private AuthType mAuthType;
private volatile String mPathPrefix; private volatile String mPathPrefix;
@ -386,6 +397,11 @@ public class ImapStore extends Store {
return mPassword; return mPassword;
} }
@Override
public String getClientCertificateAlias() {
return mClientCertificateAlias;
}
@Override @Override
public boolean useCompression(final int type) { public boolean useCompression(final int type) {
return mAccount.useCompression(type); return mAccount.useCompression(type);
@ -458,6 +474,7 @@ public class ImapStore extends Store {
mAuthType = settings.authenticationType; mAuthType = settings.authenticationType;
mUsername = settings.username; mUsername = settings.username;
mPassword = settings.password; mPassword = settings.password;
mClientCertificateAlias = settings.clientCertificateAlias;
// Make extra sure mPathPrefix is null if "auto-detect namespace" is configured // Make extra sure mPathPrefix is null if "auto-detect namespace" is configured
mPathPrefix = (settings.autoDetectNamespace) ? null : settings.pathPrefix; mPathPrefix = (settings.autoDetectNamespace) ? null : settings.pathPrefix;
@ -2419,14 +2436,8 @@ public class ImapStore extends Store {
mSettings.getPort()); mSettings.getPort());
if (connectionSecurity == ConnectionSecurity.SSL_TLS_REQUIRED) { if (connectionSecurity == ConnectionSecurity.SSL_TLS_REQUIRED) {
SSLContext sslContext = SSLContext.getInstance("TLS"); mSocket = SslHelper.createSslSocket(mSettings.getHost(),
sslContext mSettings.getPort(), mSettings.getClientCertificateAlias());
.init(null,
new TrustManager[] { TrustManagerFactory.get(
mSettings.getHost(),
mSettings.getPort()) },
new SecureRandom());
mSocket = TrustedSocketFactory.createSocket(sslContext);
} else { } else {
mSocket = new Socket(); mSocket = new Socket();
} }
@ -2475,14 +2486,9 @@ public class ImapStore extends Store {
// STARTTLS // STARTTLS
executeSimpleCommand("STARTTLS"); executeSimpleCommand("STARTTLS");
SSLContext sslContext = SSLContext.getInstance("TLS"); mSocket = SslHelper.createStartTlsSocket(mSocket,
sslContext.init(null, mSettings.getHost(), mSettings.getPort(), true,
new TrustManager[] { TrustManagerFactory.get( mSettings.getClientCertificateAlias());
mSettings.getHost(),
mSettings.getPort()) },
new SecureRandom());
mSocket = TrustedSocketFactory.createSocket(sslContext, mSocket,
mSettings.getHost(), mSettings.getPort(), true);
mSocket.setSoTimeout(Store.SOCKET_READ_TIMEOUT); mSocket.setSoTimeout(Store.SOCKET_READ_TIMEOUT);
mIn = new PeekableInputStream(new BufferedInputStream(mSocket mIn = new PeekableInputStream(new BufferedInputStream(mSocket
.getInputStream(), 1024)); .getInputStream(), 1024));
@ -2531,6 +2537,17 @@ public class ImapStore extends Store {
} }
break; break;
case EXTERNAL:
if (hasCapability(CAPABILITY_AUTH_EXTERNAL)) {
executeSimpleCommand(
String.format("AUTHENTICATE EXTERNAL %s",
Utility.base64Encode(mSettings.getUsername())), false);
} else {
throw new MessagingException(
"EXTERNAL authentication not advertised by server");
}
break;
default: default:
throw new MessagingException( throw new MessagingException(
"Unhandled authentication method found in the server settings (bug)."); "Unhandled authentication method found in the server settings (bug).");
@ -2630,7 +2647,6 @@ public class ImapStore extends Store {
} }
} }
} catch (SSLException e) { } catch (SSLException e) {
throw new CertificateValidationException(e.getMessage(), e); throw new CertificateValidationException(e.getMessage(), e);
} catch (GeneralSecurityException gse) { } catch (GeneralSecurityException gse) {

View File

@ -8,23 +8,18 @@ import com.fsck.k9.K9;
import com.fsck.k9.controller.MessageRetrievalListener; import com.fsck.k9.controller.MessageRetrievalListener;
import com.fsck.k9.helper.Utility; import com.fsck.k9.helper.Utility;
import com.fsck.k9.mail.*; import com.fsck.k9.mail.*;
import com.fsck.k9.mail.filter.Base64; import com.fsck.k9.mail.filter.Base64;
import com.fsck.k9.mail.filter.Hex; import com.fsck.k9.mail.filter.Hex;
import com.fsck.k9.mail.internet.MimeMessage; import com.fsck.k9.mail.internet.MimeMessage;
import com.fsck.k9.net.ssl.TrustManagerFactory; import com.fsck.k9.net.ssl.SslHelper;
import com.fsck.k9.net.ssl.TrustedSocketFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException; import javax.net.ssl.SSLException;
import javax.net.ssl.TrustManager;
import java.io.*; import java.io.*;
import java.net.*; import java.net.*;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException; import java.security.cert.CertificateException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -59,15 +54,16 @@ public class Pop3Store extends Store {
private static final String SASL_CAPABILITY = "SASL"; private static final String SASL_CAPABILITY = "SASL";
private static final String AUTH_PLAIN_CAPABILITY = "PLAIN"; private static final String AUTH_PLAIN_CAPABILITY = "PLAIN";
private static final String AUTH_CRAM_MD5_CAPABILITY = "CRAM-MD5"; private static final String AUTH_CRAM_MD5_CAPABILITY = "CRAM-MD5";
private static final String AUTH_EXTERNAL_CAPABILITY = "EXTERNAL";
/** /**
* Decodes a Pop3Store URI. * Decodes a Pop3Store URI.
* *
* <p>Possible forms:</p> * <p>Possible forms:</p>
* <pre> * <pre>
* pop3://user:password@server:port ConnectionSecurity.NONE * pop3://auth:user:password@server:port ConnectionSecurity.NONE
* pop3+tls+://user:password@server:port ConnectionSecurity.STARTTLS_REQUIRED * pop3+tls+://auth:user:password@server:port ConnectionSecurity.STARTTLS_REQUIRED
* pop3+ssl+://user:password@server:port ConnectionSecurity.SSL_TLS_REQUIRED * pop3+ssl+://auth:user:password@server:port ConnectionSecurity.SSL_TLS_REQUIRED
* </pre> * </pre>
*/ */
public static ServerSettings decodeUri(String uri) { public static ServerSettings decodeUri(String uri) {
@ -76,6 +72,7 @@ public class Pop3Store extends Store {
ConnectionSecurity connectionSecurity; ConnectionSecurity connectionSecurity;
String username = null; String username = null;
String password = null; String password = null;
String clientCertificateAlias = null;
URI pop3Uri; URI pop3Uri;
try { try {
@ -131,8 +128,12 @@ public class Pop3Store extends Store {
} }
username = URLDecoder.decode(userInfoParts[userIndex], "UTF-8"); username = URLDecoder.decode(userInfoParts[userIndex], "UTF-8");
if (userInfoParts.length > passwordIndex) { if (userInfoParts.length > passwordIndex) {
if (authType.equals(AuthType.EXTERNAL)) {
clientCertificateAlias = URLDecoder.decode(userInfoParts[passwordIndex], "UTF-8");
} else {
password = URLDecoder.decode(userInfoParts[passwordIndex], "UTF-8"); password = URLDecoder.decode(userInfoParts[passwordIndex], "UTF-8");
} }
}
} catch (UnsupportedEncodingException enc) { } catch (UnsupportedEncodingException enc) {
// This shouldn't happen since the encoding is hardcoded to UTF-8 // This shouldn't happen since the encoding is hardcoded to UTF-8
throw new IllegalArgumentException("Couldn't urldecode username or password.", enc); throw new IllegalArgumentException("Couldn't urldecode username or password.", enc);
@ -140,7 +141,7 @@ public class Pop3Store extends Store {
} }
return new ServerSettings(STORE_TYPE, host, port, connectionSecurity, authType, username, return new ServerSettings(STORE_TYPE, host, port, connectionSecurity, authType, username,
password); password, clientCertificateAlias);
} }
/** /**
@ -157,10 +158,13 @@ public class Pop3Store extends Store {
public static String createUri(ServerSettings server) { public static String createUri(ServerSettings server) {
String userEnc; String userEnc;
String passwordEnc; String passwordEnc;
String clientCertificateAliasEnc;
try { try {
userEnc = URLEncoder.encode(server.username, "UTF-8"); userEnc = URLEncoder.encode(server.username, "UTF-8");
passwordEnc = (server.password != null) ? passwordEnc = (server.password != null) ?
URLEncoder.encode(server.password, "UTF-8") : ""; URLEncoder.encode(server.password, "UTF-8") : "";
clientCertificateAliasEnc = (server.clientCertificateAlias != null) ?
URLEncoder.encode(server.clientCertificateAlias, "UTF-8") : "";
} }
catch (UnsupportedEncodingException e) { catch (UnsupportedEncodingException e) {
throw new IllegalArgumentException("Could not encode username or password", e); throw new IllegalArgumentException("Could not encode username or password", e);
@ -180,7 +184,14 @@ public class Pop3Store extends Store {
break; break;
} }
String userInfo = server.authenticationType.name() + ":" + userEnc + ":" + passwordEnc; AuthType authType = server.authenticationType;
String userInfo;
if (AuthType.EXTERNAL.equals(authType)) {
userInfo = authType.name() + ":" + userEnc + ":" + clientCertificateAliasEnc;
} else {
userInfo = authType.name() + ":" + userEnc + ":" + passwordEnc;
}
try { try {
return new URI(scheme, userInfo, server.host, server.port, null, null, return new URI(scheme, userInfo, server.host, server.port, null, null,
null).toString(); null).toString();
@ -194,6 +205,7 @@ public class Pop3Store extends Store {
private int mPort; private int mPort;
private String mUsername; private String mUsername;
private String mPassword; private String mPassword;
private String mClientCertificateAlias;
private AuthType mAuthType; private AuthType mAuthType;
private ConnectionSecurity mConnectionSecurity; private ConnectionSecurity mConnectionSecurity;
private HashMap<String, Folder> mFolders = new HashMap<String, Folder>(); private HashMap<String, Folder> mFolders = new HashMap<String, Folder>();
@ -224,6 +236,7 @@ public class Pop3Store extends Store {
mUsername = settings.username; mUsername = settings.username;
mPassword = settings.password; mPassword = settings.password;
mClientCertificateAlias = settings.clientCertificateAlias;
mAuthType = settings.authenticationType; mAuthType = settings.authenticationType;
} }
@ -301,11 +314,7 @@ public class Pop3Store extends Store {
try { try {
SocketAddress socketAddress = new InetSocketAddress(mHost, mPort); SocketAddress socketAddress = new InetSocketAddress(mHost, mPort);
if (mConnectionSecurity == ConnectionSecurity.SSL_TLS_REQUIRED) { if (mConnectionSecurity == ConnectionSecurity.SSL_TLS_REQUIRED) {
SSLContext sslContext = SSLContext.getInstance("TLS"); mSocket = SslHelper.createSslSocket(mHost, mPort, mClientCertificateAlias);
sslContext.init(null,
new TrustManager[] { TrustManagerFactory.get(mHost,
mPort) }, new SecureRandom());
mSocket = TrustedSocketFactory.createSocket(sslContext);
} else { } else {
mSocket = new Socket(); mSocket = new Socket();
} }
@ -327,13 +336,8 @@ public class Pop3Store extends Store {
if (mCapabilities.stls) { if (mCapabilities.stls) {
executeSimpleCommand(STLS_COMMAND); executeSimpleCommand(STLS_COMMAND);
SSLContext sslContext = SSLContext.getInstance("TLS"); mSocket = SslHelper.createStartTlsSocket(mSocket, mHost, mPort, true,
sslContext.init(null, mClientCertificateAlias);
new TrustManager[] { TrustManagerFactory.get(
mHost, mPort) },
new SecureRandom());
mSocket = TrustedSocketFactory.createSocket(sslContext, mSocket, mHost,
mPort, true);
mSocket.setSoTimeout(Store.SOCKET_READ_TIMEOUT); mSocket.setSoTimeout(Store.SOCKET_READ_TIMEOUT);
mIn = new BufferedInputStream(mSocket.getInputStream(), 1024); mIn = new BufferedInputStream(mSocket.getInputStream(), 1024);
mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512); mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512);
@ -372,6 +376,17 @@ public class Pop3Store extends Store {
} }
break; break;
case EXTERNAL:
if (mCapabilities.external) {
executeSimpleCommand(
String.format("AUTHENTICATE EXTERNAL %s",
Utility.base64Encode(mUsername)), false);
} else {
throw new MessagingException(
"EXTERNAL authentication not advertised by server");
}
break;
default: default:
throw new MessagingException( throw new MessagingException(
"Unhandled authentication method found in the server settings (bug)."); "Unhandled authentication method found in the server settings (bug).");
@ -1046,6 +1061,8 @@ public class Pop3Store extends Store {
capabilities.authPlain = true; capabilities.authPlain = true;
} else if (response.equals(AUTH_CRAM_MD5_CAPABILITY)) { } else if (response.equals(AUTH_CRAM_MD5_CAPABILITY)) {
capabilities.cramMD5 = true; capabilities.cramMD5 = true;
} else if (response.equals(AUTH_EXTERNAL_CAPABILITY)) {
capabilities.external = true;
} }
} }
} catch (MessagingException e) { } catch (MessagingException e) {
@ -1193,15 +1210,17 @@ public class Pop3Store extends Store {
public boolean stls; public boolean stls;
public boolean top; public boolean top;
public boolean uidl; public boolean uidl;
public boolean external;
@Override @Override
public String toString() { public String toString() {
return String.format("CRAM-MD5 %b, PLAIN %b, STLS %b, TOP %b, UIDL %b", return String.format("CRAM-MD5 %b, PLAIN %b, STLS %b, TOP %b, UIDL %b, EXTERNAL %b",
cramMD5, cramMD5,
authPlain, authPlain,
stls, stls,
top, top,
uidl); uidl,
external);
} }
} }

View File

@ -7,9 +7,9 @@ import com.fsck.k9.K9;
import com.fsck.k9.controller.MessageRetrievalListener; import com.fsck.k9.controller.MessageRetrievalListener;
import com.fsck.k9.helper.Utility; import com.fsck.k9.helper.Utility;
import com.fsck.k9.mail.*; import com.fsck.k9.mail.*;
import com.fsck.k9.mail.filter.EOLConvertingOutputStream; import com.fsck.k9.mail.filter.EOLConvertingOutputStream;
import com.fsck.k9.mail.internet.MimeMessage; import com.fsck.k9.mail.internet.MimeMessage;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.http.*; import org.apache.http.*;
import org.apache.http.client.CookieStore; import org.apache.http.client.CookieStore;
@ -36,6 +36,7 @@ import javax.net.ssl.SSLException;
import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory; import javax.xml.parsers.SAXParserFactory;
import java.io.*; import java.io.*;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
@ -178,7 +179,7 @@ public class WebDavStore extends Store {
} }
return new WebDavStoreSettings(host, port, connectionSecurity, null, username, password, return new WebDavStoreSettings(host, port, connectionSecurity, null, username, password,
alias, path, authPath, mailboxPath); null, alias, path, authPath, mailboxPath);
} }
/** /**
@ -257,10 +258,10 @@ public class WebDavStore extends Store {
public final String mailboxPath; public final String mailboxPath;
protected WebDavStoreSettings(String host, int port, ConnectionSecurity connectionSecurity, protected WebDavStoreSettings(String host, int port, ConnectionSecurity connectionSecurity,
AuthType authenticationType, String username, String password, String alias, AuthType authenticationType, String username, String password, String clientCertificateAlias, String alias,
String path, String authPath, String mailboxPath) { String path, String authPath, String mailboxPath) {
super(STORE_TYPE, host, port, connectionSecurity, authenticationType, username, super(STORE_TYPE, host, port, connectionSecurity, authenticationType, username,
password); password, clientCertificateAlias);
this.alias = alias; this.alias = alias;
this.path = path; this.path = path;
this.authPath = authPath; this.authPath = authPath;
@ -280,7 +281,7 @@ public class WebDavStore extends Store {
@Override @Override
public ServerSettings newPassword(String newPassword) { public ServerSettings newPassword(String newPassword) {
return new WebDavStoreSettings(host, port, connectionSecurity, authenticationType, return new WebDavStoreSettings(host, port, connectionSecurity, authenticationType,
username, newPassword, alias, path, authPath, mailboxPath); username, newPassword, clientCertificateAlias, alias, path, authPath, mailboxPath);
} }
} }
@ -289,6 +290,7 @@ public class WebDavStore extends Store {
private String mUsername; /* Stores the username for authentications */ private String mUsername; /* Stores the username for authentications */
private String mAlias; /* Stores the alias for the user's mailbox */ private String mAlias; /* Stores the alias for the user's mailbox */
private String mPassword; /* Stores the password for authentications */ private String mPassword; /* Stores the password for authentications */
private String mClientCertificateAlias;
private String mUrl; /* Stores the base URL for the server */ private String mUrl; /* Stores the base URL for the server */
private String mHost; /* Stores the host name for the server */ private String mHost; /* Stores the host name for the server */
private int mPort; private int mPort;
@ -324,6 +326,7 @@ public class WebDavStore extends Store {
mUsername = settings.username; mUsername = settings.username;
mPassword = settings.password; mPassword = settings.password;
mClientCertificateAlias = settings.clientCertificateAlias;
mAlias = settings.alias; mAlias = settings.alias;
mPath = settings.path; mPath = settings.path;

View File

@ -2,6 +2,7 @@
package com.fsck.k9.mail.transport; package com.fsck.k9.mail.transport;
import android.util.Log; import android.util.Log;
import com.fsck.k9.Account; import com.fsck.k9.Account;
import com.fsck.k9.K9; import com.fsck.k9.K9;
import com.fsck.k9.mail.*; import com.fsck.k9.mail.*;
@ -13,12 +14,9 @@ import com.fsck.k9.mail.filter.PeekableInputStream;
import com.fsck.k9.mail.filter.SmtpDataStuffing; import com.fsck.k9.mail.filter.SmtpDataStuffing;
import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mail.internet.MimeUtility;
import com.fsck.k9.mail.store.LocalStore.LocalMessage; import com.fsck.k9.mail.store.LocalStore.LocalMessage;
import com.fsck.k9.net.ssl.TrustManagerFactory; import com.fsck.k9.net.ssl.SslHelper;
import com.fsck.k9.net.ssl.TrustedSocketFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException; import javax.net.ssl.SSLException;
import javax.net.ssl.TrustManager;
import java.io.BufferedInputStream; import java.io.BufferedInputStream;
import java.io.BufferedOutputStream; import java.io.BufferedOutputStream;
@ -27,9 +25,7 @@ import java.io.OutputStream;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.net.*; import java.net.*;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import java.security.cert.CertificateException; import java.security.cert.CertificateException;
import java.util.*; import java.util.*;
public class SmtpTransport extends Transport { public class SmtpTransport extends Transport {
@ -38,20 +34,23 @@ public class SmtpTransport extends Transport {
/** /**
* Decodes a SmtpTransport URI. * Decodes a SmtpTransport URI.
* *
* NOTE: In contrast to ImapStore and Pop3Store, the authType is appended at the end!
*
* <p>Possible forms:</p> * <p>Possible forms:</p>
* <pre> * <pre>
* smtp://user:password@server:port ConnectionSecurity.NONE * smtp://user:password:auth@server:port ConnectionSecurity.NONE
* smtp+tls+://user:password@server:port ConnectionSecurity.STARTTLS_REQUIRED * smtp+tls+://user:password:auth@server:port ConnectionSecurity.STARTTLS_REQUIRED
* smtp+ssl+://user:password@server:port ConnectionSecurity.SSL_TLS_REQUIRED * smtp+ssl+://user:password:auth@server:port ConnectionSecurity.SSL_TLS_REQUIRED
* </pre> * </pre>
*/ */
public static ServerSettings decodeUri(String uri) { public static ServerSettings decodeUri(String uri) {
String host; String host;
int port; int port;
ConnectionSecurity connectionSecurity; ConnectionSecurity connectionSecurity;
AuthType authType = AuthType.PLAIN; AuthType authType = null;
String username = null; String username = null;
String password = null; String password = null;
String clientCertificateAlias = null;
URI smtpUri; URI smtpUri;
try { try {
@ -95,14 +94,22 @@ public class SmtpTransport extends Transport {
if (smtpUri.getUserInfo() != null) { if (smtpUri.getUserInfo() != null) {
try { try {
String[] userInfoParts = smtpUri.getUserInfo().split(":"); String[] userInfoParts = smtpUri.getUserInfo().split(":");
if (userInfoParts.length > 0) { if (userInfoParts.length == 1) {
authType = AuthType.PLAIN;
username = URLDecoder.decode(userInfoParts[0], "UTF-8"); username = URLDecoder.decode(userInfoParts[0], "UTF-8");
} } else if (userInfoParts.length == 2) {
if (userInfoParts.length > 1) { authType = AuthType.PLAIN;
username = URLDecoder.decode(userInfoParts[0], "UTF-8");
password = URLDecoder.decode(userInfoParts[1], "UTF-8");
} else if (userInfoParts.length == 3) {
// NOTE: In SmptTransport URIs, the authType comes last!
authType = AuthType.valueOf(userInfoParts[2]);
username = URLDecoder.decode(userInfoParts[0], "UTF-8");
if (authType.equals(AuthType.EXTERNAL)) {
clientCertificateAlias = URLDecoder.decode(userInfoParts[1], "UTF-8");
} else {
password = URLDecoder.decode(userInfoParts[1], "UTF-8"); password = URLDecoder.decode(userInfoParts[1], "UTF-8");
} }
if (userInfoParts.length > 2) {
authType = AuthType.valueOf(userInfoParts[2]);
} }
} catch (UnsupportedEncodingException enc) { } catch (UnsupportedEncodingException enc) {
// This shouldn't happen since the encoding is hardcoded to UTF-8 // This shouldn't happen since the encoding is hardcoded to UTF-8
@ -111,7 +118,7 @@ public class SmtpTransport extends Transport {
} }
return new ServerSettings(TRANSPORT_TYPE, host, port, connectionSecurity, return new ServerSettings(TRANSPORT_TYPE, host, port, connectionSecurity,
authType, username, password); authType, username, password, clientCertificateAlias);
} }
/** /**
@ -128,11 +135,14 @@ public class SmtpTransport extends Transport {
public static String createUri(ServerSettings server) { public static String createUri(ServerSettings server) {
String userEnc; String userEnc;
String passwordEnc; String passwordEnc;
String clientCertificateAliasEnc;
try { try {
userEnc = (server.username != null) ? userEnc = (server.username != null) ?
URLEncoder.encode(server.username, "UTF-8") : ""; URLEncoder.encode(server.username, "UTF-8") : "";
passwordEnc = (server.password != null) ? passwordEnc = (server.password != null) ?
URLEncoder.encode(server.password, "UTF-8") : ""; URLEncoder.encode(server.password, "UTF-8") : "";
clientCertificateAliasEnc = (server.clientCertificateAlias != null) ?
URLEncoder.encode(server.clientCertificateAlias, "UTF-8") : "";
} }
catch (UnsupportedEncodingException e) { catch (UnsupportedEncodingException e) {
throw new IllegalArgumentException("Could not encode username or password", e); throw new IllegalArgumentException("Could not encode username or password", e);
@ -152,10 +162,17 @@ public class SmtpTransport extends Transport {
break; break;
} }
String userInfo = userEnc + ":" + passwordEnc; String userInfo = null;
AuthType authType = server.authenticationType; AuthType authType = server.authenticationType;
// NOTE: authType is append at last item, in contrast to ImapStore and Pop3Store!
if (authType != null) { if (authType != null) {
userInfo += ":" + authType.name(); if (AuthType.EXTERNAL.equals(authType)) {
userInfo = userEnc + ":" + clientCertificateAliasEnc + ":" + authType.name();
} else {
userInfo = userEnc + ":" + passwordEnc + ":" + authType.name();
}
} else {
userInfo = userEnc + ":" + passwordEnc;
} }
try { try {
return new URI(scheme, userInfo, server.host, server.port, null, null, return new URI(scheme, userInfo, server.host, server.port, null, null,
@ -170,6 +187,7 @@ public class SmtpTransport extends Transport {
int mPort; int mPort;
String mUsername; String mUsername;
String mPassword; String mPassword;
String mClientCertificateAlias;
AuthType mAuthType; AuthType mAuthType;
ConnectionSecurity mConnectionSecurity; ConnectionSecurity mConnectionSecurity;
Socket mSocket; Socket mSocket;
@ -194,6 +212,7 @@ public class SmtpTransport extends Transport {
mAuthType = settings.authenticationType; mAuthType = settings.authenticationType;
mUsername = settings.username; mUsername = settings.username;
mPassword = settings.password; mPassword = settings.password;
mClientCertificateAlias = settings.clientCertificateAlias;
} }
@Override @Override
@ -205,12 +224,7 @@ public class SmtpTransport extends Transport {
try { try {
SocketAddress socketAddress = new InetSocketAddress(addresses[i], mPort); SocketAddress socketAddress = new InetSocketAddress(addresses[i], mPort);
if (mConnectionSecurity == ConnectionSecurity.SSL_TLS_REQUIRED) { if (mConnectionSecurity == ConnectionSecurity.SSL_TLS_REQUIRED) {
SSLContext sslContext = SSLContext.getInstance("TLS"); mSocket = SslHelper.createSslSocket(mHost, mPort, mClientCertificateAlias);
sslContext.init(null,
new TrustManager[] { TrustManagerFactory.get(
mHost, mPort) },
new SecureRandom());
mSocket = TrustedSocketFactory.createSocket(sslContext);
mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT); mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT);
secureConnection = true; secureConnection = true;
} else { } else {
@ -264,12 +278,9 @@ public class SmtpTransport extends Transport {
if (extensions.containsKey("STARTTLS")) { if (extensions.containsKey("STARTTLS")) {
executeSimpleCommand("STARTTLS"); executeSimpleCommand("STARTTLS");
SSLContext sslContext = SSLContext.getInstance("TLS"); mSocket = SslHelper.createStartTlsSocket(mSocket, mHost, mPort, true,
sslContext.init(null, mClientCertificateAlias);
new TrustManager[] { TrustManagerFactory.get(mHost,
mPort) }, new SecureRandom());
mSocket = TrustedSocketFactory.createSocket(sslContext, mSocket, mHost,
mPort, true);
mIn = new PeekableInputStream(new BufferedInputStream(mSocket.getInputStream(), mIn = new PeekableInputStream(new BufferedInputStream(mSocket.getInputStream(),
1024)); 1024));
mOut = new BufferedOutputStream(mSocket.getOutputStream(), 1024); mOut = new BufferedOutputStream(mSocket.getOutputStream(), 1024);

View File

@ -21,6 +21,8 @@ public interface ImapSettings {
String getPassword(); String getPassword();
String getClientCertificateAlias();
boolean useCompression(int type); boolean useCompression(int type);
String getPathPrefix(); String getPathPrefix();

View File

@ -0,0 +1,103 @@
package com.fsck.k9.net.ssl;
import java.io.IOException;
import java.net.Socket;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import android.annotation.SuppressLint;
import android.os.Build;
import android.util.Log;
import com.fsck.k9.K9;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.security.KeyChainKeyManager;
/**
* Helper class to create SSL sockets with support for client certificate
* authentication
*/
public class SslHelper {
/**
* KeyChain API available on Android >= 4.0
*
* @return true if API is available
*/
public static boolean isClientCertificateSupportAvailable() {
return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH);
}
@SuppressLint("TrulyRandom")
private static SSLContext createSslContext(String host, int port, String clientCertificateAlias)
throws NoSuchAlgorithmException, KeyManagementException, MessagingException {
if (clientCertificateAlias != null && !isClientCertificateSupportAvailable()) {
throw new MessagingException(
"Client certificate support is only availble on Android >= 4.0", true);
}
if (K9.DEBUG)
Log.d(K9.LOG_TAG, "createSslContext: Client certificate alias: "
+ clientCertificateAlias);
KeyManager[] keyManagers = null;
if (clientCertificateAlias != null) {
keyManagers = new KeyManager[] {
new KeyChainKeyManager(clientCertificateAlias)
};
} else {
keyManagers = new KeyManager[] {
new KeyChainKeyManager()
};
}
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(keyManagers,
new TrustManager[] {
TrustManagerFactory.get(
host, port)
},
new SecureRandom());
return sslContext;
}
/**
* Create SSL socket
*
* @param host
* @param port
* @param clientCertificateAlias if not null, uses client certificate
* retrieved by this alias for authentication
*/
public static Socket createSslSocket(String host, int port, String clientCertificateAlias)
throws NoSuchAlgorithmException, KeyManagementException, IOException,
MessagingException {
SSLContext sslContext = createSslContext(host, port, clientCertificateAlias);
return TrustedSocketFactory.createSocket(sslContext);
}
/**
* Create socket for START_TLS. autoClose = true
*
* @param socket
* @param host
* @param port
* @param secure
* @param clientCertificateAlias if not null, uses client certificate
* retrieved by this alias for authentication
*/
public static Socket createStartTlsSocket(Socket socket, String host, int port, boolean secure,
String clientCertificateAlias) throws NoSuchAlgorithmException,
KeyManagementException, IOException, MessagingException {
SSLContext sslContext = createSslContext(host, port, clientCertificateAlias);
boolean autoClose = true;
return TrustedSocketFactory.createSocket(sslContext, socket, host, port, autoClose);
}
}

View File

@ -1090,7 +1090,8 @@ public class SettingsImporter {
public ImportedServerSettings(ImportedServer server) { public ImportedServerSettings(ImportedServer server) {
super(server.type, server.host, convertPort(server.port), super(server.type, server.host, convertPort(server.port),
convertConnectionSecurity(server.connectionSecurity), convertConnectionSecurity(server.connectionSecurity),
server.authenticationType, server.username, server.password); server.authenticationType, server.username, server.password,
server.clientCertificateAlias);
mImportedServer = server; mImportedServer = server;
} }
@ -1155,6 +1156,7 @@ public class SettingsImporter {
public AuthType authenticationType; public AuthType authenticationType;
public String username; public String username;
public String password; public String password;
public String clientCertificateAlias;
public ImportedSettings extras; public ImportedSettings extras;
} }

View File

@ -0,0 +1,172 @@
package com.fsck.k9.security;
import java.net.Socket;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import javax.net.ssl.X509ExtendedKeyManager;
import android.annotation.TargetApi;
import android.app.Activity;
import android.os.Build;
import android.security.KeyChain;
import android.security.KeyChainAliasCallback;
import android.security.KeyChainException;
import android.util.Log;
import com.fsck.k9.K9;
import com.fsck.k9.mail.ClientCertificateRequiredException;
/**
* For client certificate authentication! Provide private keys and certificates
* during the TLS handshake using the Android 4.0 KeyChain API. If interactive
* selection is requested, we harvest the parameters during the handshake and
* abort with a custom (runtime) ClientCertificateRequiredException.
*/
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public class KeyChainKeyManager extends X509ExtendedKeyManager {
private String mAlias;
public KeyChainKeyManager() {
mAlias = null;
if (K9.DEBUG)
Log.d(K9.LOG_TAG, "KeyChainKeyManager set to interactive prompting required");
}
public KeyChainKeyManager(String alias) {
if (alias == null || "".equals(alias)) {
throw new IllegalArgumentException(
"KeyChainKeyManager: The provided alias is null or empty!");
}
mAlias = alias;
if (K9.DEBUG)
Log.d(K9.LOG_TAG, "KeyChainKeyManager set up with for auto-selected alias " + alias);
}
@Override
public String chooseClientAlias(String[] keyTypes, Principal[] issuers, Socket socket) {
if (mAlias == null) {
throw new ClientCertificateRequiredException(keyTypes, issuers,
socket.getInetAddress().getHostName(), socket.getPort());
}
if (K9.DEBUG)
Log.d(K9.LOG_TAG, "KeyChainKeyManager.chooseClientAlias returning preselected alias "
+ mAlias);
return mAlias;
}
@Override
public X509Certificate[] getCertificateChain(String alias) {
try {
if (K9.DEBUG)
Log.d(K9.LOG_TAG, "KeyChainKeyManager.getCertificateChain for " + alias);
X509Certificate[] chain = KeyChain.getCertificateChain(K9.app, alias);
if (chain == null || chain.length == 0) {
throw new IllegalStateException("No certificate chain found for: " + alias);
}
return chain;
} catch (KeyChainException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
}
}
@Override
public PrivateKey getPrivateKey(String alias) {
try {
if (K9.DEBUG)
Log.d(K9.LOG_TAG, "KeyChainKeyManager.getPrivateKey for " + alias);
PrivateKey key = KeyChain.getPrivateKey(K9.app, alias);
if (key == null) {
throw new IllegalStateException("No private key found for: " + alias);
}
/*
* We need to keep reference to this key so it won't get garbage
* collected. If it will then the whole app will crash on Android <=
* 4.2 with "Fatal signal 11 code=1". See
* https://code.google.com/p/android/issues/detail?id=62319
*/
K9.sClientCertificateReferenceWorkaround.add(key);
return key;
} catch (KeyChainException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
}
}
@Override
public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
// not valid for client side
throw new UnsupportedOperationException();
}
@Override
public String[] getClientAliases(String keyType, Principal[] issuers) {
// not valid for client side
throw new UnsupportedOperationException();
}
@Override
public String[] getServerAliases(String keyType, Principal[] issuers) {
// not valid for client side
throw new UnsupportedOperationException();
}
public static String interactivelyChooseClientCertificateAlias(Activity activity,
String[] keyTypes, Principal[] issuers, String hostName, int port,
String preSelectedAlias) {
// defined as array to be able to set it inside the callback
final String[] selectedAlias = new String[1];
KeyChain.choosePrivateKeyAlias(activity, new KeyChainAliasCallback() {
@Override
public void alias(String alias) {
synchronized (selectedAlias) {
if (K9.DEBUG)
Log.d(K9.LOG_TAG, "User has selected client certificate alias:" + alias);
// see below. not null is condition for breaking out of loop
if (alias == null) {
alias = "";
}
selectedAlias[0] = alias;
selectedAlias.notifyAll();
}
}
}, keyTypes, issuers, hostName, port, preSelectedAlias);
synchronized (selectedAlias) {
while (selectedAlias[0] == null) {
try {
selectedAlias.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
if ("".equals(selectedAlias[0])) {
selectedAlias[0] = null;
}
}
return selectedAlias[0];
}
}

View File

@ -0,0 +1,121 @@
package com.fsck.k9.view;
import com.fsck.k9.K9;
import com.fsck.k9.R;
import android.app.Activity;
import android.content.Context;
import android.security.KeyChain;
import android.security.KeyChainAliasCallback;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.LinearLayout;
public class ClientCertificateSpinner extends LinearLayout {
Activity mActivity;
OnClientCertificateChangedListener mListener;
Button mSelection;
ImageButton mDeleteButton;
String mAlias;
public interface OnClientCertificateChangedListener {
void onClientCertificateChanged(String alias);
}
public void setOnClientCertificateChangedListener(OnClientCertificateChangedListener listener) {
mListener = listener;
}
public ClientCertificateSpinner(Context context, AttributeSet attrs) {
super(context, attrs);
if (context instanceof Activity) {
mActivity = (Activity) context;
} else {
Log.e(K9.LOG_TAG, "ClientCertificateSpinner init failed! Please inflate with Activity!");
}
setOrientation(LinearLayout.HORIZONTAL);
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.client_certificate_spinner, this, true);
mSelection = (Button) getChildAt(0);
updateView();
mSelection.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
onSelect();
}
});
mDeleteButton = (ImageButton) getChildAt(1);
mDeleteButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
onDelete();
}
});
}
public ClientCertificateSpinner(Context context) {
this(context, null);
}
public void setAlias(String alias) {
// Note: KeyChainAliasCallback gives back "" on cancel
if (alias != null && alias.equals("")) {
alias = null;
}
mAlias = alias;
// Note: KeyChainAliasCallback is a different thread than the UI
mActivity.runOnUiThread(new Runnable() {
@Override
public void run() {
if (mListener != null) {
mListener.onClientCertificateChanged(mAlias);
}
updateView();
}
});
}
public String getAlias() {
return mAlias;
}
private void onDelete() {
setAlias(null);
}
private void onSelect() {
// NOTE: keyTypes, issuers, hosts, port are not known before we actually
// open a connection, thus we cannot set them here!
KeyChain.choosePrivateKeyAlias(mActivity, new KeyChainAliasCallback() {
@Override
public void alias(String alias) {
if (K9.DEBUG)
Log.d(K9.LOG_TAG, "User has selected client certificate alias:" + alias);
setAlias(alias);
}
}, null, null, null, -1, getAlias());
}
private void updateView() {
if (mAlias != null) {
mSelection.setText(mAlias);
} else {
mSelection.setText(R.string.client_certificate_spinner_empty);
}
}
}