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