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