diff --git a/app/build.gradle b/app/build.gradle index 6a6caa8e..9951fbaf 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -35,7 +35,7 @@ configurations.all { dependencies { // Apache Commons compile 'commons-lang:commons-lang:2.6' - compile 'org.apache.commons:commons-io:1.3.2' + compile 'commons-io:commons-io:2.4' // Lombok for useful @helpers provided 'org.projectlombok:lombok:1.14.8' @@ -56,9 +56,10 @@ dependencies { compile 'dnsjava:dnsjava:2.1.6' // HttpClient 4.3, Android flavour for WebDAV operations - // we have to use our patched version of 4.3.5 to avoid https://issues.apache.org/jira/browse/HTTPCLIENT-1566 - compile files('lib/httpclient-android-4.3.5.davdroid1.jar') - // compile 'org.apache.httpcomponents:httpclient-android:4.3.5' + // we have to use our own patched build of 4.3.5.2-SNAPSHOT to avoid + // https://issues.apache.org/jira/browse/HTTPCLIENT-1591 + compile files('lib/httpclient-android-4.3.5.2-davdroid1.jar') + // compile 'org.apache.httpcomponents:httpclient-android:4.3.5.2-SNAPSHOT' // SimpleXML for parsing and generating WebDAV messages compile('org.simpleframework:simple-xml:2.7.1') { diff --git a/app/lib/httpclient-android-4.3.5.davdroid1.jar b/app/lib/httpclient-android-4.3.5.2-davdroid1.jar similarity index 84% rename from app/lib/httpclient-android-4.3.5.davdroid1.jar rename to app/lib/httpclient-android-4.3.5.2-davdroid1.jar index 50bdf3dc..1586e9cc 100644 Binary files a/app/lib/httpclient-android-4.3.5.davdroid1.jar and b/app/lib/httpclient-android-4.3.5.2-davdroid1.jar differ diff --git a/app/src/androidTest/java/at/bitfire/davdroid/webdav/TlsSniSocketFactoryTest.java b/app/src/androidTest/java/at/bitfire/davdroid/webdav/TlsSniSocketFactoryTest.java index f7a3d571..54afcace 100644 --- a/app/src/androidTest/java/at/bitfire/davdroid/webdav/TlsSniSocketFactoryTest.java +++ b/app/src/androidTest/java/at/bitfire/davdroid/webdav/TlsSniSocketFactoryTest.java @@ -14,6 +14,8 @@ import java.net.Socket; import java.net.SocketAddress; import java.security.cert.CertPathValidatorException; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLSocket; @@ -29,7 +31,7 @@ import lombok.Cleanup; public class TlsSniSocketFactoryTest extends TestCase { private static final String TAG = "davdroid.TlsSniSocketFactoryTest"; - TlsSniSocketFactory factory = TlsSniSocketFactory.INSTANCE; + TlsSniSocketFactory factory = TlsSniSocketFactory.getSocketFactory(); private InetSocketAddress sampleTlsEndpoint; @@ -41,7 +43,7 @@ public class TlsSniSocketFactoryTest extends TestCase { public void testCreateSocket() { try { - @Cleanup SSLSocket socket = factory.createSocket(null); + @Cleanup Socket socket = factory.createSocket(null); assertFalse(socket.isConnected()); } catch (IOException e) { fail(); @@ -50,9 +52,7 @@ public class TlsSniSocketFactoryTest extends TestCase { public void testConnectSocket() { try { - @Cleanup SSLSocket socket = factory.createSocket(null); - - factory.connectSocket(1000, socket, new HttpHost(sampleTlsEndpoint.getHostName()), sampleTlsEndpoint, null, null); + factory.connectSocket(1000, null, new HttpHost(sampleTlsEndpoint.getHostName()), sampleTlsEndpoint, null, null); } catch (IOException e) { Log.e(TAG, "I/O exception", e); fail(); @@ -67,7 +67,7 @@ public class TlsSniSocketFactoryTest extends TestCase { assertTrue(plain.isConnected()); // then create TLS socket on top of it and establish TLS Connection - @Cleanup SSLSocket socket = factory.createLayeredSocket(plain, sampleTlsEndpoint.getHostName(), sampleTlsEndpoint.getPort(), null); + @Cleanup Socket socket = factory.createLayeredSocket(plain, sampleTlsEndpoint.getHostName(), sampleTlsEndpoint.getPort(), null); assertTrue(socket.isConnected()); } catch (IOException e) { @@ -76,11 +76,8 @@ public class TlsSniSocketFactoryTest extends TestCase { } } - public void testSetTlsParameters() throws IOException { - @Cleanup SSLSocket socket = factory.createSocket(null); - factory.setTlsParameters(socket); - - String enabledProtocols[] = socket.getEnabledProtocols(); + public void testProtocolVersions() throws IOException { + String enabledProtocols[] = factory.protocols; // SSL (all versions) should be disabled for (String protocol : enabledProtocols) assertFalse(protocol.contains("SSL")); @@ -91,27 +88,29 @@ public class TlsSniSocketFactoryTest extends TestCase { } - public void testHostnameNotInCertificate() { + public void testHostnameNotInCertificate() throws IOException { try { // host with certificate that doesn't match host name // use the IP address as host name because IP addresses are usually not in the certificate subject - InetSocketAddress host = new InetSocketAddress(sampleTlsEndpoint.getAddress().getHostAddress(), 443); - - @Cleanup SSLSocket socket = factory.connectSocket(0, null, new HttpHost(host.getHostName()), host, null, null); + final String ipHostname = sampleTlsEndpoint.getAddress().getHostAddress(); + InetSocketAddress host = new InetSocketAddress(ipHostname, 443); + @Cleanup Socket socket = factory.connectSocket(0, null, new HttpHost(ipHostname), host, null, null); fail(); - } catch (IOException e) { - assertFalse(ExceptionUtils.indexOfType(e, SSLPeerUnverifiedException.class) == -1); + } catch (SSLException e) { + Log.i(TAG, "Expected exception", e); + assertFalse(ExceptionUtils.indexOfType(e, SSLException.class) == -1); } } - public void testUntrustedCertificate() { + public void testUntrustedCertificate() throws IOException { try { // host with certificate that is not trusted by default InetSocketAddress host = new InetSocketAddress("cacert.org", 443); - @Cleanup SSLSocket socket = factory.connectSocket(0, null, new HttpHost(host.getHostName()), host, null, null); + @Cleanup Socket socket = factory.connectSocket(0, null, new HttpHost(host.getHostName()), host, null, null); fail(); - } catch (IOException e) { + } catch (SSLHandshakeException e) { + Log.i(TAG, "Expected exception", e); assertFalse(ExceptionUtils.indexOfType(e, CertPathValidatorException.class) == -1); } } diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavHttpClient.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavHttpClient.java index 55baf356..e3c3f9a3 100644 --- a/app/src/main/java/at/bitfire/davdroid/webdav/DavHttpClient.java +++ b/app/src/main/java/at/bitfire/davdroid/webdav/DavHttpClient.java @@ -31,7 +31,7 @@ public class DavHttpClient { static { socketFactoryRegistry = RegistryBuilder. create() .register("http", PlainConnectionSocketFactory.getSocketFactory()) - .register("https", TlsSniSocketFactory.INSTANCE) + .register("https", TlsSniSocketFactory.getSocketFactory()) .build(); // use request defaults from AndroidHttpClient diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/TlsSniSocketFactory.java b/app/src/main/java/at/bitfire/davdroid/webdav/TlsSniSocketFactory.java index 1aa749e3..ceadc3b7 100644 --- a/app/src/main/java/at/bitfire/davdroid/webdav/TlsSniSocketFactory.java +++ b/app/src/main/java/at/bitfire/davdroid/webdav/TlsSniSocketFactory.java @@ -7,204 +7,99 @@ */ package at.bitfire.davdroid.webdav; -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.net.SSLCertificateSocketFactory; import android.os.Build; import android.util.Log; import org.apache.commons.lang.StringUtils; -import org.apache.http.HttpHost; -import org.apache.http.conn.socket.LayeredConnectionSocketFactory; import org.apache.http.conn.ssl.BrowserCompatHostnameVerifierHC4; -import org.apache.http.protocol.HttpContext; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.conn.ssl.X509HostnameVerifier; import java.io.IOException; -import java.net.InetSocketAddress; -import java.net.Socket; import java.util.Arrays; import java.util.HashSet; import java.util.LinkedList; import java.util.List; -import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.SSLPeerUnverifiedException; -import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; -public class TlsSniSocketFactory implements LayeredConnectionSocketFactory { +import lombok.Cleanup; + +public class TlsSniSocketFactory extends SSLConnectionSocketFactory { private static final String TAG = "davdroid.TlsSniSocketFactory"; - - public final static TlsSniSocketFactory INSTANCE = new TlsSniSocketFactory(); - private final static SSLSocketFactory sslSocketFactory = (SSLSocketFactory)SSLSocketFactory.getDefault(); - - // use BrowserCompatHostnameVerifier to allow IP addresses in the Common Name - private final static HostnameVerifier hostnameVerifier = new BrowserCompatHostnameVerifierHC4(); - - - /* - For TLS connections without HTTPS (CONNECT) proxy: - 1) socket = createSocket() is called - 2) connectSocket(socket) is called which creates a new TLS connection (but no handshake yet) - 3) reasonable encryption settings are applied to socket - 4) SNI is set up for socket - 5) handshake and certificate/host name verification - - Layered sockets are used with HTTPS (CONNECT) proxies: - 1) plain = createSocket() is called - 2) the plain socket is connected to http://proxy:8080 - 3) a CONNECT request is sent to the proxy and the response is parsed - 4) socket = createLayeredSocket(plain) is called to "upgrade" the plain connection to a TLS connection (but no handshake yet) - 5) reasonable encryption settings are applied to socket - 6) SNI is set up for socket - 7) handshake and certificate/host name verification - */ - - @Override - public SSLSocket createSocket(HttpContext context) throws IOException { - return (SSLSocket)sslSocketFactory.createSocket(); + public static TlsSniSocketFactory getSocketFactory() { + return new TlsSniSocketFactory( + (SSLSocketFactory) SSLSocketFactory.getDefault(), + new BrowserCompatHostnameVerifierHC4() // use BrowserCompatHostnameVerifier to allow IP addresses in the Common Name + ); } - @Override - public SSLSocket connectSocket(int timeout, Socket sock, HttpHost host, InetSocketAddress remoteAddr, InetSocketAddress localAddr, HttpContext context) throws IOException { - Log.d(TAG, "Establishing direct TLS connection to " + host); - final SSLSocket socket = (sock != null) ? (SSLSocket)sock : createSocket(context); + // 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 + static String protocols[] = null, cipherSuites[] = null; + static { + try { + @Cleanup SSLSocket socket = (SSLSocket)SSLSocketFactory.getDefault().createSocket(); - if (localAddr != null) - socket.bind(localAddr); + /* 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 + List protocols = new LinkedList(); + for (String protocol : socket.getSupportedProtocols()) + if (!protocol.toUpperCase().contains("SSL")) + protocols.add(protocol); + Log.v(TAG, "Setting allowed TLS protocols: " + StringUtils.join(protocols, ", ")); + TlsSniSocketFactory.protocols = protocols.toArray(new String[0]); - // connect the socket on TCP level - socket.connect(remoteAddr, timeout); + /* set reasonable cipher suites */ + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + // choose secure cipher suites + List allowedCiphers = 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", + }); - // establish and verify TLS connection - establishAndVerify(socket, host.getHostName()); - return socket; - } + List availableCiphers = Arrays.asList(socket.getSupportedCipherSuites()); - @Override - public SSLSocket createLayeredSocket(Socket plain, String host, int port, HttpContext context) throws IOException { - Log.d(TAG, "Establishing layered TLS connection to " + host); + // preferred ciphers = allowed Ciphers \ availableCiphers + HashSet preferredCiphers = new HashSet(allowedCiphers); + preferredCiphers.retainAll(availableCiphers); - // create new socket for TLS connection on top of existing socket - final SSLSocket socket = (SSLSocket)sslSocketFactory.createSocket(plain, host, port, true); + // add preferred ciphers to enabled ciphers + // for maximum security, preferred ciphers should *replace* enabled ciphers, + // but I guess for the security level of DAVdroid, disabling of insecure + // ciphers should be a server-side task + HashSet enabledCiphers = preferredCiphers; + enabledCiphers.addAll(new HashSet(Arrays.asList(socket.getEnabledCipherSuites()))); - // establish and verify TLS connection - establishAndVerify(socket, host); - return socket; - } - - - /** - * 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 SSLPeerUnverifiedException - */ - private void establishAndVerify(SSLSocket socket, String host) throws IOException, SSLPeerUnverifiedException { - 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 inavlid host names - throw new SSLPeerUnverifiedException(host); - - Log.d(TAG, "Established " + session.getProtocol() + " connection with " + session.getPeerHost() + - " using " + session.getCipherSuite()); - } - - - /** - * Prepares a TLS/SSL connection socket by: - * - setting the default TrustManager (as we have created an "insecure" connection to avoid handshake problems before) - * - setting reasonable TLS protocol versions - * - setting reasonable cipher suites (if required) - * @param socket unconnected SSLSocket to prepare - */ - @SuppressLint("DefaultLocale") - 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 - List protocols = new LinkedList(); - for (String protocol : socket.getSupportedProtocols()) - if (!protocol.toUpperCase().contains("SSL")) - protocols.add(protocol); - Log.v(TAG, "Setting allowed TLS protocols: " + StringUtils.join(protocols, ", ")); - socket.setEnabledProtocols(protocols.toArray(new String[0])); - - /* set reasonable cipher suites */ - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - // choose secure cipher suites - List allowedCiphers = 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", - }); - - List availableCiphers = Arrays.asList(socket.getSupportedCipherSuites()); - - // preferred ciphers = allowed Ciphers \ availableCiphers - HashSet preferredCiphers = new HashSet(allowedCiphers); - preferredCiphers.retainAll(availableCiphers); - - // add preferred ciphers to enabled ciphers - // for maximum security, preferred ciphers should *replace* enabled ciphers, - // but I guess for the security level of DAVdroid, disabling of insecure - // ciphers should be a server-side task - HashSet enabledCiphers = preferredCiphers; - enabledCiphers.addAll(new HashSet(Arrays.asList(socket.getEnabledCipherSuites()))); - - Log.v(TAG, "Setting allowed TLS ciphers: " + StringUtils.join(enabledCiphers, ", ")); - socket.setEnabledCipherSuites(enabledCiphers.toArray(new String[0])); - } - } - - @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) - private void setSniHostname(SSLSocket socket, String hostName) { - // set SNI host name - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && sslSocketFactory instanceof SSLCertificateSocketFactory) { - Log.d(TAG, "Using documented SNI with host name " + hostName); - ((SSLCertificateSocketFactory)sslSocketFactory).setHostname(socket, hostName); - } else { - Log.d(TAG, "No documented SNI support on Android <4.2, trying reflection method with host name " + hostName); - try { - java.lang.reflect.Method setHostnameMethod = socket.getClass().getMethod("setHostname", String.class); - setHostnameMethod.invoke(socket, hostName); - } catch (Exception e) { - Log.w(TAG, "SNI not useable", e); + Log.v(TAG, "Setting allowed TLS ciphers: " + StringUtils.join(enabledCiphers, ", ")); + TlsSniSocketFactory.cipherSuites = enabledCiphers.toArray(new String[0]); } + } catch (IOException e) { } } - + + public TlsSniSocketFactory(SSLSocketFactory socketfactory, X509HostnameVerifier hostnameVerifier) { + super(socketfactory, protocols, cipherSuites, hostnameVerifier); + } + }