Update to the latest version of MemorizingTrustManager
authorDa Risk <da_risk@beem-project.com>
Mon, 07 May 2012 21:57:10 +0200
changeset 975 d8305c375b10
parent 974 2d28cb82891b
child 976 f6fed4cc4d9c
Update to the latest version of MemorizingTrustManager Also fix the LeakedReceiver issue. Fix bug #384 close #408 Mitm commit: 11b37471c5403c0510619278d453ca393405f162
src/de/duenndns/ssl/MemorizingTrustManager.java
--- a/src/de/duenndns/ssl/MemorizingTrustManager.java	Mon Apr 23 00:14:18 2012 +0200
+++ b/src/de/duenndns/ssl/MemorizingTrustManager.java	Mon May 07 21:57:10 2012 +0200
@@ -31,12 +31,9 @@
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.Service;
-import android.app.AlertDialog;
 import android.app.PendingIntent;
 import android.content.BroadcastReceiver;
 import android.content.Context;
-import android.content.DialogInterface;
-import android.content.DialogInterface.OnClickListener;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.net.Uri;
@@ -47,7 +44,7 @@
 import java.security.cert.*;
 import java.security.KeyStore;
 import java.security.KeyStoreException;
-import java.util.concurrent.atomic.AtomicInteger;
+import java.security.MessageDigest;
 import java.util.HashMap;
 import javax.net.ssl.TrustManager;
 import javax.net.ssl.TrustManagerFactory;
@@ -80,9 +77,10 @@
 	static String KEYSTORE_FILE = "KeyStore.bks";
 
 	Context master;
+	Activity foregroundAct;
 	NotificationManager notificationManager;
 	private static int decisionId = 0;
-	private static HashMap<Integer,MTMDecision> openDecisions = new HashMap();
+	private static HashMap<Integer, MTMDecision> openDecisions = new HashMap<Integer, MTMDecision>();
 
 	Handler masterHandler;
 	private File keyStoreFile;
@@ -92,15 +90,25 @@
 
 	/** Creates an instance of the MemorizingTrustManager class.
 	 *
-	 * @param m Activity or Service to show the Dialog / Notification
+	 * You need to supply the application context. This has to be one of:
+	 *    - Application
+	 *    - Activity
+	 *    - Service
+	 *
+	 * The context is used for file management, to display the dialog /
+	 * notification and for obtaining translated strings.
+	 *
+	 * @param m Context for the application.
 	 */
