KeyChain.java revision 9d7faa91be6661eccf73494f1ab96ae9a28d42d7
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.PrivateKey; 40import java.security.cert.Certificate; 41import java.security.cert.CertificateException; 42import java.security.cert.CertificateFactory; 43import java.security.cert.X509Certificate; 44import java.security.spec.InvalidKeySpecException; 45import java.security.spec.PKCS8EncodedKeySpec; 46import java.util.concurrent.BlockingQueue; 47import java.util.concurrent.LinkedBlockingQueue; 48 49/** 50 * @hide 51 */ 52public final class KeyChain { 53 54 private static final String TAG = "KeyChain"; 55 56 /** 57 * @hide Also used by KeyChainService implementation 58 */ 59 public static final String ACCOUNT_TYPE = "com.android.keychain"; 60 61 /** 62 * @hide Also used by KeyChainActivity implementation 63 */ 64 public static final String EXTRA_RESPONSE = "response"; 65 66 /** 67 * Launches an {@code Activity} for the user to select the alias 68 * for a private key and certificate pair for authentication. The 69 * selected alias or null will be returned via the 70 * IKeyChainAliasResponse callback. 71 */ 72 public static void choosePrivateKeyAlias(Activity activity, KeyChainAliasResponse response) { 73 if (activity == null) { 74 throw new NullPointerException("activity == null"); 75 } 76 if (response == null) { 77 throw new NullPointerException("response == null"); 78 } 79 Intent intent = new Intent("com.android.keychain.CHOOSER"); 80 intent.putExtra(EXTRA_RESPONSE, new AliasResponse(activity, response)); 81 activity.startActivity(intent); 82 } 83 84 private static class AliasResponse extends IKeyChainAliasResponse.Stub { 85 private final Activity activity; 86 private final KeyChainAliasResponse keyChainAliasResponse; 87 private AliasResponse(Activity activity, KeyChainAliasResponse keyChainAliasResponse) { 88 this.activity = activity; 89 this.keyChainAliasResponse = keyChainAliasResponse; 90 } 91 @Override public void alias(String alias) { 92 if (alias == null) { 93 keyChainAliasResponse.alias(null); 94 return; 95 } 96 AccountManager accountManager = AccountManager.get(activity); 97 accountManager.getAuthToken(getAccount(activity), 98 alias, 99 null, 100 activity, 101 new AliasAccountManagerCallback(keyChainAliasResponse, 102 alias), 103 null); 104 } 105 } 106 107 private static class AliasAccountManagerCallback implements AccountManagerCallback<Bundle> { 108 private final KeyChainAliasResponse keyChainAliasResponse; 109 private final String alias; 110 private AliasAccountManagerCallback(KeyChainAliasResponse keyChainAliasResponse, 111 String alias) { 112 this.keyChainAliasResponse = keyChainAliasResponse; 113 this.alias = alias; 114 } 115 @Override public void run(AccountManagerFuture<Bundle> future) { 116 Bundle bundle; 117 try { 118 bundle = future.getResult(); 119 } catch (OperationCanceledException e) { 120 keyChainAliasResponse.alias(null); 121 return; 122 } catch (IOException e) { 123 keyChainAliasResponse.alias(null); 124 return; 125 } catch (AuthenticatorException e) { 126 keyChainAliasResponse.alias(null); 127 return; 128 } 129 String authToken = bundle.getString(AccountManager.KEY_AUTHTOKEN); 130 if (authToken != null) { 131 keyChainAliasResponse.alias(alias); 132 } else { 133 keyChainAliasResponse.alias(null); 134 } 135 } 136 } 137 138 /** 139 * Returns the {@code PrivateKey} for the requested alias, or null 140 * if no there is no result. 141 */ 142 public static PrivateKey getPrivateKey(Context context, String alias) 143 throws InterruptedException, RemoteException { 144 if (alias == null) { 145 throw new NullPointerException("alias == null"); 146 } 147 KeyChainConnection keyChainConnection = bind(context); 148 try { 149 String authToken = authToken(context, alias); 150 if (authToken == null) { 151 return null; 152 } 153 IKeyChainService keyChainService = keyChainConnection.getService(); 154 byte[] privateKeyBytes = keyChainService.getPrivateKey(alias, authToken); 155 return toPrivateKey(privateKeyBytes); 156 } finally { 157 keyChainConnection.close(); 158 } 159 } 160 161 /** 162 * Returns the {@code X509Certificate} chain for the requested 163 * alias, or null if no there is no result. 164 */ 165 public static X509Certificate[] getCertificateChain(Context context, String alias) 166 throws InterruptedException, RemoteException { 167 if (alias == null) { 168 throw new NullPointerException("alias == null"); 169 } 170 KeyChainConnection keyChainConnection = bind(context); 171 try { 172 String authToken = authToken(context, alias); 173 if (authToken == null) { 174 return null; 175 } 176 IKeyChainService keyChainService = keyChainConnection.getService(); 177 byte[] certificateBytes = keyChainService.getCertificate(alias, authToken); 178 return new X509Certificate[] { toCertificate(certificateBytes) }; 179 } finally { 180 keyChainConnection.close(); 181 } 182 } 183 184 private static PrivateKey toPrivateKey(byte[] bytes) { 185 if (bytes == null) { 186 throw new IllegalArgumentException("bytes == null"); 187 } 188 try { 189 KeyPair keyPair = (KeyPair) Credentials.convertFromPem(bytes).get(0); 190 return keyPair.getPrivate(); 191 } catch (IOException e) { 192 throw new AssertionError(e); 193 } 194 } 195 196 private static X509Certificate toCertificate(byte[] bytes) { 197 if (bytes == null) { 198 throw new IllegalArgumentException("bytes == null"); 199 } 200 try { 201 CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); 202 Certificate cert = certFactory.generateCertificate(new ByteArrayInputStream(bytes)); 203 return (X509Certificate) cert; 204 } catch (CertificateException e) { 205 throw new AssertionError(e); 206 } 207 } 208 209 private static String authToken(Context context, String alias) { 210 AccountManager accountManager = AccountManager.get(context); 211 AccountManagerFuture<Bundle> future = accountManager.getAuthToken(getAccount(context), 212 alias, 213 false, 214 null, 215 null); 216 Bundle bundle; 217 try { 218 bundle = future.getResult(); 219 } catch (OperationCanceledException e) { 220 throw new AssertionError(e); 221 } catch (IOException e) { 222 // KeyChainAccountAuthenticator doesn't do I/O 223 throw new AssertionError(e); 224 } catch (AuthenticatorException e) { 225 throw new AssertionError(e); 226 } 227 Intent intent = bundle.getParcelable(AccountManager.KEY_INTENT); 228 if (intent != null) { 229 return null; 230 } 231 String authToken = bundle.getString(AccountManager.KEY_AUTHTOKEN); 232 if (authToken == null) { 233 throw new AssertionError("Invalid authtoken"); 234 } 235 return authToken; 236 } 237 238 private static Account getAccount(Context context) { 239 AccountManager accountManager = AccountManager.get(context); 240 Account[] accounts = accountManager.getAccountsByType(ACCOUNT_TYPE); 241 if (accounts.length == 0) { 242 try { 243 // Account is created if necessary during binding of the IKeyChainService 244 bind(context).close(); 245 } catch (InterruptedException e) { 246 throw new AssertionError(e); 247 } 248 accounts = accountManager.getAccountsByType(ACCOUNT_TYPE); 249 } 250 return accounts[0]; 251 } 252 253 /** 254 * @hide for reuse by CertInstaller and Settings. 255 * @see KeyChain#bind 256 */ 257 public final static class KeyChainConnection implements Closeable { 258 private final Context context; 259 private final ServiceConnection serviceConnection; 260 private final IKeyChainService service; 261 private KeyChainConnection(Context context, 262 ServiceConnection serviceConnection, 263 IKeyChainService service) { 264 this.context = context; 265 this.serviceConnection = serviceConnection; 266 this.service = service; 267 } 268 @Override public void close() { 269 context.unbindService(serviceConnection); 270 } 271 public IKeyChainService getService() { 272 return service; 273 } 274 } 275 276 /** 277 * @hide for reuse by CertInstaller and Settings. 278 * 279 * Caller should call unbindService on the result when finished. 280 */ 281 public static KeyChainConnection bind(Context context) throws InterruptedException { 282 if (context == null) { 283 throw new NullPointerException("context == null"); 284 } 285 ensureNotOnMainThread(context); 286 final BlockingQueue<IKeyChainService> q = new LinkedBlockingQueue<IKeyChainService>(1); 287 ServiceConnection keyChainServiceConnection = new ServiceConnection() { 288 @Override public void onServiceConnected(ComponentName name, IBinder service) { 289 try { 290 q.put(IKeyChainService.Stub.asInterface(service)); 291 } catch (InterruptedException e) { 292 throw new AssertionError(e); 293 } 294 } 295 @Override public void onServiceDisconnected(ComponentName name) {} 296 }; 297 boolean isBound = context.bindService(new Intent(IKeyChainService.class.getName()), 298 keyChainServiceConnection, 299 Context.BIND_AUTO_CREATE); 300 if (!isBound) { 301 throw new AssertionError("could not bind to KeyChainService"); 302 } 303 return new KeyChainConnection(context, keyChainServiceConnection, q.take()); 304 } 305 306 private static void ensureNotOnMainThread(Context context) { 307 Looper looper = Looper.myLooper(); 308 if (looper != null && looper == context.getMainLooper()) { 309 throw new IllegalStateException( 310 "calling this from your main thread can lead to deadlock"); 311 } 312 } 313} 314