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.Context;
20import android.net.SSLCertificateSocketFactory;
21import android.security.KeyChain;
22import android.security.KeyChainException;
23import android.util.Log;
24
25import com.google.common.annotations.VisibleForTesting;
26
27import java.net.InetAddress;
28import java.net.Socket;
29import java.security.Principal;
30import java.security.PrivateKey;
31import java.security.cert.CertificateException;
32import java.security.cert.X509Certificate;
33import java.util.Arrays;
34
35import javax.net.ssl.KeyManager;
36import javax.net.ssl.X509ExtendedKeyManager;
37
38public 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