KeyChain.java revision 42f6528b988e3ae320cda63a2bd63d30d9e56183
1/* 2 * Copyright (C) 2011 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 */ 16package android.security; 17 18import android.accounts.Account; 19import android.accounts.AccountManager; 20import android.accounts.AccountManagerCallback; 21import android.accounts.AccountManagerFuture; 22import android.accounts.AuthenticatorException; 23import android.accounts.OperationCanceledException; 24import android.app.Activity; 25import android.content.ComponentName; 26import android.content.Context; 27import android.content.Intent; 28import android.content.ServiceConnection; 29import android.os.Bundle; 30import android.os.IBinder; 31import android.os.Looper; 32import android.os.RemoteException; 33import java.io.ByteArrayInputStream; 34import java.io.Closeable; 35import java.io.IOException; 36import java.security.KeyFactory; 37import java.security.KeyPair; 38import java.security.NoSuchAlgorithmException; 39import java.security.Principal; 40import java.security.PrivateKey; 41import java.security.cert.Certificate; 42import java.security.cert.CertificateException; 43import java.security.cert.CertificateFactory; 44import java.security.cert.X509Certificate; 45import java.security.spec.InvalidKeySpecException; 46import java.security.spec.PKCS8EncodedKeySpec; 47import java.util.concurrent.BlockingQueue; 48import java.util.concurrent.LinkedBlockingQueue; 49 50/** 51 * The {@code KeyChain} class provides access to private keys and 52 * their corresponding certificate chains in credential storage. 53 * 54 * <p>Applications accessing the {@code KeyChain} normally go through 55 * these steps: 56 * 57 * <ol> 58 * 59 * <li>Receive a callback from an {@link javax.net.ssl.X509KeyManager 60 * X509KeyManager} that a private key is requested. 61 * 62 * <li>Call {@link #choosePrivateKeyAlias 63 * choosePrivateKeyAlias} to allow the user to select from a 64 * list of currently available private keys and corresponding 65 * certificate chains. The chosen alias will be returned by the 66 * callback {@link KeyChainAliasCallback#alias}, or null if no private 67 * key is available or the user cancels the request. 68 * 69 * <li>Call {@link #getPrivateKey} and {@link #getCertificateChain} to 70 * retrieve the credentials to return to the corresponding {@link 71 * javax.net.ssl.X509KeyManager} callbacks. 72 * 73 * </ol> 74 * 75 * <p>An application may remember the value of a selected alias to 76 * avoid prompting the user with {@link #choosePrivateKeyAlias 77 * choosePrivateKeyAlias} on subsequent connections. If the alias is 78 * no longer valid, null will be returned on lookups using that value 79 */ 80// TODO reference intent for credential installation when public 81public final class KeyChain { 82 83 private static final String TAG = "KeyChain"; 84 85 /** 86 * @hide Also used by KeyChainService implementation 87 */ 88 public static final String ACCOUNT_TYPE = "com.android.keychain"; 89 90 /** 91 * @hide Also used by KeyChainActivity implementation 92 */ 93 public static final String EXTRA_RESPONSE = "response"; 94 95 /** 96 * Launches an {@code Activity} for the user to select the alias 97 * for a private key and certificate pair for authentication. The 98 * selected alias or null will be returned via the 99 * KeyChainAliasCallback callback. 100 * 101 * <p>{@code keyTypes} and {@code issuers} may be used to 102 * highlight suggested choices to the user, although to cope with 103 * sometimes erroneous values provided by servers, the user may be 104 * able to override these suggestions. 105 * 106 * <p>{@code host} and {@code port} may be used to give the user 107 * more context about the server requesting the credentials. 108 * 109 * <p>This method requires the caller to hold the permission 110 * {@link android.Manifest.permission#USE_CREDENTIALS}. 111 * 112 * @param activity The {@link Activity} context to use for 113 * launching the new sub-Activity to prompt the user to select 114 * a private key; used only to call startActivity(); must not 115 * be null. 116 * @param response Callback to invoke when the request completes; 117 * must not be null 118 * @param keyTypes The acceptable types of asymmetric keys such as 119 * "RSA" or "DSA", or a null array. 120 * @param issuers The acceptable certificate issuers for the 121 * certificate matching the private key, or null. 122 * @param host The host name of the server requesting the 123 * certificate, or null if unavailable. 124 * @param port The port number of the server requesting the 125 * certificate, or -1 if unavailable. 126 */ 127 public static void choosePrivateKeyAlias(Activity activity, KeyChainAliasCallback response, 128 String[] keyTypes, Principal[] issuers, 129 String host, int port) { 130 /* 131 * TODO currently keyTypes, issuers, host, and port are 132 * unused. They are meant to follow the semantics and purpose 133 * of X509KeyManager method arguments. 134 * 135 * keyTypes would allow the list to be filtered and typically 136 * will be set correctly by the server. In practice today, 137 * most all users will want only RSA, rarely DSA, and usually 138 * only a small number of certs will be available. 139 * 140 * issuers is typically not useful. Some servers historically 141 * will send the entire list of public CAs known to the 142 * server. Others will send none. If this is used, if there 143 * are no matches after applying the constraint, it should be 144 * ignored. 145 * 146 * host and port may be shown to the user if available, but it 147 * should be clear that they are not validated values, perhaps 148 * shown along with requesting application identity to clarify 149 * the source of the request. 150 */ 151 if (activity == null) { 152 throw new NullPointerException("activity == null"); 153 } 154 if (response == null) { 155 throw new NullPointerException("response == null"); 156 } 157 Intent intent = new Intent("com.android.keychain.CHOOSER"); 158 intent.putExtra(EXTRA_RESPONSE, new AliasResponse(activity, response)); 159 activity.startActivity(intent); 160 } 161 162 private static class AliasResponse extends IKeyChainAliasCallback.Stub { 163 private final Activity activity; 164 private final KeyChainAliasCallback keyChainAliasResponse; 165 private AliasResponse(Activity activity, KeyChainAliasCallback keyChainAliasResponse) { 166 this.activity = activity; 167 this.keyChainAliasResponse = keyChainAliasResponse; 168 } 169 @Override public void alias(String alias) { 170 if (alias == null) { 171 keyChainAliasResponse.alias(null); 172 return; 173 } 174 AccountManager accountManager = AccountManager.get(activity); 175 accountManager.getAuthToken(getAccount(activity), 176 alias, 177 null, 178 activity, 179 new AliasAccountManagerCallback(keyChainAliasResponse, 180 alias), 181 null); 182 } 183 } 184 185 private static class AliasAccountManagerCallback implements AccountManagerCallback<Bundle> { 186 private final KeyChainAliasCallback keyChainAliasResponse; 187 private final String alias; 188 private AliasAccountManagerCallback(KeyChainAliasCallback keyChainAliasResponse, 189 String alias) { 190 this.keyChainAliasResponse = keyChainAliasResponse; 191 this.alias = alias; 192 } 193 @Override public void run(AccountManagerFuture<Bundle> future) { 194 Bundle bundle; 195 try { 196 bundle = future.getResult(); 197 } catch (OperationCanceledException e) { 198 keyChainAliasResponse.alias(null); 199 return; 200 } catch (IOException e) { 201 keyChainAliasResponse.alias(null); 202 return; 203 } catch (AuthenticatorException e) { 204 keyChainAliasResponse.alias(null); 205 return; 206 } 207 String authToken = bundle.getString(AccountManager.KEY_AUTHTOKEN); 208 if (authToken != null) { 209 keyChainAliasResponse.alias(alias); 210 } else { 211 keyChainAliasResponse.alias(null); 212 } 213 } 214 } 215 216 /** 217 * Returns the {@code PrivateKey} for the requested alias, or null 218 * if no there is no result. 219 * 220 * <p>This method requires the caller to hold the permission 221 * {@link android.Manifest.permission#USE_CREDENTIALS}. 222 * 223 * @param alias The alias of the desired private key, typically 224 * returned via {@link KeyChainAliasCallback#alias}. 225 * @throws KeyChainException if the alias was valid but there was some problem accessing it. 226 */ 227 public static PrivateKey getPrivateKey(Context context, String alias) 228 throws KeyChainException, InterruptedException { 229 if (alias == null) { 230 throw new NullPointerException("alias == null"); 231 } 232 KeyChainConnection keyChainConnection = bind(context); 233 try { 234 String authToken = authToken(context, alias); 235 if (authToken == null) { 236 return null; 237 } 238 IKeyChainService keyChainService = keyChainConnection.getService(); 239 byte[] privateKeyBytes = keyChainService.getPrivateKey(alias, authToken); 240 return toPrivateKey(privateKeyBytes); 241 } catch (RemoteException e) { 242 throw new KeyChainException(e); 243 } catch (RuntimeException e) { 244 // only certain RuntimeExceptions can be propagated across the IKeyChainService call 245 throw new KeyChainException(e); 246 } finally { 247 keyChainConnection.close(); 248 } 249 } 250 251 /** 252 * Returns the {@code X509Certificate} chain for the requested 253 * alias, or null if no there is no result. 254 * 255 * <p>This method requires the caller to hold the permission 256 * {@link android.Manifest.permission#USE_CREDENTIALS}. 257 * 258 * @param alias The alias of the desired certificate chain, typically 259 * returned via {@link KeyChainAliasCallback#alias}. 260 * @throws KeyChainException if the alias was valid but there was some problem accessing it. 261 */ 262 public static X509Certificate[] getCertificateChain(Context context, String alias) 263 throws KeyChainException, InterruptedException { 264 if (alias == null) { 265 throw new NullPointerException("alias == null"); 266 } 267 KeyChainConnection keyChainConnection = bind(context); 268 try { 269 String authToken = authToken(context, alias); 270 if (authToken == null) { 271 return null; 272 } 273 IKeyChainService keyChainService = keyChainConnection.getService(); 274 byte[] certificateBytes = keyChainService.getCertificate(alias, authToken); 275 return new X509Certificate[] { toCertificate(certificateBytes) }; 276 } catch (RemoteException e) { 277 throw new KeyChainException(e); 278 } catch (RuntimeException e) { 279 // only certain RuntimeExceptions can be propagated across the IKeyChainService call 280 throw new KeyChainException(e); 281 } finally { 282 keyChainConnection.close(); 283 } 284 } 285 286 private static PrivateKey toPrivateKey(byte[] bytes) { 287 if (bytes == null) { 288 throw new IllegalArgumentException("bytes == null"); 289 } 290 try { 291 KeyPair keyPair = (KeyPair) Credentials.convertFromPem(bytes).get(0); 292 return keyPair.getPrivate(); 293 } catch (IOException e) { 294 throw new AssertionError(e); 295 } 296 } 297 298 private static X509Certificate toCertificate(byte[] bytes) { 299 if (bytes == null) { 300 throw new IllegalArgumentException("bytes == null"); 301 } 302 try { 303 CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); 304 Certificate cert = certFactory.generateCertificate(new ByteArrayInputStream(bytes)); 305 return (X509Certificate) cert; 306 } catch (CertificateException e) { 307 throw new AssertionError(e); 308 } 309 } 310 311 private static String authToken(Context context, String alias) { 312 AccountManager accountManager = AccountManager.get(context); 313 AccountManagerFuture<Bundle> future = accountManager.getAuthToken(getAccount(context), 314 alias, 315 false, 316 null, 317 null); 318 Bundle bundle; 319 try { 320 bundle = future.getResult(); 321 } catch (OperationCanceledException e) { 322 throw new AssertionError(e); 323 } catch (IOException e) { 324 // KeyChainAccountAuthenticator doesn't do I/O 325 throw new AssertionError(e); 326 } catch (AuthenticatorException e) { 327 throw new AssertionError(e); 328 } 329 Intent intent = bundle.getParcelable(AccountManager.KEY_INTENT); 330 if (intent != null) { 331 return null; 332 } 333 String authToken = bundle.getString(AccountManager.KEY_AUTHTOKEN); 334 if (authToken == null) { 335 throw new AssertionError("Invalid authtoken"); 336 } 337 return authToken; 338 } 339 340 private static Account getAccount(Context context) { 341 AccountManager accountManager = AccountManager.get(context); 342 Account[] accounts = accountManager.getAccountsByType(ACCOUNT_TYPE); 343 if (accounts.length == 0) { 344 try { 345 // Account is created if necessary during binding of the IKeyChainService 346 bind(context).close(); 347 } catch (InterruptedException e) { 348 throw new AssertionError(e); 349 } 350 accounts = accountManager.getAccountsByType(ACCOUNT_TYPE); 351 } 352 return accounts[0]; 353 } 354 355 /** 356 * @hide for reuse by CertInstaller and Settings. 357 * @see KeyChain#bind 358 */ 359 public final static class KeyChainConnection implements Closeable { 360 private final Context context; 361 private final ServiceConnection serviceConnection; 362 private final IKeyChainService service; 363 private KeyChainConnection(Context context, 364 ServiceConnection serviceConnection, 365 IKeyChainService service) { 366 this.context = context; 367 this.serviceConnection = serviceConnection; 368 this.service = service; 369 } 370 @Override public void close() { 371 context.unbindService(serviceConnection); 372 } 373 public IKeyChainService getService() { 374 return service; 375 } 376 } 377 378 /** 379 * @hide for reuse by CertInstaller and Settings. 380 * 381 * Caller should call unbindService on the result when finished. 382 */ 383 public static KeyChainConnection bind(Context context) throws InterruptedException { 384 if (context == null) { 385 throw new NullPointerException("context == null"); 386 } 387 ensureNotOnMainThread(context); 388 final BlockingQueue<IKeyChainService> q = new LinkedBlockingQueue<IKeyChainService>(1); 389 ServiceConnection keyChainServiceConnection = new ServiceConnection() { 390 @Override public void onServiceConnected(ComponentName name, IBinder service) { 391 try { 392 q.put(IKeyChainService.Stub.asInterface(service)); 393 } catch (InterruptedException e) { 394 throw new AssertionError(e); 395 } 396 } 397 @Override public void onServiceDisconnected(ComponentName name) {} 398 }; 399 boolean isBound = context.bindService(new Intent(IKeyChainService.class.getName()), 400 keyChainServiceConnection, 401 Context.BIND_AUTO_CREATE); 402 if (!isBound) { 403 throw new AssertionError("could not bind to KeyChainService"); 404 } 405 return new KeyChainConnection(context, keyChainServiceConnection, q.take()); 406 } 407 408 private static void ensureNotOnMainThread(Context context) { 409 Looper looper = Looper.myLooper(); 410 if (looper != null && looper == context.getMainLooper()) { 411 throw new IllegalStateException( 412 "calling this from your main thread can lead to deadlock"); 413 } 414 } 415} 416