package com.vaguehope.onosendai.util; import java.io.IOException; import java.net.InetAddress; import java.net.Socket; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Locale; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocket; import javax.net.ssl.TrustManager; import org.apache.http.conn.scheme.LayeredSocketFactory; import org.apache.http.conn.ssl.StrictHostnameVerifier; import org.apache.http.params.HttpParams; import android.annotation.TargetApi; import android.net.SSLCertificateSocketFactory; import android.os.Build; /** * Based on http://blog.dev001.net/post/67082904181/android-using-sni-and-tlsv1-2-with-apache */ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) public class TlsSniSocketFactory implements LayeredSocketFactory { private static final List<String> ALLOWED_CIPHERS = Arrays.asList(new String[] { // allowed secure ciphers according to NIST.SP.800-52r1.pdf Section 3.3.1 (see docs directory). // 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", }); private static final HostnameVerifier HOSTNAME_VERIFIER = new StrictHostnameVerifier(); private static final LogWrapper LOG = new LogWrapper("TSF"); private final TrustManager[] trustManager; public TlsSniSocketFactory (final TrustManager[] trustManager) { this.trustManager = trustManager; } // Plain TCP/IP (layer below TLS) @Override public Socket connectSocket (final Socket s, final String host, final int port, final InetAddress localAddress, final int localPort, final HttpParams params) throws IOException { return null; } @Override public Socket createSocket () throws IOException { return null; } @Override public boolean isSecure (final Socket s) throws IllegalArgumentException { if (s instanceof SSLSocket) return ((SSLSocket) s).isConnected(); return false; } // TLS layer @Override public Socket createSocket (final Socket plainSocket, final String host, final int port, final boolean autoClose) throws IOException, UnknownHostException { // we don't need the plainSocket if (autoClose) plainSocket.close(); // create and connect SSL socket, but don't do hostname/certificate verification yet. final SSLCertificateSocketFactory sslSocketFactory = (SSLCertificateSocketFactory) SSLCertificateSocketFactory.getDefault(0); sslSocketFactory.setTrustManagers(this.trustManager); final SSLSocket sock = (SSLSocket) sslSocketFactory.createSocket(InetAddress.getByName(host), port); // Protocols... final List<String> protocols = new ArrayList<String>(); for (final String protocol : sock.getSupportedProtocols()) { if (!protocol.toUpperCase(Locale.ENGLISH).contains("SSL")) protocols.add(protocol); } sock.setEnabledProtocols(protocols.toArray(new String[0])); // Ciphers... final HashSet<String> ciphers = new HashSet<String>(ALLOWED_CIPHERS); ciphers.retainAll(Arrays.asList(sock.getSupportedCipherSuites())); ciphers.addAll(new HashSet<String>(Arrays.asList(sock.getEnabledCipherSuites()))); // All all already enabled ones for compatibility. sock.setEnabledCipherSuites(ciphers.toArray(new String[0])); // set up SNI before the handshake. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { sslSocketFactory.setHostname(sock, host); } else { // This hack seems to work on my 4.0.4 tablet. try { final java.lang.reflect.Method setHostnameMethod = sock.getClass().getMethod("setHostname", String.class); setHostnameMethod.invoke(sock, host); } catch (final Exception e) { LOG.w("SNI not useable: %s", ExcpetionHelper.causeTrace(e)); } } // verify hostname and certificate. final SSLSession session = sock.getSession(); if (!HOSTNAME_VERIFIER.verify(host, session)) throw new SSLPeerUnverifiedException("Cannot verify hostname: " + host); LOG.i("Connected %s %s %s.", session.getPeerHost(), session.getProtocol(), session.getCipherSuite()); return sock; } }