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.ContentUris;
     20 import android.content.ContentValues;
     21 import android.content.Context;
     22 import android.database.Cursor;
     23 import android.security.KeyChain;
     24 import android.security.KeyChainException;
     25 
     26 import com.android.emailcommon.provider.EmailContent.HostAuthColumns;
     27 import com.android.emailcommon.provider.HostAuth;
     28 import com.android.mail.utils.LogUtils;
     29 import com.google.common.annotations.VisibleForTesting;
     30 
     31 import java.io.ByteArrayInputStream;
     32 import java.io.IOException;
     33 import java.net.InetAddress;
     34 import java.net.Socket;
     35 import java.security.KeyManagementException;
     36 import java.security.NoSuchAlgorithmException;
     37 import java.security.Principal;
     38 import java.security.PrivateKey;
     39 import java.security.PublicKey;
     40 import java.security.cert.Certificate;
     41 import java.security.cert.CertificateException;
     42 import java.security.cert.CertificateFactory;
     43 import java.security.cert.X509Certificate;
     44 import java.util.Arrays;
     45 
     46 import javax.net.ssl.KeyManager;
     47 import javax.net.ssl.TrustManager;
     48 import javax.net.ssl.X509ExtendedKeyManager;
     49 import javax.net.ssl.X509TrustManager;
     50 
     51 public class SSLUtils {
     52     // All secure factories are the same; all insecure factories are associated with HostAuth's
     53     private static javax.net.ssl.SSLSocketFactory sSecureFactory;
     54 
     55     private static final boolean LOG_ENABLED = false;
     56     private static final String TAG = "Email.Ssl";
     57 
     58     // A 30 second SSL handshake should be more than enough.
     59     private static final int SSL_HANDSHAKE_TIMEOUT = 30000;
     60 
     61     /**
     62      * A trust manager specific to a particular HostAuth.  The first time a server certificate is
     63      * encountered for the HostAuth, its certificate is saved; subsequent checks determine whether
     64      * the PublicKey of the certificate presented matches that of the saved certificate
     65      * TODO: UI to ask user about changed certificates
     66      */
     67     private static class SameCertificateCheckingTrustManager implements X509TrustManager {
     68         private final HostAuth mHostAuth;
     69         private final Context mContext;
     70         // The public key associated with the HostAuth; we'll lazily initialize it
     71         private PublicKey mPublicKey;
     72 
     73         SameCertificateCheckingTrustManager(Context context, HostAuth hostAuth) {
     74             mContext = context;
     75             mHostAuth = hostAuth;
     76             // We must load the server cert manually (the ContentCache won't handle blobs
     77             Cursor c = context.getContentResolver().query(HostAuth.CONTENT_URI,
     78                     new String[] {HostAuthColumns.SERVER_CERT}, HostAuthColumns._ID + "=?",
     79                     new String[] {Long.toString(hostAuth.mId)}, null);
     80             if (c != null) {
     81                 try {
     82                     if (c.moveToNext()) {
     83                         mHostAuth.mServerCert = c.getBlob(0);
     84                     }
     85                 } finally {
     86                     c.close();
     87                 }
     88             }
     89         }
     90 
     91         @Override
     92         public void checkClientTrusted(X509Certificate[] chain, String authType)
     93                 throws CertificateException {
     94             // We don't check client certificates
     95             throw new CertificateException("We don't check client certificates");
     96         }
     97 
     98         @Override
     99         public void checkServerTrusted(X509Certificate[] chain, String authType)
    100                 throws CertificateException {
    101             if (chain.length == 0) {
    102                 throw new CertificateException("No certificates?");
    103             } else {
    104                 X509Certificate serverCert = chain[0];
    105                 if (mHostAuth.mServerCert != null) {
    106                     // Compare with the current public key
    107                     if (mPublicKey == null) {
    108                         ByteArrayInputStream bais = new ByteArrayInputStream(mHostAuth.mServerCert);
    109                         Certificate storedCert =
    110                                 CertificateFactory.getInstance("X509").generateCertificate(bais);
    111                         mPublicKey = storedCert.getPublicKey();
    112                         try {
    113                             bais.close();
    114                         } catch (IOException e) {
    115                             // Yeah, right.
    116                         }
    117                     }
    118                     if (!mPublicKey.equals(serverCert.getPublicKey())) {
    119                         throw new CertificateException(
    120                                 "PublicKey has changed since initial connection!");
    121                     }
    122                 } else {
    123                     // First time; save this away
    124                     byte[] encodedCert = serverCert.getEncoded();
    125                     mHostAuth.mServerCert = encodedCert;
    126                     ContentValues values = new ContentValues();
    127                     values.put(HostAuthColumns.SERVER_CERT, encodedCert);
    128                     mContext.getContentResolver().update(
    129                             ContentUris.withAppendedId(HostAuth.CONTENT_URI, mHostAuth.mId),
    130                             values, null, null);
    131                 }
    132             }
    133         }
    134 
    135         @Override
    136         public X509Certificate[] getAcceptedIssuers() {
    137             return null;
    138         }
    139     }
    140 
    141     public static abstract class ExternalSecurityProviderInstaller {
    142         abstract public void installIfNeeded(final Context context);
    143     }
    144 
    145     private static ExternalSecurityProviderInstaller sExternalSecurityProviderInstaller;
    146 
    147     public static void setExternalSecurityProviderInstaller (
    148             ExternalSecurityProviderInstaller installer) {
    149         sExternalSecurityProviderInstaller = installer;
    150     }
    151 
    152     /**
    153      * Returns a {@link javax.net.ssl.SSLSocketFactory}.
    154      * Optionally bypass all SSL certificate checks.
    155      *
    156      * @param insecure if true, bypass all SSL certificate checks
    157      */
    158     public synchronized static javax.net.ssl.SSLSocketFactory getSSLSocketFactory(
    159             final Context context, final HostAuth hostAuth, final KeyManager keyManager,
    160             final boolean insecure) {
    161         // If we have an external security provider installer, then install. This will
    162         // potentially replace the default implementation of SSLSocketFactory.
    163         if (sExternalSecurityProviderInstaller != null) {
    164             sExternalSecurityProviderInstaller.installIfNeeded(context);
    165         }
    166         try {
    167             final KeyManager[] keyManagers = (keyManager == null ? null :
    168                     new KeyManager[]{keyManager});
    169             if (insecure) {
    170                 final TrustManager[] trustManagers = new TrustManager[]{
    171                         new SameCertificateCheckingTrustManager(context, hostAuth)};
    172                 SSLSocketFactoryWrapper insecureFactory =
    173                         (SSLSocketFactoryWrapper) SSLSocketFactoryWrapper.getInsecure(
    174                                 keyManagers, trustManagers, SSL_HANDSHAKE_TIMEOUT);
    175                 return insecureFactory;
    176             } else {
    177                 if (sSecureFactory == null) {
    178                     SSLSocketFactoryWrapper secureFactory =
    179                             (SSLSocketFactoryWrapper) SSLSocketFactoryWrapper.getDefault(
    180                                     keyManagers, SSL_HANDSHAKE_TIMEOUT);
    181                     sSecureFactory = secureFactory;
    182                 }
    183                 return sSecureFactory;
    184             }
    185         } catch (NoSuchAlgorithmException e) {
    186             LogUtils.wtf(TAG, e, "Unable to acquire SSLSocketFactory");
    187             // TODO: what can we do about this?
    188         } catch (KeyManagementException e) {
    189             LogUtils.wtf(TAG, e, "Unable to acquire SSLSocketFactory");
    190             // TODO: what can we do about this?
    191         }
    192         return null;
    193     }
    194 
    195     /**
    196      * Returns a com.android.emailcommon.utility.SSLSocketFactory
    197      */
    198     public static SSLSocketFactory getHttpSocketFactory(Context context, HostAuth hostAuth,
    199             KeyManager keyManager, boolean insecure) {
    200         javax.net.ssl.SSLSocketFactory underlying = getSSLSocketFactory(context, hostAuth,
    201                 keyManager, insecure);
    202         SSLSocketFactory wrapped = new SSLSocketFactory(underlying);
    203         if (insecure) {
    204             wrapped.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
    205         }
    206         return wrapped;
    207     }
    208 
    209     // Character.isLetter() is locale-specific, and will potentially return true for characters
    210     // outside of ascii a-z,A-Z
    211     private static boolean isAsciiLetter(char c) {
    212         return ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z');
    213     }
    214 
    215     // Character.isDigit() is locale-specific, and will potentially return true for characters
    216     // outside of ascii 0-9
    217     private static boolean isAsciiNumber(char c) {
    218         return ('0' <= c && c <= '9');
    219     }
    220 
    221     /**
    222      * Escapes the contents a string to be used as a safe scheme name in the URI according to
    223      * http://tools.ietf.org/html/rfc3986#section-3.1
    224      *
    225      * This does not ensure that the first character is a letter (which is required by the RFC).
    226      */
    227     @VisibleForTesting
    228     public static String escapeForSchemeName(String s) {
    229         // According to the RFC, scheme names are case-insensitive.
    230         s = s.toLowerCase();
    231 
    232         StringBuilder sb = new StringBuilder();
    233         for (int i = 0; i < s.length(); i++) {
    234             char c = s.charAt(i);
    235             if (isAsciiLetter(c) || isAsciiNumber(c)
    236                     || ('-' == c) || ('.' == c)) {
    237                 // Safe - use as is.
    238                 sb.append(c);
    239             } else if ('+' == c) {
    240                 // + is used as our escape character, so double it up.
    241                 sb.append("++");
    242             } else {
    243                 // Unsafe - escape.
    244                 sb.append('+').append((int) c);
    245             }
    246         }
    247         return sb.toString();
    248     }
    249 
    250     private static abstract class StubKeyManager extends X509ExtendedKeyManager {
    251         @Override public abstract String chooseClientAlias(
    252                 String[] keyTypes, Principal[] issuers, Socket socket);
    253 
    254         @Override public abstract X509Certificate[] getCertificateChain(String alias);
    255 
    256         @Override public abstract PrivateKey getPrivateKey(String alias);
    257 
    258 
    259         // The following methods are unused.
    260 
    261         @Override
    262         public final String chooseServerAlias(
    263                 String keyType, Principal[] issuers, Socket socket) {
    264             // not a client SSLSocket callback
    265             throw new UnsupportedOperationException();
    266         }
    267 
    268         @Override
    269         public final String[] getClientAliases(String keyType, Principal[] issuers) {
    270             // not a client SSLSocket callback
    271             throw new UnsupportedOperationException();
    272         }
    273 
    274         @Override
    275         public final String[] getServerAliases(String keyType, Principal[] issuers) {
    276             // not a client SSLSocket callback
    277             throw new UnsupportedOperationException();
    278         }
    279     }
    280 
    281     /**
    282      * A dummy {@link KeyManager} which keeps track of the last time a server has requested
    283      * a client certificate.
    284      */
    285     public static class TrackingKeyManager extends StubKeyManager {
    286         private volatile long mLastTimeCertRequested = 0L;
    287 
    288         @Override
    289         public String chooseClientAlias(String[] keyTypes, Principal[] issuers, Socket socket) {
    290             if (LOG_ENABLED) {
    291                 InetAddress address = socket.getInetAddress();
    292                 LogUtils.i(TAG, "TrackingKeyManager: requesting a client cert alias for "
    293                         + address.getCanonicalHostName());
    294             }
    295             mLastTimeCertRequested = System.currentTimeMillis();
    296             return null;
    297         }
    298 
    299         @Override
    300         public X509Certificate[] getCertificateChain(String alias) {
    301             if (LOG_ENABLED) {
    302                 LogUtils.i(TAG, "TrackingKeyManager: returning a null cert chain");
    303             }
    304             return null;
    305         }
    306 
    307         @Override
    308         public PrivateKey getPrivateKey(String alias) {
    309             if (LOG_ENABLED) {
    310                 LogUtils.i(TAG, "TrackingKeyManager: returning a null private key");
    311             }
    312             return null;
    313         }
    314 
    315         /**
    316          * @return the last time that this {@link KeyManager} detected a request by a server
    317          *     for a client certificate (in millis since epoch).
    318          */
    319         public long getLastCertReqTime() {
    320             return mLastTimeCertRequested;
    321         }
    322     }
    323 
    324     /**
    325      * A {@link KeyManager} that reads uses credentials stored in the system {@link KeyChain}.
    326      */
    327     public static class KeyChainKeyManager extends StubKeyManager {
    328         private final String mClientAlias;
    329         private final X509Certificate[] mCertificateChain;
    330         private final PrivateKey mPrivateKey;
    331 
    332         /**
    333          * Builds an instance of a KeyChainKeyManager using the given certificate alias.
    334          * If for any reason retrieval of the credentials from the system {@link KeyChain} fails,
    335          * a {@code null} value will be returned.
    336          */
    337         public static KeyChainKeyManager fromAlias(Context context, String alias)
    338                 throws CertificateException {
    339             X509Certificate[] certificateChain;
    340             try {
    341                 certificateChain = KeyChain.getCertificateChain(context, alias);
    342             } catch (KeyChainException e) {
    343                 logError(alias, "certificate chain", e);
    344                 throw new CertificateException(e);
    345             } catch (InterruptedException e) {
    346                 logError(alias, "certificate chain", e);
    347                 throw new CertificateException(e);
    348             }
    349 
    350             PrivateKey privateKey;
    351             try {
    352                 privateKey = KeyChain.getPrivateKey(context, alias);
    353             } catch (KeyChainException e) {
    354                 logError(alias, "private key", e);
    355                 throw new CertificateException(e);
    356             } catch (InterruptedException e) {
    357                 logError(alias, "private key", e);
    358                 throw new CertificateException(e);
    359             }
    360 
    361             if (certificateChain == null || privateKey == null) {
    362                 throw new CertificateException("Can't access certificate from keystore");
    363             }
    364 
    365             return new KeyChainKeyManager(alias, certificateChain, privateKey);
    366         }
    367 
    368         private static void logError(String alias, String type, Exception ex) {
    369             // Avoid logging PII when explicit logging is not on.
    370             if (LOG_ENABLED) {
    371                 LogUtils.e(TAG, "Unable to retrieve " + type + " for [" + alias + "] due to " + ex);
    372             } else {
    373                 LogUtils.e(TAG, "Unable to retrieve " + type + " due to " + ex);
    374             }
    375         }
    376 
    377         private KeyChainKeyManager(
    378                 String clientAlias, X509Certificate[] certificateChain, PrivateKey privateKey) {
    379             mClientAlias = clientAlias;
    380             mCertificateChain = certificateChain;
    381             mPrivateKey = privateKey;
    382         }
    383 
    384 
    385         @Override
    386         public String chooseClientAlias(String[] keyTypes, Principal[] issuers, Socket socket) {
    387             if (LOG_ENABLED) {
    388                 LogUtils.i(TAG, "Requesting a client cert alias for " + Arrays.toString(keyTypes));
    389             }
    390             return mClientAlias;
    391         }
    392 
    393         @Override
    394         public X509Certificate[] getCertificateChain(String alias) {
    395             if (LOG_ENABLED) {
    396                 LogUtils.i(TAG, "Requesting a client certificate chain for alias [" + alias + "]");
    397             }
    398             return mCertificateChain;
    399         }
    400 
    401         @Override
    402         public PrivateKey getPrivateKey(String alias) {
    403             if (LOG_ENABLED) {
    404                 LogUtils.i(TAG, "Requesting a client private key for alias [" + alias + "]");
    405             }
    406             return mPrivateKey;
    407         }
    408     }
    409 }
    410