| /* |
| * 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<String> fullCipherSuiteList = new ArrayList<String>(Arrays.asList( |
| mFactory.getDefaultCipherSuites())); |
| final Set<String> baseDefaultCipherSuiteSet = new HashSet<String>(fullCipherSuiteList); |
| |
| final String[] baseSupportedCipherSuites = mFactory.getSupportedCipherSuites(); |
| final Set<String> baseSupportedCipherSuiteSet = new HashSet<String>(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(); |
| if (session == null) { |
| throw new SSLException("Cannot verify SSL socket without session"); |
| } |
| LogUtils.d(LogUtils.TAG, "using cipherSuite %s", session.getCipherSuite()); |
| 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"); |
| } |
| } |
| } |