|
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 } |