src/de/duenndns/ssl/MemorizingTrustManager.java
changeset 988 d7ddcccdff8a
parent 920 ff346f5bc36f
child 998 d8305c375b10
equal deleted inserted replaced
868:6c2c4bfa43d0 988:d7ddcccdff8a
       
     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.AlertDialog;
       
    35 import android.app.PendingIntent;
       
    36 import android.content.BroadcastReceiver;
       
    37 import android.content.Context;
       
    38 import android.content.DialogInterface;
       
    39 import android.content.DialogInterface.OnClickListener;
       
    40 import android.content.Intent;
       
    41 import android.content.IntentFilter;
       
    42 import android.net.Uri;
       
    43 import android.util.Log;
       
    44 import android.os.Handler;
       
    45 
       
    46 import java.io.File;
       
    47 import java.security.cert.*;
       
    48 import java.security.KeyStore;
       
    49 import java.security.KeyStoreException;
       
    50 import java.util.concurrent.atomic.AtomicInteger;
       
    51 import java.util.HashMap;
       
    52 import javax.net.ssl.TrustManager;
       
    53 import javax.net.ssl.TrustManagerFactory;
       
    54 import javax.net.ssl.X509TrustManager;
       
    55 
       
    56 import com.beem.project.beem.R;
       
    57 
       
    58 /**
       
    59  * A X509 trust manager implementation which asks the user about invalid
       
    60  * certificates and memorizes their decision.
       
    61  * <p>
       
    62  * The certificate validity is checked using the system default X509
       
    63  * TrustManager, creating a query Dialog if the check fails.
       
    64  * <p>
       
    65  * <b>WARNING:</b> This only works if a dedicated thread is used for
       
    66  * opening sockets!
       
    67  */
       
    68 public class MemorizingTrustManager implements X509TrustManager {
       
    69 	final static String TAG = "MemorizingTrustManager";
       
    70 	public final static String INTERCEPT_DECISION_INTENT = "de.duenndns.ssl.INTERCEPT_DECISION";
       
    71 	public final static String INTERCEPT_DECISION_INTENT_LAUNCH = INTERCEPT_DECISION_INTENT + ".launch_intent";
       
    72 	final static String DECISION_INTENT = "de.duenndns.ssl.DECISION";
       
    73 	final static String DECISION_INTENT_APP    = DECISION_INTENT + ".app";
       
    74 	final static String DECISION_INTENT_ID     = DECISION_INTENT + ".decisionId";
       
    75 	final static String DECISION_INTENT_CERT   = DECISION_INTENT + ".cert";
       
    76 	final static String DECISION_INTENT_CHOICE = DECISION_INTENT + ".decisionChoice";
       
    77 	private final static int NOTIFICATION_ID = 100509;
       
    78 
       
    79 	static String KEYSTORE_DIR = "KeyStore";
       
    80 	static String KEYSTORE_FILE = "KeyStore.bks";
       
    81 
       
    82 	Context master;
       
    83 	NotificationManager notificationManager;
       
    84 	private static int decisionId = 0;
       
    85 	private static HashMap<Integer,MTMDecision> openDecisions = new HashMap();
       
    86 
       
    87 	Handler masterHandler;
       
    88 	private File keyStoreFile;
       
    89 	private KeyStore appKeyStore;
       
    90 	private X509TrustManager defaultTrustManager;
       
    91 	private X509TrustManager appTrustManager;
       
    92 
       
    93 	/** Creates an instance of the MemorizingTrustManager class.
       
    94 	 *
       
    95 	 * @param m Activity or Service to show the Dialog / Notification
       
    96 	 */
       
    97 	private MemorizingTrustManager(Context m) {
       
    98 		master = m;
       
    99 		masterHandler = new Handler();
       
   100 		notificationManager = (NotificationManager)master.getSystemService(Context.NOTIFICATION_SERVICE);
       
   101 
       
   102 		Application app;
       
   103 		if (m instanceof Service) {
       
   104 			app = ((Service)m).getApplication();
       
   105 		} else if (m instanceof Activity) {
       
   106 			app = ((Activity)m).getApplication();
       
   107 		} else throw new ClassCastException("MemorizingTrustManager context must be either Activity or Service!");
       
   108 
       
   109 		File dir = app.getDir(KEYSTORE_DIR, Context.MODE_PRIVATE);
       
   110 		keyStoreFile = new File(dir + File.separator + KEYSTORE_FILE);
       
   111 
       
   112 		appKeyStore = loadAppKeyStore();
       
   113 		defaultTrustManager = getTrustManager(null);
       
   114 		appTrustManager = getTrustManager(appKeyStore);
       
   115 	}
       
   116 
       
   117 	/**
       
   118 	 * Returns a X509TrustManager list containing a new instance of
       
   119 	 * TrustManagerFactory.
       
   120 	 *
       
   121 	 * This function is meant for convenience only. You can use it
       
   122 	 * as follows to integrate TrustManagerFactory for HTTPS sockets:
       
   123 	 *
       
   124 	 * <pre>
       
   125 	 *     SSLContext sc = SSLContext.getInstance("TLS");
       
   126 	 *     sc.init(null, MemorizingTrustManager.getInstanceList(this),
       
   127 	 *         new java.security.SecureRandom());
       
   128 	 *     HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
       
   129 	 * </pre>
       
   130 	 * @param c Activity or Service to show the Dialog / Notification
       
   131 	 */
       
   132 	public static X509TrustManager[] getInstanceList(Context c) {
       
   133 		return new X509TrustManager[] { new MemorizingTrustManager(c) };
       
   134 	}
       
   135 
       
   136 	/**
       
   137 	 * Changes the path for the KeyStore file.
       
   138 	 *
       
   139 	 * The actual filename relative to the app's directory will be
       
   140 	 * <code>app_<i>dirname</i>/<i>filename</i></code>.
       
   141 	 *
       
   142 	 * @param dirname directory to store the KeyStore.
       
   143 	 * @param filename file name for the KeyStore.
       
   144 	 */
       
   145 	public static void setKeyStoreFile(String dirname, String filename) {
       
   146 		KEYSTORE_DIR = dirname;
       
   147 		KEYSTORE_FILE = filename;
       
   148 	}
       
   149 
       
   150 	X509TrustManager getTrustManager(KeyStore ks) {
       
   151 		try {
       
   152 			TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
       
   153 			tmf.init(ks);
       
   154 			for (TrustManager t : tmf.getTrustManagers()) {
       
   155 				if (t instanceof X509TrustManager) {
       
   156 					return (X509TrustManager)t;
       
   157 				}
       
   158 			}
       
   159 		} catch (Exception e) {
       
   160 			// Here, we are covering up errors. It might be more useful
       
   161 			// however to throw them out of the constructor so the
       
   162 			// embedding app knows something went wrong.
       
   163 			Log.e(TAG, "getTrustManager(" + ks + ")", e);
       
   164 		}
       
   165 		return null;
       
   166 	}
       
   167 
       
   168 	KeyStore loadAppKeyStore() {
       
   169 		KeyStore ks;
       
   170 		try {
       
   171 			ks = KeyStore.getInstance(KeyStore.getDefaultType());
       
   172 		} catch (KeyStoreException e) {
       
   173 			Log.e(TAG, "getAppKeyStore()", e);
       
   174 			return null;
       
   175 		}
       
   176 		try {
       
   177 			ks.load(null, null);
       
   178 			ks.load(new java.io.FileInputStream(keyStoreFile), "MTM".toCharArray());
       
   179 		} catch (java.io.FileNotFoundException e) {
       
   180 			Log.i(TAG, "getAppKeyStore(" + keyStoreFile + ") - file does not exist");
       
   181 		} catch (Exception e) {
       
   182 			Log.e(TAG, "getAppKeyStore(" + keyStoreFile + ")", e);
       
   183 		}
       
   184 		return ks;
       
   185 	}
       
   186 
       
   187 	void storeCert(X509Certificate[] chain) {
       
   188 		// add all certs from chain to appKeyStore
       
   189 		try {
       
   190 			for (X509Certificate c : chain)
       
   191 				appKeyStore.setCertificateEntry(c.getSubjectDN().toString(), c);
       
   192 		} catch (KeyStoreException e) {
       
   193 			Log.e(TAG, "storeCert(" + chain + ")", e);
       
   194 			return;
       
   195 		}
       
   196 		
       
   197 		// reload appTrustManager
       
   198 		appTrustManager = getTrustManager(appKeyStore);
       
   199 
       
   200 		// store KeyStore to file
       
   201 		try {
       
   202 			java.io.FileOutputStream fos = new java.io.FileOutputStream(keyStoreFile);
       
   203 			appKeyStore.store(fos, "MTM".toCharArray());
       
   204 			fos.close();
       
   205 		} catch (Exception e) {
       
   206 			Log.e(TAG, "storeCert(" + keyStoreFile + ")", e);
       
   207 		}
       
   208 	}
       
   209 
       
   210 	private boolean isExpiredException(Throwable e) {
       
   211 		do {
       
   212 			if (e instanceof CertificateExpiredException)
       
   213 				return true;
       
   214 			e = e.getCause();
       
   215 		} while (e != null);
       
   216 		return false;
       
   217 	}
       
   218 
       
   219 	public void checkCertTrusted(X509Certificate[] chain, String authType, boolean isServer)
       
   220 		throws CertificateException
       
   221 	{
       
   222 		Log.d(TAG, "checkCertTrusted(" + chain + ", " + authType + ", " + isServer + ")");
       
   223 		try {
       
   224 			Log.d(TAG, "checkCertTrusted: trying appTrustManager");
       
   225 			if (isServer)
       
   226 				appTrustManager.checkServerTrusted(chain, authType);
       
   227 			else
       
   228 				appTrustManager.checkClientTrusted(chain, authType);
       
   229 		} catch (CertificateException ae) {
       
   230 			// if the cert is stored in our appTrustManager, we ignore expiredness
       
   231 			ae.printStackTrace();
       
   232 			if (isExpiredException(ae)) {
       
   233 				Log.i(TAG, "checkCertTrusted: accepting expired certificate from keystore");
       
   234 				return;
       
   235 			}
       
   236 			try {
       
   237 				Log.d(TAG, "checkCertTrusted: trying defaultTrustManager");
       
   238 				if (isServer)
       
   239 					defaultTrustManager.checkServerTrusted(chain, authType);
       
   240 				else
       
   241 					defaultTrustManager.checkClientTrusted(chain, authType);
       
   242 			} catch (CertificateException e) {
       
   243 				e.printStackTrace();
       
   244 				interact(chain, authType, e);
       
   245 			}
       
   246 		}
       
   247 	}
       
   248 
       
   249 	public void checkClientTrusted(X509Certificate[] chain, String authType)
       
   250 		throws CertificateException
       
   251 	{
       
   252 		checkCertTrusted(chain, authType, false);
       
   253 	}
       
   254 
       
   255 	public void checkServerTrusted(X509Certificate[] chain, String authType)
       
   256 		throws CertificateException
       
   257 	{
       
   258 		checkCertTrusted(chain, authType, true);
       
   259 	}
       
   260 
       
   261 	public X509Certificate[] getAcceptedIssuers()
       
   262 	{
       
   263 		Log.d(TAG, "getAcceptedIssuers()");
       
   264 		return defaultTrustManager.getAcceptedIssuers();
       
   265 	}
       
   266 
       
   267 	private int createDecisionId(MTMDecision d) {
       
   268 		int myId;
       
   269 		synchronized(openDecisions) {
       
   270 			myId = decisionId;
       
   271 			openDecisions.put(myId, d);
       
   272 			decisionId += 1;
       
   273 		}
       
   274 		return myId;
       
   275 	}
       
   276 
       
   277 	private String certChainMessage(final X509Certificate[] chain, CertificateException cause) {
       
   278 		Throwable e = cause;
       
   279 		Log.d(TAG, "certChainMessage for " + e);
       
   280 		StringBuffer si = new StringBuffer();
       
   281 		if (e.getCause() != null) {
       
   282 			e = e.getCause();
       
   283 			si.append(e.getLocalizedMessage());
       
   284 			si.append("\n");
       
   285 		}
       
   286 		for (X509Certificate c : chain) {
       
   287 			si.append("\n");
       
   288 			si.append(c.getSubjectDN().toString());
       
   289 			si.append(" (");
       
   290 			si.append(c.getIssuerDN().toString());
       
   291 			si.append(")");
       
   292 		}
       
   293 		return si.toString();
       
   294 	}
       
   295 
       
   296 	void startActivityNotification(PendingIntent intent, String certName) {
       
   297 		Notification n = new Notification(android.R.drawable.ic_lock_lock,
       
   298 				master.getString(R.string.mtm_notification),
       
   299 				System.currentTimeMillis());
       
   300 		n.setLatestEventInfo(master.getApplicationContext(),
       
   301 				master.getString(R.string.mtm_notification),
       
   302 				certName, intent);
       
   303 		n.flags |= Notification.FLAG_AUTO_CANCEL;
       
   304 
       
   305 		notificationManager.notify(NOTIFICATION_ID, n);
       
   306 	}
       
   307 
       
   308 	void launchServiceMode(Intent activityIntent, final String certMessage) {
       
   309 		BroadcastReceiver launchNotifReceiver= new BroadcastReceiver() {
       
   310 		    public void onReceive(Context ctx, Intent i) {
       
   311 			Log.i(TAG, "Interception not done by the application. Send notification");
       
   312 			PendingIntent pi = i.getParcelableExtra(INTERCEPT_DECISION_INTENT_LAUNCH);
       
   313 			startActivityNotification(pi, certMessage);
       
   314 		    }
       
   315 		};
       
   316 		master.registerReceiver(launchNotifReceiver, new IntentFilter(INTERCEPT_DECISION_INTENT + "/" + master.getPackageName()));
       
   317 		PendingIntent call = PendingIntent.getActivity(master, 0, activityIntent, 0);
       
   318 		Intent ni = new Intent(INTERCEPT_DECISION_INTENT + "/" + master.getPackageName());
       
   319 		ni.putExtra(INTERCEPT_DECISION_INTENT_LAUNCH, call);
       
   320 		master.sendOrderedBroadcast(ni, null);
       
   321 
       
   322 	}
       
   323 
       
   324 	void interact(final X509Certificate[] chain, String authType, CertificateException cause)
       
   325 		throws CertificateException
       
   326 	{
       
   327 		/* prepare the MTMDecision blocker object */
       
   328 		MTMDecision choice = new MTMDecision();
       
   329 		final int myId = createDecisionId(choice);
       
   330 		final String certTitle = chain[0].getSubjectDN().toString();
       
   331 		final String certMessage = certChainMessage(chain, cause);
       
   332 
       
   333 		BroadcastReceiver decisionReceiver = new BroadcastReceiver() {
       
   334 			public void onReceive(Context ctx, Intent i) { interactResult(i); }
       
   335 		};
       
   336 		master.registerReceiver(decisionReceiver, new IntentFilter(DECISION_INTENT + "/" + master.getPackageName()));
       
   337 		masterHandler.post(new Runnable() {
       
   338 			public void run() {
       
   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 
       
   354 		Log.d(TAG, "openDecisions: " + openDecisions);
       
   355 		Log.d(TAG, "waiting on " + myId);
       
   356 		try {
       
   357 			synchronized(choice) { choice.wait(); }
       
   358 		} catch (InterruptedException e) {
       
   359 			e.printStackTrace();
       
   360 		}
       
   361 		master.unregisterReceiver(decisionReceiver);
       
   362 		Log.d(TAG, "finished wait on " + myId + ": " + choice.state);
       
   363 		switch (choice.state) {
       
   364 		case MTMDecision.DECISION_ALWAYS:
       
   365 			storeCert(chain);
       
   366 		case MTMDecision.DECISION_ONCE:
       
   367 			break;
       
   368 		default:
       
   369 			throw (cause);
       
   370 		}
       
   371 	}
       
   372 
       
   373 	public static void interactResult(Intent i) {
       
   374 		int decisionId = i.getIntExtra(DECISION_INTENT_ID, MTMDecision.DECISION_INVALID);
       
   375 		int choice = i.getIntExtra(DECISION_INTENT_CHOICE, MTMDecision.DECISION_INVALID);
       
   376 		Log.d(TAG, "interactResult: " + decisionId + " chose " + choice);
       
   377 		Log.d(TAG, "openDecisions: " + openDecisions);
       
   378 
       
   379 		MTMDecision d;
       
   380 		synchronized(openDecisions) {
       
   381 			 d = openDecisions.get(decisionId);
       
   382 			 openDecisions.remove(decisionId);
       
   383 		}
       
   384 		synchronized(d) {
       
   385 			d.state = choice;
       
   386 			d.notify();
       
   387 		}
       
   388 	}
       
   389 
       
   390 }