app/src/main/java/de/duenndns/ssl/MemorizingTrustManager.java
changeset 1040 197a85a35cba
parent 975 d8305c375b10
equal deleted inserted replaced
1039:7d6f2526244a 1040:197a85a35cba
       
     1 /* MemorizingTrustManager - a TrustManager which asks the user about invalid
       
     2  *  certificates and memorizes their decision.
       
     3  *
       
     4  * Copyright (c) 2010 Georg Lukas <georg@op-co.de>
       
     5  *
       
     6  * MemorizingTrustManager.java contains the actual trust manager and interface
       
     7  * code to create a MemorizingActivity and obtain the results.
       
     8  *
       
     9  * Permission is hereby granted, free of charge, to any person obtaining a copy
       
    10  * of this software and associated documentation files (the "Software"), to deal
       
    11  * in the Software without restriction, including without limitation the rights
       
    12  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
       
    13  * copies of the Software, and to permit persons to whom the Software is
       
    14  * furnished to do so, subject to the following conditions:
       
    15  *
       
    16  * The above copyright notice and this permission notice shall be included in
       
    17  * all copies or substantial portions of the Software.
       
    18  *
       
    19  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
       
    20  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
       
    21  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
       
    22  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
       
    23  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
       
    24  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
       
    25  * THE SOFTWARE.
       
    26  */
       
    27 package de.duenndns.ssl;
       
    28 
       
    29 import android.app.Activity;
       
    30 import android.app.Application;
       
    31 import android.app.Notification;
       
    32 import android.app.NotificationManager;
       
    33 import android.app.Service;
       
    34 import android.app.PendingIntent;
       
    35 import android.content.BroadcastReceiver;
       
    36 import android.content.Context;
       
    37 import android.content.Intent;
       
    38 import android.content.IntentFilter;
       
    39 import android.net.Uri;
       
    40 import android.util.Log;
       
    41 import android.os.Handler;
       
    42 
       
    43 import java.io.File;
       
    44 import java.security.cert.*;
       
    45 import java.security.KeyStore;
       
    46 import java.security.KeyStoreException;
       
    47 import java.security.MessageDigest;
       
    48 import java.util.HashMap;
       
    49 import javax.net.ssl.TrustManager;
       
    50 import javax.net.ssl.TrustManagerFactory;
       
    51 import javax.net.ssl.X509TrustManager;
       
    52 
       
    53 import com.beem.project.beem.R;
       
    54 
       
    55 /**
       
    56  * A X509 trust manager implementation which asks the user about invalid
       
    57  * certificates and memorizes their decision.
       
    58  * <p>
       
    59  * The certificate validity is checked using the system default X509
       
    60  * TrustManager, creating a query Dialog if the check fails.
       
    61  * <p>
       
    62  * <b>WARNING:</b> This only works if a dedicated thread is used for
       
    63  * opening sockets!
       
    64  */
       
    65 public class MemorizingTrustManager implements X509TrustManager {
       
    66 	final static String TAG = "MemorizingTrustManager";
       
    67 	public final static String INTERCEPT_DECISION_INTENT = "de.duenndns.ssl.INTERCEPT_DECISION";
       
    68 	public final static String INTERCEPT_DECISION_INTENT_LAUNCH = INTERCEPT_DECISION_INTENT + ".launch_intent";
       
    69 	final static String DECISION_INTENT = "de.duenndns.ssl.DECISION";
       
    70 	final static String DECISION_INTENT_APP    = DECISION_INTENT + ".app";
       
    71 	final static String DECISION_INTENT_ID     = DECISION_INTENT + ".decisionId";
       
    72 	final static String DECISION_INTENT_CERT   = DECISION_INTENT + ".cert";
       
    73 	final static String DECISION_INTENT_CHOICE = DECISION_INTENT + ".decisionChoice";
       
    74 	private final static int NOTIFICATION_ID = 100509;
       
    75 
       
    76 	static String KEYSTORE_DIR = "KeyStore";
       
    77 	static String KEYSTORE_FILE = "KeyStore.bks";
       
    78 
       
    79 	Context master;
       
    80 	Activity foregroundAct;
       
    81 	NotificationManager notificationManager;
       
    82 	private static int decisionId = 0;
       
    83 	private static HashMap<Integer, MTMDecision> openDecisions = new HashMap<Integer, MTMDecision>();
       
    84 
       
    85 	Handler masterHandler;
       
    86 	private File keyStoreFile;
       
    87 	private KeyStore appKeyStore;
       
    88 	private X509TrustManager defaultTrustManager;
       
    89 	private X509TrustManager appTrustManager;
       
    90 
       
    91 	/** Creates an instance of the MemorizingTrustManager class.
       
    92 	 *
       
    93 	 * You need to supply the application context. This has to be one of:
       
    94 	 *    - Application
       
    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) {
       
   104 		master = m;
       
   105 		masterHandler = new Handler();
       
   106 		notificationManager = (NotificationManager)master.getSystemService(Context.NOTIFICATION_SERVICE);
       
   107 
       
   108 		Application app;
       
   109 		if (m instanceof Application) {
       
   110 			app = (Application)m;
       
   111 		} else if (m instanceof Service) {
       
   112 			app = ((Service)m).getApplication();
       
   113 		} else if (m instanceof Activity) {
       
   114 			app = ((Activity)m).getApplication();
       
   115 		} else throw new ClassCastException("MemorizingTrustManager context must be either Activity or Service!");
       
   116 
       
   117 		File dir = app.getDir(KEYSTORE_DIR, Context.MODE_PRIVATE);
       
   118 		keyStoreFile = new File(dir + File.separator + KEYSTORE_FILE);
       
   119 
       
   120 		appKeyStore = loadAppKeyStore();
       
   121 		defaultTrustManager = getTrustManager(null);
       
   122 		appTrustManager = getTrustManager(appKeyStore);
       
   123 	}
       
   124 
       
   125 	/**
       
   126 	 * Returns a X509TrustManager list containing a new instance of
       
   127 	 * TrustManagerFactory.
       
   128 	 *
       
   129 	 * This function is meant for convenience only. You can use it
       
   130 	 * as follows to integrate TrustManagerFactory for HTTPS sockets:
       
   131 	 *
       
   132 	 * <pre>
       
   133 	 *     SSLContext sc = SSLContext.getInstance("TLS");
       
   134 	 *     sc.init(null, MemorizingTrustManager.getInstanceList(this),
       
   135 	 *         new java.security.SecureRandom());
       
   136 	 *     HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
       
   137 	 * </pre>
       
   138 	 * @param c Activity or Service to show the Dialog / Notification
       
   139 	 */
       
   140 	public static X509TrustManager[] getInstanceList(Context 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;
       
   172 	}
       
   173 
       
   174 	/**
       
   175 	 * Changes the path for the KeyStore file.
       
   176 	 *
       
   177 	 * The actual filename relative to the app's directory will be
       
   178 	 * <code>app_<i>dirname</i>/<i>filename</i></code>.
       
   179 	 *
       
   180 	 * @param dirname directory to store the KeyStore.
       
   181 	 * @param filename file name for the KeyStore.
       
   182 	 */
       
   183 	public static void setKeyStoreFile(String dirname, String filename) {
       
   184 		KEYSTORE_DIR = dirname;
       
   185 		KEYSTORE_FILE = filename;
       
   186 	}
       
   187 
       
   188 	X509TrustManager getTrustManager(KeyStore ks) {
       
   189 		try {
       
   190 			TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
       
   191 			tmf.init(ks);
       
   192 			for (TrustManager t : tmf.getTrustManagers()) {
       
   193 				if (t instanceof X509TrustManager) {
       
   194 					return (X509TrustManager)t;
       
   195 				}
       
   196 			}
       
   197 		} catch (Exception e) {
       
   198 			// Here, we are covering up errors. It might be more useful
       
   199 			// however to throw them out of the constructor so the
       
   200 			// embedding app knows something went wrong.
       
   201 			Log.e(TAG, "getTrustManager(" + ks + ")", e);
       
   202 		}
       
   203 		return null;
       
   204 	}
       
   205 
       
   206 	KeyStore loadAppKeyStore() {
       
   207 		KeyStore ks;
       
   208 		try {
       
   209 			ks = KeyStore.getInstance(KeyStore.getDefaultType());
       
   210 		} catch (KeyStoreException e) {
       
   211 			Log.e(TAG, "getAppKeyStore()", e);
       
   212 			return null;
       
   213 		}
       
   214 		try {
       
   215 			ks.load(null, null);
       
   216 			ks.load(new java.io.FileInputStream(keyStoreFile), "MTM".toCharArray());
       
   217 		} catch (java.io.FileNotFoundException e) {
       
   218 			Log.i(TAG, "getAppKeyStore(" + keyStoreFile + ") - file does not exist");
       
   219 		} catch (Exception e) {
       
   220 			Log.e(TAG, "getAppKeyStore(" + keyStoreFile + ")", e);
       
   221 		}
       
   222 		return ks;
       
   223 	}
       
   224 
       
   225 	void storeCert(X509Certificate[] chain) {
       
   226 		// add all certs from chain to appKeyStore
       
   227 		try {
       
   228 			for (X509Certificate c : chain)
       
   229 				appKeyStore.setCertificateEntry(c.getSubjectDN().toString(), c);
       
   230 		} catch (KeyStoreException e) {
       
   231 			Log.e(TAG, "storeCert(" + chain + ")", e);
       
   232 			return;
       
   233 		}
       
   234 		
       
   235 		// reload appTrustManager
       
   236 		appTrustManager = getTrustManager(appKeyStore);
       
   237 
       
   238 		// store KeyStore to file
       
   239 		try {
       
   240 			java.io.FileOutputStream fos = new java.io.FileOutputStream(keyStoreFile);
       
   241 			appKeyStore.store(fos, "MTM".toCharArray());
       
   242 			fos.close();
       
   243 		} catch (Exception e) {
       
   244 			Log.e(TAG, "storeCert(" + keyStoreFile + ")", e);
       
   245 		}
       
   246 	}
       
   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 
       
   257 	private boolean isExpiredException(Throwable e) {
       
   258 		do {
       
   259 			if (e instanceof CertificateExpiredException)
       
   260 				return true;
       
   261 			e = e.getCause();
       
   262 		} while (e != null);
       
   263 		return false;
       
   264 	}
       
   265 
       
   266 	public void checkCertTrusted(X509Certificate[] chain, String authType, boolean isServer)
       
   267 		throws CertificateException
       
   268 	{
       
   269 		Log.d(TAG, "checkCertTrusted(" + chain + ", " + authType + ", " + isServer + ")");
       
   270 		try {
       
   271 			Log.d(TAG, "checkCertTrusted: trying appTrustManager");
       
   272 			if (isServer)
       
   273 				appTrustManager.checkServerTrusted(chain, authType);
       
   274 			else
       
   275 				appTrustManager.checkClientTrusted(chain, authType);
       
   276 		} catch (CertificateException ae) {
       
   277 			// if the cert is stored in our appTrustManager, we ignore expiredness
       
   278 			ae.printStackTrace();
       
   279 			if (isExpiredException(ae)) {
       
   280 				Log.i(TAG, "checkCertTrusted: accepting expired certificate from keystore");
       
   281 				return;
       
   282 			}
       
   283 			if (isCertKnown(chain[0])) {
       
   284 				Log.i(TAG, "checkCertTrusted: accepting cert already stored in keystore");
       
   285 				return;
       
   286 			}
       
   287 			try {
       
   288 				Log.d(TAG, "checkCertTrusted: trying defaultTrustManager");
       
   289 				if (isServer)
       
   290 					defaultTrustManager.checkServerTrusted(chain, authType);
       
   291 				else
       
   292 					defaultTrustManager.checkClientTrusted(chain, authType);
       
   293 			} catch (CertificateException e) {
       
   294 				e.printStackTrace();
       
   295 				interact(chain, authType, e);
       
   296 			}
       
   297 		}
       
   298 	}
       
   299 
       
   300 	public void checkClientTrusted(X509Certificate[] chain, String authType)
       
   301 		throws CertificateException
       
   302 	{
       
   303 		checkCertTrusted(chain, authType, false);
       
   304 	}
       
   305 
       
   306 	public void checkServerTrusted(X509Certificate[] chain, String authType)
       
   307 		throws CertificateException
       
   308 	{
       
   309 		checkCertTrusted(chain, authType, true);
       
   310 	}
       
   311 
       
   312 	public X509Certificate[] getAcceptedIssuers()
       
   313 	{
       
   314 		Log.d(TAG, "getAcceptedIssuers()");
       
   315 		return defaultTrustManager.getAcceptedIssuers();
       
   316 	}
       
   317 
       
   318 	private int createDecisionId(MTMDecision d) {
       
   319 		int myId;
       
   320 		synchronized(openDecisions) {
       
   321 			myId = decisionId;
       
   322 			openDecisions.put(myId, d);
       
   323 			decisionId += 1;
       
   324 		}
       
   325 		return myId;
       
   326 	}
       
   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 
       
   350 	private String certChainMessage(final X509Certificate[] chain, CertificateException cause) {
       
   351 		Throwable e = cause;
       
   352 		Log.d(TAG, "certChainMessage for " + e);
       
   353 		StringBuffer si = new StringBuffer();
       
   354 		if (e.getCause() != null) {
       
   355 			e = e.getCause();
       
   356 			si.append(e.getLocalizedMessage());
       
   357 			//si.append("\n");
       
   358 		}
       
   359 		for (X509Certificate c : chain) {
       
   360 			si.append("\n\n");
       
   361 			si.append(c.getSubjectDN().toString());
       
   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: ");
       
   367 			si.append(c.getIssuerDN().toString());
       
   368 		}
       
   369 		return si.toString();
       
   370 	}
       
   371 
       
   372 	void startActivityNotification(PendingIntent intent, String certName) {
       
   373 		Notification n = new Notification(android.R.drawable.ic_lock_lock,
       
   374 				master.getString(R.string.mtm_notification),
       
   375 				System.currentTimeMillis());
       
   376 		n.setLatestEventInfo(master.getApplicationContext(),
       
   377 				master.getString(R.string.mtm_notification),
       
   378 				certName, intent);
       
   379 		n.flags |= Notification.FLAG_AUTO_CANCEL;
       
   380 
       
   381 		notificationManager.notify(NOTIFICATION_ID, n);
       
   382 	}
       
   383 
       
   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) {
       
   394 		BroadcastReceiver launchNotifReceiver= new BroadcastReceiver() {
       
   395 		    public void onReceive(Context ctx, Intent i) {
       
   396 			Log.i(TAG, "Interception not done by the application. Send notification");
       
   397 			PendingIntent pi = i.getParcelableExtra(INTERCEPT_DECISION_INTENT_LAUNCH);
       
   398 			startActivityNotification(pi, certMessage);
       
   399 		    }
       
   400 		};
       
   401 		master.registerReceiver(launchNotifReceiver, new IntentFilter(INTERCEPT_DECISION_INTENT + "/" + master.getPackageName()));
       
   402 		PendingIntent call = PendingIntent.getActivity(master, 0, activityIntent, 0);
       
   403 		Intent ni = new Intent(INTERCEPT_DECISION_INTENT + "/" + master.getPackageName());
       
   404 		ni.putExtra(INTERCEPT_DECISION_INTENT_LAUNCH, call);
       
   405 		master.sendOrderedBroadcast(ni, null);
       
   406 		return launchNotifReceiver;
       
   407 	}
       
   408 
       
   409 	void interact(final X509Certificate[] chain, String authType, CertificateException cause)
       
   410 		throws CertificateException
       
   411 	{
       
   412 		/* prepare the MTMDecision blocker object */
       
   413 		MTMDecision choice = new MTMDecision();
       
   414 		final int myId = createDecisionId(choice);
       
   415 		final String certMessage = certChainMessage(chain, cause);
       
   416 		BroadcastReceiver decisionReceiver = new BroadcastReceiver() {
       
   417 			public void onReceive(Context ctx, Intent i) { interactResult(i); }
       
   418 		};
       
   419 		master.registerReceiver(decisionReceiver, new IntentFilter(DECISION_INTENT + "/" + master.getPackageName()));
       
   420 		LaunchRunnable lr = new LaunchRunnable(myId, certMessage);
       
   421 		masterHandler.post(lr);
       
   422 
       
   423 		Log.d(TAG, "openDecisions: " + openDecisions);
       
   424 		Log.d(TAG, "waiting on " + myId);
       
   425 		try {
       
   426 			synchronized(choice) { choice.wait(); }
       
   427 		} catch (InterruptedException e) {
       
   428 			e.printStackTrace();
       
   429 		}
       
   430 		master.unregisterReceiver(decisionReceiver);
       
   431 		if (lr.launchNotifReceiver != null)
       
   432 			master.unregisterReceiver(lr.launchNotifReceiver);
       
   433 		Log.d(TAG, "finished wait on " + myId + ": " + choice.state);
       
   434 		switch (choice.state) {
       
   435 		case MTMDecision.DECISION_ALWAYS:
       
   436 			storeCert(chain);
       
   437 		case MTMDecision.DECISION_ONCE:
       
   438 			break;
       
   439 		default:
       
   440 			throw (cause);
       
   441 		}
       
   442 	}
       
   443 
       
   444 	public static void interactResult(Intent i) {
       
   445 		int decisionId = i.getIntExtra(DECISION_INTENT_ID, MTMDecision.DECISION_INVALID);
       
   446 		int choice = i.getIntExtra(DECISION_INTENT_CHOICE, MTMDecision.DECISION_INVALID);
       
   447 		Log.d(TAG, "interactResult: " + decisionId + " chose " + choice);
       
   448 		Log.d(TAG, "openDecisions: " + openDecisions);
       
   449 
       
   450 		MTMDecision d;
       
   451 		synchronized(openDecisions) {
       
   452 			 d = openDecisions.get(decisionId);
       
   453 			 openDecisions.remove(decisionId);
       
   454 		}
       
   455 		if (d == null) {
       
   456 			Log.e(TAG, "interactResult: aborting due to stale decision reference!");
       
   457 			return;
       
   458 		}
       
   459 		synchronized(d) {
       
   460 			d.state = choice;
       
   461 			d.notify();
       
   462 		}
       
   463 	}
       
   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 	
       
   493 }