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) {