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