src/de/duenndns/ssl/MemorizingTrustManager.java
changeset 897 84d62c76469e
child 898 ff346f5bc36f
equal deleted inserted replaced
896:ed8eca7fad9a 897:84d62c76469e
       
     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 	final static String DECISION_INTENT = "de.duenndns.ssl.DECISION";
       
    71 	final static String DECISION_INTENT_APP    = DECISION_INTENT + ".app";
       
    72 	final static String DECISION_INTENT_ID     = DECISION_INTENT + ".decisionId";
       
    73 	final static String DECISION_INTENT_CERT   = DECISION_INTENT + ".cert";
       
    74 	final static String DECISION_INTENT_CHOICE = DECISION_INTENT + ".decisionChoice";
       
    75 	private final static int NOTIFICATION_ID = 100509;
       
    76 
       
    77 	static String KEYSTORE_DIR = "KeyStore";
       
    78 	static String KEYSTORE_FILE = "KeyStore.bks";
       
    79 
       
    80 	Context master;
       
    81 	NotificationManager notificationManager;
       
    82 	private static int decisionId = 0;
       
    83 	private static HashMap<Integer,MTMDecision> openDecisions = new HashMap();
       
    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 	 * @param m Activity or Service to show the Dialog / Notification
       
    94 	 */
       
    95 	private MemorizingTrustManager(Context m) {
       
    96 		master = m;
       
    97 		masterHandler = new Handler();
       
    98 		notificationManager = (NotificationManager)master.getSystemService(Context.NOTIFICATION_SERVICE);
       
    99 
       
   100 		Application app;
       
   101 		if (m instanceof Service) {
       
   102 			app = ((Service)m).getApplication();
       
   103 		} else if (m instanceof Activity) {
       
   104 			app = ((Activity)m).getApplication();
       
   105 		} else throw new ClassCastException("MemorizingTrustManager context must be either Activity or Service!");
       
   106 
       
   107 		File dir = app.getDir(KEYSTORE_DIR, Context.MODE_PRIVATE);
       
   108 		keyStoreFile = new File(dir + File.separator + KEYSTORE_FILE);
       
   109 
       
   110 		appKeyStore = loadAppKeyStore();
       
   111 		defaultTrustManager = getTrustManager(null);
       
   112 		appTrustManager = getTrustManager(appKeyStore);
       
   113 	}
       
   114 
       
   115 	/**
       
   116 	 * Returns a X509TrustManager list containing a new instance of
       
   117 	 * TrustManagerFactory.
       
   118 	 *
       
   119 	 * This function is meant for convenience only. You can use it
       
   120 	 * as follows to integrate TrustManagerFactory for HTTPS sockets:
       
   121 	 *
       
   122 	 * <pre>
       
   123 	 *     SSLContext sc = SSLContext.getInstance("TLS");
       
   124 	 *     sc.init(null, MemorizingTrustManager.getInstanceList(this),
       
   125 	 *         new java.security.SecureRandom());
       
   126 	 *     HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
       
   127 	 * </pre>
       
   128 	 * @param c Activity or Service to show the Dialog / Notification
       
   129 	 */
       
   130 	public static X509TrustManager[] getInstanceList(Context c) {
       
   131 		return new X509TrustManager[] { new MemorizingTrustManager(c) };
       
   132 	}
       
   133 
       
   134 	/**
       
   135 	 * Changes the path for the KeyStore file.
       
   136 	 *
       
   137 	 * The actual filename relative to the app's directory will be
       
   138 	 * <code>app_<i>dirname</i>/<i>filename</i></code>.
       
   139 	 *
       
   140 	 * @param dirname directory to store the KeyStore.
       
   141 	 * @param filename file name for the KeyStore.
       
   142 	 */
       
   143 	public static void setKeyStoreFile(String dirname, String filename) {
       
   144 		KEYSTORE_DIR = dirname;
       
   145 		KEYSTORE_FILE = filename;
       
   146 	}
       
   147 
       
   148 	X509TrustManager getTrustManager(KeyStore ks) {
       
   149 		try {
       
   150 			TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
       
   151 			tmf.init(ks);
       
   152 			for (TrustManager t : tmf.getTrustManagers()) {
       
   153 				if (t instanceof X509TrustManager) {
       
   154 					return (X509TrustManager)t;
       
   155 				}
       
   156 			}
       
   157 		} catch (Exception e) {
       
   158 			// Here, we are covering up errors. It might be more useful
       
   159 			// however to throw them out of the constructor so the
       
   160 			// embedding app knows something went wrong.
       
   161 			Log.e(TAG, "getTrustManager(" + ks + ")", e);
       
   162 		}
       
   163 		return null;
       
   164 	}
       
   165 
       
   166 	KeyStore loadAppKeyStore() {
       
   167 		KeyStore ks;
       
   168 		try {
       
   169 			ks = KeyStore.getInstance(KeyStore.getDefaultType());
       
   170 		} catch (KeyStoreException e) {
       
   171 			Log.e(TAG, "getAppKeyStore()", e);
       
   172 			return null;
       
   173 		}
       
   174 		try {
       
   175 			ks.load(null, null);
       
   176 			ks.load(new java.io.FileInputStream(keyStoreFile), "MTM".toCharArray());
       
   177 		} catch (java.io.FileNotFoundException e) {
       
   178 			Log.i(TAG, "getAppKeyStore(" + keyStoreFile + ") - file does not exist");
       
   179 		} catch (Exception e) {
       
   180 			Log.e(TAG, "getAppKeyStore(" + keyStoreFile + ")", e);
       
   181 		}
       
   182 		return ks;
       
   183 	}
       
   184 
       
   185 	void storeCert(X509Certificate[] chain) {
       
   186 		// add all certs from chain to appKeyStore
       
   187 		try {
       
   188 			for (X509Certificate c : chain)
       
   189 				appKeyStore.setCertificateEntry(c.getSubjectDN().toString(), c);
       
   190 		} catch (KeyStoreException e) {
       
   191 			Log.e(TAG, "storeCert(" + chain + ")", e);
       
   192 			return;
       
   193 		}
       
   194 		
       
   195 		// reload appTrustManager
       
   196 		appTrustManager = getTrustManager(appKeyStore);
       
   197 
       
   198 		// store KeyStore to file
       
   199 		try {
       
   200 			java.io.FileOutputStream fos = new java.io.FileOutputStream(keyStoreFile);
       
   201 			appKeyStore.store(fos, "MTM".toCharArray());
       
   202 			fos.close();
       
   203 		} catch (Exception e) {
       
   204 			Log.e(TAG, "storeCert(" + keyStoreFile + ")", e);
       
   205 		}
       
   206 	}
       
   207 
       
   208 	private boolean isExpiredException(Throwable e) {
       
   209 		do {
       
   210 			if (e instanceof CertificateExpiredException)
       
   211 				return true;
       
   212 			e = e.getCause();
       
   213 		} while (e != null);
       
   214 		return false;
       
   215 	}
       
   216 
       
   217 	public void checkCertTrusted(X509Certificate[] chain, String authType, boolean isServer)
       
   218 		throws CertificateException
       
   219 	{
       
   220 		Log.d(TAG, "checkCertTrusted(" + chain + ", " + authType + ", " + isServer + ")");
       
   221 		try {
       
   222 			Log.d(TAG, "checkCertTrusted: trying appTrustManager");
       
   223 			if (isServer)
       
   224 				appTrustManager.checkServerTrusted(chain, authType);
       
   225 			else
       
   226 				appTrustManager.checkClientTrusted(chain, authType);
       
   227 		} catch (CertificateException ae) {
       
   228 			// if the cert is stored in our appTrustManager, we ignore expiredness
       
   229 			ae.printStackTrace();
       
   230 			if (isExpiredException(ae)) {
       
   231 				Log.i(TAG, "checkCertTrusted: accepting expired certificate from keystore");
       
   232 				return;
       
   233 			}
       
   234 			try {
       
   235 				Log.d(TAG, "checkCertTrusted: trying defaultTrustManager");
       
   236 				if (isServer)
       
   237 					defaultTrustManager.checkServerTrusted(chain, authType);
       
   238 				else
       
   239 					defaultTrustManager.checkClientTrusted(chain, authType);
       
   240 			} catch (CertificateException e) {
       
   241 				e.printStackTrace();
       
   242 				interact(chain, authType, e);
       
   243 			}
       
   244 		}
       
   245 	}
       
   246 
       
   247 	public void checkClientTrusted(X509Certificate[] chain, String authType)
       
   248 		throws CertificateException
       
   249 	{
       
   250 		checkCertTrusted(chain, authType, false);
       
   251 	}
       
   252 
       
   253 	public void checkServerTrusted(X509Certificate[] chain, String authType)
       
   254 		throws CertificateException
       
   255 	{
       
   256 		checkCertTrusted(chain, authType, true);
       
   257 	}
       
   258 
       
   259 	public X509Certificate[] getAcceptedIssuers()
       
   260 	{
       
   261 		Log.d(TAG, "getAcceptedIssuers()");
       
   262 		return defaultTrustManager.getAcceptedIssuers();
       
   263 	}
       
   264 
       
   265 	private int createDecisionId(MTMDecision d) {
       
   266 		int myId;
       
   267 		synchronized(openDecisions) {
       
   268 			myId = decisionId;
       
   269 			openDecisions.put(myId, d);
       
   270 			decisionId += 1;
       
   271 		}
       
   272 		return myId;
       
   273 	}
       
   274 
       
   275 	private String certChainMessage(final X509Certificate[] chain, CertificateException cause) {
       
   276 		Throwable e = cause;
       
   277 		Log.d(TAG, "certChainMessage for " + e);
       
   278 		StringBuffer si = new StringBuffer();
       
   279 		if (e.getCause() != null) {
       
   280 			e = e.getCause();
       
   281 			si.append(e.getLocalizedMessage());
       
   282 			si.append("\n");
       
   283 		}
       
   284 		for (X509Certificate c : chain) {
       
   285 			si.append("\n");
       
   286 			si.append(c.getSubjectDN().toString());
       
   287 			si.append(" (");
       
   288 			si.append(c.getIssuerDN().toString());
       
   289 			si.append(")");
       
   290 		}
       
   291 		return si.toString();
       
   292 	}
       
   293 
       
   294 	void startActivityNotification(Intent intent, String certName) {
       
   295 		Notification n = new Notification(android.R.drawable.ic_lock_lock,
       
   296 				master.getString(R.string.mtm_notification),
       
   297 				System.currentTimeMillis());
       
   298 		PendingIntent call = PendingIntent.getActivity(master, 0, intent, 0);
       
   299 		n.setLatestEventInfo(master.getApplicationContext(),
       
   300 				master.getString(R.string.mtm_notification),
       
   301 				certName, call);
       
   302 		n.flags |= Notification.FLAG_AUTO_CANCEL;
       
   303 
       
   304 		notificationManager.notify(NOTIFICATION_ID, n);
       
   305 	}
       
   306 
       
   307 	void interact(final X509Certificate[] chain, String authType, CertificateException cause)
       
   308 		throws CertificateException
       
   309 	{
       
   310 		/* prepare the MTMDecision blocker object */
       
   311 		MTMDecision choice = new MTMDecision();
       
   312 		final int myId = createDecisionId(choice);
       
   313 		final String certTitle = chain[0].getSubjectDN().toString();
       
   314 		final String certMessage = certChainMessage(chain, cause);
       
   315 
       
   316 		BroadcastReceiver decisionReceiver = new BroadcastReceiver() {
       
   317 			public void onReceive(Context ctx, Intent i) { interactResult(i); }
       
   318 		};
       
   319 		master.registerReceiver(decisionReceiver, new IntentFilter(DECISION_INTENT + "/" + master.getPackageName()));
       
   320 		masterHandler.post(new Runnable() {
       
   321 			public void run() {
       
   322 				Intent ni = new Intent(master, MemorizingActivity.class);
       
   323 				ni.setData(Uri.parse(MemorizingTrustManager.class.getName() + "/" + myId));
       
   324 				ni.putExtra(DECISION_INTENT_APP, master.getPackageName());
       
   325 				ni.putExtra(DECISION_INTENT_ID, myId);
       
   326 				ni.putExtra(DECISION_INTENT_CERT, certMessage);
       
   327 
       
   328 				try {
       
   329 					master.startActivity(ni);
       
   330 				} catch (Exception e) {
       
   331 					Log.e(TAG, "startActivity: " + e);
       
   332 					startActivityNotification(ni, certMessage);
       
   333 				}
       
   334 			}
       
   335 		});
       
   336 
       
   337 		Log.d(TAG, "openDecisions: " + openDecisions);
       
   338 		Log.d(TAG, "waiting on " + myId);
       
   339 		try {
       
   340 			synchronized(choice) { choice.wait(); }
       
   341 		} catch (InterruptedException e) {
       
   342 			e.printStackTrace();
       
   343 		}
       
   344 		master.unregisterReceiver(decisionReceiver);
       
   345 		Log.d(TAG, "finished wait on " + myId + ": " + choice.state);
       
   346 		switch (choice.state) {
       
   347 		case MTMDecision.DECISION_ALWAYS:
       
   348 			storeCert(chain);
       
   349 		case MTMDecision.DECISION_ONCE:
       
   350 			break;
       
   351 		default:
       
   352 			throw (cause);
       
   353 		}
       
   354 	}
       
   355 
       
   356 	public static void interactResult(Intent i) {
       
   357 		int decisionId = i.getIntExtra(DECISION_INTENT_ID, MTMDecision.DECISION_INVALID);
       
   358 		int choice = i.getIntExtra(DECISION_INTENT_CHOICE, MTMDecision.DECISION_INVALID);
       
   359 		Log.d(TAG, "interactResult: " + decisionId + " chose " + choice);
       
   360 		Log.d(TAG, "openDecisions: " + openDecisions);
       
   361 
       
   362 		MTMDecision d;
       
   363 		synchronized(openDecisions) {
       
   364 			 d = openDecisions.get(decisionId);
       
   365 			 openDecisions.remove(decisionId);
       
   366 		}
       
   367 		synchronized(d) {
       
   368 			d.state = choice;
       
   369 			d.notify();
       
   370 		}
       
   371 	}
       
   372 
       
   373 }