MemorizingTrustManager.java

  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}