package com.seafile.seadroid2.ssl;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import org.apache.http.conn.ssl.BrowserCompatHostnameVerifier;
import org.apache.http.conn.ssl.X509HostnameVerifier;
import android.util.Log;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.seafile.seadroid2.account.Account;
public final class SSLTrustManager {
public enum SslFailureReason {
CERT_NOT_TRUSTED,
CERT_CHANGED,
}
private static final String DEBUG_TAG = "SSLTrustManager";
private X509TrustManager defaultTrustManager;
private Map<Account, SecureX509TrustManager> managers =
Maps.newHashMap();
private Map<Account, SSLSocketFactory> cachedFactories =
Maps.newHashMap();
private static SSLTrustManager instance;
private SSLTrustManager() {
}
public static synchronized SSLTrustManager instance() {
if (instance == null) {
instance = new SSLTrustManager();
instance.init();
}
return instance;
}
private void init() {
try {
javax.net.ssl.TrustManagerFactory tmf;
TrustManager[] tms;
tmf = javax.net.ssl.TrustManagerFactory.getInstance("X509");
tmf.init((KeyStore) null);
tms = tmf.getTrustManagers();
if (tms != null) {
for (TrustManager tm : tms) {
if (tm instanceof X509TrustManager) {
defaultTrustManager = (X509TrustManager) tm;
break;
}
}
}
} catch (NoSuchAlgorithmException e) {
Log.e(DEBUG_TAG, "Unable to get X509 Trust Manager ", e);
} catch (KeyStoreException e) {
Log.e(DEBUG_TAG, "Key Store exception while initializing TrustManagerFactory ", e);
}
}
public synchronized TrustManager[] getTrustManagers(Account account) {
SecureX509TrustManager mgr = managers.get(account);
if (mgr == null) {
mgr = new SecureX509TrustManager(account);
managers.put(account, mgr);
}
return new TrustManager[] {mgr};
}
public synchronized SSLSocketFactory getSSLSocketFactory(Account account) {
SSLSocketFactory factory = cachedFactories.get(account);
if (factory != null) {
return factory;
}
try {
TrustManager[] mgrs = getTrustManagers(account);
factory = new SSLSeafileSocketFactory(null, mgrs, new SecureRandom());
Log.d(DEBUG_TAG, "a SSLSocketFactory is created:" + factory);
} catch (Exception e) {
Log.e(DEBUG_TAG, "error when create SSLSocketFactory", e);
}
if (factory != null) {
cachedFactories.put(account, factory);
}
return factory;
}
public List<X509Certificate> getCertsChainForAccount(Account account) {
SecureX509TrustManager mgr = managers.get(account);
if (mgr == null) {
return null;
}
return mgr.getServerCertsChain();
}
public X509Certificate getCertificateInfo(Account account) throws CertificateParsingException {
List<X509Certificate> certs = getCertsChainForAccount(account);
if (certs == null || certs.size() == 0) {
return null;
}
final X509Certificate cert = certs.get(0);
return cert;
}
public SslFailureReason getFailureReason(Account account) {
SecureX509TrustManager mgr = managers.get(account);
SslFailureReason reason = null;
if (mgr != null) {
reason = mgr.getReason();
}
return reason != null ? reason : SslFailureReason.CERT_NOT_TRUSTED;
}
/**
* Reorder the certificates chain, since it may not be in the right order when passed to us
* @see http://stackoverflow.com/questions/7822381/need-help-understanding-certificate-chains
*/
public List<X509Certificate> orderCerts(X509Certificate[] certificates) {
if (certificates == null || certificates.length == 0) {
return ImmutableList.of();
}
Set<X509Certificate> all = Sets.newHashSet(certificates);
List<X509Certificate> certs = Lists.newArrayList(all);
// certs.addAll(Arrays.asList(certificates));
X509Certificate certChain = certs.get(0);
certs.remove(certChain);
LinkedList<X509Certificate> chainList= Lists.newLinkedList();
chainList.add(certChain);
Principal certIssuer = certChain.getIssuerDN();
Principal certSubject = certChain.getSubjectDN();
// in the worst case one run per certificate
// make sure, we don't get an infinite loop here
int checksLeft = certs.size();
while(!certs.isEmpty() && checksLeft > 0){
List<X509Certificate> tempcerts = ImmutableList.copyOf(certs);
for (X509Certificate cert : tempcerts) {
if(cert.getIssuerDN().equals(certSubject)){
chainList.addFirst(cert);
certSubject = cert.getSubjectDN();
certs.remove(cert);
continue;
}
if(cert.getSubjectDN().equals(certIssuer)){
chainList.addLast(cert);
certIssuer = cert.getIssuerDN();
certs.remove(cert);
continue;
}
}
checksLeft--;
}
return chainList;
}
private class SecureX509TrustManager implements X509TrustManager {
private Account account;
private SslFailureReason reason;
private volatile List<X509Certificate> certsChain = ImmutableList.of();
public SecureX509TrustManager(Account account) {
this.account = account;
Log.d(DEBUG_TAG, "a SecureX509TrustManager is created:" + hashCode());
}
public List<X509Certificate> getServerCertsChain() {
return certsChain;
}
public SslFailureReason getReason() {
return reason;
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
defaultTrustManager.checkClientTrusted(chain, authType);
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
if (chain == null || chain.length == 0) {
defaultTrustManager.checkServerTrusted(chain, authType);
return;
}
List<X509Certificate> orderedChain = orderCerts(chain);
try {
// First try to do default check
defaultTrustManager.checkServerTrusted(chain, authType);
// Second check if hostname is valid
validateHostName(orderedChain);
} catch (CertificateException e) {
customCheck(orderedChain, authType);
}
}
public String getCeritificateInfo() throws CertificateParsingException {
X509Certificate cert = CertsManager.instance().getCertificate(account);
return "sigalgName:" + cert.getSigAlgName() + " Type: "
+ cert.getType() + " Version: " + cert.getVersion()
+ " IssuerAlternative: " + cert.getIssuerAlternativeNames()
+ " NotAfter: " + cert.getNotAfter();
}
/**
* Interface for checking if a hostname matches the names stored inside the server's X.509 certificate
*/
private void validateHostName(List<X509Certificate> chain) throws CertificateException {
X509Certificate cert = chain.get(0);
// BrowserCompatHostnameVerifier can verify hostnames in the form of IP addresses (like a browser)
// where as the DefaultHostnameVerifier will always try to lookup IP addresses via the DNS.
X509HostnameVerifier mHostnameVerifier = new BrowserCompatHostnameVerifier();
try {
mHostnameVerifier.verify(account.getServerDomainName(), cert);
} catch (SSLException e) {
throw new CertificateException();
}
}
private void customCheck(List<X509Certificate> chain, String authType)
throws CertificateException {
certsChain = ImmutableList.copyOf(chain);
X509Certificate cert = chain.get(0);
X509Certificate savedCert = CertsManager.instance().getCertificate(account);
if (savedCert == null) {
Log.d(DEBUG_TAG, "no saved cert for " + account.server);
reason = SslFailureReason.CERT_NOT_TRUSTED;
throw new CertificateException();
} else if (savedCert.equals(cert)) {
// The user has confirmed to trust this certificate
Log.d(DEBUG_TAG, "the cert of " + account.server + " is trusted");
return;
} else {
// The certificate is different from the one user confirmed to trust,
// This may be either:
// 1. The server admin has changed its cert
// 2. The user is under security attack
Log.d(DEBUG_TAG, "the cert of " + account.server + " has changed");
reason = SslFailureReason.CERT_CHANGED;
throw new CertificateException();
}
}
public X509Certificate[] getAcceptedIssuers() {
return defaultTrustManager.getAcceptedIssuers();
}
@Override
protected void finalize() {
Log.d(DEBUG_TAG, "a SecureX509TrustManager is finalized:" + hashCode());
}
}
public Map<Account, SSLSocketFactory> getCachedFactories() {
return cachedFactories;
}
/*public void setCachedFactories(Map<Account, SSLSocketFactory> cachedFactories) {
this.cachedFactories = cachedFactories;
}*/
}