# HG changeset patch # User Da Risk # Date 1336420630 -7200 # Node ID d8305c375b10ce9852afa6f1379e837123e15efe # Parent 2d28cb82891b9a9a6d1848efdda9c7e8a677fee0 Update to the latest version of MemorizingTrustManager Also fix the LeakedReceiver issue. Fix bug #384 close #408 Mitm commit: 11b37471c5403c0510619278d453ca393405f162 diff -r 2d28cb82891b -r d8305c375b10 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 openDecisions = new HashMap(); + private static HashMap openDecisions = new HashMap(); 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); + } + } + } + }