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 de.duenndns.ssl;
 28
 29import android.app.Activity;
 30import android.app.Application;
 31import android.app.Notification;
 32import android.app.NotificationManager;
 33import android.app.Service;
 34import android.app.PendingIntent;
 35import android.content.Context;
 36import android.content.Intent;
 37import android.net.Uri;
 38import android.util.SparseArray;
 39import android.os.Handler;
 40
 41import java.io.File;
 42import java.io.IOException;
 43import java.security.cert.*;
 44import java.security.KeyStore;
 45import java.security.KeyStoreException;
 46import java.security.MessageDigest;
 47import java.util.logging.Level;
 48import java.util.logging.Logger;
 49import java.text.SimpleDateFormat;
 50import java.util.Collection;
 51import java.util.Enumeration;
 52import java.util.List;
 53import java.util.Locale;
 54
 55import javax.net.ssl.HostnameVerifier;
 56import javax.net.ssl.SSLSession;
 57import javax.net.ssl.TrustManager;
 58import javax.net.ssl.TrustManagerFactory;
 59import javax.net.ssl.X509TrustManager;
 60
 61/**
 62 * A X509 trust manager implementation which asks the user about invalid
 63 * certificates and memorizes their decision.
 64 * <p>
 65 * The certificate validity is checked using the system default X509
 66 * TrustManager, creating a query Dialog if the check fails.
 67 * <p>
 68 * <b>WARNING:</b> This only works if a dedicated thread is used for
 69 * opening sockets!
 70 */
 71public class MemorizingTrustManager implements X509TrustManager {
 72	final static String DECISION_INTENT = "de.duenndns.ssl.DECISION";
 73	final static String DECISION_INTENT_ID     = DECISION_INTENT + ".decisionId";
 74	final static String DECISION_INTENT_CERT   = DECISION_INTENT + ".cert";
 75	final static String DECISION_INTENT_CHOICE = DECISION_INTENT + ".decisionChoice";
 76
 77	private final static Logger LOGGER = Logger.getLogger(MemorizingTrustManager.class.getName());
 78	final static String DECISION_TITLE_ID      = DECISION_INTENT + ".titleId";
 79	private final static int NOTIFICATION_ID = 100509;
 80
 81	final static String NO_TRUST_ANCHOR = "Trust anchor for certification path not found.";
 82	
 83	static String KEYSTORE_DIR = "KeyStore";
 84	static String KEYSTORE_FILE = "KeyStore.bks";
 85
 86	Context master;
 87	Activity foregroundAct;
 88	NotificationManager notificationManager;
 89	private static int decisionId = 0;
 90	private static SparseArray<MTMDecision> openDecisions = new SparseArray<MTMDecision>();
 91
 92	Handler masterHandler;
 93	private File keyStoreFile;
 94	private KeyStore appKeyStore;
 95	private X509TrustManager defaultTrustManager;
 96	private X509TrustManager appTrustManager;
 97
 98	/** Creates an instance of the MemorizingTrustManager class that falls back to a custom TrustManager.
 99	 *
100	 * You need to supply the application context. This has to be one of:
101	 *    - Application
102	 *    - Activity
103	 *    - Service
104	 *
105	 * The context is used for file management, to display the dialog /
106	 * notification and for obtaining translated strings.
107	 *
108	 * @param m Context for the application.
109	 * @param defaultTrustManager Delegate trust management to this TM. If null, the user must accept every certificate.
110	 */
111	public MemorizingTrustManager(Context m, X509TrustManager defaultTrustManager) {
112		init(m);
113		this.appTrustManager = getTrustManager(appKeyStore);
114		this.defaultTrustManager = defaultTrustManager;
115	}
116
117	/** Creates an instance of the MemorizingTrustManager class using the system X509TrustManager.
118	 *
119	 * You need to supply the application context. This has to be one of:
120	 *    - Application
121	 *    - Activity
122	 *    - Service
123	 *
124	 * The context is used for file management, to display the dialog /
125	 * notification and for obtaining translated strings.
126	 *
127	 * @param m Context for the application.
128	 */
129	public MemorizingTrustManager(Context m) {
130		init(m);
131		this.appTrustManager = getTrustManager(appKeyStore);
132		this.defaultTrustManager = getTrustManager(null);
133	}
134
135	void init(Context m) {
136		master = m;
137		masterHandler = new Handler(m.getMainLooper());
138		notificationManager = (NotificationManager)master.getSystemService(Context.NOTIFICATION_SERVICE);
139
140		Application app;
141		if (m instanceof Application) {
142			app = (Application)m;
143		} else if (m instanceof Service) {
144			app = ((Service)m).getApplication();
145		} else if (m instanceof Activity) {
146			app = ((Activity)m).getApplication();
147		} else throw new ClassCastException("MemorizingTrustManager context must be either Activity or Service!");
148
149		File dir = app.getDir(KEYSTORE_DIR, Context.MODE_PRIVATE);
150		keyStoreFile = new File(dir + File.separator + KEYSTORE_FILE);
151
152		appKeyStore = loadAppKeyStore();
153	}
154
155	
156	/**
157	 * Returns a X509TrustManager list containing a new instance of
158	 * TrustManagerFactory.
159	 *
160	 * This function is meant for convenience only. You can use it
161	 * as follows to integrate TrustManagerFactory for HTTPS sockets:
162	 *
163	 * <pre>
164	 *     SSLContext sc = SSLContext.getInstance("TLS");
165	 *     sc.init(null, MemorizingTrustManager.getInstanceList(this),
166	 *         new java.security.SecureRandom());
167	 *     HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
168	 * </pre>
169	 * @param c Activity or Service to show the Dialog / Notification
170	 */
171	public static X509TrustManager[] getInstanceList(Context c) {
172		return new X509TrustManager[] { new MemorizingTrustManager(c) };
173	}
174
175	/**
176	 * Binds an Activity to the MTM for displaying the query dialog.
177	 *
178	 * This is useful if your connection is run from a service that is
179	 * triggered by user interaction -- in such cases the activity is
180	 * visible and the user tends to ignore the service notification.
181	 *
182	 * You should never have a hidden activity bound to MTM! Use this
183	 * function in onResume() and @see unbindDisplayActivity in onPause().
184	 *
185	 * @param act Activity to be bound
186	 */
187	public void bindDisplayActivity(Activity act) {
188		foregroundAct = act;
189	}
190
191	/**
192	 * Removes an Activity from the MTM display stack.
193	 *
194	 * Always call this function when the Activity added with
195	 * {@link #bindDisplayActivity(Activity)} is hidden.
196	 *
197	 * @param act Activity to be unbound
198	 */
199	public void unbindDisplayActivity(Activity act) {
200		// do not remove if it was overridden by a different activity
201		if (foregroundAct == act)
202			foregroundAct = null;
203	}
204
205	/**
206	 * Changes the path for the KeyStore file.
207	 *
208	 * The actual filename relative to the app's directory will be
209	 * <code>app_<i>dirname</i>/<i>filename</i></code>.
210	 *
211	 * @param dirname directory to store the KeyStore.
212	 * @param filename file name for the KeyStore.
213	 */
214	public static void setKeyStoreFile(String dirname, String filename) {
215		KEYSTORE_DIR = dirname;
216		KEYSTORE_FILE = filename;
217	}
218
219	/**
220	 * Get a list of all certificate aliases stored in MTM.
221	 *
222	 * @return an {@link Enumeration} of all certificates
223	 */
224	public Enumeration<String> getCertificates() {
225		try {
226			return appKeyStore.aliases();
227		} catch (KeyStoreException e) {
228			// this should never happen, however...
229			throw new RuntimeException(e);
230		}
231	}
232
233	/**
234	 * Get a certificate for a given alias.
235	 *
236	 * @param alias the certificate's alias as returned by {@link #getCertificates()}.
237	 *
238	 * @return the certificate associated with the alias or <tt>null</tt> if none found.
239	 */
240	public Certificate getCertificate(String alias) {
241		try {
242			return appKeyStore.getCertificate(alias);
243		} catch (KeyStoreException e) {
244			// this should never happen, however...
245			throw new RuntimeException(e);
246		}
247	}
248
249	/**
250	 * Removes the given certificate from MTMs key store.
251	 *
252	 * <p>
253	 * <b>WARNING</b>: this does not immediately invalidate the certificate. It is
254	 * well possible that (a) data is transmitted over still existing connections or
255	 * (b) new connections are created using TLS renegotiation, without a new cert
256	 * check.
257	 * </p>
258	 * @param alias the certificate's alias as returned by {@link #getCertificates()}.
259	 *
260	 * @throws KeyStoreException if the certificate could not be deleted.
261	 */
262	public void deleteCertificate(String alias) throws KeyStoreException {
263		appKeyStore.deleteEntry(alias);
264		keyStoreUpdated();
265	}
266
267	/**
268	 * Creates a new hostname verifier supporting user interaction.
269	 *
270	 * <p>This method creates a new {@link HostnameVerifier} that is bound to
271	 * the given instance of {@link MemorizingTrustManager}, and leverages an
272	 * existing {@link HostnameVerifier}. The returned verifier performs the
273	 * following steps, returning as soon as one of them succeeds:
274	 *  </p>
275	 *  <ol>
276	 *  <li>Success, if the wrapped defaultVerifier accepts the certificate.</li>
277	 *  <li>Success, if the server certificate is stored in the keystore under the given hostname.</li>
278	 *  <li>Ask the user and return accordingly.</li>
279	 *  <li>Failure on exception.</li>
280	 *  </ol>
281	 *
282	 * @param defaultVerifier the {@link HostnameVerifier} that should perform the actual check
283	 * @return a new hostname verifier using the MTM's key store
284	 *
285	 * @throws IllegalArgumentException if the defaultVerifier parameter is null
286	 */
287	public HostnameVerifier wrapHostnameVerifier(final HostnameVerifier defaultVerifier) {
288		if (defaultVerifier == null)
289			throw new IllegalArgumentException("The default verifier may not be null");
290		
291		return new MemorizingHostnameVerifier(defaultVerifier);
292	}
293	
294	public HostnameVerifier wrapHostnameVerifierNonInteractive(final HostnameVerifier defaultVerifier) {
295		if (defaultVerifier == null)
296			throw new IllegalArgumentException("The default verifier may not be null");
297		
298		return new NonInteractiveMemorizingHostnameVerifier(defaultVerifier);
299	}
300	
301	X509TrustManager getTrustManager(KeyStore ks) {
302		try {
303			TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
304			tmf.init(ks);
305			for (TrustManager t : tmf.getTrustManagers()) {
306				if (t instanceof X509TrustManager) {
307					return (X509TrustManager)t;
308				}
309			}
310		} catch (Exception e) {
311			// Here, we are covering up errors. It might be more useful
312			// however to throw them out of the constructor so the
313			// embedding app knows something went wrong.
314			LOGGER.log(Level.SEVERE, "getTrustManager(" + ks + ")", e);
315		}
316		return null;
317	}
318
319	KeyStore loadAppKeyStore() {
320		KeyStore ks;
321		try {
322			ks = KeyStore.getInstance(KeyStore.getDefaultType());
323		} catch (KeyStoreException e) {
324			LOGGER.log(Level.SEVERE, "getAppKeyStore()", e);
325			return null;
326		}
327		try {
328			ks.load(null, null);
329			ks.load(new java.io.FileInputStream(keyStoreFile), "MTM".toCharArray());
330		} catch (java.io.FileNotFoundException e) {
331			LOGGER.log(Level.INFO, "getAppKeyStore(" + keyStoreFile + ") - file does not exist");
332		} catch (Exception e) {
333			LOGGER.log(Level.SEVERE, "getAppKeyStore(" + keyStoreFile + ")", e);
334		}
335		return ks;
336	}
337
338	void storeCert(String alias, Certificate cert) {
339		try {
340			appKeyStore.setCertificateEntry(alias, cert);
341		} catch (KeyStoreException e) {
342			LOGGER.log(Level.SEVERE, "storeCert(" + cert + ")", e);
343			return;
344		}		
345		keyStoreUpdated();
346	}
347	
348	void storeCert(X509Certificate cert) {
349		storeCert(cert.getSubjectDN().toString(), cert);
350	}
351
352	void keyStoreUpdated() {
353		// reload appTrustManager
354		appTrustManager = getTrustManager(appKeyStore);
355
356		// store KeyStore to file
357		java.io.FileOutputStream fos = null;
358		try {
359			fos = new java.io.FileOutputStream(keyStoreFile);
360			appKeyStore.store(fos, "MTM".toCharArray());
361		} catch (Exception e) {
362			LOGGER.log(Level.SEVERE, "storeCert(" + keyStoreFile + ")", e);
363		} finally {
364			if (fos != null) {
365				try {
366					fos.close();
367				} catch (IOException e) {
368					LOGGER.log(Level.SEVERE, "storeCert(" + keyStoreFile + ")", e);
369				}
370			}
371		}
372	}
373
374	// if the certificate is stored in the app key store, it is considered "known"
375	private boolean isCertKnown(X509Certificate cert) {
376		try {
377			return appKeyStore.getCertificateAlias(cert) != null;
378		} catch (KeyStoreException e) {
379			return false;
380		}
381	}
382
383	private boolean isExpiredException(Throwable e) {
384		do {
385			if (e instanceof CertificateExpiredException)
386				return true;
387			e = e.getCause();
388		} while (e != null);
389		return false;
390	}
391
392	public void checkCertTrusted(X509Certificate[] chain, String authType, boolean isServer, boolean interactive)
393		throws CertificateException
394	{
395		LOGGER.log(Level.FINE, "checkCertTrusted(" + chain + ", " + authType + ", " + isServer + ")");
396		try {
397			LOGGER.log(Level.FINE, "checkCertTrusted: trying appTrustManager");
398			if (isServer)
399				appTrustManager.checkServerTrusted(chain, authType);
400			else
401				appTrustManager.checkClientTrusted(chain, authType);
402		} catch (CertificateException ae) {
403			LOGGER.log(Level.FINER, "checkCertTrusted: appTrustManager failed", ae);
404			// if the cert is stored in our appTrustManager, we ignore expiredness
405			if (isExpiredException(ae)) {
406				LOGGER.log(Level.INFO, "checkCertTrusted: accepting expired certificate from keystore");
407				return;
408			}
409			if (isCertKnown(chain[0])) {
410				LOGGER.log(Level.INFO, "checkCertTrusted: accepting cert already stored in keystore");
411				return;
412			}
413			try {
414				if (defaultTrustManager == null)
415					throw ae;
416				LOGGER.log(Level.FINE, "checkCertTrusted: trying defaultTrustManager");
417				if (isServer)
418					defaultTrustManager.checkServerTrusted(chain, authType);
419				else
420					defaultTrustManager.checkClientTrusted(chain, authType);
421			} catch (CertificateException e) {
422				e.printStackTrace();
423				if (interactive) {
424					interactCert(chain, authType, e);
425				} else {
426					throw e;
427				}
428			}
429		}
430	}
431
432	public void checkClientTrusted(X509Certificate[] chain, String authType)
433		throws CertificateException
434	{
435		checkCertTrusted(chain, authType, false,true);
436	}
437
438	public void checkServerTrusted(X509Certificate[] chain, String authType)
439		throws CertificateException
440	{
441		checkCertTrusted(chain, authType, true,true);
442	}
443
444	public X509Certificate[] getAcceptedIssuers()
445	{
446		LOGGER.log(Level.FINE, "getAcceptedIssuers()");
447		return 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 static String hexString(byte[] data) {
461		StringBuffer si = new StringBuffer();
462		for (int i = 0; i < data.length; i++) {
463			si.append(String.format("%02x", data[i]));
464			if (i < data.length - 1)
465				si.append(":");
466		}
467		return si.toString();
468	}
469
470	private static String certHash(final X509Certificate cert, String digest) {
471		try {
472			MessageDigest md = MessageDigest.getInstance(digest);
473			md.update(cert.getEncoded());
474			return hexString(md.digest());
475		} catch (java.security.cert.CertificateEncodingException e) {
476			return e.getMessage();
477		} catch (java.security.NoSuchAlgorithmException e) {
478			return e.getMessage();
479		}
480	}
481
482	private void certDetails(StringBuffer si, X509Certificate c) {
483		SimpleDateFormat validityDateFormater = new SimpleDateFormat("yyyy-MM-dd");
484		si.append("\n");
485		si.append(c.getSubjectDN().toString());
486		si.append("\n");
487		si.append(validityDateFormater.format(c.getNotBefore()));
488		si.append(" - ");
489		si.append(validityDateFormater.format(c.getNotAfter()));
490		si.append("\nSHA-256: ");
491		si.append(certHash(c, "SHA-256"));
492		si.append("\nSHA-1: ");
493		si.append(certHash(c, "SHA-1"));
494		si.append("\nSigned by: ");
495		si.append(c.getIssuerDN().toString());
496		si.append("\n");
497	}
498	
499	private String certChainMessage(final X509Certificate[] chain, CertificateException cause) {
500		Throwable e = cause;
501		LOGGER.log(Level.FINE, "certChainMessage for " + e);
502		StringBuffer si = new StringBuffer();
503		if (e.getCause() != null) {
504			e = e.getCause();
505			// HACK: there is no sane way to check if the error is a "trust anchor
506			// not found", so we use string comparison.
507			if (NO_TRUST_ANCHOR.equals(e.getMessage())) {
508				si.append(master.getString(R.string.mtm_trust_anchor));
509			} else
510				si.append(e.getLocalizedMessage());
511			si.append("\n");
512		}
513		si.append("\n");
514		si.append(master.getString(R.string.mtm_connect_anyway));
515		si.append("\n\n");
516		si.append(master.getString(R.string.mtm_cert_details));
517		for (X509Certificate c : chain) {
518			certDetails(si, c);
519		}
520		return si.toString();
521	}
522
523	private String hostNameMessage(X509Certificate cert, String hostname) {
524		StringBuffer si = new StringBuffer();
525
526		si.append(master.getString(R.string.mtm_hostname_mismatch, hostname));
527		si.append("\n\n");
528		try {
529			Collection<List<?>> sans = cert.getSubjectAlternativeNames();
530			if (sans == null) {
531				si.append(cert.getSubjectDN());
532				si.append("\n");
533			} else for (List<?> altName : sans) {
534				Object name = altName.get(1);
535				if (name instanceof String) {
536					si.append("[");
537					si.append((Integer)altName.get(0));
538					si.append("] ");
539					si.append(name);
540					si.append("\n");
541				}
542			}
543		} catch (CertificateParsingException e) {
544			e.printStackTrace();
545			si.append("<Parsing error: ");
546			si.append(e.getLocalizedMessage());
547			si.append(">\n");
548		}
549		si.append("\n");
550		si.append(master.getString(R.string.mtm_connect_anyway));
551		si.append("\n\n");
552		si.append(master.getString(R.string.mtm_cert_details));
553		certDetails(si, cert);
554		return si.toString();
555	}
556
557	// We can use Notification.Builder once MTM's minSDK is >= 11
558	@SuppressWarnings("deprecation")
559	void startActivityNotification(Intent intent, int decisionId, String certName) {
560		Notification n = new Notification(android.R.drawable.ic_lock_lock,
561				master.getString(R.string.mtm_notification),
562				System.currentTimeMillis());
563		PendingIntent call = PendingIntent.getActivity(master, 0, intent, 0);
564		n.setLatestEventInfo(master.getApplicationContext(),
565				master.getString(R.string.mtm_notification),
566				certName, call);
567		n.flags |= Notification.FLAG_AUTO_CANCEL;
568
569		notificationManager.notify(NOTIFICATION_ID + decisionId, n);
570	}
571
572	/**
573	 * Returns the top-most entry of the activity stack.
574	 *
575	 * @return the Context of the currently bound UI or the master context if none is bound
576	 */
577	Context getUI() {
578		return (foregroundAct != null) ? foregroundAct : master;
579	}
580
581	int interact(final String message, final int titleId) {
582		/* prepare the MTMDecision blocker object */
583		MTMDecision choice = new MTMDecision();
584		final int myId = createDecisionId(choice);
585
586		masterHandler.post(new Runnable() {
587			public void run() {
588				Intent ni = new Intent(master, MemorizingActivity.class);
589				ni.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
590				ni.setData(Uri.parse(MemorizingTrustManager.class.getName() + "/" + myId));
591				ni.putExtra(DECISION_INTENT_ID, myId);
592				ni.putExtra(DECISION_INTENT_CERT, message);
593				ni.putExtra(DECISION_TITLE_ID, titleId);
594
595				// we try to directly start the activity and fall back to
596				// making a notification
597				try {
598					getUI().startActivity(ni);
599				} catch (Exception e) {
600					LOGGER.log(Level.FINE, "startActivity(MemorizingActivity)", e);
601					startActivityNotification(ni, myId, message);
602				}
603			}
604		});
605
606		LOGGER.log(Level.FINE, "openDecisions: " + openDecisions + ", waiting on " + myId);
607		try {
608			synchronized(choice) { choice.wait(); }
609		} catch (InterruptedException e) {
610			LOGGER.log(Level.FINER, "InterruptedException", e);
611		}
612		LOGGER.log(Level.FINE, "finished wait on " + myId + ": " + choice.state);
613		return choice.state;
614	}
615	
616	void interactCert(final X509Certificate[] chain, String authType, CertificateException cause)
617			throws CertificateException
618	{
619		switch (interact(certChainMessage(chain, cause), R.string.mtm_accept_cert)) {
620		case MTMDecision.DECISION_ALWAYS:
621			storeCert(chain[0]); // only store the server cert, not the whole chain
622		case MTMDecision.DECISION_ONCE:
623			break;
624		default:
625			throw (cause);
626		}
627	}
628
629	boolean interactHostname(X509Certificate cert, String hostname)
630	{
631		switch (interact(hostNameMessage(cert, hostname), R.string.mtm_accept_servername)) {
632		case MTMDecision.DECISION_ALWAYS:
633			storeCert(hostname, cert);
634		case MTMDecision.DECISION_ONCE:
635			return true;
636		default:
637			return false;
638		}
639	}
640
641	protected static void interactResult(int decisionId, int choice) {
642		MTMDecision d;
643		synchronized(openDecisions) {
644			 d = openDecisions.get(decisionId);
645			 openDecisions.remove(decisionId);
646		}
647		if (d == null) {
648			LOGGER.log(Level.SEVERE, "interactResult: aborting due to stale decision reference!");
649			return;
650		}
651		synchronized(d) {
652			d.state = choice;
653			d.notify();
654		}
655	}
656	
657	class MemorizingHostnameVerifier implements HostnameVerifier {
658		private HostnameVerifier defaultVerifier;
659		
660		public MemorizingHostnameVerifier(HostnameVerifier wrapped) {
661			defaultVerifier = wrapped;
662		}
663
664		protected boolean verify(String hostname, SSLSession session, boolean interactive) {
665			LOGGER.log(Level.FINE, "hostname verifier for " + hostname + ", trying default verifier first");
666			// if the default verifier accepts the hostname, we are done
667			if (defaultVerifier.verify(hostname, session)) {
668				LOGGER.log(Level.FINE, "default verifier accepted " + hostname);
669				return true;
670			}
671			// otherwise, we check if the hostname is an alias for this cert in our keystore
672			try {
673				X509Certificate cert = (X509Certificate)session.getPeerCertificates()[0];
674				//Log.d(TAG, "cert: " + cert);
675				if (cert.equals(appKeyStore.getCertificate(hostname.toLowerCase(Locale.US)))) {
676					LOGGER.log(Level.FINE, "certificate for " + hostname + " is in our keystore. accepting.");
677					return true;
678				} else {
679					LOGGER.log(Level.FINE, "server " + hostname + " provided wrong certificate, asking user.");
680					if (interactive) {
681						return interactHostname(cert, hostname);
682					} else {
683						return false;
684					}
685				}
686			} catch (Exception e) {
687				e.printStackTrace();
688				return false;
689			}
690		}
691		
692		@Override
693		public boolean verify(String hostname, SSLSession session) {
694			return verify(hostname, session, true);
695		}
696	}
697	
698	class NonInteractiveMemorizingHostnameVerifier extends MemorizingHostnameVerifier {
699
700		public NonInteractiveMemorizingHostnameVerifier(HostnameVerifier wrapped) {
701			super(wrapped);
702		}
703		@Override
704		public boolean verify(String hostname, SSLSession session) {
705			return verify(hostname, session, false);
706		}
707		
708		
709	}
710	
711	public X509TrustManager getNonInteractive() {
712		return new NonInteractiveMemorizingTrustManager();
713	}
714	
715	private class NonInteractiveMemorizingTrustManager implements X509TrustManager {
716
717		@Override
718		public void checkClientTrusted(X509Certificate[] chain, String authType)
719				throws CertificateException {
720			MemorizingTrustManager.this.checkCertTrusted(chain, authType, false, false);
721		}
722
723		@Override
724		public void checkServerTrusted(X509Certificate[] chain, String authType)
725				throws CertificateException {
726			MemorizingTrustManager.this.checkCertTrusted(chain, authType, true, false);
727		}
728
729		@Override
730		public X509Certificate[] getAcceptedIssuers() {
731			return MemorizingTrustManager.this.getAcceptedIssuers();
732		}
733		
734	}
735}