summaryrefslogtreecommitdiffstats
path: root/emailcommon/src/com/android/emailcommon/utility/SSLUtils.java
blob: 779bc5296d2d8f2f3102bea3373ccae48ca70d51 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
/*
 * Copyright (C) 2010 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 android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.SSLCertificateSocketFactory;
import android.net.SSLSessionCache;
import android.security.KeyChain;
import android.security.KeyChainException;

import com.android.emailcommon.provider.EmailContent.HostAuthColumns;
import com.android.emailcommon.provider.HostAuth;
import com.android.mail.utils.LogUtils;
import com.google.common.annotations.VisibleForTesting;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Arrays;

import javax.net.ssl.KeyManager;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509ExtendedKeyManager;
import javax.net.ssl.X509TrustManager;

public class SSLUtils {
    // All secure factories are the same; all insecure factories are associated with HostAuth's
    private static javax.net.ssl.SSLSocketFactory sSecureFactory;

    private static final boolean LOG_ENABLED = false;
    private static final String TAG = "Email.Ssl";

    // A 30 second SSL handshake should be more than enough.
    private static final int SSL_HANDSHAKE_TIMEOUT = 30000;

    /**
     * A trust manager specific to a particular HostAuth.  The first time a server certificate is
     * encountered for the HostAuth, its certificate is saved; subsequent checks determine whether
     * the PublicKey of the certificate presented matches that of the saved certificate
     * TODO: UI to ask user about changed certificates
     */
    private static class SameCertificateCheckingTrustManager implements X509TrustManager {
        private final HostAuth mHostAuth;
        private final Context mContext;
        // The public key associated with the HostAuth; we'll lazily initialize it
        private PublicKey mPublicKey;

        SameCertificateCheckingTrustManager(Context context, HostAuth hostAuth) {
            mContext = context;
            mHostAuth = hostAuth;
            // We must load the server cert manually (the ContentCache won't handle blobs
            Cursor c = context.getContentResolver().query(HostAuth.CONTENT_URI,
                    new String[] {HostAuthColumns.SERVER_CERT}, HostAuthColumns._ID + "=?",
                    new String[] {Long.toString(hostAuth.mId)}, null);
            if (c != null) {
                try {
                    if (c.moveToNext()) {
                        mHostAuth.mServerCert = c.getBlob(0);
                    }
                } finally {
                    c.close();
                }
            }
        }

        @Override
        public void checkClientTrusted(X509Certificate[] chain, String authType)
                throws CertificateException {
            // We don't check client certificates
            throw new CertificateException("We don't check client certificates");
        }

        @Override
        public void checkServerTrusted(X509Certificate[] chain, String authType)
                throws CertificateException {
            if (chain.length == 0) {
                throw new CertificateException("No certificates?");
            } else {
                X509Certificate serverCert = chain[0];
                if (mHostAuth.mServerCert != null) {
                    // Compare with the current public key
                    if (mPublicKey == null) {
                        ByteArrayInputStream bais = new ByteArrayInputStream(mHostAuth.mServerCert);
                        Certificate storedCert =
                                CertificateFactory.getInstance("X509").generateCertificate(bais);
                        mPublicKey = storedCert.getPublicKey();
                        try {
                            bais.close();
                        } catch (IOException e) {
                            // Yeah, right.
                        }
                    }
                    if (!mPublicKey.equals(serverCert.getPublicKey())) {
                        throw new CertificateException(
                                "PublicKey has changed since initial connection!");
                    }
                } else {
                    // First time; save this away
                    byte[] encodedCert = serverCert.getEncoded();
                    mHostAuth.mServerCert = encodedCert;
                    ContentValues values = new ContentValues();
                    values.put(HostAuthColumns.SERVER_CERT, encodedCert);
                    mContext.getContentResolver().update(
                            ContentUris.withAppendedId(HostAuth.CONTENT_URI, mHostAuth.mId),
                            values, null, null);
                }
            }
        }

        @Override
        public X509Certificate[] getAcceptedIssuers() {
            return null;
        }
    }

    public static abstract class ExternalSecureSocketFactoryBuilder {
        abstract public javax.net.ssl.SSLSocketFactory createSecureSocketFactory(
                final Context context, final int handshakeTimeoutMillis);
    }

    private static ExternalSecureSocketFactoryBuilder sExternalSocketFactoryBuilder;

    public static void setExternalSecureSocketFactoryBuilder(
            ExternalSecureSocketFactoryBuilder builder) {
        sExternalSocketFactoryBuilder = builder;
    }

    /**
     * Returns a {@link javax.net.ssl.SSLSocketFactory}.
     * Optionally bypass all SSL certificate checks.
     *
     * @param insecure if true, bypass all SSL certificate checks
     */
    public synchronized static javax.net.ssl.SSLSocketFactory getSSLSocketFactory(
            final Context context, final HostAuth hostAuth, final KeyManager keyManager,
            final boolean insecure) {
        if (insecure) {
            final SSLCertificateSocketFactory insecureFactory = (SSLCertificateSocketFactory)
                    SSLCertificateSocketFactory.getInsecure(SSL_HANDSHAKE_TIMEOUT, null);
            insecureFactory.setTrustManagers(
                    new TrustManager[] {
                            new SameCertificateCheckingTrustManager(context, hostAuth)});
            if (keyManager != null) {
                insecureFactory.setKeyManagers(new KeyManager[] { keyManager });
            }
            return insecureFactory;
        } else {
            if (sSecureFactory == null) {
                // First try to get use an externally supplied, more secure SSLSocketBuilder.
                // If so we should use that.
                javax.net.ssl.SSLSocketFactory socketFactory = null;
                if (sExternalSocketFactoryBuilder != null) {
                    socketFactory = sExternalSocketFactoryBuilder.createSecureSocketFactory(
                            context, SSL_HANDSHAKE_TIMEOUT);
                }
                if (socketFactory != null) {
                    sSecureFactory = socketFactory;
                    LogUtils.d(TAG, "Using externally created CertificateSocketFactory");
                    return sSecureFactory;
                }
                // Only fall back to the platform one if that fails.
                LogUtils.d(TAG, "Falling back to platform CertificateSocketFactory");
                final SSLCertificateSocketFactory certificateSocketFactory =
                        (SSLCertificateSocketFactory)
                        SSLCertificateSocketFactory.getDefault(SSL_HANDSHAKE_TIMEOUT,
                                new SSLSessionCache(context));
                if (keyManager != null) {
                    certificateSocketFactory.setKeyManagers(new KeyManager[] { keyManager });
                }
                sSecureFactory = certificateSocketFactory;
            }
            return sSecureFactory;
        }
    }

    /**
     * Returns a com.android.emailcommon.utility.SSLSocketFactory
     */
    public static SSLSocketFactory getHttpSocketFactory(Context context, HostAuth hostAuth,
            KeyManager keyManager, boolean insecure) {
        javax.net.ssl.SSLSocketFactory underlying = getSSLSocketFactory(context, hostAuth,
                keyManager, insecure);
        SSLSocketFactory wrapped = new SSLSocketFactory(underlying);
        if (insecure) {
            wrapped.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
        }
        return wrapped;
    }

    // Character.isLetter() is locale-specific, and will potentially return true for characters
    // outside of ascii a-z,A-Z
    private static boolean isAsciiLetter(char c) {
        return ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z');
    }

    // Character.isDigit() is locale-specific, and will potentially return true for characters
    // outside of ascii 0-9
    private static boolean isAsciiNumber(char c) {
        return ('0' <= c && c <= '9');
    }

    /**
     * Escapes the contents a string to be used as a safe scheme name in the URI according to
     * http://tools.ietf.org/html/rfc3986#section-3.1
     *
     * This does not ensure that the first character is a letter (which is required by the RFC).
     */
    @VisibleForTesting
    public static String escapeForSchemeName(String s) {
        // According to the RFC, scheme names are case-insensitive.
        s = s.toLowerCase();

        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            if (isAsciiLetter(c) || isAsciiNumber(c)
                    || ('-' == c) || ('.' == c)) {
                // Safe - use as is.
                sb.append(c);
            } else if ('+' == c) {
                // + is used as our escape character, so double it up.
                sb.append("++");
            } else {
                // Unsafe - escape.
                sb.append('+').append((int) c);
            }
        }
        return sb.toString();
    }

    private static abstract class StubKeyManager extends X509ExtendedKeyManager {
        @Override public abstract String chooseClientAlias(
                String[] keyTypes, Principal[] issuers, Socket socket);

        @Override public abstract X509Certificate[] getCertificateChain(String alias);

        @Override public abstract PrivateKey getPrivateKey(String alias);


        // The following methods are unused.

        @Override
        public final String chooseServerAlias(
                String keyType, Principal[] issuers, Socket socket) {
            // not a client SSLSocket callback
            throw new UnsupportedOperationException();
        }

        @Override
        public final String[] getClientAliases(String keyType, Principal[] issuers) {
            // not a client SSLSocket callback
            throw new UnsupportedOperationException();
        }

        @Override
        public final String[] getServerAliases(String keyType, Principal[] issuers) {
            // not a client SSLSocket callback
            throw new UnsupportedOperationException();
        }
    }

    /**
     * A dummy {@link KeyManager} which keeps track of the last time a server has requested
     * a client certificate.
     */
    public static class TrackingKeyManager extends StubKeyManager {
        private volatile long mLastTimeCertRequested = 0L;

        @Override
        public String chooseClientAlias(String[] keyTypes, Principal[] issuers, Socket socket) {
            if (LOG_ENABLED) {
                InetAddress address = socket.getInetAddress();
                LogUtils.i(TAG, "TrackingKeyManager: requesting a client cert alias for "
                        + address.getCanonicalHostName());
            }
            mLastTimeCertRequested = System.currentTimeMillis();
            return null;
        }

        @Override
        public X509Certificate[] getCertificateChain(String alias) {
            if (LOG_ENABLED) {
                LogUtils.i(TAG, "TrackingKeyManager: returning a null cert chain");
            }
            return null;
        }

        @Override
        public PrivateKey getPrivateKey(String alias) {
            if (LOG_ENABLED) {
                LogUtils.i(TAG, "TrackingKeyManager: returning a null private key");
            }
            return null;
        }

        /**
         * @return the last time that this {@link KeyManager} detected a request by a server
         *     for a client certificate (in millis since epoch).
         */
        public long getLastCertReqTime() {
            return mLastTimeCertRequested;
        }
    }

    /**
     * A {@link KeyManager} that reads uses credentials stored in the system {@link KeyChain}.
     */
    public static class KeyChainKeyManager extends StubKeyManager {
        private final String mClientAlias;
        private final X509Certificate[] mCertificateChain;
        private final PrivateKey mPrivateKey;

        /**
         * Builds an instance of a KeyChainKeyManager using the given certificate alias.
         * If for any reason retrieval of the credentials from the system {@link KeyChain} fails,
         * a {@code null} value will be returned.
         */
        public static KeyChainKeyManager fromAlias(Context context, String alias)
                throws CertificateException {
            X509Certificate[] certificateChain;
            try {
                certificateChain = KeyChain.getCertificateChain(context, alias);
            } catch (KeyChainException e) {
                logError(alias, "certificate chain", e);
                throw new CertificateException(e);
            } catch (InterruptedException e) {
                logError(alias, "certificate chain", e);
                throw new CertificateException(e);
            }

            PrivateKey privateKey;
            try {
                privateKey = KeyChain.getPrivateKey(context, alias);
            } catch (KeyChainException e) {
                logError(alias, "private key", e);
                throw new CertificateException(e);
            } catch (InterruptedException e) {
                logError(alias, "private key", e);
                throw new CertificateException(e);
            }

            if (certificateChain == null || privateKey == null) {
                throw new CertificateException("Can't access certificate from keystore");
            }

            return new KeyChainKeyManager(alias, certificateChain, privateKey);
        }

        private static void logError(String alias, String type, Exception ex) {
            // Avoid logging PII when explicit logging is not on.
            if (LOG_ENABLED) {
                LogUtils.e(TAG, "Unable to retrieve " + type + " for [" + alias + "] due to " + ex);
            } else {
                LogUtils.e(TAG, "Unable to retrieve " + type + " due to " + ex);
            }
        }

        private KeyChainKeyManager(
                String clientAlias, X509Certificate[] certificateChain, PrivateKey privateKey) {
            mClientAlias = clientAlias;
            mCertificateChain = certificateChain;
            mPrivateKey = privateKey;
        }


        @Override
        public String chooseClientAlias(String[] keyTypes, Principal[] issuers, Socket socket) {
            if (LOG_ENABLED) {
                LogUtils.i(TAG, "Requesting a client cert alias for " + Arrays.toString(keyTypes));
            }
            return mClientAlias;
        }

        @Override
        public X509Certificate[] getCertificateChain(String alias) {
            if (LOG_ENABLED) {
                LogUtils.i(TAG, "Requesting a client certificate chain for alias [" + alias + "]");
            }
            return mCertificateChain;
        }

        @Override
        public PrivateKey getPrivateKey(String alias) {
            if (LOG_ENABLED) {
                LogUtils.i(TAG, "Requesting a client private key for alias [" + alias + "]");
            }
            return mPrivateKey;
        }
    }
}