tls exceptions for untrusted certs

Daniel Gultsch created

Change summary

res/layout/cert_warning.xml                                    | 32 +
res/menu/manageaccounts_context.xml                            | 17 
src/eu/siacs/conversations/entities/Account.java               | 16 
src/eu/siacs/conversations/services/XmppConnectionService.java | 46 +
src/eu/siacs/conversations/ui/ManageAccountActivity.java       | 94 +++
src/eu/siacs/conversations/utils/CryptoHelper.java             | 14 
src/eu/siacs/conversations/xmpp/OnTLSExceptionReceived.java    |  7 
src/eu/siacs/conversations/xmpp/XmppConnection.java            | 36 +
8 files changed, 223 insertions(+), 39 deletions(-)

Detailed changes

res/layout/cert_warning.xml 🔗

@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    android:padding="8dp">
+
+    <TextView
+        android:id="@+id/hint"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textSize="18sp"/>
+
+    <TextView
+        android:paddingTop="8dp"
+        android:paddingBottom="8dp"
+        android:id="@+id/sha"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:typeface="monospace"
+        android:textSize="18sp"/>
+
+    <TextView
+        android:id="@+id/dont"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textColor="#FFe92727"
+        android:textStyle="bold"
+        android:textSize="18sp"
+        android:text="Do not connect unless you know exactly what you are doing" />
+    
+</LinearLayout>

res/menu/manageaccounts_context.xml 🔗

@@ -1,23 +1,28 @@
 <?xml version="1.0" encoding="utf-8"?>
 <menu xmlns:android="http://schemas.android.com/apk/res/android" >
     <item
-        android:id="@+id/account_delete"
+        android:id="@+id/mgmt_account_edit"
+        android:icon="@drawable/ic_action_edit"
+        android:title="Edit Account"
+        android:showAsAction="always" />
+    <item
+        android:id="@+id/mgmt_account_delete"
         android:icon="@drawable/ic_action_delete"
         android:title="Delete"
         android:showAsAction="always"
         />
     <item
-        android:id="@+id/account_disable"
+        android:id="@+id/mgmt_account_disable"
         android:title="Temporarily disable"
-        android:showAsAction="always"/>
+        android:showAsAction="never"/>
     <item
-        android:id="@+id/account_enable"
+        android:id="@+id/mgmt_account_enable"
         android:title="Enable"
-        android:showAsAction="always"
+        android:showAsAction="never"
         android:visible="false"/>
     
         <item
-        android:id="@+id/announce_pgp"
+        android:id="@+id/mgmt_account_announce_pgp"
         android:orderInCategory="75"
         android:showAsAction="never"
         android:title="@string/announce_pgp" />

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

@@ -138,6 +138,22 @@ public class Account  extends AbstractEntity{
 		return keys;
 	}
 	
