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()");
+        }
+    }
+
+}