diff --git a/emailcommon/src/com/android/emailcommon/utility/SSLSocketFactoryWrapper.java b/emailcommon/src/com/android/emailcommon/utility/SSLSocketFactoryWrapper.java new file mode 100644 index 000000000..516084fe4 --- /dev/null +++ b/emailcommon/src/com/android/emailcommon/utility/SSLSocketFactoryWrapper.java @@ -0,0 +1,258 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.emailcommon.utility; + +import com.android.mail.utils.LogUtils; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.net.InetAddress; +import java.net.Socket; +import java.net.UnknownHostException; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.net.ssl.*; +import javax.net.ssl.SSLSocketFactory; + +public class SSLSocketFactoryWrapper extends javax.net.ssl.SSLSocketFactory { + private final SSLSocketFactory mFactory; + private final boolean mSecure; + private final int mHandshakeTimeout; + private final String[] mDefaultCipherSuites; + + private final String[] DEPRECATED_CIPHER_SUITES_TO_ENABLE = new String[] { + "TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA", + "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA", + "SSL_DHE_DSS_WITH_3DES_EDE_CBC_SHA", + "SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA", + "SSL_RSA_WITH_3DES_EDE_CBC_SHA", + "SSL_RSA_WITH_RC4_128_MD5", + "TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA", + "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA", + "TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA", + "TLS_ECDH_ECDSA_WITH_RC4_128_SHA", + "TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA", + "TLS_ECDH_RSA_WITH_AES_128_CBC_SHA", + "TLS_ECDH_RSA_WITH_AES_256_CBC_SHA", + "TLS_ECDH_RSA_WITH_RC4_128_SHA", + "SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA", + "SSL_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA", + "SSL_RSA_EXPORT_WITH_DES40_CBC_SHA", + "SSL_RSA_EXPORT_WITH_RC4_40_MD5", + "SSL_DHE_DSS_WITH_DES_CBC_SHA", + "SSL_DHE_RSA_WITH_DES_CBC_SHA", + "SSL_RSA_WITH_DES_CBC_SHA" + }; + + SSLSocketFactoryWrapper(final SSLSocketFactory factory, final boolean secure, + int handshakeTimeout) { + mFactory = factory; + mSecure = secure; + mHandshakeTimeout = handshakeTimeout; + + // Find the base factory's list of defaultCipherSuites, and merge our extras with it. + // Remember that the order is important. We'll add our extras at the end, and only + // if they weren't already in the base factory's list. + final String[] baseDefaultCipherSuites = mFactory.getDefaultCipherSuites(); + final List fullCipherSuiteList = new ArrayList(Arrays.asList( + mFactory.getDefaultCipherSuites())); + final Set baseDefaultCipherSuiteSet = new HashSet(fullCipherSuiteList); + + final String[] baseSupportedCipherSuites = mFactory.getSupportedCipherSuites(); + final Set baseSupportedCipherSuiteSet = new HashSet(Arrays.asList( + mFactory.getSupportedCipherSuites())); + + for (String cipherSuite : DEPRECATED_CIPHER_SUITES_TO_ENABLE) { + if (baseSupportedCipherSuiteSet.contains(cipherSuite) && + !baseDefaultCipherSuiteSet.contains(cipherSuite)) { + fullCipherSuiteList.add(cipherSuite); + } + } + mDefaultCipherSuites = new String[fullCipherSuiteList.size()]; + fullCipherSuiteList.toArray(mDefaultCipherSuites); + } + + public static SSLSocketFactory getDefault(final KeyManager[] keyManagers, int handshakeTimeout) + throws NoSuchAlgorithmException, KeyManagementException{ + final SSLContext context = SSLContext.getInstance("TLS"); + context.init(keyManagers, null, null); + return new SSLSocketFactoryWrapper(context.getSocketFactory(), true, handshakeTimeout); + } + + public static SSLSocketFactory getInsecure(final KeyManager[] keyManagers, + final TrustManager[] trustManagers, + int handshakeTimeout) + throws NoSuchAlgorithmException, KeyManagementException { + final SSLContext context = SSLContext.getInstance("TLS"); + context.init(keyManagers, trustManagers, null); + return new SSLSocketFactoryWrapper(context.getSocketFactory(), false, handshakeTimeout); + } + + public Socket createSocket()throws IOException { + return mFactory.createSocket(); + } + + public Socket createSocket(final Socket socket, final String host, final int port, + final boolean autoClose) throws IOException { + final SSLSocket sslSocket = (SSLSocket)mFactory.createSocket(socket, host, port, autoClose); + setHandshakeTimeout(sslSocket, mHandshakeTimeout); + sslSocket.setEnabledCipherSuites(mDefaultCipherSuites); + if (mSecure) { + verifyHostname(sslSocket, host); + } + return sslSocket; + } + + @Override + public Socket createSocket(String host, int port) throws IOException, UnknownHostException { + final SSLSocket sslSocket = (SSLSocket)mFactory.createSocket(host, port); + setHandshakeTimeout(sslSocket, mHandshakeTimeout); + sslSocket.setEnabledCipherSuites(mDefaultCipherSuites); + if (mSecure) { + verifyHostname(sslSocket, host); + } + return sslSocket; + } + + @Override + public Socket createSocket(String host, int i, InetAddress inetAddress, int i2) throws + IOException, UnknownHostException { + final SSLSocket sslSocket = (SSLSocket)mFactory.createSocket(host, i, inetAddress, i2); + setHandshakeTimeout(sslSocket, mHandshakeTimeout); + sslSocket.setEnabledCipherSuites(mDefaultCipherSuites); + if (mSecure) { + verifyHostname(sslSocket, host); + } + return sslSocket; + } + + @Override + public Socket createSocket(InetAddress inetAddress, int i) throws IOException { + final SSLSocket sslSocket = (SSLSocket)mFactory.createSocket(inetAddress, i); + setHandshakeTimeout(sslSocket, mHandshakeTimeout); + sslSocket.setEnabledCipherSuites(mDefaultCipherSuites); + return sslSocket; + } + + @Override + public Socket createSocket(InetAddress inetAddress, int i, InetAddress inetAddress2, int i2) + throws IOException { + final SSLSocket sslSocket = (SSLSocket)mFactory.createSocket(inetAddress, i, inetAddress2, + i2); + setHandshakeTimeout(sslSocket, mHandshakeTimeout); + sslSocket.setEnabledCipherSuites(mDefaultCipherSuites); + return sslSocket; + } + + public String[] getDefaultCipherSuites() { + return mDefaultCipherSuites.clone(); + } + + public String[] getSupportedCipherSuites() { + return mFactory.getSupportedCipherSuites(); + } + + /** + * Attempt to set the hostname of the socket. + * @param sslSocket The SSLSocket + * @param hostname the hostname + * @return true if able to set the hostname, false if not. + */ + public static boolean potentiallyEnableSni(SSLSocket sslSocket, String hostname) { + try { + // Many implementations of SSLSocket support setHostname, although it is not part of + // the class definition. We will attempt to setHostname using reflection. If the + // particular SSLSocket implementation we are using does not support this meethod, + // we'll fail and return false. + sslSocket.getClass().getMethod("setHostname", String.class).invoke(sslSocket, hostname); + return true; + } catch (Exception ignored) { + return false; + } + } + + /** + * Attempt to enable session tickets. + * @param sslSocket the SSLSocket. + * @return true if able to enable session tickets, false otherwise. + */ + public static boolean potentiallyEnableSessionTickets(SSLSocket sslSocket) { + try { + // Many implementations of SSLSocket support setUseSessionTickets, although it is not + // part of the class definition. We will attempt to setHostname using reflection. If the + // particular SSLSocket implementation we are using does not support this meethod, + // we'll fail and return false. + sslSocket.getClass().getMethod("setUseSessionTickets", boolean.class) + .invoke(sslSocket, true); + return true; + } catch (Exception e) { + return false; + } + } + + /** + * Verify the hostname of the certificate used by the other end of a + * connected socket. You MUST call this if you did not supply a hostname + * to {@link #createSocket()}. It is harmless to call this method + * redundantly if the hostname has already been verified. + * + * @param socket An SSL socket which has been connected to a server + * @param hostname The expected hostname of the remote server + * @throws IOException if something goes wrong handshaking with the server + * @throws SSLPeerUnverifiedException if the server cannot prove its identity + * + * @hide + */ + public static void verifyHostname(Socket socket, String hostname) throws IOException { + if (!(socket instanceof SSLSocket)) { + throw new IllegalArgumentException("Attempt to verify non-SSL socket"); + } + + // The code at the start of OpenSSLSocketImpl.startHandshake() + // ensures that the call is idempotent, so we can safely call it. + SSLSocket ssl = (SSLSocket) socket; + ssl.startHandshake(); + + SSLSession session = ssl.getSession(); + LogUtils.d(LogUtils.TAG, "using cipherSuite %s", session.getCipherSuite()); + if (session == null) { + throw new SSLException("Cannot verify SSL socket without session"); + } + if (!HttpsURLConnection.getDefaultHostnameVerifier().verify(hostname, session)) { + throw new SSLPeerUnverifiedException("Cannot verify hostname: " + hostname); + } + } + + private void setHandshakeTimeout(SSLSocket sslSocket, int timeout) { + try { + // Most implementations of SSLSocket support setHandshakeTimeout(), but it is not + // actually part of the class definition. We will attempt to set it using reflection. + // If the particular implementation of SSLSocket we are using does not support this + // function, then we will just have to use the default handshake timeout. + sslSocket.getClass().getMethod("setHandshakeTimeout", int.class).invoke(sslSocket, + timeout); + } catch (Exception e) { + LogUtils.w(LogUtils.TAG, e, "unable to set handshake timeout"); + } + } +} diff --git a/emailcommon/src/com/android/emailcommon/utility/SSLUtils.java b/emailcommon/src/com/android/emailcommon/utility/SSLUtils.java index 779bc5296..f42e83c6e 100644 --- a/emailcommon/src/com/android/emailcommon/utility/SSLUtils.java +++ b/emailcommon/src/com/android/emailcommon/utility/SSLUtils.java @@ -20,8 +20,6 @@ import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; -import android.net.SSLCertificateSocketFactory; -import android.net.SSLSessionCache; import android.security.KeyChain; import android.security.KeyChainException; @@ -34,6 +32,8 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.net.InetAddress; import java.net.Socket; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; import java.security.Principal; import java.security.PrivateKey; import java.security.PublicKey; @@ -159,43 +159,33 @@ public class SSLUtils { public synchronized static javax.net.ssl.SSLSocketFactory getSSLSocketFactory( final Context context, final HostAuth hostAuth, final KeyManager keyManager, final boolean insecure) { - if (insecure) { - final SSLCertificateSocketFactory insecureFactory = (SSLCertificateSocketFactory) - SSLCertificateSocketFactory.getInsecure(SSL_HANDSHAKE_TIMEOUT, null); - insecureFactory.setTrustManagers( - new TrustManager[] { - new SameCertificateCheckingTrustManager(context, hostAuth)}); - if (keyManager != null) { - insecureFactory.setKeyManagers(new KeyManager[] { keyManager }); + try { + final KeyManager[] keyManagers = (keyManager == null ? null : + new KeyManager[]{keyManager}); + if (insecure) { + final TrustManager[] trustManagers = new TrustManager[]{ + new SameCertificateCheckingTrustManager(context, hostAuth)}; + SSLSocketFactoryWrapper insecureFactory = + (SSLSocketFactoryWrapper) SSLSocketFactoryWrapper.getInsecure( + keyManagers, trustManagers, SSL_HANDSHAKE_TIMEOUT); + return insecureFactory; + } else { + if (sSecureFactory == null) { + SSLSocketFactoryWrapper secureFactory = + (SSLSocketFactoryWrapper) SSLSocketFactoryWrapper.getDefault( + keyManagers, SSL_HANDSHAKE_TIMEOUT); + sSecureFactory = secureFactory; + } + return sSecureFactory; } - return insecureFactory; - } else { - if (sSecureFactory == null) { - // First try to get use an externally supplied, more secure SSLSocketBuilder. - // If so we should use that. - javax.net.ssl.SSLSocketFactory socketFactory = null; - if (sExternalSocketFactoryBuilder != null) { - socketFactory = sExternalSocketFactoryBuilder.createSecureSocketFactory( - context, SSL_HANDSHAKE_TIMEOUT); - } - if (socketFactory != null) { - sSecureFactory = socketFactory; - LogUtils.d(TAG, "Using externally created CertificateSocketFactory"); - return sSecureFactory; - } - // Only fall back to the platform one if that fails. - LogUtils.d(TAG, "Falling back to platform CertificateSocketFactory"); - final SSLCertificateSocketFactory certificateSocketFactory = - (SSLCertificateSocketFactory) - SSLCertificateSocketFactory.getDefault(SSL_HANDSHAKE_TIMEOUT, - new SSLSessionCache(context)); - if (keyManager != null) { - certificateSocketFactory.setKeyManagers(new KeyManager[] { keyManager }); - } - sSecureFactory = certificateSocketFactory; - } - return sSecureFactory; + } catch (NoSuchAlgorithmException e) { + LogUtils.wtf(TAG, e, "Unable to acquire SSLSocketFactory"); + // TODO: what can we do about this? + } catch (KeyManagementException e) { + LogUtils.wtf(TAG, e, "Unable to acquire SSLSocketFactory"); + // TODO: what can we do about this? } + return null; } /**