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 */
27package eu.siacs.conversations.services;
28
29import android.app.Application;
30import android.app.NotificationManager;
31import android.app.Service;
32import android.content.Context;
33import android.content.Intent;
34import android.content.SharedPreferences;
35import android.net.Uri;
36import android.os.Build;
37import android.os.Handler;
38import android.preference.PreferenceManager;
39import android.util.Base64;
40import android.util.Log;
41import android.util.SparseArray;
42import androidx.appcompat.app.AppCompatActivity;
43import com.google.common.base.Charsets;
44import com.google.common.base.Joiner;
45import com.google.common.base.Preconditions;
46import com.google.common.io.ByteStreams;
47import com.google.common.io.CharStreams;
48import eu.siacs.conversations.Config;
49import eu.siacs.conversations.R;
50import eu.siacs.conversations.crypto.TrustManagers;
51import eu.siacs.conversations.crypto.XmppDomainVerifier;
52import eu.siacs.conversations.entities.MTMDecision;
53import eu.siacs.conversations.http.HttpConnectionManager;
54import eu.siacs.conversations.persistance.FileBackend;
55import eu.siacs.conversations.ui.MemorizingActivity;
56import java.io.File;
57import java.io.FileInputStream;
58import java.io.FileOutputStream;
59import java.io.IOException;
60import java.io.InputStream;
61import java.io.InputStreamReader;
62import java.security.KeyStore;
63import java.security.KeyStoreException;
64import java.security.MessageDigest;
65import java.security.NoSuchAlgorithmException;
66import java.security.cert.Certificate;
67import java.security.cert.CertificateEncodingException;
68import java.security.cert.CertificateException;
69import java.security.cert.CertificateParsingException;
70import java.security.cert.X509Certificate;
71import java.text.SimpleDateFormat;
72import java.util.ArrayList;
73import java.util.Enumeration;
74import java.util.List;
75import java.util.Locale;
76import java.util.logging.Level;
77import java.util.logging.Logger;
78import java.util.regex.Pattern;
79import javax.net.ssl.TrustManager;
80import javax.net.ssl.TrustManagerFactory;
81import javax.net.ssl.X509TrustManager;
82import org.json.JSONArray;
83import org.json.JSONException;
84import org.json.JSONObject;
85
86/**
87 * A X509 trust manager implementation which asks the user about invalid certificates and memorizes
88 * their decision.
89 *
90 * <p>The certificate validity is checked using the system default X509 TrustManager, creating a
91 * query Dialog if the check fails.
92 *
93 * <p><b>WARNING:</b> This only works if a dedicated thread is used for opening sockets!
94 */
95public class MemorizingTrustManager {
96
97 private static final SimpleDateFormat DATE_FORMAT =
98 new SimpleDateFormat("yyyy-MM-dd", Locale.US);
99
100 static final String DECISION_INTENT = "de.duenndns.ssl.DECISION";
101 public static final String DECISION_INTENT_ID = DECISION_INTENT + ".decisionId";
102 public static final String DECISION_INTENT_CERT = DECISION_INTENT + ".cert";
103 public static final String DECISION_TITLE_ID = DECISION_INTENT + ".titleId";
104 static final String NO_TRUST_ANCHOR = "Trust anchor for certification path not found.";
105 private static final Pattern PATTERN_IPV4 =
106 Pattern.compile(
107 "\\A(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z");
108 private static final Pattern PATTERN_IPV6_HEX4DECCOMPRESSED =
109 Pattern.compile(
110 "\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)"
111 + " ::((?:[0-9A-Fa-f]{1,4}:)*)(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z");
112 private static final Pattern PATTERN_IPV6_6HEX4DEC =
113 Pattern.compile(
114 "\\A((?:[0-9A-Fa-f]{1,4}:){6,6})(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z");
115 private static final Pattern PATTERN_IPV6_HEXCOMPRESSED =
116 Pattern.compile(
117 "\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)\\z");
118 private static final Pattern PATTERN_IPV6 =
119 Pattern.compile("\\A(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\\z");
120 private static final Logger LOGGER = Logger.getLogger(MemorizingTrustManager.class.getName());
121 static String KEYSTORE_DIR = "KeyStore";
122 static String KEYSTORE_FILE = "KeyStore.bks";
123 private static int decisionId = 0;
124 private static final SparseArray<MTMDecision> openDecisions = new SparseArray<MTMDecision>();
125 Context master;
126 AppCompatActivity foregroundAct;
127 NotificationManager notificationManager;
128 Handler masterHandler;
129 private File keyStoreFile;
130 private KeyStore appKeyStore;
131 private final X509TrustManager defaultTrustManager;
132 private X509TrustManager appTrustManager;
133 private String poshCacheDir;
134
135 /**
136 * Creates an instance of the MemorizingTrustManager class that falls back to a custom
137 * TrustManager.
138 *
139 * <p>You need to supply the application context. This has to be one of: - Application -
140 * Activity - Service
141 *
142 * <p>The context is used for file management, to display the dialog / notification and for
143 * obtaining translated strings.
144 *
145 * @param context Context for the application.
146 * @param defaultTrustManager Delegate trust management to this TM. If null, the user must
147 * accept every certificate.
148 */
149 public MemorizingTrustManager(
150 final Context context, final X509TrustManager defaultTrustManager) {
151 init(context);
152 this.appTrustManager = getTrustManager(appKeyStore);
153 this.defaultTrustManager = defaultTrustManager;
154 }
155
156 /**
157 * Creates an instance of the MemorizingTrustManager class using the system X509TrustManager.
158 *
159 * <p>You need to supply the application context. This has to be one of: - Application -
160 * Activity - Service
161 *
162 * <p>The context is used for file management, to display the dialog / notification and for
163 * obtaining translated strings.
164 *
165 * @param context Context for the application.
166 */
167 public MemorizingTrustManager(final Context context) {
168 init(context);
169 this.appTrustManager = getTrustManager(appKeyStore);
170 try {
171 if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) {
172 this.defaultTrustManager =
173 TrustManagers.createDefaultWithBundledLetsEncrypt(context);
174 } else {
175 this.defaultTrustManager = TrustManagers.createDefaultTrustManager();
176 }
177 } catch (final NoSuchAlgorithmException
178 | KeyStoreException
179 | CertificateException
180 | IOException e) {
181 throw new RuntimeException(e);
182 }
183 }
184
185 private static boolean isIp(final String server) {
186 return server != null
187 && (PATTERN_IPV4.matcher(server).matches()
188 || PATTERN_IPV6.matcher(server).matches()
189 || PATTERN_IPV6_6HEX4DEC.matcher(server).matches()
190 || PATTERN_IPV6_HEX4DECCOMPRESSED.matcher(server).matches()
191 || PATTERN_IPV6_HEXCOMPRESSED.matcher(server).matches());
192 }
193
194 private static String getBase64Hash(X509Certificate certificate, String digest)
195 throws CertificateEncodingException {
196 MessageDigest md;
197 try {
198 md = MessageDigest.getInstance(digest);
199 } catch (NoSuchAlgorithmException e) {
200 return null;
201 }
202 md.update(certificate.getEncoded());
203 return Base64.encodeToString(md.digest(), Base64.NO_WRAP);
204 }
205
206 private static String hexString(byte[] data) {
207 StringBuffer si = new StringBuffer();
208 for (int i = 0; i < data.length; i++) {
209 si.append(String.format("%02x", data[i]));
210 if (i < data.length - 1) si.append(":");
211 }
212 return si.toString();
213 }
214
215 private static String certHash(final X509Certificate cert, String digest) {
216 try {
217 MessageDigest md = MessageDigest.getInstance(digest);
218 md.update(cert.getEncoded());
219 return hexString(md.digest());
220 } catch (CertificateEncodingException | NoSuchAlgorithmException e) {
221 return e.getMessage();
222 }
223 }
224
225 public static void interactResult(int decisionId, int choice) {
226 MTMDecision d;
227 synchronized (openDecisions) {
228 d = openDecisions.get(decisionId);
229 openDecisions.remove(decisionId);
230 }
231 if (d == null) {
232 LOGGER.log(Level.SEVERE, "interactResult: aborting due to stale decision reference!");
233 return;
234 }
235 synchronized (d) {
236 d.state = choice;
237 d.notify();
238 }
239 }
240
241 void init(final Context context) {
242 master = context;
243 masterHandler = new Handler(context.getMainLooper());
244 notificationManager =
245 (NotificationManager) master.getSystemService(Context.NOTIFICATION_SERVICE);
246
247 Application app;
248 if (context instanceof Application) {
249 app = (Application) context;
250 } else if (context instanceof Service) {
251 app = ((Service) context).getApplication();
252 } else if (context instanceof AppCompatActivity) {
253 app = ((AppCompatActivity) context).getApplication();
254 } else
255 throw new ClassCastException(
256 "MemorizingTrustManager context must be either Activity or Service!");
257
258 File dir = app.getDir(KEYSTORE_DIR, Context.MODE_PRIVATE);
259 keyStoreFile = new File(dir + File.separator + KEYSTORE_FILE);
260
261 poshCacheDir = app.getCacheDir().getAbsolutePath() + "/posh_cache/";
262
263 appKeyStore = loadAppKeyStore();
264 }
265
266 /**
267 * Get a list of all certificate aliases stored in MTM.
268 *
269 * @return an {@link Enumeration} of all certificates
270 */
271 public Enumeration<String> getCertificates() {
272 try {
273 return appKeyStore.aliases();
274 } catch (KeyStoreException e) {
275 // this should never happen, however...
276 throw new RuntimeException(e);
277 }
278 }
279
280 /**
281 * Removes the given certificate from MTMs key store.
282 *
283 * <p><b>WARNING</b>: this does not immediately invalidate the certificate. It is well possible
284 * that (a) data is transmitted over still existing connections or (b) new connections are
285 * created using TLS renegotiation, without a new cert check.
286 *
287 * @param alias the certificate's alias as returned by {@link #getCertificates()}.
288 * @throws KeyStoreException if the certificate could not be deleted.
289 */
290 public void deleteCertificate(String alias) throws KeyStoreException {
291 appKeyStore.deleteEntry(alias);
292 keyStoreUpdated();
293 }
294
295 private X509TrustManager getTrustManager(final KeyStore keyStore) {
296 Preconditions.checkNotNull(keyStore);
297 try {
298 TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
299 tmf.init(keyStore);
300 for (TrustManager t : tmf.getTrustManagers()) {
301 if (t instanceof X509TrustManager) {
302 return (X509TrustManager) t;
303 }
304 }
305 } catch (final Exception e) {
306 // Here, we are covering up errors. It might be more useful
307 // however to throw them out of the constructor so the
308 // embedding app knows something went wrong.
309 LOGGER.log(Level.SEVERE, "getTrustManager(" + keyStore + ")", e);
310 }
311 return null;
312 }
313
314 KeyStore loadAppKeyStore() {
315 KeyStore ks;
316 try {
317 ks = KeyStore.getInstance(KeyStore.getDefaultType());
318 } catch (KeyStoreException e) {
319 LOGGER.log(Level.SEVERE, "getAppKeyStore()", e);
320 return null;
321 }
322 FileInputStream fileInputStream = null;
323 try {
324 ks.load(null, null);
325 fileInputStream = new FileInputStream(keyStoreFile);
326 ks.load(fileInputStream, "MTM".toCharArray());
327 } catch (java.io.FileNotFoundException e) {
328 LOGGER.log(Level.INFO, "getAppKeyStore(" + keyStoreFile + ") - file does not exist");
329 } catch (Exception e) {
330 LOGGER.log(Level.SEVERE, "getAppKeyStore(" + keyStoreFile + ")", e);
331 } finally {
332 FileBackend.close(fileInputStream);
333 }
334 return ks;
335 }
336
337 void storeCert(String alias, Certificate cert) {
338 try {
339 appKeyStore.setCertificateEntry(alias, cert);
340 } catch (KeyStoreException e) {
341 LOGGER.log(Level.SEVERE, "storeCert(" + cert + ")", e);
342 return;
343 }
344 keyStoreUpdated();
345 }
346
347 void storeCert(X509Certificate cert) {
348 storeCert(cert.getSubjectDN().toString(), cert);
349 }
350
351 void keyStoreUpdated() {
352 // reload appTrustManager
353 appTrustManager = getTrustManager(appKeyStore);
354
355 // store KeyStore to file
356 java.io.FileOutputStream fos = null;
357 try {
358 fos = new java.io.FileOutputStream(keyStoreFile);
359 appKeyStore.store(fos, "MTM".toCharArray());
360 } catch (Exception e) {
361 LOGGER.log(Level.SEVERE, "storeCert(" + keyStoreFile + ")", e);
362 } finally {
363 if (fos != null) {
364 try {
365 fos.close();
366 } catch (IOException e) {
367 LOGGER.log(Level.SEVERE, "storeCert(" + keyStoreFile + ")", e);
368 }
369 }
370 }
371 }
372
373 // if the certificate is stored in the app key store, it is considered "known"
374 private boolean isCertKnown(X509Certificate cert) {
375 try {
376 return appKeyStore.getCertificateAlias(cert) != null;
377 } catch (KeyStoreException e) {
378 return false;
379 }
380 }
381
382 private void checkCertTrusted(
383 X509Certificate[] chain,
384 String authType,
385 String domain,
386 boolean isServer,
387 boolean interactive)
388 throws CertificateException {
389 LOGGER.log(
390 Level.FINE, "checkCertTrusted(" + chain + ", " + authType + ", " + isServer + ")");
391 try {
392 LOGGER.log(Level.FINE, "checkCertTrusted: trying appTrustManager");
393 if (isServer) appTrustManager.checkServerTrusted(chain, authType);
394 else appTrustManager.checkClientTrusted(chain, authType);
395 } catch (final CertificateException ae) {
396 LOGGER.log(Level.FINER, "checkCertTrusted: appTrustManager failed", ae);
397 if (isCertKnown(chain[0])) {
398 LOGGER.log(
399 Level.INFO, "checkCertTrusted: accepting cert already stored in keystore");
400 return;
401 }
402 try {
403 if (defaultTrustManager == null) throw ae;
404 LOGGER.log(Level.FINE, "checkCertTrusted: trying defaultTrustManager");
405 if (isServer) defaultTrustManager.checkServerTrusted(chain, authType);
406 else defaultTrustManager.checkClientTrusted(chain, authType);
407 } catch (final CertificateException e) {
408 final SharedPreferences preferences =
409 PreferenceManager.getDefaultSharedPreferences(master);
410 final boolean trustSystemCAs =
411 !preferences.getBoolean("dont_trust_system_cas", false);
412 if (domain != null
413 && isServer
414 && trustSystemCAs
415 && !isIp(domain)
416 && !domain.endsWith(".onion")) {
417 final String hash = getBase64Hash(chain[0], "SHA-256");
418 final List<String> fingerprints = getPoshFingerprints(domain);
419 if (hash != null && fingerprints.size() > 0) {
420 if (fingerprints.contains(hash)) {
421 Log.d(
422 Config.LOGTAG,
423 "trusted cert fingerprint of " + domain + " via posh");
424 return;
425 } else {
426 Log.d(
427 Config.LOGTAG,
428 "fingerprint " + hash + " not found in " + fingerprints);
429 }
430 if (getPoshCacheFile(domain).delete()) {
431 Log.d(
432 Config.LOGTAG,
433 "deleted posh file for "
434 + domain
435 + " after not being able to verify");
436 }
437 }
438 }
439 if (interactive) {
440 interactCert(chain, authType, e);
441 } else {
442 throw e;
443 }
444 }
445 }
446 }
447
448 private List<String> getPoshFingerprints(final String domain) {
449 final List<String> cached = getPoshFingerprintsFromCache(domain);
450 if (cached == null) {
451 return getPoshFingerprintsFromServer(domain);
452 } else {
453 return cached;
454 }
455 }
456
457 private List<String> getPoshFingerprintsFromServer(String domain) {
458 return getPoshFingerprintsFromServer(
459 domain, "https://" + domain + "/.well-known/posh/xmpp-client.json", -1, true);
460 }
461
462 private List<String> getPoshFingerprintsFromServer(
463 String domain, String url, int maxTtl, boolean followUrl) {
464 Log.d(Config.LOGTAG, "downloading json for " + domain + " from " + url);
465 final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(master);
466 final boolean useTor =
467 QuickConversationsService.isConversations()
468 && preferences.getBoolean(
469 "use_tor", master.getResources().getBoolean(R.bool.use_tor));
470 try {
471 final List<String> results = new ArrayList<>();
472 final InputStream inputStream = HttpConnectionManager.open(url, useTor);
473 final String body =
474 CharStreams.toString(
475 new InputStreamReader(
476 ByteStreams.limit(inputStream, 10_000), Charsets.UTF_8));
477 final JSONObject jsonObject = new JSONObject(body);
478 int expires = jsonObject.getInt("expires");
479 if (expires <= 0) {
480 return new ArrayList<>();
481 }
482 if (maxTtl >= 0) {
483 expires = Math.min(maxTtl, expires);
484 }
485 String redirect;
486 try {
487 redirect = jsonObject.getString("url");
488 } catch (JSONException e) {
489 redirect = null;
490 }
491 if (followUrl && redirect != null && redirect.toLowerCase().startsWith("https")) {
492 return getPoshFingerprintsFromServer(domain, redirect, expires, false);
493 }
494 final JSONArray fingerprints = jsonObject.getJSONArray("fingerprints");
495 for (int i = 0; i < fingerprints.length(); i++) {
496 final JSONObject fingerprint = fingerprints.getJSONObject(i);
497 final String sha256 = fingerprint.getString("sha-256");
498 results.add(sha256);
499 }
500 writeFingerprintsToCache(domain, results, 1000L * expires + System.currentTimeMillis());
501 return results;
502 } catch (final Exception e) {
503 Log.d(Config.LOGTAG, "error fetching posh", e);
504 return new ArrayList<>();
505 }
506 }
507
508 private File getPoshCacheFile(String domain) {
509 return new File(poshCacheDir + domain + ".json");
510 }
511
512 private void writeFingerprintsToCache(String domain, List<String> results, long expires) {
513 final File file = getPoshCacheFile(domain);
514 file.getParentFile().mkdirs();
515 try {
516 file.createNewFile();
517 JSONObject jsonObject = new JSONObject();
518 jsonObject.put("expires", expires);
519 jsonObject.put("fingerprints", new JSONArray(results));
520 FileOutputStream outputStream = new FileOutputStream(file);
521 outputStream.write(jsonObject.toString().getBytes());
522 outputStream.flush();
523 outputStream.close();
524 } catch (Exception e) {
525 e.printStackTrace();
526 }
527 }
528
529 private List<String> getPoshFingerprintsFromCache(String domain) {
530 final File file = getPoshCacheFile(domain);
531 try {
532 final InputStream inputStream = new FileInputStream(file);
533 final String json =
534 CharStreams.toString(new InputStreamReader(inputStream, Charsets.UTF_8));
535 final JSONObject jsonObject = new JSONObject(json);
536 long expires = jsonObject.getLong("expires");
537 long expiresIn = expires - System.currentTimeMillis();
538 if (expiresIn < 0) {
539 file.delete();
540 return null;
541 } else {
542 Log.d(Config.LOGTAG, "posh fingerprints expire in " + (expiresIn / 1000) + "s");
543 }
544 final List<String> result = new ArrayList<>();
545 final JSONArray jsonArray = jsonObject.getJSONArray("fingerprints");
546 for (int i = 0; i < jsonArray.length(); ++i) {
547 result.add(jsonArray.getString(i));
548 }
549 return result;
550 } catch (final IOException e) {
551 return null;
552 } catch (JSONException e) {
553 file.delete();
554 return null;
555 }
556 }
557
558 private X509Certificate[] getAcceptedIssuers() {
559 return defaultTrustManager == null
560 ? new X509Certificate[0]
561 : defaultTrustManager.getAcceptedIssuers();
562 }
563
564 private int createDecisionId(MTMDecision d) {
565 int myId;
566 synchronized (openDecisions) {
567 myId = decisionId;
568 openDecisions.put(myId, d);
569 decisionId += 1;
570 }
571 return myId;
572 }
573
574 private void certDetails(
575 final StringBuffer si, final X509Certificate c, final boolean showValidFor) {
576
577 si.append("\n");
578 if (showValidFor) {
579 try {
580 si.append("Valid for: ");
581 si.append(Joiner.on(", ").join(XmppDomainVerifier.parseValidDomains(c).all()));
582 } catch (final CertificateParsingException e) {
583 si.append("Unable to parse Certificate");
584 }
585 si.append("\n");
586 } else {
587 si.append(c.getSubjectDN());
588 }
589 si.append("\n");
590 si.append(DATE_FORMAT.format(c.getNotBefore()));
591 si.append(" - ");
592 si.append(DATE_FORMAT.format(c.getNotAfter()));
593 si.append("\nSHA-256: ");
594 si.append(certHash(c, "SHA-256"));
595 si.append("\nSHA-1: ");
596 si.append(certHash(c, "SHA-1"));
597 si.append("\nSigned by: ");
598 si.append(c.getIssuerDN().toString());
599 si.append("\n");
600 }
601
602 private String certChainMessage(final X509Certificate[] chain, CertificateException cause) {
603 Throwable e = cause;
604 LOGGER.log(Level.FINE, "certChainMessage for " + e);
605 final StringBuffer si = new StringBuffer();
606 if (e.getCause() != null) {
607 e = e.getCause();
608 // HACK: there is no sane way to check if the error is a "trust anchor
609 // not found", so we use string comparison.
610 if (NO_TRUST_ANCHOR.equals(e.getMessage())) {
611 si.append(master.getString(R.string.mtm_trust_anchor));
612 } else si.append(e.getLocalizedMessage());
613 si.append("\n");
614 }
615 si.append("\n");
616 si.append(master.getString(R.string.mtm_connect_anyway));
617 si.append("\n\n");
618 si.append(master.getString(R.string.mtm_cert_details));
619 si.append('\n');
620 for (int i = 0; i < chain.length; ++i) {
621 certDetails(si, chain[i], i == 0);
622 }
623 return si.toString();
624 }
625
626 /**
627 * Returns the top-most entry of the activity stack.
628 *
629 * @return the Context of the currently bound UI or the master context if none is bound
630 */
631 Context getUI() {
632 return (foregroundAct != null) ? foregroundAct : master;
633 }
634
635 int interact(final String message, final int titleId) {
636 /* prepare the MTMDecision blocker object */
637 MTMDecision choice = new MTMDecision();
638 final int myId = createDecisionId(choice);
639
640 masterHandler.post(
641 new Runnable() {
642 public void run() {
643 Intent ni = new Intent(master, MemorizingActivity.class);
644 ni.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
645 ni.setData(Uri.parse(MemorizingTrustManager.class.getName() + "/" + myId));
646 ni.putExtra(DECISION_INTENT_ID, myId);
647 ni.putExtra(DECISION_INTENT_CERT, message);
648 ni.putExtra(DECISION_TITLE_ID, titleId);
649
650 // we try to directly start the activity and fall back to
651 // making a notification
652 try {
653 getUI().startActivity(ni);
654 } catch (Exception e) {
655 LOGGER.log(Level.FINE, "startActivity(MemorizingActivity)", e);
656 }
657 }
658 });
659
660 LOGGER.log(Level.FINE, "openDecisions: " + openDecisions + ", waiting on " + myId);
661 try {
662 synchronized (choice) {
663 choice.wait();
664 }
665 } catch (InterruptedException e) {
666 LOGGER.log(Level.FINER, "InterruptedException", e);
667 }
668 LOGGER.log(Level.FINE, "finished wait on " + myId + ": " + choice.state);
669 return choice.state;
670 }
671
672 void interactCert(final X509Certificate[] chain, String authType, CertificateException cause)
673 throws CertificateException {
674 switch (interact(certChainMessage(chain, cause), R.string.mtm_accept_cert)) {
675 case MTMDecision.DECISION_ALWAYS:
676 storeCert(chain[0]); // only store the server cert, not the whole chain
677 case MTMDecision.DECISION_ONCE:
678 break;
679 default:
680 throw (cause);
681 }
682 }
683
684 public X509TrustManager getNonInteractive(String domain) {
685 return new NonInteractiveMemorizingTrustManager(domain);
686 }
687
688 public X509TrustManager getInteractive(String domain) {
689 return new InteractiveMemorizingTrustManager(domain);
690 }
691
692 public X509TrustManager getNonInteractive() {
693 return new NonInteractiveMemorizingTrustManager(null);
694 }
695
696 public X509TrustManager getInteractive() {
697 return new InteractiveMemorizingTrustManager(null);
698 }
699
700 private class NonInteractiveMemorizingTrustManager implements X509TrustManager {
701
702 private final String domain;
703
704 public NonInteractiveMemorizingTrustManager(String domain) {
705 this.domain = domain;
706 }
707
708 @Override
709 public void checkClientTrusted(X509Certificate[] chain, String authType)
710 throws CertificateException {
711 MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, false, false);
712 }
713
714 @Override
715 public void checkServerTrusted(X509Certificate[] chain, String authType)
716 throws CertificateException {
717 MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, true, false);
718 }
719
720 @Override
721 public X509Certificate[] getAcceptedIssuers() {
722 return MemorizingTrustManager.this.getAcceptedIssuers();
723 }
724 }
725
726 private class InteractiveMemorizingTrustManager implements X509TrustManager {
727 private final String domain;
728
729 public InteractiveMemorizingTrustManager(String domain) {
730 this.domain = domain;
731 }
732
733 @Override
734 public void checkClientTrusted(X509Certificate[] chain, String authType)
735 throws CertificateException {
736 MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, false, true);
737 }
738
739 @Override
740 public void checkServerTrusted(X509Certificate[] chain, String authType)
741 throws CertificateException {
742 MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, true, true);
743 }
744
745 @Override
746 public X509Certificate[] getAcceptedIssuers() {
747 return MemorizingTrustManager.this.getAcceptedIssuers();
748 }
749 }
750}