initial UI work to allow setting up accounts from certifcates

Daniel Gultsch created

Change summary

build.gradle                                                             |  1 
src/main/java/eu/siacs/conversations/entities/Account.java               | 16 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java | 85 
src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java         | 46 
src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java       | 38 
src/main/java/eu/siacs/conversations/ui/XmppActivity.java                |  5 
src/main/res/menu/manageaccounts.xml                                     |  6 
src/main/res/values/strings.xml                                          |  3 
8 files changed, 156 insertions(+), 44 deletions(-)

Detailed changes

build.gradle 🔗

@@ -29,6 +29,7 @@ dependencies {
 	compile project(':libs:MemorizingTrustManager')
 	compile 'com.android.support:support-v13:23.0.1'
 	compile 'org.bouncycastle:bcprov-jdk15on:1.52'
+	compile 'org.bouncycastle:bcmail-jdk15on:1.52'
 	compile 'org.jitsi:org.otr4j:0.22'
 	compile 'org.gnu.inet:libidn:1.15'
 	compile 'com.google.zxing:core:3.2.1'

src/main/java/eu/siacs/conversations/entities/Account.java 🔗

@@ -194,18 +194,14 @@ public class Account extends AbstractEntity {
 		return jid.getLocalpart();
 	}
 
-	public void setUsername(final String username) throws InvalidJidException {
-		jid = Jid.fromParts(username, jid.getDomainpart(), jid.getResourcepart());
+	public void setJid(final Jid jid) {
+		this.jid = jid;
 	}
 
 	public Jid getServer() {
 		return jid.toDomainJid();
 	}
 
-	public void setServer(final String server) throws InvalidJidException {
-		jid = Jid.fromParts(jid.getLocalpart(), server, jid.getResourcepart());
-	}
-
 	public String getPassword() {
 		return password;
 	}
@@ -272,6 +268,14 @@ public class Account extends AbstractEntity {
 		}
 	}
 
+	public boolean setPrivateKeyAlias(String alias) {
+		return setKey("private_key_alias", alias);
+	}
+
+	public String getPrivateKeyAlias() {
+		return getKey("private_key_alias");
+	}
+
 	@Override
 	public ContentValues getContentValues() {
 		final ContentValues values = new ContentValues();

src/main/java/eu/siacs/conversations/services/XmppConnectionService.java 🔗

@@ -25,6 +25,8 @@ import android.os.PowerManager.WakeLock;
 import android.os.SystemClock;
 import android.preference.PreferenceManager;
 import android.provider.ContactsContract;
+import android.security.KeyChain;
+import android.security.KeyChainException;
 import android.util.Log;
 import android.util.LruCache;
 
@@ -34,11 +36,22 @@ import net.java.otr4j.session.SessionID;
 import net.java.otr4j.session.SessionImpl;
 import net.java.otr4j.session.SessionStatus;
 
+import org.bouncycastle.asn1.x500.RDN;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x500.style.BCStyle;
+import org.bouncycastle.asn1.x500.style.IETFUtils;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
+import org.bouncycastle.jce.PrincipalUtil;
+import org.bouncycastle.jce.X509Principal;
 import org.openintents.openpgp.util.OpenPgpApi;
 import org.openintents.openpgp.util.OpenPgpServiceConnection;
 
 import java.math.BigInteger;
+import java.security.PrivateKey;
 import java.security.SecureRandom;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -1285,6 +1298,43 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
 		updateAccountUi();
 	}
 
+	public void createAccountFromKey(final String alias, final OnAccountCreated callback) {
+		new Thread(new Runnable() {
+			@Override
+			public void run() {
+				try {
+					X509Certificate[] chain = KeyChain.getCertificateChain(XmppConnectionService.this, alias);
+					PrivateKey key = KeyChain.getPrivateKey(XmppConnectionService.this, alias);
+					X500Name x500name = new JcaX509CertificateHolder(chain[0]).getSubject();
+					String email = IETFUtils.valueToString(x500name.getRDNs(BCStyle.EmailAddress)[0].getFirst().getValue());
+					String name = IETFUtils.valueToString(x500name.getRDNs(BCStyle.CN)[0].getFirst().getValue());
+					Jid jid = Jid.fromString(email);
+					if (findAccountByJid(jid) == null) {
+						Account account = new Account(jid, "");
+						account.setPrivateKeyAlias(alias);
+						account.setOption(Account.OPTION_DISABLED, true);
+						createAccount(account);
+						callback.onAccountCreated(account);
+					} else {
+						callback.informUser(R.string.account_already_exists);
+					}
+				} catch (KeyChainException e) {
+					callback.informUser(R.string.unable_to_parse_certificate);
+				} catch (InterruptedException e) {
+					callback.informUser(R.string.unable_to_parse_certificate);
+					e.printStackTrace();
+				} catch (CertificateEncodingException e) {
+					callback.informUser(R.string.unable_to_parse_certificate);
+					e.printStackTrace();
+				} catch (InvalidJidException e) {
+					callback.informUser(R.string.unable_to_parse_certificate);
+					e.printStackTrace();
+				}
+			}
+		}).start();
+
+	}
+
 	public void updateAccount(final Account account) {
 		this.statusListener.onStatusChanged(account);
 		databaseBackend.updateAccount(account);
@@ -2699,54 +2749,59 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
 		}
 	}
 
