diff options
-rwxr-xr-x | Android.mk | 1 | ||||
-rwxr-xr-x | AndroidManifest.xml | 1 | ||||
-rw-r--r-- | res/layout/cert_chooser.xml | 60 | ||||
-rw-r--r-- | res/layout/cert_item.xml | 58 | ||||
-rwxr-xr-x | res/values/strings.xml | 25 | ||||
-rw-r--r-- | res/values/styles.xml | 26 | ||||
-rw-r--r-- | src/com/android/keychain/KeyChainActivity.java | 284 | ||||
-rw-r--r-- | tests/src/com/android/keychain/tests/KeyChainTestActivity.java | 39 |
8 files changed, 450 insertions, 44 deletions
@@ -18,6 +18,7 @@ include $(CLEAR_VARS) LOCAL_MODULE_TAGS := optional LOCAL_SRC_FILES := $(call all-java-files-under, src) +LOCAL_JAVA_LIBRARIES := bouncycastle LOCAL_PACKAGE_NAME := KeyChain LOCAL_CERTIFICATE := platform diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 751b7da..efbcb8e 100755 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -17,6 +17,7 @@ android:resource="@xml/authenticator"/> </service> <activity android:name="com.android.keychain.KeyChainActivity" + android:theme="@style/Transparent" android:excludeFromRecents="true"> <intent-filter> <action android:name="com.android.keychain.CHOOSER"/> diff --git a/res/layout/cert_chooser.xml b/res/layout/cert_chooser.xml new file mode 100644 index 0000000..7381b4c --- /dev/null +++ b/res/layout/cert_chooser.xml @@ -0,0 +1,60 @@ +<?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. +--> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + > + <TextView + android:id="@+id/cert_chooser_context_message" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_margin="6dip" + android:visibility="gone" + /> + <ListView + android:id="@+id/cert_chooser_cert_list" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:drawSelectorOnTop="false" + android:textFilterEnabled="true" + android:choiceMode="singleChoice" + android:visibility="gone" + /> + <View + android:layout_width="fill_parent" + android:layout_height="1dip" + android:background="@android:drawable/divider_horizontal_dark" + /> + <TextView + android:id="@+id/cert_chooser_install_message" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_margin="6dip" + /> + <LinearLayout + android:gravity="right" + android:layout_width="fill_parent" + android:layout_height="wrap_content"> + <Button + android:id="@+id/cert_chooser_install_button" + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:text="@string/install_new_cert_button_label" + android:layout_margin="6dip" + /> + </LinearLayout> +</LinearLayout> diff --git a/res/layout/cert_item.xml b/res/layout/cert_item.xml new file mode 100644 index 0000000..b77e9fc --- /dev/null +++ b/res/layout/cert_item.xml @@ -0,0 +1,58 @@ +<?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. +--> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:gravity="center_vertical" + android:paddingRight="?android:attr/scrollbarSize" + android:background="?android:attr/selectableItemBackground" + android:padding="15dip" + > + <RelativeLayout + android:layout_width="0px" + android:layout_height="wrap_content" + android:layout_weight="1" + > + <TextView + android:id="@+id/cert_item_alias" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:singleLine="true" + android:textAppearance="?android:attr/textAppearanceMedium" + android:ellipsize="marquee" + android:fadingEdge="horizontal" + /> + <TextView + android:id="@+id/cert_item_subject" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_below="@id/cert_item_alias" + android:layout_alignLeft="@id/cert_item_alias" + android:textAppearance="?android:attr/textAppearanceSmall" + android:textColor="?android:attr/textColorSecondary" + /> + </RelativeLayout> + <RadioButton + android:id="@+id/cert_item_selected" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:gravity="center_vertical" + android:clickable="false" + android:focusable="false" + android:layout_weight="0" + /> +</LinearLayout> diff --git a/res/values/strings.xml b/res/values/strings.xml index e208bed..9bd7cb0 100755 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -16,4 +16,29 @@ <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="app_name">Key Chain</string> <string name="keychainUserLabel">keychain</string> + + <!-- Dialog title when no certificates were found --> + <string name="title_no_certs">No certificates found</string> + + <!-- Dialog title when at least one certificate was found --> + <string name="title_select_cert">Select certificate</string> + + <!-- Used at top of dialog to identify requesting application --> + <string name="requesting_application">The application %s has requested a certificate. Selecting a certificate will grant the application the ability to use this identity with servers now and in the future.</string> + + <!-- Used at top of dialog to identify requesting server (may be host:port or just host)--> + <string name="requesting_server">The application has identified the requesting server as %s but you should only grant the application access to the certificate if you trust the application.</string> + + <!-- Label of button to send the user to the CertInstaller to install a certificate (arguments are constants for .pfx and .p12) --> + <string name="install_new_cert_message">You can install certificates from a PKCS#12 file with a %1$s or a %2$s extension located in external storage.</string> + + <!-- Label of button to send the user to the CertInstaller to install a certificate --> + <string name="install_new_cert_button_label">Install</string> + + <!-- Label of button to send the allow the application certificate request --> + <string name="allow_button">Allow</string> + + <!-- Label of button to send the deny the application certificate request --> + <string name="deny_button">Deny</string> + </resources> diff --git a/res/values/styles.xml b/res/values/styles.xml new file mode 100644 index 0000000..585b92e --- /dev/null +++ b/res/values/styles.xml @@ -0,0 +1,26 @@ +<?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:android="http://schemas.android.com/apk/res/android"> + <style name="Transparent"> + <item name="android:windowBackground">@android:color/transparent</item> + <item name="android:windowNoTitle">true</item> + <item name="android:windowIsFloating">true</item> + </style> +</resources> diff --git a/src/com/android/keychain/KeyChainActivity.java b/src/com/android/keychain/KeyChainActivity.java index 8b94344..1073556 100644 --- a/src/com/android/keychain/KeyChainActivity.java +++ b/src/com/android/keychain/KeyChainActivity.java @@ -16,21 +16,42 @@ package com.android.keychain; -import android.app.ListActivity; +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.PendingIntent; +import android.content.DialogInterface; import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.os.AsyncTask; import android.os.Bundle; import android.os.RemoteException; import android.security.Credentials; import android.security.IKeyChainAliasCallback; import android.security.KeyChain; import android.security.KeyStore; +import android.view.LayoutInflater; import android.view.View; -import android.widget.AdapterView.OnItemClickListener; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.Button; import android.widget.ListView; +import android.widget.RadioButton; +import android.widget.TextView; +import com.android.org.bouncycastle.asn1.x509.X509Name; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import javax.security.auth.x500.X500Principal; -public class KeyChainActivity extends ListActivity { +public class KeyChainActivity extends Activity { private static final String TAG = "KeyChainActivity"; @@ -38,21 +59,26 @@ public class KeyChainActivity extends ListActivity { private static final int REQUEST_UNLOCK = 1; + private static final int DIALOG_CERT_CHOOSER = 0; + private static enum State { INITIAL, UNLOCK_REQUESTED }; private State mState; + // beware that some of these KeyStore operations such as saw and + // get do file I/O in the remote keystore process and while they + // do not cause StrictMode violations, they logically should not + // be done on the UI thread. private KeyStore mKeyStore = KeyStore.getInstance(); + private CertificateAdapter mCertificateAdapter; + + // the KeyStore.state operation is safe to do on the UI thread, it + // does not do a file operation. private boolean isKeyStoreUnlocked() { return mKeyStore.state() == KeyStore.State.UNLOCKED; } - private boolean isKeyStoreEmpty() { - String[] aliases = mKeyStore.saw(Credentials.USER_PRIVATE_KEY); - return (aliases == null || aliases.length == 0); - } - @Override public void onCreate(Bundle savedState) { super.onCreate(savedState); if (savedState == null) { @@ -71,11 +97,6 @@ public class KeyChainActivity extends ListActivity { // see if KeyStore has been unlocked, if not start activity to do so switch (mState) { case INITIAL: - if (isKeyStoreEmpty()) { - finish(null); - return; - } - if (!isKeyStoreUnlocked()) { mState = State.UNLOCK_REQUESTED; this.startActivityForResult(new Intent(Credentials.UNLOCK_ACTION), @@ -86,7 +107,7 @@ public class KeyChainActivity extends ListActivity { // onActivityResult is called with REQUEST_UNLOCK return; } - showAliasList(); + new AliasLoader().execute(); return; case UNLOCK_REQUESTED: // we've already asked, but have not heard back, probably just rotated. @@ -97,38 +118,225 @@ public class KeyChainActivity extends ListActivity { } } - private void showAliasList() { + private class AliasLoader extends AsyncTask<Void, Void, CertificateAdapter> { + @Override protected CertificateAdapter doInBackground(Void... params) { + String[] aliasArray = mKeyStore.saw(Credentials.USER_PRIVATE_KEY); + List<String> aliasList = ((aliasArray == null) + ? Collections.<String>emptyList() + : Arrays.asList(aliasArray)); + Collections.sort(aliasList); + return new CertificateAdapter(aliasList); + } + @Override protected void onPostExecute(CertificateAdapter result) { + mCertificateAdapter = result; + showDialog(DIALOG_CERT_CHOOSER); + } + } + + @Override protected Dialog onCreateDialog(int id, Bundle args) { + if (id == DIALOG_CERT_CHOOSER) { + return createCertChooserDialog(); + } + throw new AssertionError(); + } - String[] aliases = mKeyStore.saw(Credentials.USER_PRIVATE_KEY); - if (aliases == null || aliases.length == 0) { + private Dialog createCertChooserDialog() { + View view = View.inflate(this, R.layout.cert_chooser, null); + + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setView(view); + builder.setNegativeButton(R.string.deny_button, new DialogInterface.OnClickListener() { + @Override public void onClick(DialogInterface dialog, int id) { + dialog.cancel(); // will cause OnDismissListener to be called + } + }); + + Resources res = getResources(); + + String title; + if (mCertificateAdapter.mAliases.isEmpty()) { + title = res.getString(R.string.title_no_certs); + } else { + title = res.getString(R.string.title_select_cert); + final ListView lv = (ListView) view.findViewById(R.id.cert_chooser_cert_list); + lv.setAdapter(mCertificateAdapter); + String alias = getIntent().getStringExtra(KeyChain.EXTRA_ALIAS); + if (alias != null) { + int position = mCertificateAdapter.mAliases.indexOf(alias); + if (position != -1) { + lv.setItemChecked(position, true); + } + } + + builder.setPositiveButton(R.string.allow_button, new DialogInterface.OnClickListener() { + @Override public void onClick(DialogInterface dialog, int id) { + int pos = lv.getCheckedItemPosition(); + String alias = ((pos != ListView.INVALID_POSITION) + ? mCertificateAdapter.getItem(pos) + : null); + finish(alias); + } + }); + + lv.setVisibility(View.VISIBLE); + } + builder.setTitle(title); + + PendingIntent sender = getIntent().getParcelableExtra(KeyChain.EXTRA_SENDER); + if (sender == null) { + // if no sender, bail, we need to identify the app to the user securely. finish(null); - 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); - finish(alias); + } + + // getTargetPackage guarantees that the returned string is + // supplied by the system, so that an application can not + // spoof its package. + String pkg = sender.getIntentSender().getTargetPackage(); + PackageManager pm = getPackageManager(); + CharSequence applicationLabel; + try { + applicationLabel = pm.getApplicationLabel(pm.getApplicationInfo(pkg, 0)).toString(); + } catch (PackageManager.NameNotFoundException e) { + applicationLabel = pkg; + } + String appMessage = String.format(res.getString(R.string.requesting_application), + applicationLabel); + + String contextMessage = appMessage; + String host = getIntent().getStringExtra(KeyChain.EXTRA_HOST); + if (host != null) { + String hostString = host; + int port = getIntent().getIntExtra(KeyChain.EXTRA_PORT, -1); + if (port != -1) { + hostString += ":" + port; + } + String hostMessage = String.format(res.getString(R.string.requesting_server), + hostString); + if (contextMessage == null) { + contextMessage = hostMessage; + } else { + contextMessage += " " + hostMessage; + } + } + TextView contextView = (TextView) view.findViewById(R.id.cert_chooser_context_message); + contextView.setText(contextMessage); + contextView.setVisibility(View.VISIBLE); + + String installMessage = String.format(res.getString(R.string.install_new_cert_message), + Credentials.EXTENSION_PFX, Credentials.EXTENSION_P12); + TextView installTextView = (TextView) view.findViewById(R.id.cert_chooser_install_message); + installTextView.setText(installMessage); + + Button installButton = (Button) view.findViewById(R.id.cert_chooser_install_button); + installButton.setOnClickListener(new View.OnClickListener() { + @Override public void onClick(View v) { + // remove dialog so that we will recreate with + // possibly new content after install returns + removeDialog(DIALOG_CERT_CHOOSER); + Credentials.getInstance().install(KeyChainActivity.this); + } + }); + + Dialog dialog = builder.create(); + dialog.setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override public void onCancel(DialogInterface dialog) { + finish(null); } }); + return dialog; + } + + private class CertificateAdapter extends BaseAdapter { + private final List<String> mAliases; + private final List<String> mSubjects = new ArrayList<String>(); + private CertificateAdapter(List<String> aliases) { + mAliases = aliases; + mSubjects.addAll(Collections.nCopies(aliases.size(), (String) null)); + } + @Override public int getCount() { + return mAliases.size(); + } + @Override public String getItem(int position) { + return mAliases.get(position); + } + @Override public long getItemId(int position) { + return position; + } + @Override public View getView(final int position, View view, ViewGroup parent) { + ViewHolder holder; + if (view == null) { + LayoutInflater inflater = LayoutInflater.from(KeyChainActivity.this); + view = inflater.inflate(R.layout.cert_item, parent, false); + holder = new ViewHolder(); + holder.mAliasTextView = (TextView) view.findViewById(R.id.cert_item_alias); + holder.mSubjectTextView = (TextView) view.findViewById(R.id.cert_item_subject); + holder.mRadioButton = (RadioButton) view.findViewById(R.id.cert_item_selected); + view.setTag(holder); + } else { + holder = (ViewHolder) view.getTag(); + } + + String alias = mAliases.get(position); + + holder.mAliasTextView.setText(alias); + + String subject = mSubjects.get(position); + if (subject == null) { + new CertLoader(position, holder.mSubjectTextView).execute(); + } else { + holder.mSubjectTextView.setText(subject); + } + + ListView lv = (ListView)parent; + holder.mRadioButton.setChecked(position == lv.getCheckedItemPosition()); + return view; + } + + private class CertLoader extends AsyncTask<Void, Void, String> { + private final int mPosition; + private final TextView mSubjectView; + private CertLoader(int position, TextView subjectView) { + mPosition = position; + mSubjectView = subjectView; + } + @Override protected String doInBackground(Void... params) { + String alias = mAliases.get(mPosition); + byte[] bytes = mKeyStore.get(Credentials.USER_CERTIFICATE + alias); + if (bytes == null) { + return null; + } + InputStream in = new ByteArrayInputStream(bytes); + X509Certificate cert; + try { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + cert = (X509Certificate)cf.generateCertificate(in); + } catch (CertificateException ignored) { + return null; + } + // bouncycastle can handle the emailAddress OID of 1.2.840.113549.1.9.1 + X500Principal subjectPrincipal = cert.getSubjectX500Principal(); + X509Name subjectName = X509Name.getInstance(subjectPrincipal.getEncoded()); + String subjectString = subjectName.toString(true, X509Name.DefaultSymbols); + return subjectString; + } + @Override protected void onPostExecute(String subjectString) { + mSubjects.set(mPosition, subjectString); + mSubjectView.setText(subjectString); + } + } + } + + private static class ViewHolder { + TextView mAliasTextView; + TextView mSubjectTextView; + RadioButton mRadioButton; } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { case REQUEST_UNLOCK: if (isKeyStoreUnlocked()) { - showAliasList(); + showDialog(DIALOG_CERT_CHOOSER); } else { // user must have canceled unlock, give up finish(null); diff --git a/tests/src/com/android/keychain/tests/KeyChainTestActivity.java b/tests/src/com/android/keychain/tests/KeyChainTestActivity.java index 6c280a6..c5ff34f 100644 --- a/tests/src/com/android/keychain/tests/KeyChainTestActivity.java +++ b/tests/src/com/android/keychain/tests/KeyChainTestActivity.java @@ -21,6 +21,7 @@ import android.content.Intent; import android.os.AsyncTask; import android.os.Bundle; import android.os.RemoteException; +import android.os.StrictMode; import android.security.Credentials; import android.security.KeyChain; import android.security.KeyChainAliasCallback; @@ -59,7 +60,7 @@ public class KeyChainTestActivity extends Activity { private TextView mTextView; - private KeyChain mKeyChain; + private TestKeyStore mTestKeyStore; private final Object mAliasLock = new Object(); private String mAlias; @@ -79,6 +80,20 @@ public class KeyChainTestActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() + .detectDiskReads() + .detectDiskWrites() + .detectAll() + .penaltyLog() + .penaltyDeath() + .build()); + StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() + .detectLeakedSqlLiteObjects() + .detectLeakedClosableObjects() + .penaltyLog() + .penaltyDeath() + .build()); + mTextView = new TextView(this); mTextView.setMovementMethod(new ScrollingMovementMethod()); setContentView(mTextView); @@ -86,7 +101,7 @@ public class KeyChainTestActivity extends Activity { log("Starting test..."); testKeyChainImproperUse(); - testCaInstall(); + new SetupTestKeyStore().execute(); } private void testKeyChainImproperUse() { @@ -135,10 +150,20 @@ public class KeyChainTestActivity extends Activity { } } + private class SetupTestKeyStore extends AsyncTask<Void, Void, Void> { + @Override protected Void doInBackground(Void... params) { + mTestKeyStore = TestKeyStore.getServer(); + return null; + } + @Override protected void onPostExecute(Void result) { + testCaInstall(); + } + } + private void testCaInstall() { try { log("Requesting install of server's CA..."); - X509Certificate ca = TestKeyStore.getServer().getRootCertificate("RSA"); + X509Certificate ca = mTestKeyStore.getRootCertificate("RSA"); Intent intent = new Intent("android.credentials.INSTALL"); intent.putExtra("name", TAG); // "name" = CredentialHelper.CERT_NAME_KEY intent.putExtra(Credentials.CERTIFICATE, Credentials.convertToPem(ca)); @@ -164,8 +189,8 @@ public class KeyChainTestActivity extends Activity { } } private URL startWebServer() throws Exception { - KeyStore serverKeyStore = TestKeyStore.getServer().keyStore; - char[] serverKeyStorePassword = TestKeyStore.getServer().storePassword; + KeyStore serverKeyStore = mTestKeyStore.keyStore; + char[] serverKeyStorePassword = mTestKeyStore.storePassword; String kmfAlgoritm = KeyManagerFactory.getDefaultAlgorithm(); KeyManagerFactory kmf = KeyManagerFactory.getInstance(kmfAlgoritm); kmf.init(serverKeyStore, serverKeyStorePassword); @@ -199,7 +224,9 @@ public class KeyChainTestActivity extends Activity { log("KeyChainKeyManager chooseClientAlias..."); KeyChain.choosePrivateKeyAlias(KeyChainTestActivity.this, new AliasResponse(), - null, null, null, -1); + keyTypes, issuers, + socket.getInetAddress().getHostName(), socket.getPort(), + "My Test Certificate"); String alias; synchronized (mAliasLock) { while (mAlias == null) { |