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