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