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