add support for RFC7711 to MTM

This commit is contained in:
Daniel Gultsch 2016-12-05 21:52:44 +01:00
parent 1e7b4030bb
commit cbc9c1fb20
5 changed files with 198 additions and 62 deletions

View file

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

View file

@ -35,15 +35,32 @@ import android.app.PendingIntent;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.SystemClock;
import android.util.Base64;
import android.util.Log;
import android.util.SparseArray; import android.util.SparseArray;
import android.os.Handler; 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.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException; 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.cert.*;
import java.security.KeyStore; import java.security.KeyStore;
import java.security.KeyStoreException; import java.security.KeyStoreException;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
@ -53,6 +70,7 @@ import java.util.List;
import java.util.Locale; import java.util.Locale;
import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory; 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 * <b>WARNING:</b> This only works if a dedicated thread is used for
* opening sockets! * opening sockets!
*/ */
public class MemorizingTrustManager implements X509TrustManager { public class MemorizingTrustManager {
final static String DECISION_INTENT = "de.duenndns.ssl.DECISION"; final static String DECISION_INTENT = "de.duenndns.ssl.DECISION";
final static String DECISION_INTENT_ID = DECISION_INTENT + ".decisionId"; final static String DECISION_INTENT_ID = DECISION_INTENT + ".decisionId";
final static String DECISION_INTENT_CERT = DECISION_INTENT + ".cert"; final static String DECISION_INTENT_CERT = DECISION_INTENT + ".cert";
@ -94,6 +112,7 @@ public class MemorizingTrustManager implements X509TrustManager {
private KeyStore appKeyStore; private KeyStore appKeyStore;
private X509TrustManager defaultTrustManager; private X509TrustManager defaultTrustManager;
private X509TrustManager appTrustManager; private X509TrustManager appTrustManager;
private String poshCacheDir;
/** Creates an instance of the MemorizingTrustManager class that falls back to a custom TrustManager. /** 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); File dir = app.getDir(KEYSTORE_DIR, Context.MODE_PRIVATE);
keyStoreFile = new File(dir + File.separator + KEYSTORE_FILE); keyStoreFile = new File(dir + File.separator + KEYSTORE_FILE);
poshCacheDir = app.getFilesDir().getAbsolutePath()+"/posh_cache/";
appKeyStore = loadAppKeyStore(); 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. * Binds an Activity to the MTM for displaying the query dialog.
@ -389,7 +391,7 @@ public class MemorizingTrustManager implements X509TrustManager {
return false; 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 throws CertificateException
{ {
LOGGER.log(Level.FINE, "checkCertTrusted(" + chain + ", " + authType + ", " + isServer + ")"); LOGGER.log(Level.FINE, "checkCertTrusted(" + chain + ", " + authType + ", " + isServer + ")");
@ -419,6 +421,14 @@ public class MemorizingTrustManager implements X509TrustManager {
else else
defaultTrustManager.checkClientTrusted(chain, authType); defaultTrustManager.checkClientTrusted(chain, authType);
} catch (CertificateException e) { } 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(); e.printStackTrace();
if (interactive) { if (interactive) {
interactCert(chain, authType, e); interactCert(chain, authType, e);
@ -429,20 +439,121 @@ public class MemorizingTrustManager implements X509TrustManager {
} }
} }
public void checkClientTrusted(X509Certificate[] chain, String authType) private List<String> getPoshFingerprints(String domain) {
throws CertificateException List<String> cached = getPoshFingerprintsFromCache(domain);
{ if (cached == null) {
checkCertTrusted(chain, authType, false,true); return getPoshFingerprintsFromServer(domain);
} else {
return cached;
}
} }
public void checkServerTrusted(X509Certificate[] chain, String authType) private List<String> getPoshFingerprintsFromServer(String domain) {
throws CertificateException try {
{ List<String> results = new ArrayList<>();
checkCertTrusted(chain, authType, true,true); 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()"); LOGGER.log(Level.FINE, "getAcceptedIssuers()");
return defaultTrustManager.getAcceptedIssuers(); return defaultTrustManager.getAcceptedIssuers();
} }
@ -553,22 +664,6 @@ public class MemorizingTrustManager implements X509TrustManager {
certDetails(si, cert); certDetails(si, cert);
return si.toString(); 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. * Returns the top-most entry of the activity stack.
* *
@ -598,7 +693,6 @@ public class MemorizingTrustManager implements X509TrustManager {
getUI().startActivity(ni); getUI().startActivity(ni);
} catch (Exception e) { } catch (Exception e) {
LOGGER.log(Level.FINE, "startActivity(MemorizingActivity)", 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() { public X509TrustManager getNonInteractive() {
return new NonInteractiveMemorizingTrustManager(); return new NonInteractiveMemorizingTrustManager(null);
}
public X509TrustManager getInteractive() {
return new InteractiveMemorizingTrustManager(null);
} }
private class NonInteractiveMemorizingTrustManager implements X509TrustManager { private class NonInteractiveMemorizingTrustManager implements X509TrustManager {
private final String domain;
public NonInteractiveMemorizingTrustManager(String domain) {
this.domain = domain;
}
@Override @Override
public void checkClientTrusted(X509Certificate[] chain, String authType) public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
throws CertificateException { MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, false, false);
MemorizingTrustManager.this.checkCertTrusted(chain, authType, false, false);
} }
@Override @Override
public void checkServerTrusted(X509Certificate[] chain, String authType) public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException { throws CertificateException {
MemorizingTrustManager.this.checkCertTrusted(chain, authType, true, false); MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, true, false);
} }
@Override @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();
}
}
} }

View file

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

View file

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

View file

@ -495,7 +495,8 @@ public class XmppConnection implements Runnable {
} else { } else {
keyManager = null; 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 SSLSocketFactory factory = sc.getSocketFactory();
final HostnameVerifier verifier; final HostnameVerifier verifier;
if (mInteractive) { if (mInteractive) {