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.ContentUris; 20 import android.content.ContentValues; 21 import android.content.Context; 22 import android.database.Cursor; 23 import android.net.SSLCertificateSocketFactory; 24 import android.security.KeyChain; 25 import android.security.KeyChainException; 26 27 import com.android.emailcommon.provider.EmailContent.HostAuthColumns; 28 import com.android.emailcommon.provider.HostAuth; 29 import com.android.mail.utils.LogUtils; 30 import com.google.common.annotations.VisibleForTesting; 31 32 import java.io.ByteArrayInputStream; 33 import java.io.IOException; 34 import java.net.InetAddress; 35 import java.net.Socket; 36 import java.security.Principal; 37 import java.security.PrivateKey; 38 import java.security.PublicKey; 39 import java.security.cert.Certificate; 40 import java.security.cert.CertificateException; 41 import java.security.cert.CertificateFactory; 42 import java.security.cert.X509Certificate; 43 import java.util.Arrays; 44 45 import javax.net.ssl.KeyManager; 46 import javax.net.ssl.TrustManager; 47 import javax.net.ssl.X509ExtendedKeyManager; 48 import javax.net.ssl.X509TrustManager; 49 50 public class SSLUtils { 51 // All secure factories are the same; all insecure factories are associated with HostAuth's 52 private static SSLCertificateSocketFactory sSecureFactory; 53 54 private static final boolean LOG_ENABLED = false; 55 private static final String TAG = "Email.Ssl"; 56 57 // A 30 second SSL handshake should be more than enough. 58 private static final int SSL_HANDSHAKE_TIMEOUT = 30000; 59 60 /** 61 * A trust manager specific to a particular HostAuth. The first time a server certificate is 62 * encountered for the HostAuth, its certificate is saved; subsequent checks determine whether 63 * the PublicKey of the certificate presented matches that of the saved certificate 64 * TODO: UI to ask user about changed certificates 65 */ 66 private static class SameCertificateCheckingTrustManager implements X509TrustManager { 67 private final HostAuth mHostAuth; 68 private final Context mContext; 69 // The public key associated with the HostAuth; we'll lazily initialize it 70 private PublicKey mPublicKey; 71 SameCertificateCheckingTrustManager(Context context, HostAuth hostAuth)72 SameCertificateCheckingTrustManager(Context context, HostAuth hostAuth) { 73 mContext = context; 74 mHostAuth = hostAuth; 75 // We must load the server cert manually (the ContentCache won't handle blobs 76 Cursor c = context.getContentResolver().query(HostAuth.CONTENT_URI, 77 new String[] {HostAuthColumns.SERVER_CERT}, HostAuthColumns._ID + "=?", 78 new String[] {Long.toString(hostAuth.mId)}, null); 79 if (c != null) { 80 try { 81 if (c.moveToNext()) { 82 mHostAuth.mServerCert = c.getBlob(0); 83 } 84 } finally { 85 c.close(); 86 } 87 } 88 } 89 90 @Override checkClientTrusted(X509Certificate[] chain, String authType)91 public void checkClientTrusted(X509Certificate[] chain, String authType) 92 throws CertificateException { 93 // We don't check client certificates 94 throw new CertificateException("We don't check client certificates"); 95 } 96 97 @Override checkServerTrusted(X509Certificate[] chain, String authType)98 public void checkServerTrusted(X509Certificate[] chain, String authType) 99 throws CertificateException { 100 if (chain.length == 0) { 101 throw new CertificateException("No certificates?"); 102 } else { 103 X509Certificate serverCert = chain[0]; 104 if (mHostAuth.mServerCert != null) { 105 // Compare with the current public key 106 if (mPublicKey == null) { 107 ByteArrayInputStream bais = new ByteArrayInputStream(mHostAuth.mServerCert); 108 Certificate storedCert = 109 CertificateFactory.getInstance("X509").generateCertificate(bais); 110 mPublicKey = storedCert.getPublicKey(); 111 try { 112 bais.close(); 113 } catch (IOException e) { 114 // Yeah, right. 115 } 116 } 117 if (!mPublicKey.equals(serverCert.getPublicKey())) { 118 throw new CertificateException( 119 "PublicKey has changed since initial connection!"); 120 } 121 } else { 122 // First time; save this away 123 byte[] encodedCert = serverCert.getEncoded(); 124 mHostAuth.mServerCert = encodedCert; 125 ContentValues values = new ContentValues(); 126 values.put(HostAuthColumns.SERVER_CERT, encodedCert); 127 mContext.getContentResolver().update( 128 ContentUris.withAppendedId(HostAuth.CONTENT_URI, mHostAuth.mId), 129 values, null, null); 130 } 131 } 132 } 133 134 @Override getAcceptedIssuers()135 public X509Certificate[] getAcceptedIssuers() { 136 return null; 137 } 138 } 139 140 /** 141 * Returns a {@link javax.net.ssl.SSLSocketFactory}. 142 * Optionally bypass all SSL certificate checks. 143 * 144 * @param insecure if true, bypass all SSL certificate checks 145 */ getSSLSocketFactory(Context context, HostAuth hostAuth, boolean insecure)146 public synchronized static SSLCertificateSocketFactory getSSLSocketFactory(Context context, 147 HostAuth hostAuth, boolean insecure) { 148 if (insecure) { 149 SSLCertificateSocketFactory insecureFactory = (SSLCertificateSocketFactory) 150 SSLCertificateSocketFactory.getInsecure(SSL_HANDSHAKE_TIMEOUT, null); 151 insecureFactory.setTrustManagers( 152 new TrustManager[] { 153 new SameCertificateCheckingTrustManager(context, hostAuth)}); 154 return insecureFactory; 155 } else { 156 if (sSecureFactory == null) { 157 sSecureFactory = (SSLCertificateSocketFactory) 158 SSLCertificateSocketFactory.getDefault(SSL_HANDSHAKE_TIMEOUT, null); 159 } 160 return sSecureFactory; 161 } 162 } 163 164 /** 165 * Returns a {@link org.apache.http.conn.ssl.SSLSocketFactory SSLSocketFactory} for use with the 166 * Apache HTTP stack. 167 */ getHttpSocketFactory(Context context, HostAuth hostAuth, KeyManager keyManager, boolean insecure)168 public static SSLSocketFactory getHttpSocketFactory(Context context, HostAuth hostAuth, 169 KeyManager keyManager, boolean insecure) { 170 SSLCertificateSocketFactory underlying = getSSLSocketFactory(context, hostAuth, insecure); 171 if (keyManager != null) { 172 underlying.setKeyManagers(new KeyManager[] { keyManager }); 173 } 174 SSLSocketFactory wrapped = new SSLSocketFactory(underlying); 175 if (insecure) { 176 wrapped.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); 177 } 178 return wrapped; 179 } 180 181 // Character.isLetter() is locale-specific, and will potentially return true for characters 182 // outside of ascii a-z,A-Z isAsciiLetter(char c)183 private static boolean isAsciiLetter(char c) { 184 return ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z'); 185 } 186 187 // Character.isDigit() is locale-specific, and will potentially return true for characters 188 // outside of ascii 0-9 isAsciiNumber(char c)189 private static boolean isAsciiNumber(char c) { 190 return ('0' <= c && c <= '9'); 191 } 192 193 /** 194 * Escapes the contents a string to be used as a safe scheme name in the URI according to 195 * http://tools.ietf.org/html/rfc3986#section-3.1 196 * 197 * This does not ensure that the first character is a letter (which is required by the RFC). 198 */ 199 @VisibleForTesting escapeForSchemeName(String s)200 public static String escapeForSchemeName(String s) { 201 // According to the RFC, scheme names are case-insensitive. 202 s = s.toLowerCase(); 203 204 StringBuilder sb = new StringBuilder(); 205 for (int i = 0; i < s.length(); i++) { 206 char c = s.charAt(i); 207 if (isAsciiLetter(c) || isAsciiNumber(c) 208 || ('-' == c) || ('.' == c)) { 209 // Safe - use as is. 210 sb.append(c); 211 } else if ('+' == c) { 212 // + is used as our escape character, so double it up. 213 sb.append("++"); 214 } else { 215 // Unsafe - escape. 216 sb.append('+').append((int) c); 217 } 218 } 219 return sb.toString(); 220 } 221 222 private static abstract class StubKeyManager extends X509ExtendedKeyManager { chooseClientAlias( String[] keyTypes, Principal[] issuers, Socket socket)223 @Override public abstract String chooseClientAlias( 224 String[] keyTypes, Principal[] issuers, Socket socket); 225 getCertificateChain(String alias)226 @Override public abstract X509Certificate[] getCertificateChain(String alias); 227 getPrivateKey(String alias)228 @Override public abstract PrivateKey getPrivateKey(String alias); 229 230 231 // The following methods are unused. 232 233 @Override chooseServerAlias( String keyType, Principal[] issuers, Socket socket)234 public final String chooseServerAlias( 235 String keyType, Principal[] issuers, Socket socket) { 236 // not a client SSLSocket callback 237 throw new UnsupportedOperationException(); 238 } 239 240 @Override getClientAliases(String keyType, Principal[] issuers)241 public final String[] getClientAliases(String keyType, Principal[] issuers) { 242 // not a client SSLSocket callback 243 throw new UnsupportedOperationException(); 244 } 245 246 @Override getServerAliases(String keyType, Principal[] issuers)247 public final String[] getServerAliases(String keyType, Principal[] issuers) { 248 // not a client SSLSocket callback 249 throw new UnsupportedOperationException(); 250 } 251 } 252 253 /** 254 * A dummy {@link KeyManager} which keeps track of the last time a server has requested 255 * a client certificate. 256 */ 257 public static class TrackingKeyManager extends StubKeyManager { 258 private volatile long mLastTimeCertRequested = 0L; 259 260 @Override chooseClientAlias(String[] keyTypes, Principal[] issuers, Socket socket)261 public String chooseClientAlias(String[] keyTypes, Principal[] issuers, Socket socket) { 262 if (LOG_ENABLED) { 263 InetAddress address = socket.getInetAddress(); 264 LogUtils.i(TAG, "TrackingKeyManager: requesting a client cert alias for " 265 + address.getCanonicalHostName()); 266 } 267 mLastTimeCertRequested = System.currentTimeMillis(); 268 return null; 269 } 270 271 @Override getCertificateChain(String alias)272 public X509Certificate[] getCertificateChain(String alias) { 273 if (LOG_ENABLED) { 274 LogUtils.i(TAG, "TrackingKeyManager: returning a null cert chain"); 275 } 276 return null; 277 } 278 279 @Override getPrivateKey(String alias)280 public PrivateKey getPrivateKey(String alias) { 281 if (LOG_ENABLED) { 282 LogUtils.i(TAG, "TrackingKeyManager: returning a null private key"); 283 } 284 return null; 285 } 286 287 /** 288 * @return the last time that this {@link KeyManager} detected a request by a server 289 * for a client certificate (in millis since epoch). 290 */ getLastCertReqTime()291 public long getLastCertReqTime() { 292 return mLastTimeCertRequested; 293 } 294 } 295 296 /** 297 * A {@link KeyManager} that reads uses credentials stored in the system {@link KeyChain}. 298 */ 299 public static class KeyChainKeyManager extends StubKeyManager { 300 private final String mClientAlias; 301 private final X509Certificate[] mCertificateChain; 302 private final PrivateKey mPrivateKey; 303 304 /** 305 * Builds an instance of a KeyChainKeyManager using the given certificate alias. 306 * If for any reason retrieval of the credentials from the system {@link KeyChain} fails, 307 * a {@code null} value will be returned. 308 */ fromAlias(Context context, String alias)309 public static KeyChainKeyManager fromAlias(Context context, String alias) 310 throws CertificateException { 311 X509Certificate[] certificateChain; 312 try { 313 certificateChain = KeyChain.getCertificateChain(context, alias); 314 } catch (KeyChainException e) { 315 logError(alias, "certificate chain", e); 316 throw new CertificateException(e); 317 } catch (InterruptedException e) { 318 logError(alias, "certificate chain", e); 319 throw new CertificateException(e); 320 } 321 322 PrivateKey privateKey; 323 try { 324 privateKey = KeyChain.getPrivateKey(context, alias); 325 } catch (KeyChainException e) { 326 logError(alias, "private key", e); 327 throw new CertificateException(e); 328 } catch (InterruptedException e) { 329 logError(alias, "private key", e); 330 throw new CertificateException(e); 331 } 332 333 if (certificateChain == null || privateKey == null) { 334 throw new CertificateException("Can't access certificate from keystore"); 335 } 336 337 return new KeyChainKeyManager(alias, certificateChain, privateKey); 338 } 339 logError(String alias, String type, Exception ex)340 private static void logError(String alias, String type, Exception ex) { 341 // Avoid logging PII when explicit logging is not on. 342 if (LOG_ENABLED) { 343 LogUtils.e(TAG, "Unable to retrieve " + type + " for [" + alias + "] due to " + ex); 344 } else { 345 LogUtils.e(TAG, "Unable to retrieve " + type + " due to " + ex); 346 } 347 } 348 KeyChainKeyManager( String clientAlias, X509Certificate[] certificateChain, PrivateKey privateKey)349 private KeyChainKeyManager( 350 String clientAlias, X509Certificate[] certificateChain, PrivateKey privateKey) { 351 mClientAlias = clientAlias; 352 mCertificateChain = certificateChain; 353 mPrivateKey = privateKey; 354 } 355 356 357 @Override chooseClientAlias(String[] keyTypes, Principal[] issuers, Socket socket)358 public String chooseClientAlias(String[] keyTypes, Principal[] issuers, Socket socket) { 359 if (LOG_ENABLED) { 360 LogUtils.i(TAG, "Requesting a client cert alias for " + Arrays.toString(keyTypes)); 361 } 362 return mClientAlias; 363 } 364 365 @Override getCertificateChain(String alias)366 public X509Certificate[] getCertificateChain(String alias) { 367 if (LOG_ENABLED) { 368 LogUtils.i(TAG, "Requesting a client certificate chain for alias [" + alias + "]"); 369 } 370 return mCertificateChain; 371 } 372 373 @Override getPrivateKey(String alias)374 public PrivateKey getPrivateKey(String alias) { 375 if (LOG_ENABLED) { 376 LogUtils.i(TAG, "Requesting a client private key for alias [" + alias + "]"); 377 } 378 return mPrivateKey; 379 } 380 } 381 } 382