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 
SameCertificateCheckingTrustManager(Context context, HostAuth hostAuth)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
checkClientTrusted(X509Certificate[] chain, String authType)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
checkServerTrusted(X509Certificate[] chain, String authType)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
getAcceptedIssuers()136         public X509Certificate[] getAcceptedIssuers() {
137             return null;
138         }
139     }
140 
141     public static abstract class ExternalSecurityProviderInstaller {
installIfNeeded(final Context context)142         abstract public void installIfNeeded(final Context context);
143     }
144 
145     private static ExternalSecurityProviderInstaller sExternalSecurityProviderInstaller;
146 
setExternalSecurityProviderInstaller( ExternalSecurityProviderInstaller installer)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      */
getSSLSocketFactory( final Context context, final HostAuth hostAuth, final KeyManager keyManager, final boolean insecure)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      */
getHttpSocketFactory(Context context, HostAuth hostAuth, KeyManager keyManager, boolean insecure)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
isAsciiLetter(char c)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
isAsciiNumber(char c)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
escapeForSchemeName(String s)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 {
chooseClientAlias( String[] keyTypes, Principal[] issuers, Socket socket)251         @Override public abstract String chooseClientAlias(
252                 String[] keyTypes, Principal[] issuers, Socket socket);
253 
getCertificateChain(String alias)254         @Override public abstract X509Certificate[] getCertificateChain(String alias);
255 
getPrivateKey(String alias)256         @Override public abstract PrivateKey getPrivateKey(String alias);
257 
258 
259         // The following methods are unused.
260 
261         @Override
chooseServerAlias( String keyType, Principal[] issuers, Socket socket)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
getClientAliases(String keyType, Principal[] issuers)269         public final String[] getClientAliases(String keyType, Principal[] issuers) {
270             // not a client SSLSocket callback
271             throw new UnsupportedOperationException();
272         }
273 
274         @Override
getServerAliases(String keyType, Principal[] issuers)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
chooseClientAlias(String[] keyTypes, Principal[] issuers, Socket socket)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
getCertificateChain(String alias)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
getPrivateKey(String alias)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          */
getLastCertReqTime()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          */
fromAlias(Context context, String alias)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 
logError(String alias, String type, Exception ex)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 
KeyChainKeyManager( String clientAlias, X509Certificate[] certificateChain, PrivateKey privateKey)377         private KeyChainKeyManager(
378                 String clientAlias, X509Certificate[] certificateChain, PrivateKey privateKey) {
379             mClientAlias = clientAlias;
380             mCertificateChain = certificateChain;
381             mPrivateKey = privateKey;
382         }
383 
384 
385         @Override
chooseClientAlias(String[] keyTypes, Principal[] issuers, Socket socket)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
getCertificateChain(String alias)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
getPrivateKey(String alias)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