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.ActivityManagerNative;
21 import android.app.admin.IDevicePolicyManager;
22 import android.app.AlertDialog;
23 import android.app.Dialog;
24 import android.app.PendingIntent;
25 import android.content.Context;
26 import android.content.DialogInterface;
27 import android.content.Intent;
28 import android.content.pm.PackageManager;
29 import android.content.res.Resources;
30 import android.net.Uri;
31 import android.os.AsyncTask;
32 import android.os.Bundle;
33 import android.os.IBinder;
34 import android.os.RemoteException;
35 import android.os.ServiceManager;
36 import android.security.Credentials;
37 import android.security.IKeyChainAliasCallback;
38 import android.security.KeyChain;
39 import android.security.KeyStore;
40 import android.util.Log;
41 import android.view.LayoutInflater;
42 import android.view.View;
43 import android.view.ViewGroup;
44 import android.widget.AdapterView;
45 import android.widget.BaseAdapter;
46 import android.widget.Button;
47 import android.widget.ListView;
48 import android.widget.RadioButton;
49 import android.widget.TextView;
50 import com.android.org.bouncycastle.asn1.x509.X509Name;
51 import java.io.ByteArrayInputStream;
52 import java.io.InputStream;
53 import java.security.cert.CertificateException;
54 import java.security.cert.CertificateFactory;
55 import java.security.cert.X509Certificate;
56 import java.util.ArrayList;
57 import java.util.Arrays;
58 import java.util.Collections;
59 import java.util.concurrent.ExecutionException;
60 import java.util.List;
61 
62 import javax.security.auth.x500.X500Principal;
63 
64 public 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 
onCreate(Bundle savedState)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 
onResume()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 
chooseCertificate()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> {
doInBackground(Void... params)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 
displayCertChooserDialog(final CertificateAdapter adapter)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>();
CertificateAdapter(List<String> aliases)324         private CertificateAdapter(List<String> aliases) {
325             mAliases = aliases;
326             mSubjects.addAll(Collections.nCopies(aliases.size(), (String) null));
327         }
getCount()328         @Override public int getCount() {
329             return mAliases.size();
330         }
getItem(int adapterPosition)331         @Override public String getItem(int adapterPosition) {
332             return mAliases.get(adapterPosition);
333         }
getItemId(int adapterPosition)334         @Override public long getItemId(int adapterPosition) {
335             return adapterPosition;
336         }
getView(final int adapterPosition, View view, ViewGroup parent)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;
CertLoader(int adapterPosition, TextView subjectView)372             private CertLoader(int adapterPosition, TextView subjectView) {
373                 mAdapterPosition = adapterPosition;
374                 mSubjectView = subjectView;
375             }
doInBackground(Void... params)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             }
onPostExecute(String subjectString)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 
onActivityResult(int requestCode, int resultCode, Intent data)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 
finish(String alias)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;
ResponseSender(IKeyChainAliasCallback keyChainAliasResponse, String alias)446         private ResponseSender(IKeyChainAliasCallback keyChainAliasResponse, String alias) {
447             mKeyChainAliasResponse = keyChainAliasResponse;
448             mAlias = alias;
449         }
doInBackground(Void... unused)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         }
onPostExecute(Void unused)472         @Override protected void onPostExecute(Void unused) {
473             finish();
474         }
475     }
476 
onBackPressed()477     @Override public void onBackPressed() {
478         finish(null);
479     }
480 
onSaveInstanceState(Bundle savedState)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