Brian Carlstrom | b9a07c1 | 2011-04-11 09:03:51 -0700 | [diff] [blame] | 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 | */ |
| 16 | package android.security; |
| 17 | |
| 18 | import android.accounts.Account; |
| 19 | import android.accounts.AccountManager; |
| 20 | import android.accounts.AccountManagerFuture; |
| 21 | import android.accounts.AuthenticatorException; |
| 22 | import android.accounts.OperationCanceledException; |
| 23 | import android.content.ComponentName; |
| 24 | import android.content.Context; |
| 25 | import android.content.Intent; |
| 26 | import android.content.ServiceConnection; |
| 27 | import android.os.Bundle; |
| 28 | import android.os.IBinder; |
| 29 | import android.os.Looper; |
| 30 | import android.os.RemoteException; |
| 31 | import android.util.Log; |
| 32 | import dalvik.system.CloseGuard; |
| 33 | import java.io.ByteArrayInputStream; |
| 34 | import java.io.IOException; |
| 35 | import java.security.KeyFactory; |
| 36 | import java.security.NoSuchAlgorithmException; |
| 37 | import java.security.PrivateKey; |
| 38 | import java.security.cert.CertPathValidatorException; |
| 39 | import java.security.cert.Certificate; |
| 40 | import java.security.cert.CertificateEncodingException; |
| 41 | import java.security.cert.CertificateException; |
| 42 | import java.security.cert.CertificateFactory; |
| 43 | import java.security.cert.TrustAnchor; |
| 44 | import java.security.cert.X509Certificate; |
| 45 | import java.security.spec.InvalidKeySpecException; |
| 46 | import java.security.spec.PKCS8EncodedKeySpec; |
| 47 | import org.apache.harmony.xnet.provider.jsse.IndexedPKIXParameters; |
| 48 | import org.apache.harmony.xnet.provider.jsse.SSLParametersImpl; |
| 49 | |
| 50 | /** |
| 51 | * @hide |
| 52 | */ |
| 53 | public final class KeyChain { |
| 54 | |
| 55 | private static final String TAG = "KeyChain"; |
| 56 | |
| 57 | /** |
| 58 | * @hide Also used by KeyChainService implementation |
| 59 | */ |
| 60 | public static final String ACCOUNT_TYPE = "com.android.keychain"; |
| 61 | |
| 62 | /** |
| 63 | * @hide Also used by KeyChainService implementation |
| 64 | */ |
| 65 | // TODO This non-localized CA string to be removed when CAs moved out of keystore |
| 66 | public static final String CA_SUFFIX = " CA"; |
| 67 | |
| 68 | public static final String KEY_INTENT = "intent"; |
| 69 | |
| 70 | /** |
| 71 | * Intentionally not public to leave open the future possibility |
| 72 | * of hardware based keys. Callers should use {@link #toPrivateKey |
| 73 | * toPrivateKey} in order to convert a bundle to a {@code |
| 74 | * PrivateKey} |
| 75 | */ |
| 76 | private static final String KEY_PKCS8 = "pkcs8"; |
| 77 | |
| 78 | /** |
| 79 | * Intentionally not public to leave open the future possibility |
| 80 | * of hardware based certs. Callers should use {@link |
| 81 | * #toCertificate toCertificate} in order to convert a bundle to a |
| 82 | * {@code PrivateKey} |
| 83 | */ |
| 84 | private static final String KEY_X509 = "x509"; |
| 85 | |
| 86 | /** |
| 87 | * Returns an {@code Intent} for use with {@link |
| 88 | * android.app.Activity#startActivityForResult |
| 89 | * startActivityForResult}. The result will be returned via {@link |
| 90 | * android.app.Activity#onActivityResult onActivityResult} with |
| 91 | * {@link android.app.Activity#RESULT_OK RESULT_OK} and the alias |
| 92 | * in the returned {@code Intent}'s extra data with key {@link |
| 93 | * android.content.Intent#EXTRA_TEXT Intent.EXTRA_TEXT}. |
| 94 | */ |
| 95 | public static Intent chooseAlias() { |
| 96 | return new Intent("com.android.keychain.CHOOSER"); |
| 97 | } |
| 98 | |
| 99 | /** |
| 100 | * Returns a new {@code KeyChain} instance. When the caller is |
| 101 | * done using the {@code KeyChain}, it must be closed with {@link |
| 102 | * #close()} or resource leaks will occur. |
| 103 | */ |
| 104 | public static KeyChain getInstance(Context context) throws InterruptedException { |
| 105 | return new KeyChain(context); |
| 106 | } |
| 107 | |
| 108 | private final AccountManager mAccountManager; |
| 109 | |
| 110 | private final Object mServiceLock = new Object(); |
| 111 | private IKeyChainService mService; |
| 112 | private boolean mIsBound; |
| 113 | |
| 114 | private Account mAccount; |
| 115 | |
| 116 | private ServiceConnection mServiceConnection = new ServiceConnection() { |
| 117 | @Override public void onServiceConnected(ComponentName name, IBinder service) { |
| 118 | synchronized (mServiceLock) { |
| 119 | mService = IKeyChainService.Stub.asInterface(service); |
| 120 | mServiceLock.notifyAll(); |
| 121 | |
| 122 | // Account is created if necessary during binding of the IKeyChainService |
| 123 | mAccount = mAccountManager.getAccountsByType(ACCOUNT_TYPE)[0]; |
| 124 | } |
| 125 | } |
| 126 | |
| 127 | @Override public void onServiceDisconnected(ComponentName name) { |
| 128 | synchronized (mServiceLock) { |
| 129 | mService = null; |
| 130 | } |
| 131 | } |
| 132 | }; |
| 133 | |
| 134 | private final Context mContext; |
| 135 | |
| 136 | private final CloseGuard mGuard = CloseGuard.get(); |
| 137 | |
| 138 | private KeyChain(Context context) throws InterruptedException { |
| 139 | if (context == null) { |
| 140 | throw new NullPointerException("context == null"); |
| 141 | } |
| 142 | mContext = context; |
| 143 | ensureNotOnMainThread(); |
| 144 | mAccountManager = AccountManager.get(mContext); |
| 145 | mIsBound = mContext.bindService(new Intent(IKeyChainService.class.getName()), |
| 146 | mServiceConnection, |
| 147 | Context.BIND_AUTO_CREATE); |
| 148 | if (!mIsBound) { |
| 149 | throw new AssertionError(); |
| 150 | } |
| 151 | synchronized (mServiceLock) { |
| 152 | // there is a race between binding on this thread and the |
| 153 | // callback on the main thread. wait until binding is done |
| 154 | // to be sure we have the mAccount initialized. |
| 155 | if (mService == null) { |
| 156 | mServiceLock.wait(); |
| 157 | } |
| 158 | } |
| 159 | mGuard.open("close"); |
| 160 | } |
| 161 | |
| 162 | /** |
| 163 | * {@code Bundle} will contain {@link #KEY_INTENT} if user needs |
| 164 | * to confirm application access to requested key. In the alias |
| 165 | * does not exist or there is an error, null is |
| 166 | * returned. Otherwise the {@code Bundle} contains information |
| 167 | * representing the private key which can be interpreted with |
| 168 | * {@link #toPrivateKey toPrivateKey}. |
| 169 | * |
| 170 | * non-null alias |
| 171 | */ |
| 172 | public Bundle getPrivate(String alias) { |
| 173 | return get(alias, Credentials.USER_PRIVATE_KEY); |
| 174 | } |
| 175 | |
| 176 | public Bundle getCertificate(String alias) { |
| 177 | return get(alias, Credentials.USER_CERTIFICATE); |
| 178 | } |
| 179 | |
| 180 | public Bundle getCaCertificate(String alias) { |
| 181 | return get(alias, Credentials.CA_CERTIFICATE); |
| 182 | } |
| 183 | |
| 184 | private Bundle get(String alias, String type) { |
| 185 | if (alias == null) { |
| 186 | throw new NullPointerException("alias == null"); |
| 187 | } |
| 188 | ensureNotOnMainThread(); |
| 189 | |
| 190 | String authAlias = (type.equals(Credentials.CA_CERTIFICATE)) ? (alias + CA_SUFFIX) : alias; |
| 191 | AccountManagerFuture<Bundle> future = mAccountManager.getAuthToken(mAccount, |
| 192 | authAlias, |
| 193 | false, |
| 194 | null, |
| 195 | null); |
| 196 | Bundle bundle; |
| 197 | try { |
| 198 | bundle = future.getResult(); |
| 199 | } catch (OperationCanceledException e) { |
| 200 | throw new AssertionError(e); |
| 201 | } catch (IOException e) { |
| 202 | throw new AssertionError(e); |
| 203 | } catch (AuthenticatorException e) { |
| 204 | throw new AssertionError(e); |
| 205 | } |
| 206 | Intent intent = bundle.getParcelable(AccountManager.KEY_INTENT); |
| 207 | if (intent != null) { |
| 208 | Bundle result = new Bundle(); |
| 209 | // we don't want this Eclair compatability flag, |
| 210 | // it will prevent onActivityResult from being called |
| 211 | intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_NEW_TASK); |
| 212 | result.putParcelable(KEY_INTENT, intent); |
| 213 | return result; |
| 214 | } |
| 215 | String authToken = bundle.getString(AccountManager.KEY_AUTHTOKEN); |
| 216 | if (authToken == null) { |
| 217 | throw new AssertionError("Invalid authtoken"); |
| 218 | } |
| 219 | |
| 220 | byte[] bytes; |
| 221 | try { |
| 222 | if (type.equals(Credentials.USER_PRIVATE_KEY)) { |
| 223 | bytes = mService.getPrivate(alias, authToken); |
| 224 | } else if (type.equals(Credentials.USER_CERTIFICATE)) { |
| 225 | bytes = mService.getCertificate(alias, authToken); |
| 226 | } else if (type.equals(Credentials.CA_CERTIFICATE)) { |
| 227 | bytes = mService.getCaCertificate(alias, authToken); |
| 228 | } else { |
| 229 | throw new AssertionError(); |
| 230 | } |
| 231 | } catch (RemoteException e) { |
| 232 | throw new AssertionError(e); |
| 233 | } |
| 234 | if (bytes == null) { |
| 235 | throw new AssertionError(); |
| 236 | } |
| 237 | Bundle result = new Bundle(); |
| 238 | if (type.equals(Credentials.USER_PRIVATE_KEY)) { |
| 239 | result.putByteArray(KEY_PKCS8, bytes); |
| 240 | } else if (type.equals(Credentials.USER_CERTIFICATE)) { |
| 241 | result.putByteArray(KEY_X509, bytes); |
| 242 | } else if (type.equals(Credentials.CA_CERTIFICATE)) { |
| 243 | result.putByteArray(KEY_X509, bytes); |
| 244 | } else { |
| 245 | throw new AssertionError(); |
| 246 | } |
| 247 | return result; |
| 248 | } |
| 249 | |
| 250 | public static PrivateKey toPrivateKey(Bundle bundle) { |
| 251 | byte[] bytes = bundle.getByteArray(KEY_PKCS8); |
| 252 | if (bytes == null) { |
| 253 | throw new IllegalArgumentException("not a private key bundle"); |
| 254 | } |
| 255 | try { |
| 256 | KeyFactory keyFactory = KeyFactory.getInstance("RSA"); |
| 257 | return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(bytes)); |
| 258 | } catch (NoSuchAlgorithmException e) { |
| 259 | throw new AssertionError(e); |
| 260 | } catch (InvalidKeySpecException e) { |
| 261 | throw new AssertionError(e); |
| 262 | } |
| 263 | } |
| 264 | |
| 265 | public static Bundle fromPrivateKey(PrivateKey privateKey) { |
| 266 | Bundle bundle = new Bundle(); |
| 267 | String format = privateKey.getFormat(); |
| 268 | if (!format.equals("PKCS#8")) { |
| 269 | throw new IllegalArgumentException("Unsupported private key format " + format); |
| 270 | } |
| 271 | bundle.putByteArray(KEY_PKCS8, privateKey.getEncoded()); |
| 272 | return bundle; |
| 273 | } |
| 274 | |
| 275 | public static X509Certificate toCertificate(Bundle bundle) { |
| 276 | byte[] bytes = bundle.getByteArray(KEY_X509); |
| 277 | if (bytes == null) { |
| 278 | throw new IllegalArgumentException("not a certificate bundle"); |
| 279 | } |
| 280 | try { |
| 281 | CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); |
| 282 | Certificate cert = certFactory.generateCertificate(new ByteArrayInputStream(bytes)); |
| 283 | return (X509Certificate) cert; |
| 284 | } catch (CertificateException e) { |
| 285 | throw new AssertionError(e); |
| 286 | } |
| 287 | } |
| 288 | |
| 289 | public static Bundle fromCertificate(Certificate cert) { |
| 290 | Bundle bundle = new Bundle(); |
| 291 | String type = cert.getType(); |
| 292 | if (!type.equals("X.509")) { |
| 293 | throw new IllegalArgumentException("Unsupported certificate type " + type); |
| 294 | } |
| 295 | try { |
| 296 | bundle.putByteArray(KEY_X509, cert.getEncoded()); |
| 297 | } catch (CertificateEncodingException e) { |
| 298 | throw new AssertionError(e); |
| 299 | } |
| 300 | return bundle; |
| 301 | } |
| 302 | |
| 303 | private void ensureNotOnMainThread() { |
| 304 | Looper looper = Looper.myLooper(); |
| 305 | if (looper != null && looper == mContext.getMainLooper()) { |
| 306 | throw new IllegalStateException( |
| 307 | "calling this from your main thread can lead to deadlock"); |
| 308 | } |
| 309 | } |
| 310 | |
| 311 | public Bundle findIssuer(X509Certificate cert) { |
| 312 | if (cert == null) { |
| 313 | throw new NullPointerException("cert == null"); |
| 314 | } |
| 315 | ensureNotOnMainThread(); |
| 316 | |
| 317 | // check and see if the issuer is already known to the default IndexedPKIXParameters |
| 318 | IndexedPKIXParameters index = SSLParametersImpl.getDefaultIndexedPKIXParameters(); |
| 319 | try { |
| 320 | TrustAnchor anchor = index.findTrustAnchor(cert); |
| 321 | if (anchor != null && anchor.getTrustedCert() != null) { |
| 322 | X509Certificate ca = anchor.getTrustedCert(); |
| 323 | return fromCertificate(ca); |
| 324 | } |
| 325 | } catch (CertPathValidatorException ignored) { |
| 326 | } |
| 327 | |
| 328 | // otherwise, it might be a user installed CA in the keystore |
| 329 | String alias; |
| 330 | try { |
| 331 | alias = mService.findIssuer(fromCertificate(cert)); |
| 332 | } catch (RemoteException e) { |
| 333 | throw new AssertionError(e); |
| 334 | } |
| 335 | if (alias == null) { |
| 336 | Log.w(TAG, "Lookup failed for issuer"); |
| 337 | return null; |
| 338 | } |
| 339 | |
| 340 | Bundle bundle = get(alias, Credentials.CA_CERTIFICATE); |
| 341 | Intent intent = bundle.getParcelable(KEY_INTENT); |
| 342 | if (intent != null) { |
| 343 | // permission still required |
| 344 | return bundle; |
| 345 | } |
| 346 | // add the found CA to the index for next time |
| 347 | X509Certificate ca = toCertificate(bundle); |
| 348 | index.index(new TrustAnchor(ca, null)); |
| 349 | return bundle; |
| 350 | } |
| 351 | |
| 352 | public void close() { |
| 353 | if (mIsBound) { |
| 354 | mContext.unbindService(mServiceConnection); |
| 355 | mIsBound = false; |
| 356 | mGuard.close(); |
| 357 | } |
| 358 | } |
| 359 | |
| 360 | protected void finalize() throws Throwable { |
| 361 | // note we don't close, we just warn. |
| 362 | // shouldn't be doing I/O in a finalizer, |
| 363 | // which the unbind would cause. |
| 364 | try { |
| 365 | if (mGuard != null) { |
| 366 | mGuard.warnIfOpen(); |
| 367 | } |
| 368 | } finally { |
| 369 | super.finalize(); |
| 370 | } |
| 371 | } |
| 372 | } |