src/de/duenndns/ssl/MemorizingTrustManager.java
changeset 998 d8305c375b10
parent 920 ff346f5bc36f
equal deleted inserted replaced
997:2d28cb82891b 998:d8305c375b10
    29 import android.app.Activity;
    29 import android.app.Activity;
    30 import android.app.Application;
    30 import android.app.Application;
    31 import android.app.Notification;
    31 import android.app.Notification;
    32 import android.app.NotificationManager;
    32 import android.app.NotificationManager;
    33 import android.app.Service;
    33 import android.app.Service;
    34 import android.app.AlertDialog;
       
    35 import android.app.PendingIntent;
    34 import android.app.PendingIntent;
    36 import android.content.BroadcastReceiver;
    35 import android.content.BroadcastReceiver;
    37 import android.content.Context;
    36 import android.content.Context;
    38 import android.content.DialogInterface;
       
    39 import android.content.DialogInterface.OnClickListener;
       
    40 import android.content.Intent;
    37 import android.content.Intent;
    41 import android.content.IntentFilter;
    38 import android.content.IntentFilter;
    42 import android.net.Uri;
    39 import android.net.Uri;
    43 import android.util.Log;
    40 import android.util.Log;
    44 import android.os.Handler;
    41 import android.os.Handler;
    45 
    42 
    46 import java.io.File;
    43 import java.io.File;
    47 import java.security.cert.*;
    44 import java.security.cert.*;
    48 import java.security.KeyStore;
    45 import java.security.KeyStore;
    49 import java.security.KeyStoreException;
    46 import java.security.KeyStoreException;
    50 import java.util.concurrent.atomic.AtomicInteger;
    47 import java.security.MessageDigest;
    51 import java.util.HashMap;
    48 import java.util.HashMap;
    52 import javax.net.ssl.TrustManager;
    49 import javax.net.ssl.TrustManager;
    53 import javax.net.ssl.TrustManagerFactory;
    50 import javax.net.ssl.TrustManagerFactory;
    54 import javax.net.ssl.X509TrustManager;
    51 import javax.net.ssl.X509TrustManager;
    55 
    52 
    78 
    75 
    79 	static String KEYSTORE_DIR = "KeyStore";
    76 	static String KEYSTORE_DIR = "KeyStore";
    80 	static String KEYSTORE_FILE = "KeyStore.bks";
    77 	static String KEYSTORE_FILE = "KeyStore.bks";
    81 
    78 
    82 	Context master;
    79 	Context master;
       
    80 	Activity foregroundAct;
    83 	NotificationManager notificationManager;
    81 	NotificationManager notificationManager;
    84 	private static int decisionId = 0;
    82 	private static int decisionId = 0;
    85 	private static HashMap<Integer,MTMDecision> openDecisions = new HashMap();
    83 	private static HashMap<Integer, MTMDecision> openDecisions = new HashMap<Integer, MTMDecision>();
    86 
    84 
    87 	Handler masterHandler;
    85 	Handler masterHandler;
    88 	private File keyStoreFile;
    86 	private File keyStoreFile;
    89 	private KeyStore appKeyStore;
    87 	private KeyStore appKeyStore;
    90 	private X509TrustManager defaultTrustManager;
    88 	private X509TrustManager defaultTrustManager;
    91 	private X509TrustManager appTrustManager;
    89 	private X509TrustManager appTrustManager;
    92 
    90 
    93 	/** Creates an instance of the MemorizingTrustManager class.
    91 	/** Creates an instance of the MemorizingTrustManager class.
    94 	 *
    92 	 *
    95 	 * @param m Activity or Service to show the Dialog / Notification
    93 	 * You need to supply the application context. This has to be one of:
    96 	 */
    94 	 *    - Application
    97 	private MemorizingTrustManager(Context m) {
    95 	 *    - Activity
       
    96 	 *    - Service
       
    97 	 *
       
    98 	 * The context is used for file management, to display the dialog /
       
    99 	 * notification and for obtaining translated strings.
       
   100 	 *
       
   101 	 * @param m Context for the application.
       
   102 	 */
       
   103 	public MemorizingTrustManager(Context m) {
    98 		master = m;
   104 		master = m;
    99 		masterHandler = new Handler();
   105 		masterHandler = new Handler();
   100 		notificationManager = (NotificationManager)master.getSystemService(Context.NOTIFICATION_SERVICE);
   106 		notificationManager = (NotificationManager)master.getSystemService(Context.NOTIFICATION_SERVICE);
   101 
   107 
   102 		Application app;
   108 		Application app;
   103 		if (m instanceof Service) {
   109 		if (m instanceof Application) {
       
   110 			app = (Application)m;
       
   111 		} else if (m instanceof Service) {
   104 			app = ((Service)m).getApplication();
   112 			app = ((Service)m).getApplication();
   105 		} else if (m instanceof Activity) {
   113 		} else if (m instanceof Activity) {
   106 			app = ((Activity)m).getApplication();
   114 			app = ((Activity)m).getApplication();
   107 		} else throw new ClassCastException("MemorizingTrustManager context must be either Activity or Service!");
   115 		} else throw new ClassCastException("MemorizingTrustManager context must be either Activity or Service!");
   108 
   116 
   129 	 * </pre>
   137 	 * </pre>
   130 	 * @param c Activity or Service to show the Dialog / Notification
   138 	 * @param c Activity or Service to show the Dialog / Notification
   131 	 */
   139 	 */
   132 	public static X509TrustManager[] getInstanceList(Context c) {
   140 	public static X509TrustManager[] getInstanceList(Context c) {
   133 		return new X509TrustManager[] { new MemorizingTrustManager(c) };
   141 		return new X509TrustManager[] { new MemorizingTrustManager(c) };
       
   142 	}
       
   143 
       
   144 	/**
       
   145 	 * Binds an Activity to the MTM for displaying the query dialog.
       
   146 	 *
       
   147 	 * This is useful if your connection is run from a service that is
       
   148 	 * triggered by user interaction -- in such cases the activity is
       
   149 	 * visible and the user tends to ignore the service notification.
       
   150 	 *
       
   151 	 * You should never have a hidden activity bound to MTM! Use this
       
   152 	 * function in onResume() and @see unbindDisplayActivity in onPause().
       
   153 	 *
       
   154 	 * @param act Activity to be bound
       
   155 	 */
       
   156 	public void bindDisplayActivity(Activity act) {
       
   157 		foregroundAct = act;
       
   158 	}
       
   159 
       
   160 	/**
       
   161 	 * Removes an Activity from the MTM display stack.
       
   162 	 *
       
   163 	 * Always call this function when the Activity added with
       
   164 	 * @see bindDisplayActivity is hidden.
       
   165 	 *
       
   166 	 * @param act Activity to be unbound
       
   167 	 */
       
   168 	public void unbindDisplayActivity(Activity act) {
       
   169 		// do not remove if it was overridden by a different activity
       
   170 		if (foregroundAct == act)
       
   171 			foregroundAct = null;
   134 	}
   172 	}
   135 
   173 
   136 	/**
   174 	/**
   137 	 * Changes the path for the KeyStore file.
   175 	 * Changes the path for the KeyStore file.
   138 	 *
   176 	 *
   205 		} catch (Exception e) {
   243 		} catch (Exception e) {
   206 			Log.e(TAG, "storeCert(" + keyStoreFile + ")", e);
   244 			Log.e(TAG, "storeCert(" + keyStoreFile + ")", e);
   207 		}
   245 		}
   208 	}
   246 	}
   209 
   247 
       
   248 	// if the certificate is stored in the app key store, it is considered "known"
       
   249 	private boolean isCertKnown(X509Certificate cert) {
       
   250 		try {
       
   251 			return appKeyStore.getCertificateAlias(cert) != null;
       
   252 		} catch (KeyStoreException e) {
       
   253 			return false;
       
   254 		}
       
   255 	}
       
   256 
   210 	private boolean isExpiredException(Throwable e) {
   257 	private boolean isExpiredException(Throwable e) {
   211 		do {
   258 		do {
   212 			if (e instanceof CertificateExpiredException)
   259 			if (e instanceof CertificateExpiredException)
   213 				return true;
   260 				return true;
   214 			e = e.getCause();
   261 			e = e.getCause();
   231 			ae.printStackTrace();
   278 			ae.printStackTrace();
   232 			if (isExpiredException(ae)) {
   279 			if (isExpiredException(ae)) {
   233 				Log.i(TAG, "checkCertTrusted: accepting expired certificate from keystore");
   280 				Log.i(TAG, "checkCertTrusted: accepting expired certificate from keystore");
   234 				return;
   281 				return;
   235 			}
   282 			}
       
   283 			if (isCertKnown(chain[0])) {
       
   284 				Log.i(TAG, "checkCertTrusted: accepting cert already stored in keystore");
       
   285 				return;
       
   286 			}
   236 			try {
   287 			try {
   237 				Log.d(TAG, "checkCertTrusted: trying defaultTrustManager");
   288 				Log.d(TAG, "checkCertTrusted: trying defaultTrustManager");
   238 				if (isServer)
   289 				if (isServer)
   239 					defaultTrustManager.checkServerTrusted(chain, authType);
   290 					defaultTrustManager.checkServerTrusted(chain, authType);
   240 				else
   291 				else
   272 			decisionId += 1;
   323 			decisionId += 1;
   273 		}
   324 		}
   274 		return myId;
   325 		return myId;
   275 	}
   326 	}
   276 
   327 
       
   328 	private static String hexString(byte[] data) {
       
   329 		StringBuffer si = new StringBuffer();
       
   330 		for (int i = 0; i < data.length; i++) {
       
   331 			si.append(String.format("%02x", data[i]));
       
   332 			if (i < data.length - 1)
       
   333 				si.append(":");
       
   334 		}
       
   335 		return si.toString();
       
   336 	}
       
   337 
       
   338 	private static String certHash(final X509Certificate cert, String digest) {
       
   339 		try {
       
   340 			MessageDigest md = MessageDigest.getInstance(digest);
       
   341 			md.update(cert.getEncoded());
       
   342 			return hexString(md.digest());
       
   343 		} catch (java.security.cert.CertificateEncodingException e) {
       
   344 			return e.getMessage();
       
   345 		} catch (java.security.NoSuchAlgorithmException e) {
       
   346 			return e.getMessage();
       
   347 		}
       
   348 	}
       
   349 
   277 	private String certChainMessage(final X509Certificate[] chain, CertificateException cause) {
   350 	private String certChainMessage(final X509Certificate[] chain, CertificateException cause) {
   278 		Throwable e = cause;
   351 		Throwable e = cause;
   279 		Log.d(TAG, "certChainMessage for " + e);
   352 		Log.d(TAG, "certChainMessage for " + e);
   280 		StringBuffer si = new StringBuffer();
   353 		StringBuffer si = new StringBuffer();
   281 		if (e.getCause() != null) {
   354 		if (e.getCause() != null) {
   282 			e = e.getCause();
   355 			e = e.getCause();
   283 			si.append(e.getLocalizedMessage());
   356 			si.append(e.getLocalizedMessage());
   284 			si.append("\n");
   357 			//si.append("\n");
   285 		}
   358 		}
   286 		for (X509Certificate c : chain) {
   359 		for (X509Certificate c : chain) {
   287 			si.append("\n");
   360 			si.append("\n\n");
   288 			si.append(c.getSubjectDN().toString());
   361 			si.append(c.getSubjectDN().toString());
   289 			si.append(" (");
   362 			si.append("\nMD5: ");
       
   363 			si.append(certHash(c, "MD5"));
       
   364 			si.append("\nSHA1: ");
       
   365 			si.append(certHash(c, "SHA-1"));
       
   366 			si.append("\nSigned by: ");
   290 			si.append(c.getIssuerDN().toString());
   367 			si.append(c.getIssuerDN().toString());
   291 			si.append(")");
       
   292 		}
   368 		}
   293 		return si.toString();
   369 		return si.toString();
   294 	}
   370 	}
   295 
   371 
   296 	void startActivityNotification(PendingIntent intent, String certName) {
   372 	void startActivityNotification(PendingIntent intent, String certName) {
   303 		n.flags |= Notification.FLAG_AUTO_CANCEL;
   379 		n.flags |= Notification.FLAG_AUTO_CANCEL;
   304 
   380 
   305 		notificationManager.notify(NOTIFICATION_ID, n);
   381 		notificationManager.notify(NOTIFICATION_ID, n);
   306 	}
   382 	}
   307 
   383 
   308 	void launchServiceMode(Intent activityIntent, final String certMessage) {
   384 	/**
       
   385 	 * Returns the top-most entry of the activity stack.
       
   386 	 *
       
   387 	 * @return the Context of the currently bound UI or the master context if none is bound
       
   388 	 */
       
   389 	Context getUI() {
       
   390 		return (foregroundAct != null) ? foregroundAct : master;
       
   391 	}
       
   392 
       
   393 	BroadcastReceiver launchServiceMode(Intent activityIntent, final String certMessage) {
   309 		BroadcastReceiver launchNotifReceiver= new BroadcastReceiver() {
   394 		BroadcastReceiver launchNotifReceiver= new BroadcastReceiver() {
   310 		    public void onReceive(Context ctx, Intent i) {
   395 		    public void onReceive(Context ctx, Intent i) {
   311 			Log.i(TAG, "Interception not done by the application. Send notification");
   396 			Log.i(TAG, "Interception not done by the application. Send notification");
   312 			PendingIntent pi = i.getParcelableExtra(INTERCEPT_DECISION_INTENT_LAUNCH);
   397 			PendingIntent pi = i.getParcelableExtra(INTERCEPT_DECISION_INTENT_LAUNCH);
   313 			startActivityNotification(pi, certMessage);
   398 			startActivityNotification(pi, certMessage);
   316 		master.registerReceiver(launchNotifReceiver, new IntentFilter(INTERCEPT_DECISION_INTENT + "/" + master.getPackageName()));
   401 		master.registerReceiver(launchNotifReceiver, new IntentFilter(INTERCEPT_DECISION_INTENT + "/" + master.getPackageName()));
   317 		PendingIntent call = PendingIntent.getActivity(master, 0, activityIntent, 0);
   402 		PendingIntent call = PendingIntent.getActivity(master, 0, activityIntent, 0);
   318 		Intent ni = new Intent(INTERCEPT_DECISION_INTENT + "/" + master.getPackageName());
   403 		Intent ni = new Intent(INTERCEPT_DECISION_INTENT + "/" + master.getPackageName());
   319 		ni.putExtra(INTERCEPT_DECISION_INTENT_LAUNCH, call);
   404 		ni.putExtra(INTERCEPT_DECISION_INTENT_LAUNCH, call);
   320 		master.sendOrderedBroadcast(ni, null);
   405 		master.sendOrderedBroadcast(ni, null);
   321 
   406 		return launchNotifReceiver;
   322 	}
   407 	}
   323 
   408 
   324 	void interact(final X509Certificate[] chain, String authType, CertificateException cause)
   409 	void interact(final X509Certificate[] chain, String authType, CertificateException cause)
   325 		throws CertificateException
   410 		throws CertificateException
   326 	{
   411 	{
   327 		/* prepare the MTMDecision blocker object */
   412 		/* prepare the MTMDecision blocker object */
   328 		MTMDecision choice = new MTMDecision();
   413 		MTMDecision choice = new MTMDecision();
   329 		final int myId = createDecisionId(choice);
   414 		final int myId = createDecisionId(choice);
   330 		final String certTitle = chain[0].getSubjectDN().toString();
       
   331 		final String certMessage = certChainMessage(chain, cause);
   415 		final String certMessage = certChainMessage(chain, cause);
   332 
       
   333 		BroadcastReceiver decisionReceiver = new BroadcastReceiver() {
   416 		BroadcastReceiver decisionReceiver = new BroadcastReceiver() {
   334 			public void onReceive(Context ctx, Intent i) { interactResult(i); }
   417 			public void onReceive(Context ctx, Intent i) { interactResult(i); }
   335 		};
   418 		};
   336 		master.registerReceiver(decisionReceiver, new IntentFilter(DECISION_INTENT + "/" + master.getPackageName()));
   419 		master.registerReceiver(decisionReceiver, new IntentFilter(DECISION_INTENT + "/" + master.getPackageName()));
   337 		masterHandler.post(new Runnable() {
   420 		LaunchRunnable lr = new LaunchRunnable(myId, certMessage);
   338 			public void run() {
   421 		masterHandler.post(lr);
   339 				Intent ni = new Intent(master, MemorizingActivity.class);
       
   340 				ni.setData(Uri.parse(MemorizingTrustManager.class.getName() + "/" + myId));
       
   341 				ni.putExtra(DECISION_INTENT_APP, master.getPackageName());
       
   342 				ni.putExtra(DECISION_INTENT_ID, myId);
       
   343 				ni.putExtra(DECISION_INTENT_CERT, certMessage);
       
   344 
       
   345 				try {
       
   346 					master.startActivity(ni);
       
   347 				} catch (Exception e) {
       
   348 					Log.e(TAG, "startActivity: " + e);
       
   349 					launchServiceMode(ni, certMessage);
       
   350 				}
       
   351 			}
       
   352 		});
       
   353 
   422 
   354 		Log.d(TAG, "openDecisions: " + openDecisions);
   423 		Log.d(TAG, "openDecisions: " + openDecisions);
   355 		Log.d(TAG, "waiting on " + myId);
   424 		Log.d(TAG, "waiting on " + myId);
   356 		try {
   425 		try {
   357 			synchronized(choice) { choice.wait(); }
   426 			synchronized(choice) { choice.wait(); }
   358 		} catch (InterruptedException e) {
   427 		} catch (InterruptedException e) {
   359 			e.printStackTrace();
   428 			e.printStackTrace();
   360 		}
   429 		}
   361 		master.unregisterReceiver(decisionReceiver);
   430 		master.unregisterReceiver(decisionReceiver);
       
   431 		if (lr.launchNotifReceiver != null)
       
   432 			master.unregisterReceiver(lr.launchNotifReceiver);
   362 		Log.d(TAG, "finished wait on " + myId + ": " + choice.state);
   433 		Log.d(TAG, "finished wait on " + myId + ": " + choice.state);
   363 		switch (choice.state) {
   434 		switch (choice.state) {
   364 		case MTMDecision.DECISION_ALWAYS:
   435 		case MTMDecision.DECISION_ALWAYS:
   365 			storeCert(chain);
   436 			storeCert(chain);
   366 		case MTMDecision.DECISION_ONCE:
   437 		case MTMDecision.DECISION_ONCE:
   379 		MTMDecision d;
   450 		MTMDecision d;
   380 		synchronized(openDecisions) {
   451 		synchronized(openDecisions) {
   381 			 d = openDecisions.get(decisionId);
   452 			 d = openDecisions.get(decisionId);
   382 			 openDecisions.remove(decisionId);
   453 			 openDecisions.remove(decisionId);
   383 		}
   454 		}
       
   455 		if (d == null) {
       
   456 			Log.e(TAG, "interactResult: aborting due to stale decision reference!");
       
   457 			return;
       
   458 		}
   384 		synchronized(d) {
   459 		synchronized(d) {
   385 			d.state = choice;
   460 			d.state = choice;
   386 			d.notify();
   461 			d.notify();
   387 		}
   462 		}
   388 	}
   463 	}
   389 
   464 
       
   465 	private class LaunchRunnable implements Runnable {
       
   466 		private int myId;
       
   467 		private String certMessage;
       
   468 		BroadcastReceiver launchNotifReceiver;
       
   469 		
       
   470 		public LaunchRunnable(final int id, final String certMsg) {
       
   471 			myId = id;
       
   472 			certMessage = certMsg;
       
   473 		}
       
   474 		
       
   475 		public void run() {
       
   476 			Intent ni = new Intent(master, MemorizingActivity.class);
       
   477 			ni.setData(Uri.parse(MemorizingTrustManager.class.getName() + "/" + myId));
       
   478 			ni.putExtra(DECISION_INTENT_APP, master.getPackageName());
       
   479 			ni.putExtra(DECISION_INTENT_ID, myId);
       
   480 			ni.putExtra(DECISION_INTENT_CERT, certMessage);
       
   481 
       
   482 			// we try to directly start the activity and fall back to
       
   483 			// making a notification
       
   484 			try {
       
   485 				getUI().startActivity(ni);
       
   486 			} catch (Exception e) {
       
   487 				Log.e(TAG, "startActivity: " + e);
       
   488 				launchNotifReceiver = launchServiceMode(ni, certMessage);
       
   489 			}
       
   490 		}
       
   491 	}
       
   492 	
   390 }
   493 }