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