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