diff --git a/keystore/java/android/security/IKeyChainService.aidl b/keystore/java/android/security/IKeyChainService.aidl index 0bca5b50c43ab..be59f23fe1b34 100644 --- a/keystore/java/android/security/IKeyChainService.aidl +++ b/keystore/java/android/security/IKeyChainService.aidl @@ -25,10 +25,8 @@ import android.os.Bundle; */ interface IKeyChainService { // APIs used by KeyChain - byte[] getPrivate(String alias, String authToken); + byte[] getPrivateKey(String alias, String authToken); byte[] getCertificate(String alias, String authToken); - byte[] getCaCertificate(String alias, String authToken); - String findIssuer(in Bundle cert); // APIs used by CertInstaller void installCaCertificate(in byte[] caCertificate); diff --git a/keystore/java/android/security/KeyChain.java b/keystore/java/android/security/KeyChain.java index 69847bf0eba3b..bcfd6da8f430c 100644 --- a/keystore/java/android/security/KeyChain.java +++ b/keystore/java/android/security/KeyChain.java @@ -28,24 +28,19 @@ import android.os.Bundle; import android.os.IBinder; import android.os.Looper; import android.os.RemoteException; -import android.util.Log; -import dalvik.system.CloseGuard; import java.io.ByteArrayInputStream; import java.io.IOException; import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; -import java.security.cert.CertPathValidatorException; import java.security.cert.Certificate; -import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; -import java.security.cert.TrustAnchor; import java.security.cert.X509Certificate; import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; -import org.apache.harmony.xnet.provider.jsse.IndexedPKIXParameters; -import org.apache.harmony.xnet.provider.jsse.SSLParametersImpl; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; /** * @hide @@ -59,30 +54,6 @@ public final class KeyChain { */ public static final String ACCOUNT_TYPE = "com.android.keychain"; - /** - * @hide Also used by KeyChainService implementation - */ - // TODO This non-localized CA string to be removed when CAs moved out of keystore - public static final String CA_SUFFIX = " CA"; - - public static final String KEY_INTENT = "intent"; - - /** - * Intentionally not public to leave open the future possibility - * of hardware based keys. Callers should use {@link #toPrivateKey - * toPrivateKey} in order to convert a bundle to a {@code - * PrivateKey} - */ - private static final String KEY_PKCS8 = "pkcs8"; - - /** - * Intentionally not public to leave open the future possibility - * of hardware based certs. Callers should use {@link - * #toCertificate toCertificate} in order to convert a bundle to a - * {@code PrivateKey} - */ - private static final String KEY_X509 = "x509"; - /** * Returns an {@code Intent} for use with {@link * android.app.Activity#startActivityForResult @@ -97,160 +68,81 @@ public final class KeyChain { } /** - * Returns a new {@code KeyChain} instance. When the caller is - * done using the {@code KeyChain}, it must be closed with {@link - * #close()} or resource leaks will occur. + * Returns a new {@code KeyChainResult} instance. */ - public static KeyChain getInstance(Context context) throws InterruptedException { - return new KeyChain(context); - } - - private final AccountManager mAccountManager; - - private final Object mServiceLock = new Object(); - private IKeyChainService mService; - private boolean mIsBound; - - private Account mAccount; - - private ServiceConnection mServiceConnection = new ServiceConnection() { - @Override public void onServiceConnected(ComponentName name, IBinder service) { - synchronized (mServiceLock) { - mService = IKeyChainService.Stub.asInterface(service); - mServiceLock.notifyAll(); - - // Account is created if necessary during binding of the IKeyChainService - mAccount = mAccountManager.getAccountsByType(ACCOUNT_TYPE)[0]; - } - } - - @Override public void onServiceDisconnected(ComponentName name) { - synchronized (mServiceLock) { - mService = null; - } - } - }; - - private final Context mContext; - - private final CloseGuard mGuard = CloseGuard.get(); - - private KeyChain(Context context) throws InterruptedException { + public static KeyChainResult get(Context context, String alias) + throws InterruptedException, RemoteException { if (context == null) { throw new NullPointerException("context == null"); } - mContext = context; - ensureNotOnMainThread(); - mAccountManager = AccountManager.get(mContext); - mIsBound = mContext.bindService(new Intent(IKeyChainService.class.getName()), - mServiceConnection, - Context.BIND_AUTO_CREATE); - if (!mIsBound) { - throw new AssertionError(); - } - synchronized (mServiceLock) { - // there is a race between binding on this thread and the - // callback on the main thread. wait until binding is done - // to be sure we have the mAccount initialized. - if (mService == null) { - mServiceLock.wait(); - } - } - mGuard.open("close"); - } - - /** - * {@code Bundle} will contain {@link #KEY_INTENT} if user needs - * to confirm application access to requested key. In the alias - * does not exist or there is an error, null is - * returned. Otherwise the {@code Bundle} contains information - * representing the private key which can be interpreted with - * {@link #toPrivateKey toPrivateKey}. - * - * non-null alias - */ - public Bundle getPrivate(String alias) { - return get(alias, Credentials.USER_PRIVATE_KEY); - } - - public Bundle getCertificate(String alias) { - return get(alias, Credentials.USER_CERTIFICATE); - } - - public Bundle getCaCertificate(String alias) { - return get(alias, Credentials.CA_CERTIFICATE); - } - - private Bundle get(String alias, String type) { if (alias == null) { throw new NullPointerException("alias == null"); } - ensureNotOnMainThread(); - - String authAlias = (type.equals(Credentials.CA_CERTIFICATE)) ? (alias + CA_SUFFIX) : alias; - AccountManagerFuture future = mAccountManager.getAuthToken(mAccount, - authAlias, - false, - null, - null); - Bundle bundle; - try { - bundle = future.getResult(); - } catch (OperationCanceledException e) { - throw new AssertionError(e); - } catch (IOException e) { - throw new AssertionError(e); - } catch (AuthenticatorException e) { - throw new AssertionError(e); - } - Intent intent = bundle.getParcelable(AccountManager.KEY_INTENT); - if (intent != null) { - Bundle result = new Bundle(); - // we don't want this Eclair compatability flag, - // it will prevent onActivityResult from being called - intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_NEW_TASK); - result.putParcelable(KEY_INTENT, intent); - return result; - } - String authToken = bundle.getString(AccountManager.KEY_AUTHTOKEN); - if (authToken == null) { - throw new AssertionError("Invalid authtoken"); - } - - byte[] bytes; - try { - if (type.equals(Credentials.USER_PRIVATE_KEY)) { - bytes = mService.getPrivate(alias, authToken); - } else if (type.equals(Credentials.USER_CERTIFICATE)) { - bytes = mService.getCertificate(alias, authToken); - } else if (type.equals(Credentials.CA_CERTIFICATE)) { - bytes = mService.getCaCertificate(alias, authToken); - } else { - throw new AssertionError(); + ensureNotOnMainThread(context); + final BlockingQueue q = new LinkedBlockingQueue(1); + ServiceConnection keyChainServiceConnection = new ServiceConnection() { + @Override public void onServiceConnected(ComponentName name, IBinder service) { + try { + q.put(IKeyChainService.Stub.asInterface(service)); + } catch (InterruptedException e) { + throw new AssertionError(e); + } } - } catch (RemoteException e) { - throw new AssertionError(e); + @Override public void onServiceDisconnected(ComponentName name) {} + }; + boolean isBound = context.bindService(new Intent(IKeyChainService.class.getName()), + keyChainServiceConnection, + Context.BIND_AUTO_CREATE); + if (!isBound) { + throw new AssertionError("could not bind to KeyChainService"); } - if (bytes == null) { - throw new AssertionError(); + IKeyChainService keyChainService; + try { + keyChainService = q.take(); + + // Account is created if necessary during binding of the IKeyChainService + AccountManager accountManager = AccountManager.get(context); + Account account = accountManager.getAccountsByType(ACCOUNT_TYPE)[0]; + AccountManagerFuture future = accountManager.getAuthToken(account, + alias, + false, + null, + null); + Bundle bundle; + try { + bundle = future.getResult(); + } catch (OperationCanceledException e) { + throw new AssertionError(e); + } catch (IOException e) { + throw new AssertionError(e); + } catch (AuthenticatorException e) { + throw new AssertionError(e); + } + Intent intent = bundle.getParcelable(AccountManager.KEY_INTENT); + if (intent != null) { + Bundle result = new Bundle(); + // we don't want this Eclair compatability flag, + // it will prevent onActivityResult from being called + intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_NEW_TASK); + return new KeyChainResult(intent); + } + + String authToken = bundle.getString(AccountManager.KEY_AUTHTOKEN); + if (authToken == null) { + throw new AssertionError("Invalid authtoken"); + } + byte[] privateKeyBytes = keyChainService.getPrivateKey(alias, authToken); + byte[] certificateBytes = keyChainService.getCertificate(alias, authToken); + return new KeyChainResult(toPrivateKey(privateKeyBytes), + toCertificate(certificateBytes)); + } finally { + context.unbindService(keyChainServiceConnection); } - Bundle result = new Bundle(); - if (type.equals(Credentials.USER_PRIVATE_KEY)) { - result.putByteArray(KEY_PKCS8, bytes); - } else if (type.equals(Credentials.USER_CERTIFICATE)) { - result.putByteArray(KEY_X509, bytes); - } else if (type.equals(Credentials.CA_CERTIFICATE)) { - result.putByteArray(KEY_X509, bytes); - } else { - throw new AssertionError(); - } - return result; } - public static PrivateKey toPrivateKey(Bundle bundle) { - byte[] bytes = bundle.getByteArray(KEY_PKCS8); + private static PrivateKey toPrivateKey(byte[] bytes) { if (bytes == null) { - throw new IllegalArgumentException("not a private key bundle"); + throw new IllegalArgumentException("bytes == null"); } try { KeyFactory keyFactory = KeyFactory.getInstance("RSA"); @@ -262,20 +154,9 @@ public final class KeyChain { } } - public static Bundle fromPrivateKey(PrivateKey privateKey) { - Bundle bundle = new Bundle(); - String format = privateKey.getFormat(); - if (!format.equals("PKCS#8")) { - throw new IllegalArgumentException("Unsupported private key format " + format); - } - bundle.putByteArray(KEY_PKCS8, privateKey.getEncoded()); - return bundle; - } - - public static X509Certificate toCertificate(Bundle bundle) { - byte[] bytes = bundle.getByteArray(KEY_X509); + private static X509Certificate toCertificate(byte[] bytes) { if (bytes == null) { - throw new IllegalArgumentException("not a certificate bundle"); + throw new IllegalArgumentException("bytes == null"); } try { CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); @@ -286,87 +167,11 @@ public final class KeyChain { } } - public static Bundle fromCertificate(Certificate cert) { - Bundle bundle = new Bundle(); - String type = cert.getType(); - if (!type.equals("X.509")) { - throw new IllegalArgumentException("Unsupported certificate type " + type); - } - try { - bundle.putByteArray(KEY_X509, cert.getEncoded()); - } catch (CertificateEncodingException e) { - throw new AssertionError(e); - } - return bundle; - } - - private void ensureNotOnMainThread() { + private static void ensureNotOnMainThread(Context context) { Looper looper = Looper.myLooper(); - if (looper != null && looper == mContext.getMainLooper()) { + if (looper != null && looper == context.getMainLooper()) { throw new IllegalStateException( "calling this from your main thread can lead to deadlock"); } } - - public Bundle findIssuer(X509Certificate cert) { - if (cert == null) { - throw new NullPointerException("cert == null"); - } - ensureNotOnMainThread(); - - // check and see if the issuer is already known to the default IndexedPKIXParameters - IndexedPKIXParameters index = SSLParametersImpl.getDefaultIndexedPKIXParameters(); - try { - TrustAnchor anchor = index.findTrustAnchor(cert); - if (anchor != null && anchor.getTrustedCert() != null) { - X509Certificate ca = anchor.getTrustedCert(); - return fromCertificate(ca); - } - } catch (CertPathValidatorException ignored) { - } - - // otherwise, it might be a user installed CA in the keystore - String alias; - try { - alias = mService.findIssuer(fromCertificate(cert)); - } catch (RemoteException e) { - throw new AssertionError(e); - } - if (alias == null) { - Log.w(TAG, "Lookup failed for issuer"); - return null; - } - - Bundle bundle = get(alias, Credentials.CA_CERTIFICATE); - Intent intent = bundle.getParcelable(KEY_INTENT); - if (intent != null) { - // permission still required - return bundle; - } - // add the found CA to the index for next time - X509Certificate ca = toCertificate(bundle); - index.index(new TrustAnchor(ca, null)); - return bundle; - } - - public void close() { - if (mIsBound) { - mContext.unbindService(mServiceConnection); - mIsBound = false; - mGuard.close(); - } - } - - protected void finalize() throws Throwable { - // note we don't close, we just warn. - // shouldn't be doing I/O in a finalizer, - // which the unbind would cause. - try { - if (mGuard != null) { - mGuard.warnIfOpen(); - } - } finally { - super.finalize(); - } - } } diff --git a/keystore/java/android/security/KeyChainResult.java b/keystore/java/android/security/KeyChainResult.java new file mode 100644 index 0000000000000..85a29214154e5 --- /dev/null +++ b/keystore/java/android/security/KeyChainResult.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.security; + +import android.content.Intent; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; + +/** + * The KeyChainResult is the complex result value from {@link + * KeyChain#get}. The caller should first inspect {@link #getIntent} + * to determine if the user needs to grant the application access to + * the protected contents. If {@code getIntent} returns null, access + * has been granted and the methods {@link #getPrivateKey} and {@link + * #getCertificate} can be used to access the credentials. + * + * @hide + */ +public final class KeyChainResult { + + private final Intent intent; + private final PrivateKey privateKey; + private final X509Certificate certificate; + + KeyChainResult(Intent intent) { + this(intent, null, null); + } + + KeyChainResult(PrivateKey privateKey, X509Certificate certificate) { + this(null, privateKey, certificate); + } + + private KeyChainResult(Intent intent, PrivateKey privateKey, X509Certificate certificate) { + this.intent = intent; + this.privateKey = privateKey; + this.certificate = certificate; + } + + public Intent getIntent() { + return intent; + } + + public PrivateKey getPrivateKey() { + checkIntent(); + return privateKey; + } + + public X509Certificate getCertificate() { + checkIntent(); + return certificate; + } + + private void checkIntent() { + if (intent != null) { + throw new IllegalStateException("non-null Intent, check getIntent()"); + } + } + +}