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