Merge "Simplify KeyChain API by removing now unneeded CA certificate lookup (1 of 3)"
diff --git a/keystore/java/android/security/IKeyChainService.aidl b/keystore/java/android/security/IKeyChainService.aidl
index 0bca5b5..be59f23 100644
--- a/keystore/java/android/security/IKeyChainService.aidl
+++ b/keystore/java/android/security/IKeyChainService.aidl
@@ -25,10 +25,8 @@
*/
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 69847bf..bcfd6da 100644
--- a/keystore/java/android/security/KeyChain.java
+++ b/keystore/java/android/security/KeyChain.java
@@ -28,24 +28,19 @@
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
@@ -60,30 +55,6 @@
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
* startActivityForResult}. The result will be returned via {@link
@@ -97,160 +68,81 @@
}
/**
- * 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<Bundle> 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<IKeyChainService> q = new LinkedBlockingQueue<IKeyChainService>(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<Bundle> 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 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 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 0000000..85a2921
--- /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()");
+ }
+ }
+
+}