• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
onCreate(Bundle savedState)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 
onResume()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 
chooseCertificate()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> {
doInBackground(Void... params)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 
displayCertChooserDialog(final CertificateAdapter adapter)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>();
CertificateAdapter(List<String> aliases)329         private CertificateAdapter(List<String> aliases) {
330             mAliases = aliases;
331             mSubjects.addAll(Collections.nCopies(aliases.size(), (String) null));
332         }
getCount()333         @Override public int getCount() {
334             return mAliases.size();
335         }
getItem(int adapterPosition)336         @Override public String getItem(int adapterPosition) {
337             return mAliases.get(adapterPosition);
338         }
getItemId(int adapterPosition)339         @Override public long getItemId(int adapterPosition) {
340             return adapterPosition;
341         }
getView(final int adapterPosition, View view, ViewGroup parent)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;
CertLoader(int adapterPosition, TextView subjectView)377             private CertLoader(int adapterPosition, TextView subjectView) {
378                 mAdapterPosition = adapterPosition;
379                 mSubjectView = subjectView;
380             }
doInBackground(Void... params)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             }
onPostExecute(String subjectString)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 
onActivityResult(int requestCode, int resultCode, Intent data)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 
finish(String alias)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;
ResponseSender(IKeyChainAliasCallback keyChainAliasResponse, String alias)451         private ResponseSender(IKeyChainAliasCallback keyChainAliasResponse, String alias) {
452             mKeyChainAliasResponse = keyChainAliasResponse;
453             mAlias = alias;
454         }
doInBackground(Void... unused)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         }
onPostExecute(Void unused)477         @Override protected void onPostExecute(Void unused) {
478             finish();
479         }
480     }
481 
onBackPressed()482     @Override public void onBackPressed() {
483         finish(null);
484     }
485 
onSaveInstanceState(Bundle savedState)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