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