summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorBrian Carlstrom <bdc@google.com>2011-06-24 02:13:28 -0700
committerBrian Carlstrom <bdc@google.com>2011-06-26 15:27:48 -0700
commit65e649e856d88520ac04f5b16313a3f167e569e0 (patch)
tree514cd785e6cc5c98d564d45da5881be2ddd968e1 /src
parentf5b50a4678120890d62bb07bb47cbd3f1ba4b243 (diff)
downloadandroid_packages_apps_KeyChain-65e649e856d88520ac04f5b16313a3f167e569e0.tar.gz
android_packages_apps_KeyChain-65e649e856d88520ac04f5b16313a3f167e569e0.tar.bz2
android_packages_apps_KeyChain-65e649e856d88520ac04f5b16313a3f167e569e0.zip
Replace KeyChainActivity placeholder UI with more polished dialog (5 of 5)
frameworks/base Extended KeyChain.chooserPrivateKeyAlias to allow caller to supply preferred choice to be selected in chooser. This allows Email settings to highlight the current choice when allowing user to change settings. keystore/java/android/security/KeyChain.java api/current.txt Implemented KeyChain functionality to pass host and port information to KeyChainActivity for display. keystore/java/android/security/KeyChain.java KeyChain now sends a PendingIntent as part of the Intent it sends to the KeyChainActivity which can be used to identify the caller in reliable way. keystore/java/android/security/KeyChain.java Moved .pfx/.p12/.cer/.crt constants to Credentials for reuse. Added Credentials.install variant with no value for use from KeyChainActivity keystore/java/android/security/Credentials.java packages/apps/CertInstaller Source of extension constants now in Credentials src/com/android/certinstaller/CertFile.java packages/apps/Browser Have browser supply host and port information to KeyChain.choosePrivateKeyAlias Tracking KeyChain.choosePrivateKeyAlias API change src/com/android/browser/Tab.java packages/apps/Email Tracking KeyChain.choosePrivateKeyAlias API change src/com/android/email/view/CertificateSelector.java packages/apps/KeyChain KeyChain now depends on bouncycastle X509Name for formatting X500Principals, since the 4 X500Principal formatting options could not format emailAddress attributes in a human readable way and its the most important attribute to display for client certificates in most cases. Android.mk Changing the UI to a dialog, make the activity style transparent. AndroidManifest.xml res/values/styles.xml Layout for chooser dialog res/layout/cert_chooser.xml Layout for list items in chooser res/layout/cert_item.xml New resources for dialog including comments for translators. res/values/strings.xml New dialog based KeyChainActivity. Now also shows requesting app and requesting server. Now can preselect a specified alias. New link directly to CertInstaller. src/com/android/keychain/KeyChainActivity.java Fix KeyChainTestActivity to work with TestKeyStore changes that were causing network activity on the UI to look up the name of localhost. Also track KeyChain.choosePrivateKeyAlias API change. tests/src/com/android/keychain/tests/KeyChainTestActivity.java Change-Id: I6923e5bf77041500a8ebb44a7972f5d01d297263
Diffstat (limited to 'src')
-rw-r--r--src/com/android/keychain/KeyChainActivity.java284
1 files changed, 246 insertions, 38 deletions
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);