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