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