diff --git a/libs/MemorizingTrustManager/build.gradle b/libs/MemorizingTrustManager/build.gradle index dc2e7b60..89678c77 100644 --- a/libs/MemorizingTrustManager/build.gradle +++ b/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 { diff --git a/libs/MemorizingTrustManager/src/de/duenndns/ssl/MemorizingTrustManager.java b/libs/MemorizingTrustManager/src/de/duenndns/ssl/MemorizingTrustManager.java index 4aa17e04..649fc074 100644 --- a/libs/MemorizingTrustManager/src/de/duenndns/ssl/MemorizingTrustManager.java +++ b/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; * WARNING: 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: - * - *
-	 *     SSLContext sc = SSLContext.getInstance("TLS");
-	 *     sc.init(null, MemorizingTrustManager.getInstanceList(this),
-	 *         new java.security.SecureRandom());
-	 *     HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
-	 * 
- * @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 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 getPoshFingerprints(String domain) { + List 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 getPoshFingerprintsFromServer(String domain) { + try { + List 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 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 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 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(); + } + } } diff --git a/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java b/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java index 18c60bff..cd0f299c 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java +++ b/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()); diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index b9aeffee..9dca00b0 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/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); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 128e1187..7cd4707d 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/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) {