-	private MemorizingTrustManager(Context m) {
+	public MemorizingTrustManager(Context m) {
 		master = m;
 		masterHandler = new Handler();
 		notificationManager = (NotificationManager)master.getSystemService(Context.NOTIFICATION_SERVICE);
 
 		Application app;
-		if (m instanceof Service) {
+		if (m instanceof Application) {
+			app = (Application)m;
+		} else if (m instanceof Service) {
 			app = ((Service)m).getApplication();
 		} else if (m instanceof Activity) {
 			app = ((Activity)m).getApplication();
@@ -134,6 +142,36 @@
 	}
 
 	/**
+	 * Binds an Activity to the MTM for displaying the query dialog.
+	 *
+	 * This is useful if your connection is run from a service that is
+	 * triggered by user interaction -- in such cases the activity is
+	 * visible and the user tends to ignore the service notification.
+	 *
+	 * You should never have a hidden activity bound to MTM! Use this
+	 * function in onResume() and @see unbindDisplayActivity in onPause().
+	 *
+	 * @param act Activity to be bound
+	 */
+	public void bindDisplayActivity(Activity act) {
+		foregroundAct = act;
+	}
+
+	/**
+	 * Removes an Activity from the MTM display stack.
+	 *
+	 * Always call this function when the Activity added with
+	 * @see bindDisplayActivity is hidden.
+	 *
+	 * @param act Activity to be unbound
+	 */
+	public void unbindDisplayActivity(Activity act) {
+		// do not remove if it was overridden by a different activity
+		if (foregroundAct == act)
+			foregroundAct = null;
+	}
+
+	/**
 	 * Changes the path for the KeyStore file.
 	 *
 	 * The actual filename relative to the app's directory will be
@@ -207,6 +245,15 @@
 		}
 	}
 
+	// if the certificate is stored in the app key store, it is considered "known"
+	private boolean isCertKnown(X509Certificate cert) {
+		try {
+			return appKeyStore.getCertificateAlias(cert) != null;
+		} catch (KeyStoreException e) {
+			return false;
+		}
+	}
+
 	private boolean isExpiredException(Throwable e) {
 		do {
 			if (e instanceof CertificateExpiredException)
@@ -233,6 +280,10 @@
 				Log.i(TAG, "checkCertTrusted: accepting expired certificate from keystore");
 				return;
 			}
+			if (isCertKnown(chain[0])) {
+				Log.i(TAG, "checkCertTrusted: accepting cert already stored in keystore");
+				return;
+			}
 			try {
 				Log.d(TAG, "checkCertTrusted: trying defaultTrustManager");
 				if (isServer)
@@ -274,6 +325,28 @@
 		return myId;
 	}
 
+	private static String hexString(byte[] data) {
+		StringBuffer si = new StringBuffer();
+		for (int i = 0; i < data.length; i++) {
+			si.append(String.format("%02x", data[i]));
+			if (i < data.length - 1)
+				si.append(":");
+		}
+		return si.toString();
+	}
+
+	private static String certHash(final X509Certificate cert, String digest) {
+		try {
+			MessageDigest md = MessageDigest.getInstance(digest);
+			md.update(cert.getEncoded());
+			return hexString(md.digest());
+		} catch (java.security.cert.CertificateEncodingException e) {
+			return e.getMessage();
+		} catch (java.security.NoSuchAlgorithmException e) {
+			return e.getMessage();
+		}
+	}
+
 	private String certChainMessage(final X509Certificate[] chain, CertificateException cause) {
 		Throwable e = cause;
 		Log.d(TAG, "certChainMessage for " + e);
@@ -281,14 +354,17 @@
 		if (e.getCause() != null) {
 			e = e.getCause();
 			si.append(e.getLocalizedMessage());
-			si.append("\n");
+			//si.append("\n");
 		}
 		for (X509Certificate c : chain) {
-			si.append("\n");
+			si.append("\n\n");
 			si.append(c.getSubjectDN().toString());
-			si.append(" (");
+			si.append("\nMD5: ");
+			si.append(certHash(c, "MD5"));
+			si.append("\nSHA1: ");
+			si.append(certHash(c, "SHA-1"));
+			si.append("\nSigned by: ");
 			si.append(c.getIssuerDN().toString());
-			si.append(")");
 		}
 		return si.toString();
 	}
@@ -305,7 +381,16 @@
 		notificationManager.notify(NOTIFICATION_ID, n);
 	}
 
-	void launchServiceMode(Intent activityIntent, final String certMessage) {
+	/**
+	 * Returns the top-most entry of the activity stack.
+	 *
+	 * @return the Context of the currently bound UI or the master context if none is bound
+	 */
+	Context getUI() {
+		return (foregroundAct != null) ? foregroundAct : master;
+	}
+
+	BroadcastReceiver launchServiceMode(Intent activityIntent, final String certMessage) {
 		BroadcastReceiver launchNotifReceiver= new BroadcastReceiver() {
 		    public void onReceive(Context ctx, Intent i) {
 			Log.i(TAG, "Interception not done by the application. Send notification");
@@ -318,7 +403,7 @@
 		Intent ni = new Intent(INTERCEPT_DECISION_INTENT + "/" + master.getPackageName());
 		ni.putExtra(INTERCEPT_DECISION_INTENT_LAUNCH, call);
 		master.sendOrderedBroadcast(ni, null);
-
+		return launchNotifReceiver;
 	}
 
 	void interact(final X509Certificate[] chain, String authType, CertificateException cause)
@@ -327,29 +412,13 @@
 		/* prepare the MTMDecision blocker object */
 		MTMDecision choice = new MTMDecision();
 		final int myId = createDecisionId(choice);
-		final String certTitle = chain[0].getSubjectDN().toString();
 		final String certMessage = certChainMessage(chain, cause);
-
 		BroadcastReceiver decisionReceiver = new BroadcastReceiver() {
 			public void onReceive(Context ctx, Intent i) { interactResult(i); }
 		};
 		master.registerReceiver(decisionReceiver, new IntentFilter(DECISION_INTENT + "/" + master.getPackageName()));
-		masterHandler.post(new Runnable() {
-			public void run() {
-				Intent ni = new Intent(master, MemorizingActivity.class);
-				ni.setData(Uri.parse(MemorizingTrustManager.class.getName() + "/" + myId));
-				ni.putExtra(DECISION_INTENT_APP, master.getPackageName());
-				ni.putExtra(DECISION_INTENT_ID, myId);
-				ni.putExtra(DECISION_INTENT_CERT, certMessage);
-
-				try {
-					master.startActivity(ni);
-				} catch (Exception e) {
-					Log.e(TAG, "startActivity: " + e);
-					launchServiceMode(ni, certMessage);
-				}
-			}
-		});
+		LaunchRunnable lr = new LaunchRunnable(myId, certMessage);
+		masterHandler.post(lr);
 
 		Log.d(TAG, "openDecisions: " + openDecisions);
 		Log.d(TAG, "waiting on " + myId);
@@ -359,6 +428,8 @@
 			e.printStackTrace();
 		}
 		master.unregisterReceiver(decisionReceiver);
+		if (lr.launchNotifReceiver != null)
+			master.unregisterReceiver(lr.launchNotifReceiver);
 		Log.d(TAG, "finished wait on " + myId + ": " + choice.state);
 		switch (choice.state) {
 		case MTMDecision.DECISION_ALWAYS:
@@ -381,10 +452,42 @@
 			 d = openDecisions.get(decisionId);
 			 openDecisions.remove(decisionId);
 		}
+		if (d == null) {
+			Log.e(TAG, "interactResult: aborting due to stale decision reference!");
+			return;
+		}
 		synchronized(d) {
 			d.state = choice;
 			d.notify();
 		}
 	}
 
+	private class LaunchRunnable implements Runnable {
+		private int myId;
+		private String certMessage;
+		BroadcastReceiver launchNotifReceiver;
+		
+		public LaunchRunnable(final int id, final String certMsg) {
+			myId = id;
+			certMessage = certMsg;
+		}
+		
+		public void run() {
+			Intent ni = new Intent(master, MemorizingActivity.class);
+			ni.setData(Uri.parse(MemorizingTrustManager.class.getName() + "/" + myId));
+			ni.putExtra(DECISION_INTENT_APP, master.getPackageName());
+			ni.putExtra(DECISION_INTENT_ID, myId);
+			ni.putExtra(DECISION_INTENT_CERT, certMessage);
+
+			// we try to directly start the activity and fall back to
+			// making a notification
+			try {
+				getUI().startActivity(ni);
+			} catch (Exception e) {
+				Log.e(TAG, "startActivity: " + e);
+				launchNotifReceiver = launchServiceMode(ni, certMessage);
+			}
+		}
+	}
+	
 }