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