+	public interface OnAccountCreated {
+		void onAccountCreated(Account account);
+		void informUser(int r);
+	}
+
 	public interface OnMoreMessagesLoaded {
-		public void onMoreMessagesLoaded(int count, Conversation conversation);
+		void onMoreMessagesLoaded(int count, Conversation conversation);
 
-		public void informUser(int r);
+		void informUser(int r);
 	}
 
 	public interface OnAccountPasswordChanged {
-		public void onPasswordChangeSucceeded();
+		void onPasswordChangeSucceeded();
 
-		public void onPasswordChangeFailed();
+		void onPasswordChangeFailed();
 	}
 
 	public interface OnAffiliationChanged {
-		public void onAffiliationChangedSuccessful(Jid jid);
+		void onAffiliationChangedSuccessful(Jid jid);
 
-		public void onAffiliationChangeFailed(Jid jid, int resId);
+		void onAffiliationChangeFailed(Jid jid, int resId);
 	}
 
 	public interface OnRoleChanged {
-		public void onRoleChangedSuccessful(String nick);
+		void onRoleChangedSuccessful(String nick);
 
-		public void onRoleChangeFailed(String nick, int resid);
+		void onRoleChangeFailed(String nick, int resid);
 	}
 
 	public interface OnConversationUpdate {
-		public void onConversationUpdate();
+		void onConversationUpdate();
 	}
 
 	public interface OnAccountUpdate {
-		public void onAccountUpdate();
+		void onAccountUpdate();
 	}
 
 	public interface OnRosterUpdate {
-		public void onRosterUpdate();
+		void onRosterUpdate();
 	}
 
 	public interface OnMucRosterUpdate {
-		public void onMucRosterUpdate();
+		void onMucRosterUpdate();
 	}
 
 	public interface OnConferenceConfigurationFetched {
-		public void onConferenceConfigurationFetched(Conversation conversation);
+		void onConferenceConfigurationFetched(Conversation conversation);
 	}
 
 	public interface OnConferenceOptionsPushed {
-		public void onPushSucceeded();
+		void onPushSucceeded();
 
-		public void onPushFailed();
+		void onPushFailed();
 	}
 
 	public interface OnShowErrorToast {

src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java 🔗

@@ -74,6 +74,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
 	private LinearLayout keysCard;
 
 	private Jid jidToEdit;
+	private boolean mInitMode = false;
 	private Account mAccount;
 	private String messageFingerprint;
 
@@ -83,6 +84,9 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
 
 		@Override
 		public void onClick(final View v) {
+			if (mInitMode && mAccount != null) {
+				mAccount.setOption(Account.OPTION_DISABLED, false);
+			}
 			if (mAccount != null && mAccount.getStatus() == Account.State.DISABLED && !accountInfoEdited()) {
 				mAccount.setOption(Account.OPTION_DISABLED, false);
 				xmppConnectionService.updateAccount(mAccount);
@@ -129,12 +133,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
 				}
 			}
 			if (mAccount != null) {
-				try {
-					mAccount.setUsername(jid.hasLocalpart() ? jid.getLocalpart() : "");
-					mAccount.setServer(jid.getDomainpart());
-				} catch (final InvalidJidException ignored) {
-					return;
-				}
+				mAccount.setJid(jid);
 				mAccountJid.setError(null);
 				mPasswordConfirm.setError(null);
 				mAccount.setPassword(password);
@@ -152,9 +151,9 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
 				mAccount.setOption(Account.OPTION_REGISTER, registerNewAccount);
 				xmppConnectionService.createAccount(mAccount);
 			}
-			if (jidToEdit != null
-					&& !mAccount.isOptionSet(Account.OPTION_DISABLED)
-					&& !registerNewAccount) {
+			if (!mAccount.isOptionSet(Account.OPTION_DISABLED)
+					&& !registerNewAccount
+					&& !mInitMode) {
 				finish();
 			} else {
 				updateSaveButton();
@@ -179,12 +178,10 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
 			startActivity(new Intent(getApplicationContext(),
 						ManageAccountActivity.class));
 			finish();
-		} else if (jidToEdit == null && mAccount != null
-				&& mAccount.getStatus() == Account.State.ONLINE) {
+		} else if (mInitMode && mAccount != null && mAccount.getStatus() == Account.State.ONLINE) {
 			if (!mFetchingAvatar) {
 				mFetchingAvatar = true;
-				xmppConnectionService.checkForAvatar(mAccount,
-						mAvatarFetchCallback);
+				xmppConnectionService.checkForAvatar(mAccount, mAvatarFetchCallback);
 			}
 		} else {
 			updateSaveButton();
@@ -236,8 +233,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
 		@Override
 		public void onClick(final View view) {
 			if (mAccount != null) {
-				final Intent intent = new Intent(getApplicationContext(),
-						PublishProfilePictureActivity.class);
+				final Intent intent = new Intent(getApplicationContext(), PublishProfilePictureActivity.class);
 				intent.putExtra("account", mAccount.getJid().toBareJid().toString());
 				startActivity(intent);
 			}
@@ -269,7 +265,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
 	}
 
 	protected void updateSaveButton() {
-		if (accountInfoEdited() && jidToEdit != null) {
+		if (accountInfoEdited() && !mInitMode) {
 			this.mSaveButton.setText(R.string.save);
 			this.mSaveButton.setEnabled(true);
 			this.mSaveButton.setTextColor(getPrimaryTextColor());
@@ -277,14 +273,14 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
 			this.mSaveButton.setEnabled(false);
 			this.mSaveButton.setTextColor(getSecondaryTextColor());
 			this.mSaveButton.setText(R.string.account_status_connecting);
-		} else if (mAccount != null && mAccount.getStatus() == Account.State.DISABLED) {
+		} else if (mAccount != null && mAccount.getStatus() == Account.State.DISABLED && !mInitMode) {
 			this.mSaveButton.setEnabled(true);
 			this.mSaveButton.setTextColor(getPrimaryTextColor());
 			this.mSaveButton.setText(R.string.enable);
 		} else {
 			this.mSaveButton.setEnabled(true);
 			this.mSaveButton.setTextColor(getPrimaryTextColor());
-			if (jidToEdit != null) {
+			if (!mInitMode) {
 				if (mAccount != null && mAccount.isOnlineAndConnected()) {
 					this.mSaveButton.setText(R.string.save);
 					if (!accountInfoEdited()) {
@@ -421,8 +417,9 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
 			} catch (final InvalidJidException | NullPointerException ignored) {
 				this.jidToEdit = null;
 			}
+			this.mInitMode = getIntent().getBooleanExtra("init", false) || this.jidToEdit == null;
 			this.messageFingerprint = getIntent().getStringExtra("fingerprint");
-			if (this.jidToEdit != null) {
+			if (!mInitMode) {
 				this.mRegisterNew.setVisibility(View.GONE);
 				if (getActionBar() != null) {
 					getActionBar().setTitle(getString(R.string.account_details));
@@ -440,7 +437,14 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
 	protected void onBackendConnected() {
 		if (this.jidToEdit != null) {
 			this.mAccount = xmppConnectionService.findAccountByJid(jidToEdit);
-			updateAccountInformation(true);
+			if (this.mAccount != null) {
+				if (this.mAccount.getPrivateKeyAlias() != null) {
+				this.mPassword.setHint(R.string.authenticate_with_certificate);
+				if (this.mInitMode) {
+					this.mPassword.requestFocus();
+				}
+			}	updateAccountInformation(true);
+			}
 		} else if (this.xmppConnectionService.getAccounts().size() == 0) {
 			if (getActionBar() != null) {
 				getActionBar().setDisplayHomeAsUpEnabled(false);
@@ -492,7 +496,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
 			}
 			this.mPassword.setText(this.mAccount.getPassword());
 		}
-		if (this.jidToEdit != null) {
+		if (!mInitMode) {
 			this.mAvatar.setVisibility(View.VISIBLE);
 			this.mAvatar.setImageBitmap(avatarService().get(this.mAccount, getPixel(72)));
 		}

src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java 🔗

@@ -5,6 +5,9 @@ import android.content.DialogInterface;
 import android.content.DialogInterface.OnClickListener;
 import android.content.Intent;
 import android.os.Bundle;
+import android.security.KeyChain;
+import android.security.KeyChainAliasCallback;
+import android.util.Log;
 import android.view.ContextMenu;
 import android.view.ContextMenu.ContextMenuInfo;
 import android.view.Menu;
@@ -14,6 +17,7 @@ import android.widget.AdapterView;
 import android.widget.AdapterView.AdapterContextMenuInfo;
 import android.widget.AdapterView.OnItemClickListener;
 import android.widget.ListView;
+import android.widget.Toast;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -21,10 +25,11 @@ import java.util.List;
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
 import eu.siacs.conversations.ui.adapter.AccountAdapter;
 
-public class ManageAccountActivity extends XmppActivity implements OnAccountUpdate {
+public class ManageAccountActivity extends XmppActivity implements OnAccountUpdate, KeyChainAliasCallback, XmppConnectionService.OnAccountCreated {
 
 	protected Account selectedAccount = null;
 
@@ -61,7 +66,7 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
 
 			@Override
 			public void onItemClick(AdapterView<?> arg0, View view,
-					int position, long arg3) {
+									int position, long arg3) {
 				switchToAccount(accountList.get(position));
 			}
 		});
@@ -144,6 +149,9 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
 			case R.id.action_enable_all:
 				enableAllAccounts();
 				break;
+			case R.id.action_add_account_from_key:
+				addAccountFromKey();
+				break;
 			default:
 				break;
 		}
@@ -179,6 +187,10 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
 		}
 	}
 
+	private void addAccountFromKey() {
+		KeyChain.choosePrivateKeyAlias(this, this, null, null, null, -1, null);
+	}
+
 	private void publishAvatar(Account account) {
 		Intent intent = new Intent(getApplicationContext(),
 				PublishProfilePictureActivity.class);
@@ -281,4 +293,26 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
 			}
 		}
 	}
+
+	@Override
+	public void alias(String alias) {
+		if (alias != null) {
+			xmppConnectionService.createAccountFromKey(alias, this);
+		}
+	}
+
+	@Override
+	public void onAccountCreated(Account account) {
+		switchToAccount(account, true);
+	}
+
+	@Override
+	public void informUser(final int r) {
+		runOnUiThread(new Runnable() {
+			@Override
+			public void run() {
+				Toast.makeText(ManageAccountActivity.this,r,Toast.LENGTH_LONG).show();
+			}
+		});
+	}
 }

src/main/java/eu/siacs/conversations/ui/XmppActivity.java 🔗

@@ -435,8 +435,13 @@ public abstract class XmppActivity extends Activity {
 	}
 
 	public void switchToAccount(Account account) {
+		switchToAccount(account,false);
+	}
+
+	public void switchToAccount(Account account, boolean init) {
 		Intent intent = new Intent(this, EditAccountActivity.class);
 		intent.putExtra("jid", account.getJid().toBareJid().toString());
+		intent.putExtra("init", init);
 		startActivity(intent);
 	}
 

src/main/res/menu/manageaccounts.xml 🔗

@@ -6,6 +6,12 @@
 		android:icon="?attr/icon_add_person"
 		android:showAsAction="always"
 		android:title="@string/action_add_account"/>
+	<item
+		android:id="@+id/action_add_account_from_key"
+		android:showAsAction="never"
+		android:icon="?attr/icon_add_person"
+		android:title="@string/action_add_account_from_key"
+		android:visible="false"/>
 	<item
 		android:id="@+id/action_enable_all"
 		android:title="@string/enable_all_accounts"/>

src/main/res/values/strings.xml 🔗

@@ -524,4 +524,7 @@
 	<string name="pref_away_when_screen_off_summary">Marks your resource as away when the screen is turned off</string>
 	<string name="pref_xa_on_silent_mode">Not available in silent mode</string>
 	<string name="pref_xa_on_silent_mode_summary">Marks your resource as not available when phone is in silent mode</string>
+	<string name="action_add_account_from_key">Add account from key</string>
+	<string name="unable_to_parse_certificate">Unable to parse certificate</string>
+	<string name="authenticate_with_certificate">Leave empty to authenticate w/ certificate</string>
 </resources>