/**
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*/
package com.ubergeek42.WeechatAndroid.service;
import android.content.Context;
import android.net.SSLCertificateSocketFactory;
import android.support.annotation.CheckResult;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
public class SSLHandler {
private static Logger logger = LoggerFactory.getLogger("SSLHandler");
private static final String KEYSTORE_PASSWORD = "weechat-android";
// best-effort RDN regex, matches CN="foo,bar",OU=... and CN=foobar,OU=...
private static final Pattern RDN_PATTERN = Pattern.compile("CN\\s*=\\s*((?:\"[^\"]*\")|(?:[^\",]*))");
private File keystoreFile;
private KeyStore sslKeystore;
public SSLHandler(File keystoreFile) {
this.keystoreFile = keystoreFile;
loadKeystore();
}
public static @Nullable SSLHandler sslHandler = null;
public static @NonNull SSLHandler getInstance(@NonNull Context context) {
if (sslHandler == null) {
File f = new File(context.getDir("sslDir", Context.MODE_PRIVATE), "keystore.jks");
sslHandler = new SSLHandler(f);
}
return sslHandler;
}
/**
* Initiate an SSL handshake on given host, port to check the hostname using
* {@link HttpsURLConnection} default hostname verifier.
* @return {@code true} if hostname could be verified
*/
public static boolean checkHostname(@NonNull String host, int port) {
// as the check is done *before* checking the certificate, an insecure factory is needed
final SSLSocketFactory factory = SSLCertificateSocketFactory.getInsecure(0, null);
final SSLSocket ssl;
try {
ssl = (SSLSocket) factory.createSocket(host, port);
ssl.startHandshake();
} catch (IOException e) {
return false;
}
SSLSession session = ssl.getSession();
boolean result = HttpsURLConnection.getDefaultHostnameVerifier().verify(host, session);
try { ssl.close(); } catch (IOException ignored) {}
return result;
}
public static Set<String> getCertificateHosts(X509Certificate certificate) {
final Set<String> hosts = new HashSet<>();
try {
final Matcher matcher = RDN_PATTERN.matcher(certificate.getSubjectDN().getName());
if (matcher.find())
hosts.add(matcher.group(1));
} catch (NullPointerException ignored) {}
try {
for (List<?> pair : certificate.getSubjectAlternativeNames()) {
try {
hosts.add(pair.get(1).toString());
} catch (IndexOutOfBoundsException ignored) {}
}
} catch(NullPointerException | CertificateParsingException ignored) {}
return hosts;
}
////////////////////////////////////////////////////////////////////////////////////////////////
public int getUserCertificateCount() {
try {
return sslKeystore.size();
} catch (KeyStoreException e) {
logger.error("getUserCertificateCount()", e);
return 0;
}
}
public void trustCertificate(@NonNull X509Certificate cert) {
try {
KeyStore.TrustedCertificateEntry x = new KeyStore.TrustedCertificateEntry(cert);
sslKeystore.setEntry(cert.getSubjectDN().getName(), x, null);
} catch (KeyStoreException e) {
logger.error("trustCertificate()", e);
}
saveKeystore();
}
public SSLSocketFactory getSSLSocketFactory() {
SSLCertificateSocketFactory sslSocketFactory = (SSLCertificateSocketFactory) SSLCertificateSocketFactory.getDefault(0, null);
sslSocketFactory.setTrustManagers(UserTrustManager.build(sslKeystore));
return sslSocketFactory;
}
@CheckResult public boolean removeKeystore() {
if (keystoreFile.delete()) {
sslHandler = null;
return true;
}
return false;
}
////////////////////////////////////////////////////////////////////////////////////////////////
// Load our keystore for storing SSL certificates
private void loadKeystore() {
try {
sslKeystore = KeyStore.getInstance("BKS");
sslKeystore.load(new FileInputStream(keystoreFile), KEYSTORE_PASSWORD.toCharArray());
} catch (KeyStoreException | NoSuchAlgorithmException | CertificateException | IOException e) {
if (e instanceof FileNotFoundException) createKeystore();
else logger.error("loadKeystore()", e);
}
}
private void createKeystore() {
try {
sslKeystore.load(null, null);
} catch (Exception e) {
logger.error("createKeystore()", e);
}
saveKeystore();
}
private void saveKeystore() {
try {
sslKeystore.store(new FileOutputStream(keystoreFile), KEYSTORE_PASSWORD.toCharArray());
} catch (KeyStoreException | NoSuchAlgorithmException | CertificateException | IOException e) {
logger.error("saveKeystore()", e);
}
}
private static class UserTrustManager implements X509TrustManager {
static final X509TrustManager systemTrustManager = buildTrustManger(null);
private final X509TrustManager userTrustManager;
private static TrustManager[] build(KeyStore sslKeystore) {
return new TrustManager[] { new UserTrustManager(sslKeystore) };
}
static X509TrustManager buildTrustManger(@Nullable KeyStore store) {
try {
TrustManagerFactory tmf =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(store);
return (X509TrustManager) tmf.getTrustManagers()[0];
} catch (NoSuchAlgorithmException | KeyStoreException e) {
return null;
}
}
private UserTrustManager(KeyStore userKeyStore) {
this.userTrustManager = buildTrustManger(userKeyStore);
}
@Override
public void checkClientTrusted(X509Certificate[] x509Certificates, String s)
throws CertificateException {
try {
systemTrustManager.checkClientTrusted(x509Certificates, s);
logger.debug("Client is trusted by system");
} catch (CertificateException e) {
logger.debug("Client is NOT trusted by system, trying user");
userTrustManager.checkClientTrusted(x509Certificates, s);
logger.debug("Client is trusted by user");
}
}
@Override
public void checkServerTrusted(X509Certificate[] x509Certificates, String s)
throws CertificateException {
try {
systemTrustManager.checkServerTrusted(x509Certificates, s);
logger.debug("Server is trusted by system");
} catch (CertificateException e) {
logger.debug("Server is NOT trusted by system, trying user");
userTrustManager.checkServerTrusted(x509Certificates, s);
logger.debug("Server is trusted by user");
}
}
@Override
public X509Certificate[] getAcceptedIssuers() {
X509Certificate[] system = systemTrustManager.getAcceptedIssuers();
X509Certificate[] user = userTrustManager.getAcceptedIssuers();
X509Certificate[] result = Arrays.copyOf(system, system.length + user.length);
System.arraycopy(user, 0, result, system.length, user.length);
return result;
}
}
}