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