add support for RFC7711 to MTM

Daniel Gultsch created

Change summary

libs/MemorizingTrustManager/build.gradle                                    |  10 
libs/MemorizingTrustManager/src/de/duenndns/ssl/MemorizingTrustManager.java | 241 
src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java        |   2 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java    |   4 
src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java               |   3 
5 files changed, 198 insertions(+), 62 deletions(-)

Detailed changes

libs/MemorizingTrustManager/build.gradle 🔗

@@ -7,14 +7,14 @@ buildscript {
 	}
 }
 
-apply plugin: 'android-library'
+apply plugin: 'com.android.library'
 
 android {
-	compileSdkVersion 19
-	buildToolsVersion "19.1"
+	compileSdkVersion 24
+	buildToolsVersion "23.0.3"
 	defaultConfig {
-        	minSdkVersion 7
-		targetSdkVersion 19
+		minSdkVersion 14
+		targetSdkVersion 24
 	}
 
 	sourceSets {

libs/MemorizingTrustManager/src/de/duenndns/ssl/MemorizingTrustManager.java 🔗

@@ -35,15 +35,32 @@ import android.app.PendingIntent;
 import android.content.Context;
 import android.content.Intent;
 import android.net.Uri;
+import android.os.SystemClock;
+import android.util.Base64;
+import android.util.Log;
 import android.util.SparseArray;
 import android.os.Handler;
 
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.BufferedReader;
 import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.security.NoSuchAlgorithmException;
 import java.security.cert.*;
 import java.security.KeyStore;
 import java.security.KeyStoreException;
 import java.security.MessageDigest;
+import java.util.ArrayList;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 import java.text.SimpleDateFormat;
@@ -53,6 +70,7 @@ import java.util.List;
 import java.util.Locale;
 
 import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
 import javax.net.ssl.SSLSession;
 import javax.net.ssl.TrustManager;
 import javax.net.ssl.TrustManagerFactory;
@@ -68,7 +86,7 @@ import javax.net.ssl.X509TrustManager;
  * <b>WARNING:</b> This only works if a dedicated thread is used for
  * opening sockets!
  */
-public class MemorizingTrustManager implements X509TrustManager {
+public class MemorizingTrustManager {
 	final static String DECISION_INTENT = "de.duenndns.ssl.DECISION";
 	final static String DECISION_INTENT_ID     = DECISION_INTENT + ".decisionId";
 	final static String DECISION_INTENT_CERT   = DECISION_INTENT + ".cert";
@@ -94,6 +112,7 @@ public class MemorizingTrustManager implements X509TrustManager {
 	private KeyStore appKeyStore;
 	private X509TrustManager defaultTrustManager;
 	private X509TrustManager appTrustManager;
+	private String poshCacheDir;
 
 	/** Creates an instance of the MemorizingTrustManager class that falls back to a custom TrustManager.
 	 *
@@ -149,28 +168,11 @@ public class MemorizingTrustManager implements X509TrustManager {
 		File dir = app.getDir(KEYSTORE_DIR, Context.MODE_PRIVATE);
 		keyStoreFile = new File(dir + File.separator + KEYSTORE_FILE);
 
+		poshCacheDir = app.getFilesDir().getAbsolutePath()+"/posh_cache/";
+
 		appKeyStore = loadAppKeyStore();
 	}
 
-	
-	/**
-	 * Returns a X509TrustManager list containing a new instance of
-	 * TrustManagerFactory.
-	 *
-	 * This function is meant for convenience only. You can use it
-	 * as follows to integrate TrustManagerFactory for HTTPS sockets:
-	 *
-	 * <pre>
-	 *     SSLContext sc = SSLContext.getInstance("TLS");
-	 *     sc.init(null, MemorizingTrustManager.getInstanceList(this),
-	 *         new java.security.SecureRandom());
-	 *     HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
-	 * </pre>
-	 * @param c Activity or Service to show the Dialog / Notification
-	 */
-	public static X509TrustManager[] getInstanceList(Context c) {
-		return new X509TrustManager[] { new MemorizingTrustManager(c) };
-	}
 
 	/**
 	 * Binds an Activity to the MTM for displaying the query dialog.
@@ -389,7 +391,7 @@ public class MemorizingTrustManager implements X509TrustManager {
 		return false;
 	}
 
-	public void checkCertTrusted(X509Certificate[] chain, String authType, boolean isServer, boolean interactive)
+	public void checkCertTrusted(X509Certificate[] chain, String authType, String domain, boolean isServer, boolean interactive)
 		throws CertificateException
 	{
 		LOGGER.log(Level.FINE, "checkCertTrusted(" + chain + ", " + authType + ", " + isServer + ")");
@@ -419,6 +421,14 @@ public class MemorizingTrustManager implements X509TrustManager {
 				else
 					defaultTrustManager.checkClientTrusted(chain, authType);
 			} catch (CertificateException e) {
+				if (domain != null && isServer) {
+					String hash = getBase64Hash(chain[0],"SHA-256");
+					List<String> fingerprints = getPoshFingerprints(domain);
+					if (hash != null && fingerprints.contains(hash)) {
+						Log.d("mtm","trusted cert fingerprint of "+domain+" via posh");
+						return;
+					}
+				}
 				e.printStackTrace();
 				if (interactive) {
 					interactCert(chain, authType, e);
@@ -429,20 +439,121 @@ public class MemorizingTrustManager implements X509TrustManager {
 		}
 	}
 
-	public void checkClientTrusted(X509Certificate[] chain, String authType)
-		throws CertificateException
-	{
-		checkCertTrusted(chain, authType, false,true);
+	private List<String> getPoshFingerprints(String domain) {
+		List<String> cached = getPoshFingerprintsFromCache(domain);
+		if (cached == null) {
+			return getPoshFingerprintsFromServer(domain);
+		} else {
+			return cached;
+		}
 	}
 
-	public void checkServerTrusted(X509Certificate[] chain, String authType)
-		throws CertificateException
-	{
-		checkCertTrusted(chain, authType, true,true);
+	private List<String> getPoshFingerprintsFromServer(String domain) {
+		try {
+			List<String> results = new ArrayList<>();
+			URL url = new URL("https://"+domain+"/.well-known/posh/xmpp-client.json");
+			HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
+			BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
+			String inputLine;
+			StringBuilder builder = new StringBuilder();
+			while ((inputLine = in.readLine()) != null) {
+				builder.append(inputLine);
+			}
+			JSONObject jsonObject = new JSONObject(builder.toString());
+			in.close();
+			JSONArray fingerprints = jsonObject.getJSONArray("fingerprints");
+			for(int i = 0; i < fingerprints.length(); i++) {
+				JSONObject fingerprint = fingerprints.getJSONObject(i);
+				String sha256 = fingerprint.getString("sha-256");
+				if (sha256 != null) {
+					results.add(sha256);
+				}
+			}
+			int expires = jsonObject.getInt("expires");
+			if (expires <= 0) {
+				return new ArrayList<>();
+			}
+			in.close();
+			writeFingerprintsToCache(domain, results,1000L * expires+System.currentTimeMillis());
+			return results;
+		} catch (Exception e) {
+			Log.d("mtm","error fetching posh "+e.getMessage());
+			return new ArrayList<>();
+		}
 	}
 
-	public X509Certificate[] getAcceptedIssuers()
-	{
+	private File getPoshCacheFile(String domain) {
+		return new File(poshCacheDir+domain+".json");
+	}
+
+	private void writeFingerprintsToCache(String domain, List<String> results, long expires) {
+		File file = getPoshCacheFile(domain);
+		file.getParentFile().mkdirs();
+		try {
+			file.createNewFile();
+			JSONObject jsonObject = new JSONObject();
+			jsonObject.put("expires",expires);
+			jsonObject.put("fingerprints",new JSONArray(results));
+			FileOutputStream outputStream = new FileOutputStream(file);
+			outputStream.write(jsonObject.toString().getBytes());
+			outputStream.flush();
+			outputStream.close();
+		} catch (Exception e) {
+			e.printStackTrace();
+		}
+	}
+
+	private List<String> getPoshFingerprintsFromCache(String domain) {
+		File file = getPoshCacheFile(domain);
+		try {
+			InputStream is = new FileInputStream(file);
+			BufferedReader buf = new BufferedReader(new InputStreamReader(is));
+
+			String line = buf.readLine();
+			StringBuilder sb = new StringBuilder();
+
+			while(line != null){
+				sb.append(line).append("\n");
+				line = buf.readLine();
+			}
+			JSONObject jsonObject = new JSONObject(sb.toString());
+			is.close();
+			long expires = jsonObject.getLong("expires");
+			long expiresIn = expires - System.currentTimeMillis();
+			if (expiresIn < 0) {
+				file.delete();
+				return null;
+			} else {
+				Log.d("mtm","posh fingerprints expire in "+(expiresIn/1000)+"s");
+			}
+			List<String> result = new ArrayList<>();
+			JSONArray jsonArray = jsonObject.getJSONArray("fingerprints");
+			for(int i = 0; i < jsonArray.length(); ++i) {
+				result.add(jsonArray.getString(i));
+			}
+			return result;
+		} catch (FileNotFoundException e) {
+			return null;
+		} catch (IOException e) {
+			return null;
+		} catch (JSONException e) {
+			file.delete();
+			return null;
+		}
+	}
+
+	private static String getBase64Hash(X509Certificate certificate, String digest) throws CertificateEncodingException {
+		MessageDigest md;
+		try {
+			md = MessageDigest.getInstance(digest);
+		} catch (NoSuchAlgorithmException e) {
+			return null;
+		}
+		md.update(certificate.getEncoded());
+		return Base64.encodeToString(md.digest(),Base64.NO_WRAP);
+	}
+
+	private X509Certificate[] getAcceptedIssuers() {
 		LOGGER.log(Level.FINE, "getAcceptedIssuers()");
 		return defaultTrustManager.getAcceptedIssuers();
 	}
@@ -553,22 +664,6 @@ public class MemorizingTrustManager implements X509TrustManager {
 		certDetails(si, cert);
 		return si.toString();
 	}
-
-	// We can use Notification.Builder once MTM's minSDK is >= 11
-	@SuppressWarnings("deprecation")
-	void startActivityNotification(Intent intent, int decisionId, String certName) {
-		Notification n = new Notification(android.R.drawable.ic_lock_lock,
-				master.getString(R.string.mtm_notification),
-				System.currentTimeMillis());
-		PendingIntent call = PendingIntent.getActivity(master, 0, intent, 0);
-		n.setLatestEventInfo(master.getApplicationContext(),
-				master.getString(R.string.mtm_notification),
-				certName, call);
-		n.flags |= Notification.FLAG_AUTO_CANCEL;
-
-		notificationManager.notify(NOTIFICATION_ID + decisionId, n);
-	}
-
 	/**
 	 * Returns the top-most entry of the activity stack.
 	 *
@@ -598,7 +693,6 @@ public class MemorizingTrustManager implements X509TrustManager {
 					getUI().startActivity(ni);
 				} catch (Exception e) {
 					LOGGER.log(Level.FINE, "startActivity(MemorizingActivity)", e);
-					startActivityNotification(ni, myId, message);
 				}
 			}
 		});
@@ -708,22 +802,39 @@ public class MemorizingTrustManager implements X509TrustManager {
 		
 	}
 	
+	public X509TrustManager getNonInteractive(String domain) {
+		return new NonInteractiveMemorizingTrustManager(domain);
+	}
+
+	public X509TrustManager getInteractive(String domain) {
+		return new InteractiveMemorizingTrustManager(domain);
+	}
+
 	public X509TrustManager getNonInteractive() {
-		return new NonInteractiveMemorizingTrustManager();
+		return new NonInteractiveMemorizingTrustManager(null);
+	}
+
+	public X509TrustManager getInteractive() {
+		return new InteractiveMemorizingTrustManager(null);
 	}
 	
 	private class NonInteractiveMemorizingTrustManager implements X509TrustManager {
 
+		private final String domain;
+
+		public NonInteractiveMemorizingTrustManager(String domain) {
+			this.domain = domain;
+		}
+
 		@Override
-		public void checkClientTrusted(X509Certificate[] chain, String authType)
-				throws CertificateException {
-			MemorizingTrustManager.this.checkCertTrusted(chain, authType, false, false);
+		public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+			MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, false, false);
 		}
 
 		@Override
 		public void checkServerTrusted(X509Certificate[] chain, String authType)
 				throws CertificateException {
-			MemorizingTrustManager.this.checkCertTrusted(chain, authType, true, false);
+			MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, true, false);
 		}
 
 		@Override
@@ -732,4 +843,28 @@ public class MemorizingTrustManager implements X509TrustManager {
 		}
 		
 	}
+
+	private class InteractiveMemorizingTrustManager implements X509TrustManager {
+		private final String domain;
+
+		public InteractiveMemorizingTrustManager(String domain) {
+			this.domain = domain;
+		}
+
+		@Override
+		public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+			MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, false, true);
+		}
+
+		@Override
+		public void checkServerTrusted(X509Certificate[] chain, String authType)
+				throws CertificateException {
+			MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, true, true);
+		}
+
+		@Override
+		public X509Certificate[] getAcceptedIssuers() {
+			return MemorizingTrustManager.this.getAcceptedIssuers();
+		}
+	}
 }

src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java 🔗

@@ -65,7 +65,7 @@ public class HttpConnectionManager extends AbstractConnectionManager {
 		final X509TrustManager trustManager;
 		final HostnameVerifier hostnameVerifier;
 		if (interactive) {
-			trustManager = mXmppConnectionService.getMemorizingTrustManager();
+			trustManager = mXmppConnectionService.getMemorizingTrustManager().getInteractive();
 			hostnameVerifier = mXmppConnectionService
 					.getMemorizingTrustManager().wrapHostnameVerifier(
 							new StrictHostnameVerifier());

src/main/java/eu/siacs/conversations/services/XmppConnectionService.java 🔗

@@ -1666,7 +1666,7 @@ public class XmppConnectionService extends Service {
 						callback.onAccountCreated(account);
 						if (Config.X509_VERIFICATION) {
 							try {
-								getMemorizingTrustManager().getNonInteractive().checkClientTrusted(chain, "RSA");
+								getMemorizingTrustManager().getNonInteractive(account.getJid().getDomainpart()).checkClientTrusted(chain, "RSA");
 							} catch (CertificateException e) {
 								callback.informUser(R.string.certificate_chain_is_not_trusted);
 							}
@@ -1694,7 +1694,7 @@ public class XmppConnectionService extends Service {
 				databaseBackend.updateAccount(account);
 				if (Config.X509_VERIFICATION) {
 					try {
-						getMemorizingTrustManager().getNonInteractive().checkClientTrusted(chain, "RSA");
+						getMemorizingTrustManager().getNonInteractive(account.getJid().getDomainpart()).checkClientTrusted(chain, "RSA");
 					} catch (CertificateException e) {
 						showErrorToastInUi(R.string.certificate_chain_is_not_trusted);
 					}

src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java 🔗

@@ -495,7 +495,8 @@ public class XmppConnection implements Runnable {
 		} else {
 			keyManager = null;
 		}
-		sc.init(keyManager, new X509TrustManager[]{mInteractive ? trustManager : trustManager.getNonInteractive()}, mXmppConnectionService.getRNG());
+		String domain = account.getJid().getDomainpart();
+		sc.init(keyManager, new X509TrustManager[]{mInteractive ? trustManager.getInteractive(domain) : trustManager.getNonInteractive(domain)}, mXmppConnectionService.getRNG());
 		final SSLSocketFactory factory = sc.getSocketFactory();
 		final HostnameVerifier verifier;
 		if (mInteractive) {