blob: 80b537a52b4a25eec50bbc8aaf67946633ca6fd4 [file] [log] [blame]
Brian Carlstrom3e6251d2011-04-11 09:05:06 -07001/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.keychain;
18
Brian Carlstrom65e649e2011-06-24 02:13:28 -070019import android.app.Activity;
20import android.app.AlertDialog;
21import android.app.Dialog;
22import android.app.PendingIntent;
23import android.content.DialogInterface;
Brian Carlstrom3e6251d2011-04-11 09:05:06 -070024import android.content.Intent;
Brian Carlstrom65e649e2011-06-24 02:13:28 -070025import android.content.pm.PackageManager;
26import android.content.res.Resources;
27import android.os.AsyncTask;
Brian Carlstrom3e6251d2011-04-11 09:05:06 -070028import android.os.Bundle;
Brian Carlstrombb04f702011-05-24 21:54:51 -070029import android.os.RemoteException;
Brian Carlstrom3e6251d2011-04-11 09:05:06 -070030import android.security.Credentials;
Brian Carlstromf5b50a42011-06-09 16:05:09 -070031import android.security.IKeyChainAliasCallback;
Brian Carlstrombb04f702011-05-24 21:54:51 -070032import android.security.KeyChain;
Brian Carlstrom3e6251d2011-04-11 09:05:06 -070033import android.security.KeyStore;
Brian Carlstrom65e649e2011-06-24 02:13:28 -070034import android.view.LayoutInflater;
Brian Carlstrom3e6251d2011-04-11 09:05:06 -070035import android.view.View;
Brian Carlstrom65e649e2011-06-24 02:13:28 -070036import android.view.ViewGroup;
37import android.widget.BaseAdapter;
38import android.widget.Button;
Brian Carlstrom3e6251d2011-04-11 09:05:06 -070039import android.widget.ListView;
Brian Carlstrom65e649e2011-06-24 02:13:28 -070040import android.widget.RadioButton;
41import android.widget.TextView;
42import com.android.org.bouncycastle.asn1.x509.X509Name;
43import java.io.ByteArrayInputStream;
44import java.io.InputStream;
45import java.security.cert.CertificateException;
46import java.security.cert.CertificateFactory;
47import java.security.cert.X509Certificate;
48import java.util.ArrayList;
49import java.util.Arrays;
50import java.util.Collections;
51import java.util.List;
52import javax.security.auth.x500.X500Principal;
Brian Carlstrom3e6251d2011-04-11 09:05:06 -070053
Brian Carlstrom65e649e2011-06-24 02:13:28 -070054public class KeyChainActivity extends Activity {
Brian Carlstrom3e6251d2011-04-11 09:05:06 -070055
56 private static final String TAG = "KeyChainActivity";
57
58 private static String KEY_STATE = "state";
59
60 private static final int REQUEST_UNLOCK = 1;
61
62 private static enum State { INITIAL, UNLOCK_REQUESTED };
63
64 private State mState;
65
Brian Carlstrom65e649e2011-06-24 02:13:28 -070066 // beware that some of these KeyStore operations such as saw and
67 // get do file I/O in the remote keystore process and while they
68 // do not cause StrictMode violations, they logically should not
69 // be done on the UI thread.
Brian Carlstrom3e6251d2011-04-11 09:05:06 -070070 private KeyStore mKeyStore = KeyStore.getInstance();
71
Brian Carlstrom65e649e2011-06-24 02:13:28 -070072 // the KeyStore.state operation is safe to do on the UI thread, it
73 // does not do a file operation.
Brian Carlstrom3e6251d2011-04-11 09:05:06 -070074 private boolean isKeyStoreUnlocked() {
Brian Carlstrome3b33902011-05-31 01:06:20 -070075 return mKeyStore.state() == KeyStore.State.UNLOCKED;
Brian Carlstrom3e6251d2011-04-11 09:05:06 -070076 }
77
78 @Override public void onCreate(Bundle savedState) {
79 super.onCreate(savedState);
80 if (savedState == null) {
81 mState = State.INITIAL;
82 } else {
83 mState = (State) savedState.getSerializable(KEY_STATE);
84 if (mState == null) {
85 mState = State.INITIAL;
86 }
87 }
88 }
89
90 @Override public void onResume() {
91 super.onResume();
92
93 // see if KeyStore has been unlocked, if not start activity to do so
94 switch (mState) {
95 case INITIAL:
96 if (!isKeyStoreUnlocked()) {
97 mState = State.UNLOCK_REQUESTED;
98 this.startActivityForResult(new Intent(Credentials.UNLOCK_ACTION),
99 REQUEST_UNLOCK);
100 // Note that Credentials.unlock will start an
101 // Activity and we will be paused but then resumed
102 // when the unlock Activity completes and our
103 // onActivityResult is called with REQUEST_UNLOCK
104 return;
105 }
Brian Carlstrom91940e72011-06-28 20:37:31 -0700106 showCertChooserDialog();
Brian Carlstrom3e6251d2011-04-11 09:05:06 -0700107 return;
108 case UNLOCK_REQUESTED:
109 // we've already asked, but have not heard back, probably just rotated.
110 // wait to hear back via onActivityResult
111 return;
112 default:
113 throw new AssertionError();
114 }
115 }
116
Brian Carlstrom91940e72011-06-28 20:37:31 -0700117 private void showCertChooserDialog() {
118 new AliasLoader().execute();
119 }
120
Brian Carlstrom65e649e2011-06-24 02:13:28 -0700121 private class AliasLoader extends AsyncTask<Void, Void, CertificateAdapter> {
122 @Override protected CertificateAdapter doInBackground(Void... params) {
123 String[] aliasArray = mKeyStore.saw(Credentials.USER_PRIVATE_KEY);
124 List<String> aliasList = ((aliasArray == null)
125 ? Collections.<String>emptyList()
126 : Arrays.asList(aliasArray));
127 Collections.sort(aliasList);
128 return new CertificateAdapter(aliasList);
Brian Carlstrom3e6251d2011-04-11 09:05:06 -0700129 }
Brian Carlstrom91940e72011-06-28 20:37:31 -0700130 @Override protected void onPostExecute(CertificateAdapter adapter) {
131 displayCertChooserDialog(adapter);
Brian Carlstrom65e649e2011-06-24 02:13:28 -0700132 }
133 }
Brian Carlstrom3e6251d2011-04-11 09:05:06 -0700134
Brian Carlstrom91940e72011-06-28 20:37:31 -0700135 private void displayCertChooserDialog(final CertificateAdapter adapter) {
Brian Carlstrom65e649e2011-06-24 02:13:28 -0700136 AlertDialog.Builder builder = new AlertDialog.Builder(this);
Brian Carlstromdf172302011-06-26 17:13:54 -0700137
138 View view = View.inflate(this, R.layout.cert_chooser, null);
Brian Carlstrom65e649e2011-06-24 02:13:28 -0700139 builder.setView(view);
Brian Carlstromdf172302011-06-26 17:13:54 -0700140
Brian Carlstrom91940e72011-06-28 20:37:31 -0700141 boolean empty = adapter.mAliases.isEmpty();
Brian Carlstromdf172302011-06-26 17:13:54 -0700142 int negativeLabel = empty ? android.R.string.cancel : R.string.deny_button;
143 builder.setNegativeButton(negativeLabel, new DialogInterface.OnClickListener() {
Brian Carlstrom65e649e2011-06-24 02:13:28 -0700144 @Override public void onClick(DialogInterface dialog, int id) {
145 dialog.cancel(); // will cause OnDismissListener to be called
Brian Carlstrom3e6251d2011-04-11 09:05:06 -0700146 }
147 });
Brian Carlstrom65e649e2011-06-24 02:13:28 -0700148
Brian Carlstrom65e649e2011-06-24 02:13:28 -0700149 String title;
Brian Carlstromdf172302011-06-26 17:13:54 -0700150 Resources res = getResources();
151 if (empty) {
Brian Carlstrom65e649e2011-06-24 02:13:28 -0700152 title = res.getString(R.string.title_no_certs);
153 } else {
154 title = res.getString(R.string.title_select_cert);
155 final ListView lv = (ListView) view.findViewById(R.id.cert_chooser_cert_list);
Brian Carlstrom91940e72011-06-28 20:37:31 -0700156 lv.setAdapter(adapter);
Brian Carlstrom65e649e2011-06-24 02:13:28 -0700157 String alias = getIntent().getStringExtra(KeyChain.EXTRA_ALIAS);
158 if (alias != null) {
Brian Carlstrom91940e72011-06-28 20:37:31 -0700159 int position = adapter.mAliases.indexOf(alias);
Brian Carlstrom65e649e2011-06-24 02:13:28 -0700160 if (position != -1) {
161 lv.setItemChecked(position, true);
162 }
163 }
164
165 builder.setPositiveButton(R.string.allow_button, new DialogInterface.OnClickListener() {
166 @Override public void onClick(DialogInterface dialog, int id) {
167 int pos = lv.getCheckedItemPosition();
168 String alias = ((pos != ListView.INVALID_POSITION)
Brian Carlstrom91940e72011-06-28 20:37:31 -0700169 ? adapter.getItem(pos)
Brian Carlstrom65e649e2011-06-24 02:13:28 -0700170 : null);
171 finish(alias);
172 }
173 });
174
175 lv.setVisibility(View.VISIBLE);
176 }
177 builder.setTitle(title);
Brian Carlstrom91940e72011-06-28 20:37:31 -0700178 final Dialog dialog = builder.create();
Brian Carlstrom65e649e2011-06-24 02:13:28 -0700179
180 PendingIntent sender = getIntent().getParcelableExtra(KeyChain.EXTRA_SENDER);
181 if (sender == null) {
182 // if no sender, bail, we need to identify the app to the user securely.
183 finish(null);
184 }
185
186 // getTargetPackage guarantees that the returned string is
187 // supplied by the system, so that an application can not
188 // spoof its package.
189 String pkg = sender.getIntentSender().getTargetPackage();
190 PackageManager pm = getPackageManager();
191 CharSequence applicationLabel;
192 try {
193 applicationLabel = pm.getApplicationLabel(pm.getApplicationInfo(pkg, 0)).toString();
194 } catch (PackageManager.NameNotFoundException e) {
195 applicationLabel = pkg;
196 }
197 String appMessage = String.format(res.getString(R.string.requesting_application),
198 applicationLabel);
199
200 String contextMessage = appMessage;
201 String host = getIntent().getStringExtra(KeyChain.EXTRA_HOST);
202 if (host != null) {
203 String hostString = host;
204 int port = getIntent().getIntExtra(KeyChain.EXTRA_PORT, -1);
205 if (port != -1) {
206 hostString += ":" + port;
207 }
208 String hostMessage = String.format(res.getString(R.string.requesting_server),
209 hostString);
210 if (contextMessage == null) {
211 contextMessage = hostMessage;
212 } else {
213 contextMessage += " " + hostMessage;
214 }
215 }
216 TextView contextView = (TextView) view.findViewById(R.id.cert_chooser_context_message);
217 contextView.setText(contextMessage);
218 contextView.setVisibility(View.VISIBLE);
219
220 String installMessage = String.format(res.getString(R.string.install_new_cert_message),
221 Credentials.EXTENSION_PFX, Credentials.EXTENSION_P12);
222 TextView installTextView = (TextView) view.findViewById(R.id.cert_chooser_install_message);
223 installTextView.setText(installMessage);
224
225 Button installButton = (Button) view.findViewById(R.id.cert_chooser_install_button);
226 installButton.setOnClickListener(new View.OnClickListener() {
227 @Override public void onClick(View v) {
228 // remove dialog so that we will recreate with
229 // possibly new content after install returns
Brian Carlstrom91940e72011-06-28 20:37:31 -0700230 dialog.dismiss();
Brian Carlstrom65e649e2011-06-24 02:13:28 -0700231 Credentials.getInstance().install(KeyChainActivity.this);
232 }
233 });
234
Brian Carlstrom65e649e2011-06-24 02:13:28 -0700235 dialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
236 @Override public void onCancel(DialogInterface dialog) {
237 finish(null);
238 }
239 });
Brian Carlstrom91940e72011-06-28 20:37:31 -0700240 dialog.show();
Brian Carlstrom65e649e2011-06-24 02:13:28 -0700241 }
242
243 private class CertificateAdapter extends BaseAdapter {
244 private final List<String> mAliases;
245 private final List<String> mSubjects = new ArrayList<String>();
246 private CertificateAdapter(List<String> aliases) {
247 mAliases = aliases;
248 mSubjects.addAll(Collections.nCopies(aliases.size(), (String) null));
249 }
250 @Override public int getCount() {
251 return mAliases.size();
252 }
253 @Override public String getItem(int position) {
254 return mAliases.get(position);
255 }
256 @Override public long getItemId(int position) {
257 return position;
258 }
259 @Override public View getView(final int position, View view, ViewGroup parent) {
260 ViewHolder holder;
261 if (view == null) {
262 LayoutInflater inflater = LayoutInflater.from(KeyChainActivity.this);
263 view = inflater.inflate(R.layout.cert_item, parent, false);
264 holder = new ViewHolder();
265 holder.mAliasTextView = (TextView) view.findViewById(R.id.cert_item_alias);
266 holder.mSubjectTextView = (TextView) view.findViewById(R.id.cert_item_subject);
267 holder.mRadioButton = (RadioButton) view.findViewById(R.id.cert_item_selected);
268 view.setTag(holder);
269 } else {
270 holder = (ViewHolder) view.getTag();
271 }
272
273 String alias = mAliases.get(position);
274
275 holder.mAliasTextView.setText(alias);
276
277 String subject = mSubjects.get(position);
278 if (subject == null) {
279 new CertLoader(position, holder.mSubjectTextView).execute();
280 } else {
281 holder.mSubjectTextView.setText(subject);
282 }
283
284 ListView lv = (ListView)parent;
285 holder.mRadioButton.setChecked(position == lv.getCheckedItemPosition());
286 return view;
287 }
288
289 private class CertLoader extends AsyncTask<Void, Void, String> {
290 private final int mPosition;
291 private final TextView mSubjectView;
292 private CertLoader(int position, TextView subjectView) {
293 mPosition = position;
294 mSubjectView = subjectView;
295 }
296 @Override protected String doInBackground(Void... params) {
297 String alias = mAliases.get(mPosition);
298 byte[] bytes = mKeyStore.get(Credentials.USER_CERTIFICATE + alias);
299 if (bytes == null) {
300 return null;
301 }
302 InputStream in = new ByteArrayInputStream(bytes);
303 X509Certificate cert;
304 try {
305 CertificateFactory cf = CertificateFactory.getInstance("X.509");
306 cert = (X509Certificate)cf.generateCertificate(in);
307 } catch (CertificateException ignored) {
308 return null;
309 }
310 // bouncycastle can handle the emailAddress OID of 1.2.840.113549.1.9.1
311 X500Principal subjectPrincipal = cert.getSubjectX500Principal();
312 X509Name subjectName = X509Name.getInstance(subjectPrincipal.getEncoded());
313 String subjectString = subjectName.toString(true, X509Name.DefaultSymbols);
314 return subjectString;
315 }
316 @Override protected void onPostExecute(String subjectString) {
317 mSubjects.set(mPosition, subjectString);
318 mSubjectView.setText(subjectString);
319 }
320 }
321 }
322
323 private static class ViewHolder {
324 TextView mAliasTextView;
325 TextView mSubjectTextView;
326 RadioButton mRadioButton;
Brian Carlstrom3e6251d2011-04-11 09:05:06 -0700327 }
328
329 @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) {
330 switch (requestCode) {
331 case REQUEST_UNLOCK:
332 if (isKeyStoreUnlocked()) {
Brian Carlstrom91940e72011-06-28 20:37:31 -0700333 showCertChooserDialog();
Brian Carlstrom3e6251d2011-04-11 09:05:06 -0700334 } else {
335 // user must have canceled unlock, give up
Brian Carlstrombb04f702011-05-24 21:54:51 -0700336 finish(null);
Brian Carlstrom3e6251d2011-04-11 09:05:06 -0700337 }
338 return;
339 default:
340 throw new AssertionError();
341 }
342 }
343
Brian Carlstrombb04f702011-05-24 21:54:51 -0700344 private void finish(String alias) {
345 if (alias == null) {
346 setResult(RESULT_CANCELED);
347 } else {
348 Intent result = new Intent();
349 result.putExtra(Intent.EXTRA_TEXT, alias);
350 setResult(RESULT_OK, result);
351 }
Brian Carlstromf5b50a42011-06-09 16:05:09 -0700352 IKeyChainAliasCallback keyChainAliasResponse
353 = IKeyChainAliasCallback.Stub.asInterface(
Brian Carlstrombb04f702011-05-24 21:54:51 -0700354 getIntent().getIBinderExtra(KeyChain.EXTRA_RESPONSE));
355 if (keyChainAliasResponse != null) {
356 try {
357 keyChainAliasResponse.alias(alias);
Brian Carlstrom2a858832011-05-26 09:30:26 -0700358 } catch (Exception ignored) {
359 // don't just catch RemoteException, caller could
360 // throw back a RuntimeException across processes
361 // which we should protect against.
Brian Carlstrombb04f702011-05-24 21:54:51 -0700362 }
363 }
364 finish();
365 }
366
Brian Carlstrom9e606df2011-06-07 12:03:08 -0700367 @Override public void onBackPressed() {
368 finish(null);
369 }
370
Brian Carlstrom3e6251d2011-04-11 09:05:06 -0700371 @Override protected void onSaveInstanceState(Bundle savedState) {
372 super.onSaveInstanceState(savedState);
373 if (mState != State.INITIAL) {
374 savedState.putSerializable(KEY_STATE, mState);
375 }
376 }
377}