aboutsummaryrefslogtreecommitdiffstats
path: root/libs/MemorizingTrustManager/src/de/duenndns/ssl/MemorizingTrustManager.java
diff options
context:
space:
mode:
Diffstat (limited to 'libs/MemorizingTrustManager/src/de/duenndns/ssl/MemorizingTrustManager.java')
-rw-r--r--libs/MemorizingTrustManager/src/de/duenndns/ssl/MemorizingTrustManager.java276
1 files changed, 224 insertions, 52 deletions
diff --git a/libs/MemorizingTrustManager/src/de/duenndns/ssl/MemorizingTrustManager.java b/libs/MemorizingTrustManager/src/de/duenndns/ssl/MemorizingTrustManager.java
index d2fc75db8..e46b6d5fb 100644
--- a/libs/MemorizingTrustManager/src/de/duenndns/ssl/MemorizingTrustManager.java
+++ b/libs/MemorizingTrustManager/src/de/duenndns/ssl/MemorizingTrustManager.java
@@ -28,35 +28,52 @@ package de.duenndns.ssl;
import android.app.Activity;
import android.app.Application;
-import android.app.Notification;
import android.app.NotificationManager;
-import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Handler;
+import android.preference.PreferenceManager;
+import android.util.Base64;
+import android.util.Log;
import android.util.SparseArray;
+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.URL;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate;
+import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.text.SimpleDateFormat;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.List;
import java.util.Locale;
import java.util.logging.Level;
import java.util.logging.Logger;
+import java.util.regex.Pattern;
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;
@@ -72,7 +89,15 @@ 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 {
+
+
+ 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");
+ 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");
+ 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");
+ 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");
+ private static final Pattern PATTERN_IPV6 = Pattern.compile("\\A(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\\z");
+
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";
@@ -98,6 +123,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.
@@ -156,31 +182,13 @@ 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.
- * <p>
- * This function is meant for convenience only. You can use it
- * as follows to integrate TrustManagerFactory for HTTPS sockets:
- * <p>
- * <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.
* <p>
* This is useful if your connection is run from a service that is
@@ -395,7 +403,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 + ")");
try {
@@ -424,6 +432,15 @@ public class MemorizingTrustManager implements X509TrustManager {
else
defaultTrustManager.checkClientTrusted(chain, authType);
} catch (CertificateException e) {
+ boolean trustSystemCAs = !PreferenceManager.getDefaultSharedPreferences(master).getBoolean("dont_trust_system_cas", false);
+ if (domain != null && isServer && trustSystemCAs && !isIp(domain)) {
+ 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);
@@ -434,17 +451,147 @@ 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) {
+ return getPoshFingerprintsFromServer(domain, "https://" + domain + "/.well-known/posh/xmpp-client.json", -1, true);
+ }
+
+ private List<String> getPoshFingerprintsFromServer(String domain, String url, int maxTtl, boolean followUrl) {
+ Log.d("mtm", "downloading json for " + domain + " from " + url);
+ try {
+ List<String> results = new ArrayList<>();
+ HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection();
+ connection.setConnectTimeout(5000);
+ connection.setReadTimeout(5000);
+ 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();
+ int expires = jsonObject.getInt("expires");
+ if (expires <= 0) {
+ return new ArrayList<>();
+ }
+ if (maxTtl >= 0) {
+ expires = Math.min(maxTtl, expires);
+ }
+ String redirect;
+ try {
+ redirect = jsonObject.getString("url");
+ } catch (JSONException e) {
+ redirect = null;
+ }
+ if (followUrl && redirect != null && redirect.toLowerCase().startsWith("https")) {
+ return getPoshFingerprintsFromServer(domain, redirect, expires, false);
+ }
+ 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);
+ }
+ }
+ writeFingerprintsToCache(domain, results, 1000L * expires + System.currentTimeMillis());
+ return results;
+ } catch (Exception e) {
+ Log.d("mtm", "error fetching posh " + e.getMessage());
+ return new ArrayList<>();
+ }
+ }
+
+ 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();
+ }
}
- public X509Certificate[] getAcceptedIssuers() {
+ 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 boolean isIp(final String server) {
+ return server != null && (
+ PATTERN_IPV4.matcher(server).matches()
+ || PATTERN_IPV6.matcher(server).matches()
+ || PATTERN_IPV6_6HEX4DEC.matcher(server).matches()
+ || PATTERN_IPV6_HEX4DECCOMPRESSED.matcher(server).matches()
+ || PATTERN_IPV6_HEXCOMPRESSED.matcher(server).matches());
+ }
+
+ 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();
}
@@ -556,21 +703,6 @@ public class MemorizingTrustManager implements X509TrustManager {
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.
*
@@ -600,7 +732,6 @@ public class MemorizingTrustManager implements X509TrustManager {
getUI().startActivity(ni);
} catch (Exception e) {
LOGGER.log(Level.FINE, "startActivity(MemorizingActivity)", e);
- startActivityNotification(ni, myId, message);
}
}
});
@@ -711,22 +842,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
@@ -735,4 +883,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();
+ }
+ }
}