+	public String getSSLFingerprint() {
+		if (keys.has("ssl_cert")) {
+			try {
+				return keys.getString("ssl_cert");
+			} catch (JSONException e) {
+				return null;
+			}
+		} else {
+			return null;
+		}
+	}
+	
+	public void setSSLCertFingerprint(String fingerprint) {
+		this.setKey("ssl_cert", fingerprint);
+	}
+	
 	public boolean setKey(String keyName, String keyValue) {
 		try {
 			this.keys.put(keyName, keyValue);

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

@@ -41,6 +41,7 @@ import eu.siacs.conversations.xmpp.OnIqPacketReceived;
 import eu.siacs.conversations.xmpp.OnMessagePacketReceived;
 import eu.siacs.conversations.xmpp.OnPresencePacketReceived;
 import eu.siacs.conversations.xmpp.OnStatusChanged;
+import eu.siacs.conversations.xmpp.OnTLSExceptionReceived;
 import eu.siacs.conversations.xmpp.PresencePacket;
 import eu.siacs.conversations.xmpp.XmppConnection;
 import android.app.AlarmManager;
@@ -76,6 +77,11 @@ public class XmppConnectionService extends Service {
 
 	public OnConversationListChangedListener convChangedListener = null;
 	private OnAccountListChangedListener accountChangedListener = null;
+	private OnTLSExceptionReceived tlsException = null;
+	
+	public void setOnTLSExceptionReceivedListener(OnTLSExceptionReceived listener) {
+		tlsException = listener;
+	}
 	
 	private Random mRandom = new Random(System.currentTimeMillis());
 
@@ -169,7 +175,9 @@ public class XmppConnectionService extends Service {
 
 		@Override
 		public void onStatusChanged(Account account) {
+			Log.d(LOGTAG,account.getJid()+" status switched to " + account.getStatus());
 			if (accountChangedListener != null) {
+				Log.d(LOGTAG,"notifiy ui");
 				accountChangedListener.onAccountListChangedListener();
 			}
 			if (account.getStatus() == Account.STATUS_ONLINE) {
@@ -452,6 +460,16 @@ public class XmppConnectionService extends Service {
 		connection.setOnPresencePacketReceivedListener(this.presenceListener);
 		connection
 				.setOnUnregisteredIqPacketReceivedListener(this.unknownIqListener);
+		connection.setOnTLSExceptionReceivedListener(new OnTLSExceptionReceived() {
+			
+			@Override
+			public void onTLSExceptionReceived(String fingerprint, Account account) {
+				Log.d(LOGTAG,"tls exception arrived in service");
+				if (tlsException!=null) {
+					tlsException.onTLSExceptionReceived(fingerprint,account);
+				}
+			}
+		});
 		return connection;
 	}
 
@@ -816,16 +834,7 @@ public class XmppConnectionService extends Service {
 
 	public void updateAccount(Account account) {
 		databaseBackend.updateAccount(account);
-		if (account.getXmppConnection() != null) {
-			disconnect(account);
-		}
-		if (!account.isOptionSet(Account.OPTION_DISABLED)) {
-			if (account.getXmppConnection()==null) {
-				account.setXmppConnection(this.createConnection(account));
-			}
-			Thread thread = new Thread(account.getXmppConnection());
-			thread.start();
-		}
+		reconnectAccount(account);
 		if (accountChangedListener != null)
 			accountChangedListener.onAccountListChangedListener();
 	}
@@ -1097,4 +1106,21 @@ public class XmppConnectionService extends Service {
 		}
 		return contact;
 	}
+
+	public void removeOnTLSExceptionReceivedListener() {
+		this.tlsException = null;
+	}
+
+	public void reconnectAccount(Account account) {
+		if (account.getXmppConnection() != null) {
+			disconnect(account);
+		}
+		if (!account.isOptionSet(Account.OPTION_DISABLED)) {
+			if (account.getXmppConnection()==null) {
+				account.setXmppConnection(this.createConnection(account));
+			}
+			Thread thread = new Thread(account.getXmppConnection());
+			thread.start();
+		}
+	}
 }

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

@@ -8,6 +8,7 @@ import eu.siacs.conversations.crypto.PgpEngine;
 import eu.siacs.conversations.crypto.PgpEngine.UserInputRequiredException;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.ui.EditAccount.EditAccountListener;
+import eu.siacs.conversations.xmpp.OnTLSExceptionReceived;
 import android.app.Activity;
 import android.app.AlertDialog;
 import android.content.Context;
@@ -18,7 +19,6 @@ import android.content.IntentSender.SendIntentException;
 import android.os.Bundle;
 import android.util.Log;
 import android.view.ActionMode;
-import android.view.ActionMode.Callback;
 import android.view.LayoutInflater;
 import android.view.Menu;
 import android.view.MenuInflater;
@@ -39,6 +39,7 @@ public class ManageAccountActivity extends XmppActivity {
 	protected boolean isActionMode = false;
 	protected ActionMode actionMode;
 	protected Account selectedAccountForActionMode = null;
+	protected ManageAccountActivity activity = this;
 	
 	protected List<Account> accountList = new ArrayList<Account>();
 	protected ListView accountListView;
@@ -59,6 +60,45 @@ public class ManageAccountActivity extends XmppActivity {
 			});
 		}
 	};
+	
+	protected OnTLSExceptionReceived tlsExceptionReceived = new OnTLSExceptionReceived() {
+		
+		@Override
+		public void onTLSExceptionReceived(final String fingerprint, final Account account) {
+			activity.runOnUiThread(new Runnable() {
+				
+				@Override
+				public void run() {
+					AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+					builder.setTitle("Untrusted Certificate");
+					builder.setIconAttribute(android.R.attr.alertDialogIcon);
+					View view = (View) getLayoutInflater().inflate(R.layout.cert_warning, null);
+					TextView sha = (TextView) view.findViewById(R.id.sha);
+					TextView hint = (TextView) view.findViewById(R.id.hint);
+					StringBuilder humanReadableSha = new StringBuilder();
+					humanReadableSha.append(fingerprint);
+					for(int i = 2; i < 58; i += 3) {
+						humanReadableSha.insert(i, ":");
+					}
+					hint.setText(account.getServer()+" presented you with an unstrusted, possible self signed, certificate.\nThe SHA1 fingerprint is");
+					sha.setText(humanReadableSha.toString());
+					builder.setView(view);
+					//builder.setMessage(server+" presented you with an unstrusted, possible self signed, certificate. The SHA1 fingerprint is "+fingerprint+" Do not connect unless you know exactly what you are doing");
+					builder.setNegativeButton("Don't connect", null);
+					builder.setPositiveButton("Trust certificate", new OnClickListener() {
+						
+						@Override
+						public void onClick(DialogInterface dialog, int which) {
+							account.setSSLCertFingerprint(fingerprint);
+							activity.xmppConnectionService.updateAccount(account);
+						}
+					});
+					builder.create().show();
+				}
+			});
+			
+		}
+	};
 
 	@Override
 	protected void onCreate(Bundle savedInstanceState) {
@@ -114,6 +154,10 @@ public class ManageAccountActivity extends XmppActivity {
 					statusView.setText("server requires TLS");
 					statusView.setTextColor(0xFFe92727);
 					break;
+				case Account.STATUS_TLS_ERROR:
+					statusView.setText("untrusted cerficate");
+					statusView.setTextColor(0xFFe92727);
+					break;
 				default:
 					break;
 				}
@@ -129,16 +173,14 @@ public class ManageAccountActivity extends XmppActivity {
 			public void onItemClick(AdapterView<?> arg0, View view,
 					int position, long arg3) {
 				if (!isActionMode) {
-					EditAccount dialog = new EditAccount();
-					dialog.setAccount(accountList.get(position));
-					dialog.setEditAccountListener(new EditAccountListener() {
-	
-						@Override
-						public void onAccountEdited(Account account) {
-							xmppConnectionService.updateAccount(account);
-						}
-					});
-					dialog.show(getFragmentManager(), "edit_account");
+					Account account = accountList.get(position);
+					if ((account.getStatus() != Account.STATUS_ONLINE)&&(account.getStatus() != Account.STATUS_CONNECTING)&&(!account.isOptionSet(Account.OPTION_DISABLED))) {
+						activity.xmppConnectionService.reconnectAccount(accountList.get(position));
+					} else if (account.getStatus() == Account.STATUS_ONLINE) {
+						activity.startActivity(new Intent(activity.getApplicationContext(),NewConversationActivity.class));
+					}
+					
+					Log.d("gultsch","clicked on account "+accountList.get(position).getJid());
 				} else {
 					selectedAccountForActionMode = accountList.get(position);
 					actionMode.invalidate();
@@ -159,11 +201,11 @@ public class ManageAccountActivity extends XmppActivity {
 						@Override
 						public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
 							if (selectedAccountForActionMode.isOptionSet(Account.OPTION_DISABLED)) {
-					        	menu.findItem(R.id.account_enable).setVisible(true);
-					        	menu.findItem(R.id.account_disable).setVisible(false);
+					        	menu.findItem(R.id.mgmt_account_enable).setVisible(true);
+					        	menu.findItem(R.id.mgmt_account_disable).setVisible(false);
 					        } else {
-					        	menu.findItem(R.id.account_disable).setVisible(true);
-					        	menu.findItem(R.id.account_enable).setVisible(false);
+					        	menu.findItem(R.id.mgmt_account_disable).setVisible(true);
+					        	menu.findItem(R.id.mgmt_account_enable).setVisible(false);
 					        }
 							return true;
 						}
@@ -183,15 +225,27 @@ public class ManageAccountActivity extends XmppActivity {
 						
 						@Override
 						public boolean onActionItemClicked(final ActionMode mode, MenuItem item) {
-							if (item.getItemId()==R.id.account_disable) {
+							if (item.getItemId()==R.id.mgmt_account_edit) {
+								EditAccount dialog = new EditAccount();
+								dialog.setAccount(selectedAccountForActionMode);
+								dialog.setEditAccountListener(new EditAccountListener() {
+				
+									@Override
+									public void onAccountEdited(Account account) {
+										xmppConnectionService.updateAccount(account);
+										actionMode.finish();
+									}
+								});
+								dialog.show(getFragmentManager(), "edit_account");
+							} else if (item.getItemId()==R.id.mgmt_account_disable) {
 								selectedAccountForActionMode.setOption(Account.OPTION_DISABLED, true);
 								xmppConnectionService.updateAccount(selectedAccountForActionMode);
 								mode.finish();
-							} else if (item.getItemId()==R.id.account_enable) {
+							} else if (item.getItemId()==R.id.mgmt_account_enable) {
 								selectedAccountForActionMode.setOption(Account.OPTION_DISABLED, false);
 								xmppConnectionService.updateAccount(selectedAccountForActionMode);
 								mode.finish();
-							} else if (item.getItemId()==R.id.account_delete) {
+							} else if (item.getItemId()==R.id.mgmt_account_delete) {
 								AlertDialog.Builder builder = new AlertDialog.Builder(activity);
 								builder.setTitle("Are you sure?");
 								builder.setIconAttribute(android.R.attr.alertDialogIcon);
@@ -207,7 +261,7 @@ public class ManageAccountActivity extends XmppActivity {
 								});
 								builder.setNegativeButton("Cancel",null);
 								builder.create().show();
-							} else if (item.getItemId()==R.id.announce_pgp) {
+							} else if (item.getItemId()==R.id.mgmt_account_announce_pgp) {
 								if (activity.hasPgp()) {
 									mode.finish();
 									try {
@@ -236,6 +290,7 @@ public class ManageAccountActivity extends XmppActivity {
 	protected void onStop() {
 		if (xmppConnectionServiceBound) {
 			xmppConnectionService.removeOnAccountListChangedListener();
+			xmppConnectionService.removeOnTLSExceptionReceivedListener();
 		}
 		super.onStop();
 	}
@@ -243,6 +298,7 @@ public class ManageAccountActivity extends XmppActivity {
 	@Override
 	void onBackendConnected() {
 		xmppConnectionService.setOnAccountListChangedListener(accountChanged);
+		xmppConnectionService.setOnTLSExceptionReceivedListener(tlsExceptionReceived);
 		this.accountList.clear();
 		this.accountList.addAll(xmppConnectionService.getAccounts());
 		accountListViewAdapter.notifyDataSetChanged();

src/eu/siacs/conversations/utils/CryptoHelper.java 🔗

@@ -0,0 +1,14 @@
+package eu.siacs.conversations.utils;
+
+public class CryptoHelper {
+	final protected static char[] hexArray = "0123456789ABCDEF".toCharArray();
+	public static String bytesToHex(byte[] bytes) {
+	    char[] hexChars = new char[bytes.length * 2];
+	    for ( int j = 0; j < bytes.length; j++ ) {
+	        int v = bytes[j] & 0xFF;
+	        hexChars[j * 2] = hexArray[v >>> 4];
+	        hexChars[j * 2 + 1] = hexArray[v & 0x0F];
+	    }
+	    return new String(hexChars);
+	}
+}

src/eu/siacs/conversations/xmpp/XmppConnection.java 🔗

@@ -9,6 +9,7 @@ import java.net.UnknownHostException;
 import java.security.KeyManagementException;
 import java.security.KeyStore;
 import java.security.KeyStoreException;
+import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.security.SecureRandom;
 import java.security.cert.CertPathValidatorException;
@@ -33,6 +34,7 @@ import android.os.Bundle;
 import android.os.PowerManager;
 import android.util.Log;
 import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.utils.CryptoHelper;
 import eu.siacs.conversations.utils.DNSHelper;
 import eu.siacs.conversations.utils.SASL;
 import eu.siacs.conversations.xml.Element;
@@ -71,6 +73,7 @@ public class XmppConnection implements Runnable {
 	private OnIqPacketReceived unregisteredIqListener = null;
 	private OnMessagePacketReceived messageListener = null;
 	private OnStatusChanged statusListener = null;
+	private OnTLSExceptionReceived tlsListener;
 
 	public XmppConnection(Account account, PowerManager pm) {
 		this.account = account;
@@ -127,7 +130,9 @@ public class XmppConnection implements Runnable {
 			}
 			return;
 		} catch (IOException e) {
-			this.changeStatus(Account.STATUS_OFFLINE);
+			if (account.getStatus() != Account.STATUS_TLS_ERROR) {
+				this.changeStatus(Account.STATUS_OFFLINE);
+			}
 			if (wakeLock.isHeld()) {
 				wakeLock.release();
 			}
@@ -312,7 +317,26 @@ public class XmppConnection implements Runnable {
 					try {
 						origTrustmanager.checkServerTrusted(chain, authType);
 					} catch (CertificateException e) {
-						Log.d(LOGTAG,"cert exeption");
+						if (e.getCause() instanceof CertPathValidatorException) {
+							String sha;
+							try {
+								MessageDigest sha1 = MessageDigest.getInstance("SHA1");
+								sha1.update(chain[0].getEncoded());
+								sha = CryptoHelper.bytesToHex(sha1.digest());
+								if (!sha.equals(account.getSSLFingerprint())) {
+									changeStatus(Account.STATUS_TLS_ERROR);
+									if (tlsListener!=null) {
+										tlsListener.onTLSExceptionReceived(sha,account);
+									}
+									throw new CertificateException();
+								}
+							} catch (NoSuchAlgorithmException e1) {
+								// TODO Auto-generated catch block
+								e1.printStackTrace();
+							}
+						} else {
+							throw new CertificateException();
+						}
 					}
 				}
 
@@ -325,8 +349,8 @@ public class XmppConnection implements Runnable {
 			sc.init(null, wrappedTrustManagers, null);
 			SSLSocketFactory factory = sc.getSocketFactory();
 			SSLSocket sslSocket = (SSLSocket) factory.createSocket(socket,
-					socket.getInetAddress().getHostAddress(), socket.getPort(),
-					true);
+						socket.getInetAddress().getHostAddress(), socket.getPort(),
+						true);
 			tagReader.setInputStream(sslSocket.getInputStream());
 			Log.d(LOGTAG, "reset inputstream");
 			tagWriter.setOutputStream(sslSocket.getOutputStream());
@@ -528,6 +552,10 @@ public class XmppConnection implements Runnable {
 	public void setOnStatusChangedListener(OnStatusChanged listener) {
 		this.statusListener = listener;
 	}
+	
+	public void setOnTLSExceptionReceivedListener(OnTLSExceptionReceived listener) {
+		this.tlsListener = listener;
+	}
 
 	public void disconnect() {
 		shouldConnect = false;