KeyChainActivity.java revision 01b70dc5433d4a462df0a2d63163fcd47f52fa30
1/*
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
19import android.app.Activity;
20import android.app.ActivityManagerNative;
21import android.app.admin.IDevicePolicyManager;
22import android.app.AlertDialog;
23import android.app.Dialog;
24import android.app.PendingIntent;
25import android.content.Context;
26import android.content.DialogInterface;
27import android.content.Intent;
28import android.content.pm.PackageManager;
29import android.content.res.Resources;
30import android.net.Uri;
31import android.os.AsyncTask;
32import android.os.Bundle;
33import android.os.IBinder;
34import android.os.RemoteException;
35import android.os.ServiceManager;
36import android.security.Credentials;
37import android.security.IKeyChainAliasCallback;
38import android.security.KeyChain;
39import android.security.KeyStore;
40import android.util.Log;
41import android.view.LayoutInflater;
42import android.view.View;
43import android.view.ViewGroup;
44import android.widget.AdapterView;
45import android.widget.BaseAdapter;
46import android.widget.Button;
47import android.widget.ListView;
48import android.widget.RadioButton;
49import android.widget.TextView;
50import com.android.org.bouncycastle.asn1.x509.X509Name;
51import java.io.ByteArrayInputStream;
52import java.io.InputStream;
53import java.security.cert.CertificateException;
54import java.security.cert.CertificateFactory;
55import java.security.cert.X509Certificate;
56import java.util.ArrayList;
57import java.util.Arrays;
58import java.util.Collections;
59import java.util.concurrent.ExecutionException;
60import java.util.List;
61
62import javax.security.auth.x500.X500Principal;
63
64public class KeyChainActivity extends Activity {
65    private static final String TAG = "KeyChain";
66
67    private static String KEY_STATE = "state";
68
69    private static final int REQUEST_UNLOCK = 1;
70
71    private int mSenderUid;
72
73    private PendingIntent mSender;
74
75    private static enum State { INITIAL, UNLOCK_REQUESTED, UNLOCK_CANCELED };
76
77    private State mState;
78
79    // beware that some of these KeyStore operations such as saw and
80    // get do file I/O in the remote keystore process and while they
81    // do not cause StrictMode violations, they logically should not
82    // be done on the UI thread.
83    private KeyStore mKeyStore = KeyStore.getInstance();
84
85    @Override public void onCreate(Bundle savedState) {
86        super.onCreate(savedState);
87        if (savedState == null) {
88            mState = State.INITIAL;
89        } else {
90            mState = (State) savedState.getSerializable(KEY_STATE);
91            if (mState == null) {
92                mState = State.INITIAL;
93            }
94        }
95    }
96
97    @Override public void onResume() {
98        super.onResume();
99
100        mSender = getIntent().getParcelableExtra(KeyChain.EXTRA_SENDER);
101        if (mSender == null) {
102            // if no sender, bail, we need to identify the app to the user securely.
103            finish(null);
104            return;
105        }
106        try {
107            mSenderUid = getPackageManager().getPackageInfo(
108                    mSender.getIntentSender().getTargetPackage(), 0).applicationInfo.uid;
109        } catch (PackageManager.NameNotFoundException e) {
110            // if unable to find the sender package info bail,
111            // we need to identify the app to the user securely.
112            finish(null);
113            return;
114        }
115
116        // see if KeyStore has been unlocked, if not start activity to do so
117        switch (mState) {
118            case INITIAL:
119                if (!mKeyStore.isUnlocked()) {
120                    mState = State.UNLOCK_REQUESTED;
121                    this.startActivityForResult(new Intent(Credentials.UNLOCK_ACTION),
122                                                REQUEST_UNLOCK);
123                    // Note that Credentials.unlock will start an
124                    // Activity and we will be paused but then resumed
125                    // when the unlock Activity completes and our
126                    // onActivityResult is called with REQUEST_UNLOCK
127                    return;
128                }
129                chooseCertificate();
130                return;
131            case UNLOCK_REQUESTED:
132                // we've already asked, but have not heard back, probably just rotated.
133                // wait to hear back via onActivityResult
134                return;
135            case UNLOCK_CANCELED:
136                // User wanted to cancel the request, so exit.
137                mState = State.INITIAL;
138                finish(null);
139                return;
140            default:
141                throw new AssertionError();
142        }
143    }
144
145    private void chooseCertificate() {
146        // Start loading the set of certs to choose from now- if device policy doesn't return an
147        // alias, having aliases loading already will save some time waiting for UI to start.
148        final AliasLoader loader = new AliasLoader();
149        loader.execute();
150
151        final IKeyChainAliasCallback.Stub callback = new IKeyChainAliasCallback.Stub() {
152            @Override public void alias(String alias) {
153                // Use policy-suggested alias if provided
154                if (alias != null) {
155                    finish(alias);
156                    return;
157                }
158
159                // No suggested alias - instead finish loading and show UI to pick one
160                final CertificateAdapter certAdapter;
161                try {
162                    certAdapter = loader.get();
163                } catch (InterruptedException | ExecutionException e) {
164                    Log.e(TAG, "Loading certificate aliases interrupted", e);
165                    finish(null);
166                    return;
167                }
168                runOnUiThread(new Runnable() {
169                    @Override public void run() {
170                        displayCertChooserDialog(certAdapter);
171                    }
172                });
173            }
174        };
175
176        // Give a profile or device owner the chance to intercept the request, if a private key
177        // access listener is registered with the DevicePolicyManagerService.
178        IDevicePolicyManager devicePolicyManager = IDevicePolicyManager.Stub.asInterface(
179                ServiceManager.getService(Context.DEVICE_POLICY_SERVICE));
180
181        Uri uri = getIntent().getParcelableExtra(KeyChain.EXTRA_URI);
182        String alias = getIntent().getStringExtra(KeyChain.EXTRA_ALIAS);
183        try {
184            devicePolicyManager.choosePrivateKeyAlias(mSenderUid, uri, alias, callback);
185        } catch (RemoteException e) {
186            Log.e(TAG, "Unable to request alias from DevicePolicyManager", e);
187            // Proceed without a suggested alias.
188            try {
189                callback.alias(null);
190            } catch (RemoteException shouldNeverHappen) {
191                finish(null);
192            }
193        }
194    }
195
196    private class AliasLoader extends AsyncTask<Void, Void, CertificateAdapter> {
197        @Override protected CertificateAdapter doInBackground(Void... params) {
198            String[] aliasArray = mKeyStore.list(Credentials.USER_PRIVATE_KEY);
199            List<String> aliasList = ((aliasArray == null)
200                                      ? Collections.<String>emptyList()
201                                      : Arrays.asList(aliasArray));
202            Collections.sort(aliasList);
203            return new CertificateAdapter(aliasList);
204        }
205    }
206
207    private void displayCertChooserDialog(final CertificateAdapter adapter) {
208        AlertDialog.Builder builder = new AlertDialog.Builder(this);
209
210        TextView contextView = (TextView) View.inflate(this, R.layout.cert_chooser_header, null);
211        View footer = View.inflate(this, R.layout.cert_chooser_footer, null);
212
213        final ListView lv = (ListView) View.inflate(this, R.layout.cert_chooser, null);
214        lv.addHeaderView(contextView, null, false);
215        lv.addFooterView(footer, null, false);
216        lv.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
217        lv.setAdapter(adapter);
218        builder.setView(lv);
219
220        lv.setOnItemClickListener(new AdapterView.OnItemClickListener() {
221
222                public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
223                    lv.setItemChecked(position, true);
224                    adapter.notifyDataSetChanged();
225                }
226        });
227
228        boolean empty = adapter.mAliases.isEmpty();
229        int negativeLabel = empty ? android.R.string.cancel : R.string.deny_button;
230        builder.setNegativeButton(negativeLabel, new DialogInterface.OnClickListener() {
231            @Override public void onClick(DialogInterface dialog, int id) {
232                dialog.cancel(); // will cause OnDismissListener to be called
233            }
234        });
235
236        String title;
237        Resources res = getResources();
238        if (empty) {
239            title = res.getString(R.string.title_no_certs);
240        } else {
241            title = res.getString(R.string.title_select_cert);
242            String alias = getIntent().getStringExtra(KeyChain.EXTRA_ALIAS);
243            if (alias != null) {
244                // if alias was requested, set it if found
245                int adapterPosition = adapter.mAliases.indexOf(alias);
246                if (adapterPosition != -1) {
247                    int listViewPosition = adapterPosition+1;
248                    lv.setItemChecked(listViewPosition, true);
249                }
250            } else if (adapter.mAliases.size() == 1) {
251                // if only one choice, preselect it
252                int adapterPosition = 0;
253                int listViewPosition = adapterPosition+1;
254                lv.setItemChecked(listViewPosition, true);
255            }
256
257            builder.setPositiveButton(R.string.allow_button, new DialogInterface.OnClickListener() {
258                @Override public void onClick(DialogInterface dialog, int id) {
259                    int listViewPosition = lv.getCheckedItemPosition();
260                    int adapterPosition = listViewPosition-1;
261                    String alias = ((adapterPosition >= 0)
262                                    ? adapter.getItem(adapterPosition)
263                                    : null);
264                    finish(alias);
265                }
266            });
267        }
268        builder.setTitle(title);
269        final Dialog dialog = builder.create();
270
271
272        // getTargetPackage guarantees that the returned string is
273        // supplied by the system, so that an application can not
274        // spoof its package.
275        String pkg = mSender.getIntentSender().getTargetPackage();
276        PackageManager pm = getPackageManager();
277        CharSequence applicationLabel;
278        try {
279            applicationLabel = pm.getApplicationLabel(pm.getApplicationInfo(pkg, 0)).toString();
280        } catch (PackageManager.NameNotFoundException e) {
281            applicationLabel = pkg;
282        }
283        String appMessage = String.format(res.getString(R.string.requesting_application),
284                                          applicationLabel);
285        String contextMessage = appMessage;
286        Uri uri = getIntent().getParcelableExtra(KeyChain.EXTRA_URI);
287        if (uri != null) {
288            String hostMessage = String.format(res.getString(R.string.requesting_server),
289                                               uri.getAuthority());
290            if (contextMessage == null) {
291                contextMessage = hostMessage;
292            } else {
293                contextMessage += " " + hostMessage;
294            }
295        }
296        contextView.setText(contextMessage);
297
298        String installMessage = String.format(res.getString(R.string.install_new_cert_message),
299                                              Credentials.EXTENSION_PFX, Credentials.EXTENSION_P12);
300        TextView installText = (TextView) footer.findViewById(R.id.cert_chooser_install_message);
301        installText.setText(installMessage);
302
303        Button installButton = (Button) footer.findViewById(R.id.cert_chooser_install_button);
304        installButton.setOnClickListener(new View.OnClickListener() {
305            @Override public void onClick(View v) {
306                // remove dialog so that we will recreate with
307                // possibly new content after install returns
308                dialog.dismiss();
309                Credentials.getInstance().install(KeyChainActivity.this);
310            }
311        });
312
313        dialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
314            @Override public void onCancel(DialogInterface dialog) {
315                finish(null);
316            }
317        });
318        dialog.show();
319    }
320
321    private class CertificateAdapter extends BaseAdapter {
322        private final List<String> mAliases;
323        private final List<String> mSubjects = new ArrayList<String>();
324        private CertificateAdapter(List<String> aliases) {
325            mAliases = aliases;
326            mSubjects.addAll(Collections.nCopies(aliases.size(), (String) null));
327        }
328        @Override public int getCount() {
329            return mAliases.size();
330        }
331        @Override public String getItem(int adapterPosition) {
332            return mAliases.get(adapterPosition);
333        }
334        @Override public long getItemId(int adapterPosition) {
335            return adapterPosition;
336        }
337        @Override public View getView(final int adapterPosition, View view, ViewGroup parent) {
338            ViewHolder holder;
339            if (view == null) {
340                LayoutInflater inflater = LayoutInflater.from(KeyChainActivity.this);
341                view = inflater.inflate(R.layout.cert_item, parent, false);
342                holder = new ViewHolder();
343                holder.mAliasTextView = (TextView) view.findViewById(R.id.cert_item_alias);
344                holder.mSubjectTextView = (TextView) view.findViewById(R.id.cert_item_subject);
345                holder.mRadioButton = (RadioButton) view.findViewById(R.id.cert_item_selected);
346                view.setTag(holder);
347            } else {
348                holder = (ViewHolder) view.getTag();
349            }
350
351            String alias = mAliases.get(adapterPosition);
352
353            holder.mAliasTextView.setText(alias);
354
355            String subject = mSubjects.get(adapterPosition);
356            if (subject == null) {
357                new CertLoader(adapterPosition, holder.mSubjectTextView).execute();
358            } else {
359                holder.mSubjectTextView.setText(subject);
360            }
361
362            ListView lv = (ListView)parent;
363            int listViewCheckedItemPosition = lv.getCheckedItemPosition();
364            int adapterCheckedItemPosition = listViewCheckedItemPosition-1;
365            holder.mRadioButton.setChecked(adapterPosition == adapterCheckedItemPosition);
366            return view;
367        }
368
369        private class CertLoader extends AsyncTask<Void, Void, String> {
370            private final int mAdapterPosition;
371            private final TextView mSubjectView;
372            private CertLoader(int adapterPosition, TextView subjectView) {
373                mAdapterPosition = adapterPosition;
374                mSubjectView = subjectView;
375            }
376            @Override protected String doInBackground(Void... params) {
377                String alias = mAliases.get(mAdapterPosition);
378                byte[] bytes = mKeyStore.get(Credentials.USER_CERTIFICATE + alias);
379                if (bytes == null) {
380                    return null;
381                }
382                InputStream in = new ByteArrayInputStream(bytes);
383                X509Certificate cert;
384                try {
385                    CertificateFactory cf = CertificateFactory.getInstance("X.509");
386                    cert = (X509Certificate)cf.generateCertificate(in);
387                } catch (CertificateException ignored) {
388                    return null;
389                }
390                // bouncycastle can handle the emailAddress OID of 1.2.840.113549.1.9.1
391                X500Principal subjectPrincipal = cert.getSubjectX500Principal();
392                X509Name subjectName = X509Name.getInstance(subjectPrincipal.getEncoded());
393                String subjectString = subjectName.toString(true, X509Name.DefaultSymbols);
394                return subjectString;
395            }
396            @Override protected void onPostExecute(String subjectString) {
397                mSubjects.set(mAdapterPosition, subjectString);
398                mSubjectView.setText(subjectString);
399            }
400        }
401    }
402
403    private static class ViewHolder {
404        TextView mAliasTextView;
405        TextView mSubjectTextView;
406        RadioButton mRadioButton;
407    }
408
409    @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) {
410        switch (requestCode) {
411            case REQUEST_UNLOCK:
412                if (mKeyStore.isUnlocked()) {
413                    mState = State.INITIAL;
414                    chooseCertificate();
415                } else {
416                    // user must have canceled unlock, give up
417                    mState = State.UNLOCK_CANCELED;
418                }
419                return;
420            default:
421                throw new AssertionError();
422        }
423    }
424
425    private void finish(String alias) {
426        if (alias == null) {
427            setResult(RESULT_CANCELED);
428        } else {
429            Intent result = new Intent();
430            result.putExtra(Intent.EXTRA_TEXT, alias);
431            setResult(RESULT_OK, result);
432        }
433        IKeyChainAliasCallback keyChainAliasResponse
434                = IKeyChainAliasCallback.Stub.asInterface(
435                        getIntent().getIBinderExtra(KeyChain.EXTRA_RESPONSE));
436        if (keyChainAliasResponse != null) {
437            new ResponseSender(keyChainAliasResponse, alias).execute();
438            return;
439        }
440        finish();
441    }
442
443    private class ResponseSender extends AsyncTask<Void, Void, Void> {
444        private IKeyChainAliasCallback mKeyChainAliasResponse;
445        private String mAlias;
446        private ResponseSender(IKeyChainAliasCallback keyChainAliasResponse, String alias) {
447            mKeyChainAliasResponse = keyChainAliasResponse;
448            mAlias = alias;
449        }
450        @Override protected Void doInBackground(Void... unused) {
451            try {
452                if (mAlias != null) {
453                    KeyChain.KeyChainConnection connection = KeyChain.bind(KeyChainActivity.this);
454                    try {
455                        connection.getService().setGrant(mSenderUid, mAlias, true);
456                    } finally {
457                        connection.close();
458                    }
459                }
460                mKeyChainAliasResponse.alias(mAlias);
461            } catch (InterruptedException ignored) {
462                Thread.currentThread().interrupt();
463                Log.d(TAG, "interrupted while granting access", ignored);
464            } catch (Exception ignored) {
465                // don't just catch RemoteException, caller could
466                // throw back a RuntimeException across processes
467                // which we should protect against.
468                Log.e(TAG, "error while granting access", ignored);
469            }
470            return null;
471        }
472        @Override protected void onPostExecute(Void unused) {
473            finish();
474        }
475    }
476
477    @Override public void onBackPressed() {
478        finish(null);
479    }
480
481    @Override protected void onSaveInstanceState(Bundle savedState) {
482        super.onSaveInstanceState(savedState);
483        if (mState != State.INITIAL) {
484            savedState.putSerializable(KEY_STATE, mState);
485        }
486    }
487}
488