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