From 3bb5fcb3ca3586f2c641e0810ba0e019604ad7e4 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 7 Mar 2014 14:24:33 +0100 Subject: [PATCH] tls exceptions for untrusted certs --- res/layout/cert_warning.xml | 32 +++++++ res/menu/manageaccounts_context.xml | 17 ++-- .../siacs/conversations/entities/Account.java | 16 ++++ .../services/XmppConnectionService.java | 46 +++++++-- .../ui/ManageAccountActivity.java | 94 +++++++++++++++---- .../conversations/utils/CryptoHelper.java | 14 +++ .../xmpp/OnTLSExceptionReceived.java | 7 ++ .../conversations/xmpp/XmppConnection.java | 36 ++++++- 8 files changed, 223 insertions(+), 39 deletions(-) create mode 100644 res/layout/cert_warning.xml create mode 100644 src/eu/siacs/conversations/utils/CryptoHelper.java create mode 100644 src/eu/siacs/conversations/xmpp/OnTLSExceptionReceived.java diff --git a/res/layout/cert_warning.xml b/res/layout/cert_warning.xml new file mode 100644 index 000000000..2e1e55117 --- /dev/null +++ b/res/layout/cert_warning.xml @@ -0,0 +1,32 @@ + + + + + + + + + + diff --git a/res/menu/manageaccounts_context.xml b/res/menu/manageaccounts_context.xml index a51a8e52f..22b7ac34d 100644 --- a/res/menu/manageaccounts_context.xml +++ b/res/menu/manageaccounts_context.xml @@ -1,23 +1,28 @@ + + android:showAsAction="never"/> diff --git a/src/eu/siacs/conversations/entities/Account.java b/src/eu/siacs/conversations/entities/Account.java index 618e4a7c4..2ed7ade73 100644 --- a/src/eu/siacs/conversations/entities/Account.java +++ b/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); diff --git a/src/eu/siacs/conversations/services/XmppConnectionService.java b/src/eu/siacs/conversations/services/XmppConnectionService.java index 716b3edc2..c6af9d1de 100644 --- a/src/eu/siacs/conversations/services/XmppConnectionService.java +++ b/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(); + } + } } \ No newline at end of file diff --git a/src/eu/siacs/conversations/ui/ManageAccountActivity.java b/src/eu/siacs/conversations/ui/ManageAccountActivity.java index 5e56dde2b..91314686c 100644 --- a/src/eu/siacs/conversations/ui/ManageAccountActivity.java +++ b/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 accountList = new ArrayList(); 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(); diff --git a/src/eu/siacs/conversations/utils/CryptoHelper.java b/src/eu/siacs/conversations/utils/CryptoHelper.java new file mode 100644 index 000000000..6e606fa10 --- /dev/null +++ b/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); + } +} diff --git a/src/eu/siacs/conversations/xmpp/OnTLSExceptionReceived.java b/src/eu/siacs/conversations/xmpp/OnTLSExceptionReceived.java new file mode 100644 index 000000000..0e232ee40 --- /dev/null +++ b/src/eu/siacs/conversations/xmpp/OnTLSExceptionReceived.java @@ -0,0 +1,7 @@ +package eu.siacs.conversations.xmpp; + +import eu.siacs.conversations.entities.Account; + +public interface OnTLSExceptionReceived { + public void onTLSExceptionReceived(String fingerprint, Account account); +} diff --git a/src/eu/siacs/conversations/xmpp/XmppConnection.java b/src/eu/siacs/conversations/xmpp/XmppConnection.java index c5aa1d7da..24168aef9 100644 --- a/src/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/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;