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
17package com.android.emailcommon.utility;
18
19import android.content.ContentUris;
20import android.content.ContentValues;
21import android.content.Context;
22import android.database.Cursor;
23import android.security.KeyChain;
24import android.security.KeyChainException;
25
26import com.android.emailcommon.provider.EmailContent.HostAuthColumns;
27import com.android.emailcommon.provider.HostAuth;
28import com.android.mail.utils.LogUtils;
29import com.google.common.annotations.VisibleForTesting;
30
31import java.io.ByteArrayInputStream;
32import java.io.IOException;
33import java.net.InetAddress;
34import java.net.Socket;
35import java.security.KeyManagementException;
36import java.security.NoSuchAlgorithmException;
37import java.security.Principal;
38import java.security.PrivateKey;
39import java.security.PublicKey;
40import java.security.cert.Certificate;
41import java.security.cert.CertificateException;
42import java.security.cert.CertificateFactory;
43import java.security.cert.X509Certificate;
44import java.util.Arrays;
45
46import javax.net.ssl.KeyManager;
47import javax.net.ssl.TrustManager;
48import javax.net.ssl.X509ExtendedKeyManager;
49import javax.net.ssl.X509TrustManager;
50
51public 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