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