SSLUtils.java revision 745b33b8ff55e9a9c4871f07f9d97db893f784b2
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 com.google.common.annotations.VisibleForTesting;
20
21import android.content.Context;
22import android.net.SSLCertificateSocketFactory;
23import android.security.KeyChain;
24import android.security.KeyChainException;
25import android.util.Log;
26
27import java.net.Socket;
28import java.security.Principal;
29import java.security.PrivateKey;
30import java.security.cert.CertificateException;
31import java.security.cert.X509Certificate;
32import java.util.Arrays;
33
34import javax.net.ssl.KeyManager;
35import javax.net.ssl.X509ExtendedKeyManager;
36
37public class SSLUtils {
38    private static SSLCertificateSocketFactory sInsecureFactory;
39    private static SSLCertificateSocketFactory sSecureFactory;
40
41    private static final boolean LOG_ENABLED = false;
42    private static final String TAG = "Email.Ssl";
43
44    /**
45     * Returns a {@link javax.net.ssl.SSLSocketFactory}.
46     * Optionally bypass all SSL certificate checks.
47     *
48     * @param insecure if true, bypass all SSL certificate checks
49     */
50    public synchronized static SSLCertificateSocketFactory getSSLSocketFactory(
51            boolean insecure) {
52        if (insecure) {
53            if (sInsecureFactory == null) {
54                sInsecureFactory = (SSLCertificateSocketFactory)
55                        SSLCertificateSocketFactory.getInsecure(0, null);
56            }
57            return sInsecureFactory;
58        } else {
59            if (sSecureFactory == null) {
60                sSecureFactory = (SSLCertificateSocketFactory)
61                        SSLCertificateSocketFactory.getDefault(0, null);
62            }
63            return sSecureFactory;
64        }
65    }
66
67    /**
68     * Returns a {@link org.apache.http.conn.ssl.SSLSocketFactory SSLSocketFactory} for use with the
69     * Apache HTTP stack.
70     */
71    public static SSLSocketFactory getHttpSocketFactory(boolean insecure, KeyManager keyManager) {
72        SSLCertificateSocketFactory underlying = getSSLSocketFactory(insecure);
73        if (keyManager != null) {
74            underlying.setKeyManagers(new KeyManager[] { keyManager });
75        }
76        return new SSLSocketFactory(underlying);
77    }
78
79    /**
80     * Escapes the contents a string to be used as a safe scheme name in the URI according to
81     * http://tools.ietf.org/html/rfc3986#section-3.1
82     *
83     * This does not ensure that the first character is a letter (which is required by the RFC).
84     */
85    @VisibleForTesting
86    public static String escapeForSchemeName(String s) {
87        // According to the RFC, scheme names are case-insensitive.
88        s = s.toLowerCase();
89
90        StringBuilder sb = new StringBuilder();
91        for (int i = 0; i < s.length(); i++) {
92            char c = s.charAt(i);
93            if (Character.isLetter(c) || Character.isDigit(c)
94                    || ('-' == c) || ('.' == c)) {
95                // Safe - use as is.
96                sb.append(c);
97            } else if ('+' == c) {
98                // + is used as our escape character, so double it up.
99                sb.append("++");
100            } else {
101                // Unsafe - escape.
102                sb.append('+').append((int) c);
103            }
104        }
105        return sb.toString();
106    }
107
108    private static abstract class StubKeyManager extends X509ExtendedKeyManager {
109        @Override public abstract String chooseClientAlias(
110                String[] keyTypes, Principal[] issuers, Socket socket);
111
112        @Override public abstract X509Certificate[] getCertificateChain(String alias);
113
114        @Override public abstract PrivateKey getPrivateKey(String alias);
115
116
117        // The following methods are unused.
118
119        @Override
120        public final String chooseServerAlias(
121                String keyType, Principal[] issuers, Socket socket) {
122            // not a client SSLSocket callback
123            throw new UnsupportedOperationException();
124        }
125
126        @Override
127        public final String[] getClientAliases(String keyType, Principal[] issuers) {
128            // not a client SSLSocket callback
129            throw new UnsupportedOperationException();
130        }
131
132        @Override
133        public final String[] getServerAliases(String keyType, Principal[] issuers) {
134            // not a client SSLSocket callback
135            throw new UnsupportedOperationException();
136        }
137    }
138
139    /**
140     * A dummy {@link KeyManager} which throws a {@link CertificateRequestedException} if the
141     * server requests a certificate.
142     */
143    public static class TrackingKeyManager extends StubKeyManager {
144        @Override
145        public String chooseClientAlias(String[] keyTypes, Principal[] issuers, Socket socket) {
146            if (LOG_ENABLED) {
147                Log.i(TAG, "TrackingKeyManager: requesting a client cert alias for "
148                        + Arrays.toString(keyTypes));
149            }
150            throw new CertificateRequestedException();
151        }
152
153        @Override
154        public X509Certificate[] getCertificateChain(String alias) {
155            return null;
156        }
157
158        @Override
159        public PrivateKey getPrivateKey(String alias) {
160            return null;
161        }
162    }
163
164    /**
165     * An exception indicating that a server requested a client certificate but none was
166     * available to be presented.
167     */
168    public static class CertificateRequestedException extends RuntimeException {
169    }
170
171    /**
172     * A {@link KeyManager} that reads uses credentials stored in the system {@link KeyChain}.
173     */
174    public static class KeyChainKeyManager extends StubKeyManager {
175        private final String mClientAlias;
176        private final X509Certificate[] mCertificateChain;
177        private final PrivateKey mPrivateKey;
178
179        /**
180         * Builds an instance of a KeyChainKeyManager using the given certificate alias.
181         * If for any reason retrieval of the credentials from the system {@link KeyChain} fails,
182         * a {@code null} value will be returned.
183         */
184        public static KeyChainKeyManager fromAlias(Context context, String alias)
185                throws CertificateException {
186            X509Certificate[] certificateChain;
187            try {
188                certificateChain = KeyChain.getCertificateChain(context, alias);
189            } catch (KeyChainException e) {
190                logError(alias, "certificate chain", e);
191                throw new CertificateException(e);
192            } catch (InterruptedException e) {
193                logError(alias, "certificate chain", e);
194                throw new CertificateException(e);
195            }
196
197            PrivateKey privateKey;
198            try {
199                privateKey = KeyChain.getPrivateKey(context, alias);
200            } catch (KeyChainException e) {
201                logError(alias, "private key", e);
202                throw new CertificateException(e);
203            } catch (InterruptedException e) {
204                logError(alias, "private key", e);
205                throw new CertificateException(e);
206            }
207
208            if (certificateChain == null || privateKey == null) {
209                throw new CertificateException("Can't access certificate from keystore");
210            }
211
212            return new KeyChainKeyManager(alias, certificateChain, privateKey);
213        }
214
215        private static void logError(String alias, String type, Exception ex) {
216            // Avoid logging PII when explicit logging is not on.
217            if (LOG_ENABLED) {
218                Log.e(TAG, "Unable to retrieve " + type + " for [" + alias + "] due to " + ex);
219            } else {
220                Log.e(TAG, "Unable to retrieve " + type + " due to " + ex);
221            }
222        }
223
224        private KeyChainKeyManager(
225                String clientAlias, X509Certificate[] certificateChain, PrivateKey privateKey) {
226            mClientAlias = clientAlias;
227            mCertificateChain = certificateChain;
228            mPrivateKey = privateKey;
229        }
230
231
232        @Override
233        public String chooseClientAlias(String[] keyTypes, Principal[] issuers, Socket socket) {
234            if (LOG_ENABLED) {
235                Log.i(TAG, "Requesting a client cert alias for " + Arrays.toString(keyTypes));
236            }
237            return mClientAlias;
238        }
239
240        @Override
241        public X509Certificate[] getCertificateChain(String alias) {
242            if (LOG_ENABLED) {
243                Log.i(TAG, "Requesting a client certificate chain for alias [" + alias + "]");
244            }
245            return mCertificateChain;
246        }
247
248        @Override
249        public PrivateKey getPrivateKey(String alias) {
250            if (LOG_ENABLED) {
251                Log.i(TAG, "Requesting a client private key for alias [" + alias + "]");
252            }
253            return mPrivateKey;
254        }
255    }
256}
257