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