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