Adding KeyChainService and KeyChainActivity

Change-Id: I6c862d3e687cf80fb882966adb3245f2244244fe
diff --git a/Android.mk b/Android.mk
new file mode 100755
index 0000000..ad6b85a
--- /dev/null
+++ b/Android.mk
@@ -0,0 +1,28 @@
+# Copyright 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.
+
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := KeyChain
+LOCAL_CERTIFICATE := platform
+
+include $(BUILD_PACKAGE)
+
+# additionally, build unit tests in a separate .apk
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
new file mode 100755
index 0000000..eb46fb7
--- /dev/null
+++ b/AndroidManifest.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.keychain"
+          android:sharedUserId="android.uid.keychain"
+          android:sharedUserLabel="@string/keychainUserLabel"
+          >
+    <uses-permission android:name="android.permission.GET_ACCOUNTS"/>
+    <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS"/>
+    <application>
+        <service android:name="com.android.keychain.KeyChainService">
+            <intent-filter>
+                <action android:name="android.security.IKeyChainService"/>
+                <action android:name="android.accounts.AccountAuthenticator"/>
+            </intent-filter>
+            <meta-data android:name="android.accounts.AccountAuthenticator"
+                       android:resource="@xml/authenticator"/>
+        </service>
+        <activity android:name="com.android.keychain.KeyChainActivity"
+		  android:excludeFromRecents="true">
+	    <intent-filter>
+	        <action android:name="com.android.keychain.CHOOSER"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+            </intent-filter>
+        </activity>
+  </application>
+</manifest>
diff --git a/res/values/strings.xml b/res/values/strings.xml
new file mode 100755
index 0000000..e208bed
--- /dev/null
+++ b/res/values/strings.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_name">Key Chain</string>
+    <string name="keychainUserLabel">keychain</string>
+</resources>
diff --git a/res/xml/authenticator.xml b/res/xml/authenticator.xml
new file mode 100644
index 0000000..88e2f94
--- /dev/null
+++ b/res/xml/authenticator.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<!-- The attributes in this XML file provide configuration information -->
+<!-- for the Account Manager. -->
+
+<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
+    android:accountType="com.android.keychain"
+    android:label="@string/app_name"
+/>
diff --git a/src/com/android/keychain/KeyChainActivity.java b/src/com/android/keychain/KeyChainActivity.java
new file mode 100644
index 0000000..c6e055d
--- /dev/null
+++ b/src/com/android/keychain/KeyChainActivity.java
@@ -0,0 +1,141 @@
+/*
+ * 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 com.android.keychain;
+
+import android.app.ListActivity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.security.Credentials;
+import android.security.KeyStore;
+import android.view.View;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+public class KeyChainActivity extends ListActivity {
+
+    private static final String TAG = "KeyChainActivity";
+
+    private static String KEY_STATE = "state";
+
+    private static final int REQUEST_UNLOCK = 1;
+
+    private static enum State { INITIAL, UNLOCK_REQUESTED };
+
+    private State mState;
+
+    private KeyStore mKeyStore = KeyStore.getInstance();
+
+    private boolean isKeyStoreUnlocked() {
+        return mKeyStore.test() == KeyStore.NO_ERROR;
+    }
+
+    @Override public void onCreate(Bundle savedState) {
+        super.onCreate(savedState);
+        if (savedState == null) {
+            mState = State.INITIAL;
+        } else {
+            mState = (State) savedState.getSerializable(KEY_STATE);
+            if (mState == null) {
+                mState = State.INITIAL;
+            }
+        }
+    }
+
+    @Override public void onResume() {
+        super.onResume();
+
+        // see if KeyStore has been unlocked, if not start activity to do so
+        switch (mState) {
+            case INITIAL:
+                if (!isKeyStoreUnlocked()) {
+                    mState = State.UNLOCK_REQUESTED;
+                    this.startActivityForResult(new Intent(Credentials.UNLOCK_ACTION),
+                                                REQUEST_UNLOCK);
+                    // Note that Credentials.unlock will start an
+                    // Activity and we will be paused but then resumed
+                    // when the unlock Activity completes and our
+                    // onActivityResult is called with REQUEST_UNLOCK
+                    return;
+                }
+                showAliasList();
+                return;
+            case UNLOCK_REQUESTED:
+                // we've already asked, but have not heard back, probably just rotated.
+                // wait to hear back via onActivityResult
+                return;
+            default:
+                throw new AssertionError();
+        }
+    }
+
+    private void showAliasList() {
+
+        String[] aliases = mKeyStore.saw(Credentials.USER_PRIVATE_KEY);
+        if (aliases == null) {
+            setResult(RESULT_CANCELED);
+            finish();
+            return;
+        }
+
+        final ArrayAdapter<String> adapter
+                = new ArrayAdapter<String>(this,
+                                           android.R.layout.simple_list_item_1,
+                                           aliases);
+        setListAdapter(adapter);
+
+        ListView lv = getListView();
+        lv.setTextFilterEnabled(true);
+        lv.setOnItemClickListener(new OnItemClickListener() {
+            @Override public void onItemClick(AdapterView<?> parent,
+                                              View view,
+                                              int position,
+                                              long id) {
+                String alias = adapter.getItem(position);
+                Intent result = new Intent();
+                result.putExtra(Intent.EXTRA_TEXT, alias);
+                setResult(RESULT_OK, result);
+                finish();
+            }
+        });
+    }
+
+    @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        switch (requestCode) {
+            case REQUEST_UNLOCK:
+                if (isKeyStoreUnlocked()) {
+                    showAliasList();
+                } else {
+                    // user must have canceled unlock, give up
+                    finish();
+                }
+                return;
+            default:
+                throw new AssertionError();
+        }
+    }
+
+    @Override protected void onSaveInstanceState(Bundle savedState) {
+        super.onSaveInstanceState(savedState);
+        if (mState != State.INITIAL) {
+            savedState.putSerializable(KEY_STATE, mState);
+        }
+    }
+}
diff --git a/src/com/android/keychain/KeyChainService.java b/src/com/android/keychain/KeyChainService.java
new file mode 100644
index 0000000..8b0b0a1
--- /dev/null
+++ b/src/com/android/keychain/KeyChainService.java
@@ -0,0 +1,271 @@
+/*
+ * 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 com.android.keychain;
+
+import android.accounts.AbstractAccountAuthenticator;
+import android.accounts.Account;
+import android.accounts.AccountAuthenticatorResponse;
+import android.accounts.AccountManager;
+import android.accounts.NetworkErrorException;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.security.Credentials;
+import android.security.IKeyChainService;
+import android.security.KeyChain;
+import android.security.KeyStore;
+import android.util.Log;
+import java.io.ByteArrayInputStream;
+import java.nio.charset.Charsets;
+import java.security.SecureRandom;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import javax.security.auth.x500.X500Principal;
+import org.apache.harmony.luni.util.Base64;
+
+public class KeyChainService extends Service {
+
+    private static final String TAG = "KeyChainService";
+
+    private AccountManager mAccountManager;
+
+    private final Object mAccountLock = new Object();
+    private Account mAccount;
+
+    @Override public void onCreate() {
+        super.onCreate();
+        mAccountManager = AccountManager.get(this);
+    }
+
+    private final IKeyChainService.Stub mIKeyChainService = new IKeyChainService.Stub() {
+
+        private final KeyStore mKeyStore = KeyStore.getInstance();
+
+        private boolean isKeyStoreUnlocked() {
+            return (mKeyStore.test() == KeyStore.NO_ERROR);
+        }
+
+        @Override public byte[] getPrivate(String alias, String authToken) throws RemoteException {
+            if (alias == null) {
+                throw new NullPointerException("alias == null");
+            }
+            if (authToken == null) {
+                throw new NullPointerException("authToken == null");
+            }
+            if (!isKeyStoreUnlocked()) {
+                throw new IllegalStateException("keystore locked");
+            }
+            if (!mAccountManager.peekAuthToken(mAccount, alias).equals(authToken)) {
+                throw new IllegalStateException("authtoken mismatch");
+            }
+            String key = Credentials.USER_PRIVATE_KEY + alias;
+            byte[] bytes = mKeyStore.get(key.getBytes(Charsets.UTF_8));
+            if (bytes == null) {
+                throw new IllegalStateException("keystore value missing");
+            }
+            return bytes;
+        }
+
+        @Override public byte[] getCertificate(String alias, String authToken)
+                throws RemoteException {
+            return getCert(Credentials.USER_CERTIFICATE, alias, authToken);
+        }
+        @Override public byte[] getCaCertificate(String alias, String authToken)
+                throws RemoteException {
+            return getCert(Credentials.CA_CERTIFICATE, alias, authToken);
+        }
+
+        private byte[] getCert(String type, String alias, String authToken)
+                throws RemoteException {
+            if (alias == null) {
+                throw new NullPointerException("alias == null");
+            }
+            if (authToken == null) {
+                throw new NullPointerException("authtoken == null");
+            }
+            if (!isKeyStoreUnlocked()) {
+                throw new IllegalStateException("keystore locked");
+            }
+            String authAlias = (type.equals(Credentials.CA_CERTIFICATE))
+                    ? (alias + KeyChain.CA_SUFFIX)
+                    : alias;
+            if (!mAccountManager.peekAuthToken(mAccount, authAlias).equals(authToken)) {
+                throw new IllegalStateException("authtoken mismatch");
+            }
+            String key = type + alias;
+            byte[] bytes =  mKeyStore.get(key.getBytes(Charsets.UTF_8));
+            if (bytes == null) {
+                throw new IllegalStateException("keystore value missing");
+            }
+            return bytes;
+        }
+
+        @Override public String findIssuer(Bundle bundle) {
+            if (bundle == null) {
+                throw new NullPointerException("bundle == null");
+            }
+            X509Certificate cert = KeyChain.toCertificate(bundle);
+            if (cert == null) {
+                throw new IllegalArgumentException("no cert in bundle");
+            }
+            X500Principal issuer = cert.getIssuerX500Principal();
+            if (issuer == null) {
+                throw new IllegalStateException();
+            }
+            byte[] aliasPrefix = Credentials.CA_CERTIFICATE.getBytes(Charsets.UTF_8);
+            byte[][] aliasSuffixes = mKeyStore.saw(aliasPrefix);
+            if (aliasSuffixes == null) {
+                return null;
+            }
+
+            // TODO if the keystore would notify us of changes, we
+            // could cache the certs and perform a lookup by issuer
+            for (byte[] aliasSuffix : aliasSuffixes) {
+                byte[] alias = concatenate(aliasPrefix, aliasSuffix);
+                byte[] bytes = mKeyStore.get(alias);
+                try {
+                    // TODO we could at least cache the byte to cert parsing
+                    CertificateFactory cf = CertificateFactory.getInstance("X.509");
+                    Certificate ca = cf.generateCertificate(new ByteArrayInputStream(bytes));
+                    X509Certificate caCert = (X509Certificate) ca;
+                    if (issuer.equals(caCert.getSubjectX500Principal())) {
+                        // will throw exception on failure to verify.
+                        // this can happen if there are two CAs with
+                        // the same name but with different public
+                        // keys, which does in fact happen, so we will
+                        // try to continue and not just fail fast.
+                        cert.verify(caCert.getPublicKey());
+                        return new String(aliasSuffix, Charsets.UTF_8);
+                    }
+                } catch (Exception ignored) {
+                }
+            }
+            return null;
+        }
+
+        private byte[] concatenate(byte[] a, byte[] b) {
+            byte[] result = new byte[a.length + b.length];
+            System.arraycopy(a, 0, result, 0, a.length);
+            System.arraycopy(b, 0, result, a.length, b.length);
+            return result;
+        }
+    };
+
+    private class KeyChainAccountAuthenticator extends AbstractAccountAuthenticator {
+
+        /**
+         * 264 was picked becuase it is the length in bytes of Google
+         * authtokens which seems sufficiently long and guaranteed to
+         * be storable by AccountManager.
+         */
+        private final int AUTHTOKEN_LENGTH = 264;
+        private final SecureRandom mSecureRandom = new SecureRandom();
+
+        private KeyChainAccountAuthenticator(Context context) {
+            super(context);
+        }
+
+        @Override public Bundle editProperties(AccountAuthenticatorResponse response,
+                                               String accountType) {
+            return null;
+        }
+
+        @Override public Bundle addAccount(AccountAuthenticatorResponse response,
+                                           String accountType,
+                                           String authTokenType,
+                                           String[] requiredFeatures,
+                                           Bundle options) throws NetworkErrorException {
+            return null;
+        }
+
+        @Override public Bundle confirmCredentials(AccountAuthenticatorResponse response,
+                                                   Account account,
+                                                   Bundle options) throws NetworkErrorException {
+            return null;
+        }
+
+        /**
+         * Called on an AccountManager cache miss, so generate a new value.
+         */
+        @Override public Bundle getAuthToken(AccountAuthenticatorResponse response,
+                                             Account account,
+                                             String authTokenType,
+                                             Bundle options) throws NetworkErrorException {
+            byte[] bytes = new byte[AUTHTOKEN_LENGTH];
+            mSecureRandom.nextBytes(bytes);
+            String authToken = Base64.encode(bytes, Charsets.US_ASCII);
+            Bundle bundle = new Bundle();
+            bundle.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
+            bundle.putString(AccountManager.KEY_ACCOUNT_TYPE, KeyChain.ACCOUNT_TYPE);
+            bundle.putString(AccountManager.KEY_AUTHTOKEN, authToken);
+            return bundle;
+        }
+
+        @Override public String getAuthTokenLabel(String authTokenType) {
+            // return authTokenType unchanged, it was a user specified
+            // alias name, doesn't need to be localized
+            return authTokenType;
+        }
+
+        @Override public Bundle updateCredentials(AccountAuthenticatorResponse response,
+                                                  Account account,
+                                                  String authTokenType,
+                                                  Bundle options) throws NetworkErrorException {
+            return null;
+        }
+
+        @Override public Bundle hasFeatures(AccountAuthenticatorResponse response,
+                                            Account account,
+                                            String[] features) throws NetworkErrorException {
+            return null;
+        }
+    };
+
+    private final IBinder mAuthenticator = new KeyChainAccountAuthenticator(this).getIBinder();
+
+    @Override public IBinder onBind(Intent intent) {
+        if (IKeyChainService.class.getName().equals(intent.getAction())) {
+
+            // ensure singleton keychain account exists
+            synchronized (mAccountLock) {
+                Account[] accounts = mAccountManager.getAccountsByType(KeyChain.ACCOUNT_TYPE);
+                if (accounts.length == 0) {
+                    // TODO localize account name
+                    mAccount = new Account("Android Key Chain", KeyChain.ACCOUNT_TYPE);
+                    mAccountManager.addAccountExplicitly(mAccount, null, null);
+                } else if (accounts.length == 1) {
+                    mAccount = accounts[0];
+                } else {
+                    throw new IllegalStateException();
+                }
+            }
+
+            return mIKeyChainService;
+        }
+
+        if (AccountManager.ACTION_AUTHENTICATOR_INTENT.equals(intent.getAction())) {
+            return mAuthenticator;
+        }
+
+        return null;
+    }
+}
diff --git a/support/Android.mk b/support/Android.mk
new file mode 100644
index 0000000..1860584
--- /dev/null
+++ b/support/Android.mk
@@ -0,0 +1,29 @@
+# Copyright 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.
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE_TAGS := tests
+LOCAL_SRC_FILES := src/com/android/keychain/tests/support/IKeyChainServiceTestSupport.aidl
+LOCAL_MODULE := com.android.keychain.tests.support
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE_TAGS := tests
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_PACKAGE_NAME := KeyChainTestsSupport
+LOCAL_STATIC_JAVA_LIBRARIES := com.android.keychain.tests.support
+LOCAL_CERTIFICATE := platform
+include $(BUILD_PACKAGE)
diff --git a/support/AndroidManifest.xml b/support/AndroidManifest.xml
new file mode 100644
index 0000000..934660a
--- /dev/null
+++ b/support/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.keychain.tests.support"
+	  android:sharedUserId="android.uid.system">
+    <application android:process="system">
+        <service android:name="com.android.keychain.tests.support.KeyChainServiceTestSupport">
+            <intent-filter>
+                <action android:name="com.android.keychain.tests.support.IKeyChainServiceTestSupport"/>
+            </intent-filter>
+        </service>
+    </application>
+</manifest>
diff --git a/support/src/com/android/keychain/tests/support/IKeyChainServiceTestSupport.aidl b/support/src/com/android/keychain/tests/support/IKeyChainServiceTestSupport.aidl
new file mode 100644
index 0000000..cb03429
--- /dev/null
+++ b/support/src/com/android/keychain/tests/support/IKeyChainServiceTestSupport.aidl
@@ -0,0 +1,38 @@
+/*
+ * 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 com.android.keychain.tests.support;
+
+import android.accounts.Account;
+
+/**
+ * Service that runs as the system user for the use of the
+ * KeyChainServiceTest which needs to run as a regular app user, but
+ * needs to automate some steps only permissable to the system
+ * user. The KeyChainService itself runs as the keychain user and
+ * cannot do these steps itself. In a real application, they user is
+ * prompted to perform these steps via the
+ * com.android.credentials.UNLOCK Intent and
+ * GrantCredentialsPermissionActivity.
+ *
+ * @hide
+ */
+interface IKeyChainServiceTestSupport {
+    boolean keystoreReset();
+    boolean keystorePassword(String oldPassword, String newPassword);
+    boolean keystorePut(in byte[] key, in byte[] value);
+    void revokeAppPermission(in Account account, String authTokenType, int uid);
+    void grantAppPermission(in Account account, String authTokenType, int uid);
+}
diff --git a/support/src/com/android/keychain/tests/support/KeyChainServiceTestSupport.java b/support/src/com/android/keychain/tests/support/KeyChainServiceTestSupport.java
new file mode 100644
index 0000000..46ef6f8
--- /dev/null
+++ b/support/src/com/android/keychain/tests/support/KeyChainServiceTestSupport.java
@@ -0,0 +1,69 @@
+/*
+ * 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 com.android.keychain.tests.support;
+
+import android.accounts.Account;
+import android.accounts.AccountManagerService;
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+import android.security.KeyStore;
+import android.util.Log;
+
+public class KeyChainServiceTestSupport extends Service {
+
+    private static final String TAG = "KeyChainServiceTestSupport";
+
+    private final Object mServiceLock = new Object();
+    private IKeyChainServiceTestSupport mService;
+    private boolean mIsBound;
+
+    private final KeyStore mKeyStore = KeyStore.getInstance();
+    private final AccountManagerService accountManagerService
+            = AccountManagerService.getSingleton();
+
+    private final IKeyChainServiceTestSupport.Stub mIKeyChainServiceTestSupport
+            = new IKeyChainServiceTestSupport.Stub() {
+        @Override public boolean keystoreReset() {
+            Log.d(TAG, "keystoreReset");
+            return mKeyStore.reset();
+        }
+        @Override public boolean keystorePassword(String oldPassword, String newPassword) {
+            Log.d(TAG, "keystorePassword");
+            return mKeyStore.password(oldPassword, newPassword);
+        }
+        @Override public boolean keystorePut(byte[] key, byte[] value) {
+            Log.d(TAG, "keystorePut");
+            return mKeyStore.put(key, value);
+        }
+        @Override public void revokeAppPermission(Account account, String authTokenType, int uid) {
+            Log.d(TAG, "revokeAppPermission");
+            accountManagerService.revokeAppPermission(account, authTokenType, uid);
+        }
+        @Override public void grantAppPermission(Account account, String authTokenType, int uid) {
+            Log.d(TAG, "grantAppPermission");
+            accountManagerService.grantAppPermission(account, authTokenType, uid);
+        }
+    };
+
+    @Override public IBinder onBind(Intent intent) {
+        if (IKeyChainServiceTestSupport.class.getName().equals(intent.getAction())) {
+            return mIKeyChainServiceTestSupport;
+        }
+        return null;
+    }
+}
diff --git a/tests/Android.mk b/tests/Android.mk
new file mode 100644
index 0000000..2d9afad
--- /dev/null
+++ b/tests/Android.mk
@@ -0,0 +1,22 @@
+# Copyright 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.
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE_TAGS := tests
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_PACKAGE_NAME := KeyChainTests
+LOCAL_STATIC_JAVA_LIBRARIES := com.android.keychain.tests.support core-tests
+include $(BUILD_PACKAGE)
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
new file mode 100644
index 0000000..36b91d6
--- /dev/null
+++ b/tests/AndroidManifest.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.keychain.tests">
+  
+    <uses-permission android:name="android.permission.GET_ACCOUNTS"/>
+    <uses-permission android:name="android.permission.USE_CREDENTIALS"/>
+    <uses-permission android:name="android.permission.INTERNET"/>
+
+    <!--
+        To run service:
+	adb shell am startservice -n com.android.keychain.tests/.KeyChainServiceTest
+
+	To run activity:
+	adb shell am start -n com.android.keychain.tests/com.android.keychain.tests.KeyChainTestActivity
+    -->
+    <application>
+        <service android:name="com.android.keychain.tests.KeyChainServiceTest">
+            <intent-filter>
+                <action android:name="com.android.keychain.tests.KeyChainServiceTest"/>
+            </intent-filter>
+        </service>
+        <activity android:name="com.android.keychain.tests.KeyChainTestActivity">
+            <intent-filter>
+                <action android:name="com.android.keychain.tests.KeyChainTestActivity"/>
+            </intent-filter>
+        </activity>
+        <activity android:name="com.android.keychain.tests.KeyChainSocketTestActivity">
+            <intent-filter>
+                <action android:name="com.android.keychain.tests.KeyChainSocketTestActivity"/>
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/tests/src/com/android/keychain/tests/KeyChainServiceTest.java b/tests/src/com/android/keychain/tests/KeyChainServiceTest.java
new file mode 100644
index 0000000..75883b2
--- /dev/null
+++ b/tests/src/com/android/keychain/tests/KeyChainServiceTest.java
@@ -0,0 +1,288 @@
+/*
+ * 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 com.android.keychain.tests;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.AccountManagerFuture;
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.security.Credentials;
+import android.security.IKeyChainService;
+import android.security.KeyChain;
+import android.security.KeyStore;
+import android.util.Log;
+import com.android.keychain.tests.support.IKeyChainServiceTestSupport;
+import java.security.KeyStore.PrivateKeyEntry;
+import java.security.cert.Certificate;
+import java.util.Arrays;
+import junit.framework.Assert;
+import libcore.java.security.TestKeyStore;
+
+public class KeyChainServiceTest extends Service {
+
+    private static final String TAG = "KeyChainServiceTest";
+
+    private final Object mSupportLock = new Object();
+    private IKeyChainServiceTestSupport mSupport;
+    private boolean mIsBoundSupport;
+
+    private final Object mServiceLock = new Object();
+    private IKeyChainService mService;
+    private boolean mIsBoundService;
+
+    private ServiceConnection mSupportConnection = new ServiceConnection() {
+        @Override public void onServiceConnected(ComponentName name, IBinder service) {
+            synchronized (mSupportLock) {
+                mSupport = IKeyChainServiceTestSupport.Stub.asInterface(service);
+                mSupportLock.notifyAll();
+            }
+        }
+
+        @Override public void onServiceDisconnected(ComponentName name) {
+            synchronized (mSupportLock) {
+                mSupport = null;
+            }
+        }
+    };
+
+    private ServiceConnection mServiceConnection = new ServiceConnection() {
+        @Override public void onServiceConnected(ComponentName name, IBinder service) {
+            synchronized (mServiceLock) {
+                mService = IKeyChainService.Stub.asInterface(service);
+                mServiceLock.notifyAll();
+            }
+        }
+
+        @Override public void onServiceDisconnected(ComponentName name) {
+            synchronized (mServiceLock) {
+                mService = null;
+            }
+        }
+    };
+
+    private void bindSupport() {
+        mIsBoundSupport = bindService(new Intent(IKeyChainServiceTestSupport.class.getName()),
+                                      mSupportConnection,
+                                      Context.BIND_AUTO_CREATE);
+    }
+
+    private void bindService() {
+        mIsBoundService = bindService(new Intent(IKeyChainService.class.getName()),
+                                      mServiceConnection,
+                                      Context.BIND_AUTO_CREATE);
+    }
+
+    private void unbindServices() {
+        if (mIsBoundSupport) {
+            unbindService(mSupportConnection);
+            mIsBoundSupport = false;
+        }
+        if (mIsBoundService) {
+            unbindService(mServiceConnection);
+            mIsBoundService = false;
+        }
+    }
+
+    @Override public IBinder onBind(Intent intent) {
+        Log.d(TAG, "onBind");
+        return null;
+    }
+
+    @Override public int onStartCommand(Intent intent, int flags, int startId) {
+        Log.d(TAG, "onStartCommand");
+        new Thread(new Test(), TAG).start();
+        return START_STICKY;
+    }
+
+    @Override public void onDestroy () {
+        Log.d(TAG, "onDestroy");
+        unbindServices();
+    }
+
+    private final class Test extends Assert implements Runnable {
+
+        @Override public void run() {
+            try {
+                test_KeyChainService();
+            } catch (RuntimeException e) {
+                // rethrow RuntimeException without wrapping
+                throw e;
+            } catch (Exception e) {
+                throw new RuntimeException(e);
+            } finally {
+                stopSelf();
+            }
+        }
+
+        public void test_KeyChainService() throws Exception {
+            Log.d(TAG, "test_KeyChainService uid=" + getApplicationInfo().uid);
+
+            Log.d(TAG, "test_KeyChainService bind support");
+            bindSupport();
+            assertTrue(mIsBoundSupport);
+            synchronized (mSupportLock) {
+                if (mSupport == null) {
+                    mSupportLock.wait(10 * 1000);
+                }
+            }
+            assertNotNull(mSupport);
+
+            Log.d(TAG, "test_KeyChainService setup keystore and AccountManager");
+            KeyStore keyStore = KeyStore.getInstance();
+            assertTrue(mSupport.keystoreReset());
+            assertTrue(mSupport.keystorePassword("ignored", "newpasswd"));
+
+            String intermediate = "-intermediate";
+            String root = "-root";
+
+            String alias1 = "client";
+            String alias1Intermediate = alias1 + intermediate;
+            String alias1Root = alias1 + root;
+            byte[] alias1Pkey = (Credentials.USER_PRIVATE_KEY + alias1).getBytes();
+            byte[] alias1Cert = (Credentials.USER_CERTIFICATE + alias1).getBytes();
+            byte[] alias1ICert = (Credentials.CA_CERTIFICATE + alias1Intermediate).getBytes();
+            byte[] alias1RCert = (Credentials.CA_CERTIFICATE + alias1Root).getBytes();
+            PrivateKeyEntry pke1 = TestKeyStore.getClientCertificate().getPrivateKey("RSA", "RSA");
+            Certificate intermediate1 = pke1.getCertificateChain()[1];
+            Certificate root1 = TestKeyStore.getClientCertificate().getRootCertificate("RSA");
+
+            final String alias2 = "server";
+            String alias2Intermediate = alias2 + intermediate;
+            String alias2Root = alias2 + root;
+            byte[] alias2Pkey = (Credentials.USER_PRIVATE_KEY + alias2).getBytes();
+            byte[] alias2Cert = (Credentials.USER_CERTIFICATE + alias2).getBytes();
+            byte[] alias2ICert = (Credentials.CA_CERTIFICATE + alias2Intermediate).getBytes();
+            byte[] alias2RCert = (Credentials.CA_CERTIFICATE + alias2Root).getBytes();
+            PrivateKeyEntry pke2 = TestKeyStore.getServer().getPrivateKey("RSA", "RSA");
+            Certificate intermediate2 = pke2.getCertificateChain()[1];
+            Certificate root2 = TestKeyStore.getServer().getRootCertificate("RSA");
+
+            assertTrue(mSupport.keystorePut(alias1Pkey, pke1.getPrivateKey().getEncoded()));
+            assertTrue(mSupport.keystorePut(alias1Cert, pke1.getCertificate().getEncoded()));
+            assertTrue(mSupport.keystorePut(alias1ICert, intermediate1.getEncoded()));
+            assertTrue(mSupport.keystorePut(alias1RCert, root1.getEncoded()));
+            assertTrue(mSupport.keystorePut(alias2Pkey, pke2.getPrivateKey().getEncoded()));
+            assertTrue(mSupport.keystorePut(alias2Cert, pke2.getCertificate().getEncoded()));
+            assertTrue(mSupport.keystorePut(alias2ICert, intermediate2.getEncoded()));
+            assertTrue(mSupport.keystorePut(alias2RCert, root2.getEncoded()));
+
+            assertEquals(KeyStore.NO_ERROR, keyStore.test());
+            AccountManager accountManager = AccountManager.get(KeyChainServiceTest.this);
+            assertNotNull(accountManager);
+            for (Account account : accountManager.getAccountsByType(KeyChain.ACCOUNT_TYPE)) {
+                mSupport.revokeAppPermission(account, alias1, getApplicationInfo().uid);
+                mSupport.revokeAppPermission(
+                        account, alias1Intermediate + KeyChain.CA_SUFFIX, getApplicationInfo().uid);
+                mSupport.revokeAppPermission(
+                        account, alias1Root + KeyChain.CA_SUFFIX, getApplicationInfo().uid);
+                mSupport.revokeAppPermission(account, alias2, getApplicationInfo().uid);
+                mSupport.revokeAppPermission(
+                        account, alias2Intermediate + KeyChain.CA_SUFFIX, getApplicationInfo().uid);
+                mSupport.revokeAppPermission(
+                        account, alias2Root + KeyChain.CA_SUFFIX, getApplicationInfo().uid);
+            }
+
+            Log.d(TAG, "test_KeyChainService bind service");
+            bindService();
+            assertTrue(mIsBoundService);
+            synchronized (mServiceLock) {
+                if (mService == null) {
+                    mServiceLock.wait(10 * 1000);
+                }
+            }
+            assertNotNull(mService);
+
+            Account[] accounts = accountManager.getAccountsByType(KeyChain.ACCOUNT_TYPE);
+            assertNotNull(accounts);
+            assertEquals(1, accounts.length);
+            Account account = accounts[0];
+            Log.d(TAG, "test_KeyChainService getAuthTokenByFeatures for Intent");
+            AccountManagerFuture<Bundle> accountManagerFutureFail
+                    = accountManager.getAuthToken(account, alias1, false, null, null);
+            Bundle bundleFail = accountManagerFutureFail.getResult();
+            assertNotNull(bundleFail);
+            Object intentObject = bundleFail.get(AccountManager.KEY_INTENT);
+            assertNotNull(intentObject);
+            assertTrue(Intent.class.isAssignableFrom(intentObject.getClass()));
+            Intent intent = (Intent) intentObject;
+            assertEquals("android",
+                         intent.getComponent().getPackageName());
+            assertEquals("android.accounts.GrantCredentialsPermissionActivity",
+                         intent.getComponent().getClassName());
+
+            mSupport.grantAppPermission(account, alias1, getApplicationInfo().uid);
+            // don't grant alias2, so it can be done manually with KeyChainTestActivity
+            Log.d(TAG, "test_KeyChainService getAuthTokenByFeatures for authtoken");
+            AccountManagerFuture<Bundle> accountManagerFuture
+                    = accountManager.getAuthToken(account, alias1, false, null, null);
+            Bundle bundle = accountManagerFuture.getResult();
+            String accountName = bundle.getString(AccountManager.KEY_ACCOUNT_NAME);
+            assertNotNull(accountName);
+            String accountType = bundle.getString(AccountManager.KEY_ACCOUNT_TYPE);
+            assertEquals(KeyChain.ACCOUNT_TYPE, accountType);
+            String authToken = bundle.getString(AccountManager.KEY_AUTHTOKEN);
+            assertNotNull(authToken);
+            assertFalse(authToken.isEmpty());
+
+            byte[] privateKey = mService.getPrivate(alias1, authToken);
+            assertNotNull(privateKey);
+            assertEquals(Arrays.toString(pke1.getPrivateKey().getEncoded()),
+                         Arrays.toString(privateKey));
+
+            byte[] certificate = mService.getCertificate(alias1, authToken);
+            assertNotNull(certificate);
+            assertEquals(Arrays.toString(pke1.getCertificate().getEncoded()),
+                         Arrays.toString(certificate));
+
+            String aliasI = mService.findIssuer(KeyChain.fromCertificate(pke1.getCertificate()));
+            assertNotNull(aliasI);
+            assertEquals(alias1Intermediate, aliasI);
+
+            String aliasR = mService.findIssuer(KeyChain.fromCertificate(intermediate1));
+            assertNotNull(aliasR);
+            assertEquals(alias1Root, aliasR);
+
+            String aliasRR = mService.findIssuer(KeyChain.fromCertificate(intermediate1));
+            assertNotNull(aliasRR);
+            assertEquals(alias1Root, aliasRR);
+
+            try {
+                mService.findIssuer(new Bundle());
+                fail();
+            } catch (IllegalArgumentException expected) {
+            }
+            try {
+                mService.findIssuer(null);
+                fail();
+            } catch (NullPointerException expected) {
+            }
+
+            Log.d(TAG, "test_KeyChainService unbind");
+            unbindServices();
+            assertFalse(mIsBoundSupport);
+            assertFalse(mIsBoundService);
+
+            Log.d(TAG, "test_KeyChainService end");
+        }
+    }
+}
diff --git a/tests/src/com/android/keychain/tests/KeyChainTestActivity.java b/tests/src/com/android/keychain/tests/KeyChainTestActivity.java
new file mode 100644
index 0000000..b7610e3
--- /dev/null
+++ b/tests/src/com/android/keychain/tests/KeyChainTestActivity.java
@@ -0,0 +1,338 @@
+/*
+ * 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 com.android.keychain.tests;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.security.KeyChain;
+import android.text.method.ScrollingMovementMethod;
+import android.util.Log;
+import android.widget.TextView;
+import java.net.Socket;
+import java.net.URL;
+import java.security.KeyStore;
+import java.security.Principal;
+import java.security.PrivateKey;
+import java.security.cert.CertPathValidatorException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509ExtendedKeyManager;
+import javax.net.ssl.X509TrustManager;
+import libcore.java.security.TestKeyStore;
+import libcore.javax.net.ssl.TestSSLContext;
+import org.apache.harmony.xnet.provider.jsse.IndexedPKIXParameters;
+import org.apache.harmony.xnet.provider.jsse.SSLParametersImpl;
+import tests.http.MockResponse;
+import tests.http.MockWebServer;
+
+/**
+ * Simple activity based test that exercises the KeyChain API
+ */
+public class KeyChainTestActivity extends Activity {
+
+    private static final String TAG = "KeyChainTestActivity";
+
+    private static final int REQUEST_ALIAS = 1;
+    private static final int REQUEST_GRANT = 2;
+
+    private TextView mTextView;
+
+    private KeyChain mKeyChain;
+
+    private final Object mAliasLock = new Object();
+    private String mAlias;
+
+    private final Object mGrantedLock = new Object();
+    private boolean mGranted;
+
+    private void log(final String message) {
+        Log.d(TAG, message);
+        runOnUiThread(new Runnable() {
+            @Override public void run() {
+                mTextView.append(message + "\n");
+            }
+        });
+    }
+
+    @Override public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        mTextView = new TextView(this);
+        mTextView.setMovementMethod(new ScrollingMovementMethod());
+        setContentView(mTextView);
+
+        log("Starting test...");
+
+        try {
+            KeyChain.getInstance(this);
+            throw new AssertionError();
+        } catch (InterruptedException e) {
+            throw new RuntimeException(e);
+        } catch (IllegalStateException expected) {
+            log("KeyChain failed as expected on main thread.");
+        }
+
+        new AsyncTask<Void, Void, Void>() {
+            @Override protected Void doInBackground(Void... params) {
+                try {
+                    mKeyChain = KeyChain.getInstance(KeyChainTestActivity.this);
+                    log("Starting web server...");
+                    URL url = startWebServer();
+                    log("Making https request to " + url);
+                    makeHttpsRequest(url);
+                    log("Tests succeeded.");
+
+                    return null;
+                } catch (Exception e) {
+                    throw new AssertionError(e);
+                }
+            }
+            private URL startWebServer() throws Exception {
+                KeyStore serverKeyStore = TestKeyStore.getServer().keyStore;
+                char[] serverKeyStorePassword = TestKeyStore.getServer().storePassword;
+                String kmfAlgoritm = KeyManagerFactory.getDefaultAlgorithm();
+                KeyManagerFactory kmf = KeyManagerFactory.getInstance(kmfAlgoritm);
+                kmf.init(serverKeyStore, serverKeyStorePassword);
+                SSLContext serverContext = SSLContext.getInstance("SSL");
+                serverContext.init(kmf.getKeyManagers(),
+                                   new TrustManager[] { new TrustAllTrustManager() },
+                                   null);
+                SSLSocketFactory sf = serverContext.getSocketFactory();
+                SSLSocketFactory needClientAuth = TestSSLContext.clientAuth(sf, false, true);
+                MockWebServer server = new MockWebServer();
+                server.useHttps(needClientAuth, false);
+                server.enqueue(new MockResponse().setBody("this response comes via HTTPS"));
+                server.play();
+                return server.getUrl("/");
+            }
+            private void makeHttpsRequest(URL url) throws Exception {
+                SSLContext clientContext = SSLContext.getInstance("SSL");
+                clientContext.init(new KeyManager[] { new KeyChainKeyManager() },
+                                   new TrustManager[] { new KeyChainTrustManager() },
+                                   null);
+                HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
+                connection.setSSLSocketFactory(clientContext.getSocketFactory());
+                if (connection.getResponseCode() != 200) {
+                    throw new AssertionError();
+                }
+            }
+        }.execute();
+    }
+
+    /**
+     * Called when the user did not have access to requested
+     * alias. Ask the user for permission and wait for a result.
+     */
+    private void waitForGrant(Intent intent) {
+        mGranted = false;
+        log("Grant intent=" + intent);
+        startActivityForResult(intent, REQUEST_GRANT);
+        synchronized (mGrantedLock) {
+            while (!mGranted) {
+                try {
+                    mGrantedLock.wait();
+                } catch (InterruptedException ignored) {
+                }
+            }
+        }
+    }
+
+    private class KeyChainKeyManager extends X509ExtendedKeyManager {
+        @Override public String chooseClientAlias(String[] keyTypes,
+                                                  Principal[] issuers,
+                                                  Socket socket) {
+            log("KeyChainKeyManager chooseClientAlias...");
+
+            Intent intent = KeyChain.chooseAlias();
+            startActivityForResult(intent, REQUEST_ALIAS);
+            log("Starting chooser...");
+            String alias;
+            synchronized (mAliasLock) {
+                while (mAlias == null) {
+                    try {
+                        mAliasLock.wait();
+                    } catch (InterruptedException ignored) {
+                    }
+                }
+                alias = mAlias;
+            }
+            return alias;
+        }
+        @Override public String chooseServerAlias(String keyType,
+                                                  Principal[] issuers,
+                                                  Socket socket) {
+            // not a client SSLSocket callback
+            throw new UnsupportedOperationException();
+        }
+        @Override public X509Certificate[] getCertificateChain(String alias) {
+            log("KeyChainKeyManager getCertificateChain...");
+            Bundle cert = mKeyChain.getCertificate(alias);
+            Intent intent = cert.getParcelable(KeyChain.KEY_INTENT);
+            if (intent != null) {
+                waitForGrant(intent);
+                cert = mKeyChain.getCertificate(alias);
+            }
+            X509Certificate certificate = KeyChain.toCertificate(cert);
+            log("certificate=" + certificate);
+            return new X509Certificate[] { certificate };
+        }
+        @Override public String[] getClientAliases(String keyType, Principal[] issuers) {
+            // not a client SSLSocket callback
+            throw new UnsupportedOperationException();
+        }
+        @Override public String[] getServerAliases(String keyType, Principal[] issuers) {
+            // not a client SSLSocket callback
+            throw new UnsupportedOperationException();
+        }
+        @Override public PrivateKey getPrivateKey(String alias) {
+            log("KeyChainKeyManager getPrivateKey...");
+            Bundle pkey = mKeyChain.getPrivate(alias);
+            Intent intent = pkey.getParcelable(KeyChain.KEY_INTENT);
+            if (intent != null) {
+                waitForGrant(intent);
+                pkey = mKeyChain.getPrivate(alias);
+            }
+            PrivateKey privateKey = KeyChain.toPrivateKey(pkey);
+            log("privateKey=" + privateKey);
+            return privateKey;
+        }
+    }
+
+    private class KeyChainTrustManager implements X509TrustManager {
+        private final X509TrustManager trustManager = SSLParametersImpl.getDefaultTrustManager();
+        private final IndexedPKIXParameters index
+                = SSLParametersImpl.getDefaultIndexedPKIXParameters();
+
+        @Override public void checkClientTrusted(X509Certificate[] chain, String authType)
+                throws CertificateException {
+            // not a client SSLSocket callback
+            throw new UnsupportedOperationException();
+        }
+
+        @Override public void checkServerTrusted(X509Certificate[] chain, String authType)
+                throws CertificateException {
+            log("KeyChainTrustManager checkServerTrusted...");
+            // start at the end of the chain and make sure we have a trust anchor.
+            // if not, ask KeyChain for one.
+            X509Certificate end = chain[chain.length-1];
+            if (findTrustAnchor(end)) {
+                trustManager.checkServerTrusted(chain, authType);
+                return;
+            }
+
+            // try to extend the chain
+            List<X509Certificate> list = new ArrayList<X509Certificate>(Arrays.asList(chain));
+            do {
+                Bundle ca = mKeyChain.findIssuer(end);
+                if (ca == null) {
+                    break;
+                }
+                Intent intent = ca.getParcelable(KeyChain.KEY_INTENT);
+                if (intent != null) {
+                    waitForGrant(intent);
+                    ca = mKeyChain.findIssuer(end);
+                }
+                end = KeyChain.toCertificate(ca);
+                list.add(end);
+            } while (!findTrustAnchor(end));
+
+            // convert extended chain back to array
+            if (list.size() != chain.length) {
+                chain = list.toArray(new X509Certificate[list.size()]);
+            }
+            trustManager.checkServerTrusted(chain, authType);
+        }
+
+        /**
+         * Returns true if we have found a trust anchor, with or
+         * without error, indicating that we should call the
+         * underlying TrustManager to verify the chain in its current
+         * state. Otherwise, returns false to indicate the chain
+         * should be extended.
+         */
+        private boolean findTrustAnchor(X509Certificate cert) {
+            try {
+                if (index.findTrustAnchor(cert) == null) {
+                    return false;
+                }
+            } catch (CertPathValidatorException ignored) {
+            }
+            return true;
+        }
+
+        @Override public X509Certificate[] getAcceptedIssuers() {
+            // not a client SSLSocket callback
+            throw new UnsupportedOperationException();
+        }
+    }
+
+    private static class TrustAllTrustManager implements X509TrustManager {
+        @Override public void checkClientTrusted(X509Certificate[] chain, String authType)
+                throws CertificateException {
+        }
+        @Override public void checkServerTrusted(X509Certificate[] chain, String authType)
+                throws CertificateException {
+        }
+        @Override public X509Certificate[] getAcceptedIssuers() {
+            return null;
+        }
+    }
+
+    @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        switch (requestCode) {
+            case REQUEST_ALIAS: {
+                log("onActivityResult REQUEST_ALIAS...");
+                if (resultCode != RESULT_OK) {
+                    log("REQUEST_ALIAS failed!");
+                    return;
+                }
+                String alias = data.getExtras().getString(Intent.EXTRA_TEXT);
+                log("Alias choosen '" + alias + "'");
+                synchronized (mAliasLock) {
+                    mAlias = alias;
+                    mAliasLock.notifyAll();
+                }
+                break;
+            }
+            case REQUEST_GRANT: {
+                log("onActivityResult REQUEST_GRANT...");
+                if (resultCode != RESULT_OK) {
+                    log("REQUEST_GRANT failed!");
+                    return;
+                }
+                synchronized (mGrantedLock) {
+                    mGranted = true;
+                    mGrantedLock.notifyAll();
+                }
+                break;
+            }
+            default:
+                throw new IllegalStateException("requestCode == " + requestCode);
+        }
+    }
+}