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