/*
* This class was copied from this blog post:
* http://blog.dev001.net/post/67082904181/android-using-sni-and-tlsv1-2-with-apache-httpclient
* Thanks go to Dev001!
* Also, changes for using only secure cipher suites were included from code of DAVdroid.
* Thankgs go to Ricki Hirner (bitfire web engineering)!
*/
package org.acra.util;
import android.net.SSLCertificateSocketFactory;
import android.os.Build;
import android.text.TextUtils;
import org.acra.ACRA;
import org.acra.collector.Compatibility;
import org.apache.http.conn.scheme.LayeredSocketFactory;
import org.apache.http.conn.ssl.BrowserCompatHostnameVerifier;
import org.apache.http.params.HttpParams;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.Socket;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
/**
* Provides a SSLSocketFactory that is able to use SNI for SSL connections and
* therefore allows multiple SSL servers on one IP address.<br/>
* 1) socket = createSocket() is called
* 2) reasonable encryption settings are applied to socket
* 3) SNI is set up for socket
* 4) handshake and certificate/host name verification
* <p/>
* @author Philipp Kapfer
* @since 4.6.0
*/
public class TlsSniSocketFactory implements LayeredSocketFactory {
private static final String TAG = TlsSniSocketFactory.class.getSimpleName();
private final SSLCertificateSocketFactory sslSocketFactory = (SSLCertificateSocketFactory) SSLCertificateSocketFactory.getDefault(0);
// use BrowserCompatHostnameVerifier to allow IP addresses in the Common Name
private final static HostnameVerifier hostnameVerifier = new BrowserCompatHostnameVerifier();
private static final List<String> ALLOWED_CIPHERS = Arrays.asList(
// allowed secure ciphers according to NIST.SP.800-52r1.pdf Section 3.3.1 (see http://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-52r1.pdf)
// TLS 1.2
"TLS_RSA_WITH_AES_256_GCM_SHA384",
"TLS_RSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256",
"TLS_ECHDE_RSA_WITH_AES_128_GCM_SHA256",
// maximum interoperability
"TLS_RSA_WITH_3DES_EDE_CBC_SHA",
"TLS_RSA_WITH_AES_128_CBC_SHA",
// additionally
"TLS_RSA_WITH_AES_256_CBC_SHA",
"TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA",
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
"TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA",
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA"
);
// Plain TCP/IP (layer below TLS)
@Override
public Socket connectSocket(Socket s, String host, int port, InetAddress localAddress, int localPort, HttpParams params) throws IOException {
return null;
}
@Override
public Socket createSocket() throws IOException {
return null;
}
@Override
public boolean isSecure(Socket s) throws IllegalArgumentException {
return (s instanceof SSLSocket) && s.isConnected();
}
// TLS layer
@Override
public Socket createSocket(Socket plainSocket, String host, int port, boolean autoClose) throws IOException {
if (autoClose) {
// we don't need the plainSocket
plainSocket.close();
}
// create and connect SSL socket, but don't do hostname/certificate verification yet
final SSLSocket ssl = (SSLSocket) sslSocketFactory.createSocket(InetAddress.getByName(host), port);
// establish and verify TLS connection
establishAndVerify(ssl, host);
return ssl;
}
/**
* Establishes and verifies a TLS connection to a (TCP-)connected SSLSocket:
* - set TLS parameters like allowed protocols and ciphers
* - set SNI host name
* - verify host name
* - verify certificate
* @param socket unconnected SSLSocket
* @param host host name for SNI
* @throws IOException if the connection could not be established.
*/
private void establishAndVerify(SSLSocket socket, String host) throws IOException {
setTlsParameters(socket);
setSniHostname(socket, host);
// TLS handshake, throws an exception for untrusted certificates
socket.startHandshake();
// verify hostname and certificate
SSLSession session = socket.getSession();
if (!hostnameVerifier.verify(host, session)) {
// throw exception for invalid host names
throw new SSLPeerUnverifiedException(host);
}
ACRA.log.i(TAG, "Established " + session.getProtocol() + " connection with " + session.getPeerHost() + " using " + session.getCipherSuite());
}
/**
* Prepares a TLS/SSL connection socket by:
* - setting reasonable TLS protocol versions
* - setting reasonable cipher suites (if required)
* @param socket unconnected SSLSocket to prepare
*/
private void setTlsParameters(SSLSocket socket) {
// Android 5.0+ (API level21) provides reasonable default settings
// but it still allows SSLv3
// https://developer.android.com/about/versions/android-5.0-changes.html#ssl
/* set reasonable protocol versions */
// - enable all supported protocols (enables TLSv1.1 and TLSv1.2 on Android <5.0)
// - remove all SSL versions (especially SSLv3) because they're insecure now
final List<String> protocols = new LinkedList<String>();
for (String protocol : socket.getSupportedProtocols()) {
if (!protocol.toUpperCase().contains("SSL")) {
protocols.add(protocol);
}
}
ACRA.log.v(TAG, "Setting allowed TLS protocols: " + TextUtils.join(", ", protocols));
socket.setEnabledProtocols(protocols.toArray(new String[protocols.size()]));
/* set reasonable cipher suites */
if (Compatibility.getAPILevel() < Compatibility.VERSION_CODES.LOLLIPOP) {
// choose secure cipher suites
final List<String> availableCiphers = Arrays.asList(socket.getSupportedCipherSuites());
// preferred ciphers = allowed Ciphers \ availableCiphers
final Set<String> preferredCiphers = new HashSet<String>(ALLOWED_CIPHERS);
preferredCiphers.retainAll(availableCiphers);
// add enabled ciphers to preferred ciphers
// for maximum security, preferred ciphers should *replace* enabled ciphers,
// but for the security level of ACRA, disabling of insecure
// ciphers should be a server-side task
preferredCiphers.addAll(Arrays.asList(socket.getEnabledCipherSuites()));
ACRA.log.v(TAG, "Setting allowed TLS ciphers: " + TextUtils.join(", ", preferredCiphers));
socket.setEnabledCipherSuites(preferredCiphers.toArray(new String[preferredCiphers.size()]));
}
}
private void setSniHostname(SSLSocket socket, String hostName) {
// set SNI host name
if (Compatibility.getAPILevel() >= Compatibility.VERSION_CODES.JELLY_BEAN_MR1) {
ACRA.log.d(TAG, "Using documented SNI with host name " + hostName);
sslSocketFactory.setHostname(socket, hostName);
} else {
ACRA.log.d(TAG, "No documented SNI support on Android <4.2, trying reflection method with host name " + hostName);
try {
final Method setHostnameMethod = socket.getClass().getMethod("setHostname", String.class);
setHostnameMethod.invoke(socket, hostName);
} catch (Exception e) {
ACRA.log.w(TAG, "SNI not usable", e);
}
}
}
}