Home | History | Annotate | Download | only in utility
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.emailcommon.utility;
     18 
     19 import android.content.Context;
     20 import android.net.SSLCertificateSocketFactory;
     21 import android.security.KeyChain;
     22 import android.security.KeyChainException;
     23 import android.util.Log;
     24 
     25 import com.google.common.annotations.VisibleForTesting;
     26 
     27 import java.net.InetAddress;
     28 import java.net.Socket;
     29 import java.security.Principal;
     30 import java.security.PrivateKey;
     31 import java.security.cert.CertificateException;
     32 import java.security.cert.X509Certificate;
     33 import java.util.Arrays;
     34 
     35 import javax.net.ssl.KeyManager;
     36 import javax.net.ssl.X509ExtendedKeyManager;
     37 
     38 public class SSLUtils {
     39     private static SSLCertificateSocketFactory sInsecureFactory;
     40     private static SSLCertificateSocketFactory sSecureFactory;
     41 
     42     private static final boolean LOG_ENABLED = false;
     43     private static final String TAG = "Email.Ssl";
     44 
     45     /**
     46      * Returns a {@link javax.net.ssl.SSLSocketFactory}.
     47      * Optionally bypass all SSL certificate checks.
     48      *
     49      * @param insecure if true, bypass all SSL certificate checks
     50      * @param timeout the timeout value in milliseconds or {@code 0} for an infinite timeout.
     51      */
     52     public synchronized static SSLCertificateSocketFactory getSSLSocketFactory(
     53             boolean insecure, int timeout) {
     54         if (insecure) {
     55             if (sInsecureFactory == null) {
     56                 sInsecureFactory = (SSLCertificateSocketFactory)
     57                         SSLCertificateSocketFactory.getInsecure(timeout, null);
     58             }
     59             return sInsecureFactory;
     60         } else {
     61             if (sSecureFactory == null) {
     62                 sSecureFactory = (SSLCertificateSocketFactory)
     63                         SSLCertificateSocketFactory.getDefault(timeout, null);
     64             }
     65             return sSecureFactory;
     66         }
     67     }
     68 
     69     /**
     70      * Returns a {@link org.apache.http.conn.ssl.SSLSocketFactory SSLSocketFactory} for use with the
     71      * Apache HTTP stack.
     72      */
     73     public static SSLSocketFactory getHttpSocketFactory(boolean insecure, KeyManager keyManager) {
     74         SSLCertificateSocketFactory underlying = getSSLSocketFactory(insecure, 0 /* no timeout */);
     75         if (keyManager != null) {
     76             underlying.setKeyManagers(new KeyManager[] { keyManager });
     77         }
     78         SSLSocketFactory wrapped = new SSLSocketFactory(underlying);
     79         if (insecure) {
     80             wrapped.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
     81         }
     82         return wrapped;
     83     }
     84 
     85     /**
     86      * Escapes the contents a string to be used as a safe scheme name in the URI according to
     87      * http://tools.ietf.org/html/rfc3986#section-3.1
     88      *
     89      * This does not ensure that the first character is a letter (which is required by the RFC).
     90      */
     91     @VisibleForTesting
     92     public static String escapeForSchemeName(String s) {
     93         // According to the RFC, scheme names are case-insensitive.
     94         s = s.toLowerCase();
     95 
     96         StringBuilder sb = new StringBuilder();
     97         for (int i = 0; i < s.length(); i++) {
     98             char c = s.charAt(i);
     99             if (Character.isLetter(c) || Character.isDigit(c)
    100                     || ('-' == c) || ('.' == c)) {
    101                 // Safe - use as is.
    102                 sb.append(c);
    103             } else if ('+' == c) {
    104                 // + is used as our escape character, so double it up.
    105                 sb.append("++");
    106             } else {
    107                 // Unsafe - escape.
    108                 sb.append('+').append((int) c);
    109             }
    110         }
    111         return sb.toString();
    112     }
    113 
    114     private static abstract class StubKeyManager extends X509ExtendedKeyManager {
    115         @Override public abstract String chooseClientAlias(
    116                 String[] keyTypes, Principal[] issuers, Socket socket);
    117 
    118         @Override public abstract X509Certificate[] getCertificateChain(String alias);
    119 
    120         @Override public abstract PrivateKey getPrivateKey(String alias);
    121 
    122 
    123         // The following methods are unused.
    124 
    125         @Override
    126         public final String chooseServerAlias(
    127                 String keyType, Principal[] issuers, Socket socket) {
    128             // not a client SSLSocket callback
    129             throw new UnsupportedOperationException();
    130         }
    131 
    132         @Override
    133         public final String[] getClientAliases(String keyType, Principal[] issuers) {
    134             // not a client SSLSocket callback
    135             throw new UnsupportedOperationException();
    136         }
    137 
    138         @Override
    139         public final String[] getServerAliases(String keyType, Principal[] issuers) {
    140             // not a client SSLSocket callback
    141             throw new UnsupportedOperationException();
    142         }
    143     }
    144 
    145     /**
    146      * A dummy {@link KeyManager} which keeps track of the last time a server has requested
    147      * a client certificate.
    148      */
    149     public static class TrackingKeyManager extends StubKeyManager {
    150         private volatile long mLastTimeCertRequested = 0L;
    151 
    152         @Override
    153         public String chooseClientAlias(String[] keyTypes, Principal[] issuers, Socket socket) {
    154             if (LOG_ENABLED) {
    155                 InetAddress address = socket.getInetAddress();
    156                 Log.i(TAG, "TrackingKeyManager: requesting a client cert alias for "
    157                         + address.getCanonicalHostName());
    158             }
    159             mLastTimeCertRequested = System.currentTimeMillis();
    160             return null;
    161         }
    162 
    163         @Override
    164         public X509Certificate[] getCertificateChain(String alias) {
    165             if (LOG_ENABLED) {
    166                 Log.i(TAG, "TrackingKeyManager: returning a null cert chain");
    167             }
    168             return null;
    169         }
    170 
    171         @Override
    172         public PrivateKey getPrivateKey(String alias) {
    173             if (LOG_ENABLED) {
    174                 Log.i(TAG, "TrackingKeyManager: returning a null private key");
    175             }
    176             return null;
    177         }
    178 
    179         /**
    180          * @return the last time that this {@link KeyManager} detected a request by a server
    181          *     for a client certificate (in millis since epoch).
    182          */
    183         public long getLastCertReqTime() {
    184             return mLastTimeCertRequested;
    185         }
    186     }
    187 
    188     /**
    189      * A {@link KeyManager} that reads uses credentials stored in the system {@link KeyChain}.
    190      */
    191     public static class KeyChainKeyManager extends StubKeyManager {
    192         private final String mClientAlias;
    193         private final X509Certificate[] mCertificateChain;
    194         private final PrivateKey mPrivateKey;
    195 
    196         /**
    197          * Builds an instance of a KeyChainKeyManager using the given certificate alias.
    198          * If for any reason retrieval of the credentials from the system {@link KeyChain} fails,
    199          * a {@code null} value will be returned.
    200          */
    201         public static KeyChainKeyManager fromAlias(Context context, String alias)
    202                 throws CertificateException {
    203             X509Certificate[] certificateChain;
    204             try {
    205                 certificateChain = KeyChain.getCertificateChain(context, alias);
    206             } catch (KeyChainException e) {
    207                 logError(alias, "certificate chain", e);
    208                 throw new CertificateException(e);
    209             } catch (InterruptedException e) {
    210                 logError(alias, "certificate chain", e);
    211                 throw new CertificateException(e);
    212             }
    213 
    214             PrivateKey privateKey;
    215             try {
    216                 privateKey = KeyChain.getPrivateKey(context, alias);
    217             } catch (KeyChainException e) {
    218                 logError(alias, "private key", e);
    219                 throw new CertificateException(e);
    220             } catch (InterruptedException e) {
    221                 logError(alias, "private key", e);
    222                 throw new CertificateException(e);
    223             }
    224 
    225             if (certificateChain == null || privateKey == null) {
    226                 throw new CertificateException("Can't access certificate from keystore");
    227             }
    228 
    229             return new KeyChainKeyManager(alias, certificateChain, privateKey);
    230         }
    231 
    232         private static void logError(String alias, String type, Exception ex) {
    233             // Avoid logging PII when explicit logging is not on.
    234             if (LOG_ENABLED) {
    235                 Log.e(TAG, "Unable to retrieve " + type + " for [" + alias + "] due to " + ex);
    236             } else {
    237                 Log.e(TAG, "Unable to retrieve " + type + " due to " + ex);
    238             }
    239         }
    240 
    241         private KeyChainKeyManager(
    242                 String clientAlias, X509Certificate[] certificateChain, PrivateKey privateKey) {
    243             mClientAlias = clientAlias;
    244             mCertificateChain = certificateChain;
    245             mPrivateKey = privateKey;
    246         }
    247 
    248 
    249         @Override
    250         public String chooseClientAlias(String[] keyTypes, Principal[] issuers, Socket socket) {
    251             if (LOG_ENABLED) {
    252                 Log.i(TAG, "Requesting a client cert alias for " + Arrays.toString(keyTypes));
    253             }
    254             return mClientAlias;
    255         }
    256 
    257         @Override
    258         public X509Certificate[] getCertificateChain(String alias) {
    259             if (LOG_ENABLED) {
    260                 Log.i(TAG, "Requesting a client certificate chain for alias [" + alias + "]");
    261             }
    262             return mCertificateChain;
    263         }
    264 
    265         @Override
    266         public PrivateKey getPrivateKey(String alias) {
    267             if (LOG_ENABLED) {
    268                 Log.i(TAG, "Requesting a client private key for alias [" + alias + "]");
    269             }
    270             return mPrivateKey;
    271         }
    272     }
    273 }
    274