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.annotation.NonNull;
20 import android.app.AlertDialog;
21 import android.app.PendingIntent;
22 import android.app.admin.DevicePolicyEventLogger;
23 import android.app.admin.DevicePolicyManager;
24 import android.app.admin.IDevicePolicyManager;
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.pm.UserInfo;
30 import android.content.res.Resources;
31 import android.net.Uri;
32 import android.os.AsyncTask;
33 import android.os.Bundle;
34 import android.os.Handler;
35 import android.os.Looper;
36 import android.os.RemoteException;
37 import android.os.ServiceManager;
38 import android.os.UserManager;
39 import android.security.IKeyChainAliasCallback;
40 import android.security.KeyChain;
41 import android.stats.devicepolicy.DevicePolicyEnums;
42 import android.util.Log;
43 import android.view.LayoutInflater;
44 import android.view.View;
45 import android.view.ViewGroup;
46 import android.widget.AdapterView;
47 import android.widget.BaseAdapter;
48 import android.widget.ListView;
49 import android.widget.RadioButton;
50 import android.widget.TextView;
51 
52 import androidx.appcompat.app.AppCompatActivity;
53 
54 import com.android.internal.annotations.VisibleForTesting;
55 import com.android.keychain.internal.KeyInfoProvider;
56 
57 import com.google.android.material.snackbar.Snackbar;
58 
59 import org.bouncycastle.asn1.x509.X509Name;
60 
61 import java.io.IOException;
62 import java.security.KeyStore;
63 import java.security.KeyStoreException;
64 import java.security.NoSuchAlgorithmException;
65 import java.security.cert.Certificate;
66 import java.security.cert.CertificateException;
67 import java.security.cert.X509Certificate;
68 import java.util.ArrayList;
69 import java.util.Arrays;
70 import java.util.Collections;
71 import java.util.Enumeration;
72 import java.util.List;
73 import java.util.concurrent.ExecutionException;
74 import java.util.concurrent.ExecutorService;
75 import java.util.concurrent.Executors;
76 import java.util.stream.Collectors;
77 
78 import javax.security.auth.x500.X500Principal;
79 
80 import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
81 
82 public class KeyChainActivity extends AppCompatActivity {
83     private static final String TAG = "KeyChain";
84 
85     // The amount of time to delay showing a snackbar. If the alias is received before the snackbar
86     // is shown, the activity will finish. If the certificate selection dialog is shown before the
87     // snackbar, no snackbar will be shown.
88     private static final long SNACKBAR_DELAY_TIME = 2000;
89     // The minimum amount of time to display a snackbar while loading certificates.
90     private static final long SNACKBAR_MIN_TIME = 1000;
91 
92     private int mSenderUid;
93     private String mSenderPackageName;
94 
95     private PendingIntent mSender;
96 
97     // beware that some of these KeyStore operations such as saw and
98     // get do file I/O in the remote keystore process and while they
99     // do not cause StrictMode violations, they logically should not
100     // be done on the UI thread.
101     private final KeyStore mKeyStore = getKeyStore();
102 
getKeyStore()103     private static KeyStore getKeyStore() {
104         try {
105             final KeyStore keystore = KeyStore.getInstance("AndroidKeyStore");
106             keystore.load(null);
107             return keystore;
108         } catch (KeyStoreException | IOException | NoSuchAlgorithmException
109                 | CertificateException e) {
110             Log.e(TAG, "Error opening AndroidKeyStore.", e);
111             throw new RuntimeException("Error opening AndroidKeyStore.", e);
112         }
113     }
114 
115     // A snackbar to show the user while the KeyChain Activity is loading the certificates.
116     private Snackbar mSnackbar;
117 
118     // A remote service may call {@link android.security.KeyChain#choosePrivateKeyAlias} multiple
119     // times, which will result in multiple intents being sent to KeyChainActivity. The time of the
120     // first received intent is recorded in order to ensure the snackbar is displayed for a
121     // minimum amount of time after receiving the first intent.
122     private long mFirstIntentReceivedTimeMillis = 0L;
123 
124     private ExecutorService executor = Executors.newSingleThreadExecutor();
125     private Handler handler = new Handler(Looper.getMainLooper());
126     private final Runnable mFinishActivity = KeyChainActivity.this::finish;
127     private final Runnable mShowSnackBar = this::showSnackBar;
128 
129     @Override
onCreate(Bundle savedState)130     protected void onCreate(Bundle savedState) {
131         super.onCreate(savedState);
132         setContentView(R.layout.keychain_activity);
133         getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
134     }
135 
onResume()136     @Override public void onResume() {
137         super.onResume();
138 
139         mSender = getIntent().getParcelableExtra(KeyChain.EXTRA_SENDER);
140         if (mSender == null) {
141             // if no sender, bail, we need to identify the app to the user securely.
142             finish(null);
143             return;
144         }
145         try {
146             // getTargetPackage guarantees that the returned string is
147             // supplied by the system, so that an application can not
148             // spoof its package.
149             mSenderPackageName = mSender.getIntentSender().getTargetPackage();
150             mSenderUid = getPackageManager().getPackageInfo(
151                     mSenderPackageName, 0).applicationInfo.uid;
152         } catch (PackageManager.NameNotFoundException e) {
153             // if unable to find the sender package info bail,
154             // we need to identify the app to the user securely.
155             finish(null);
156             return;
157         }
158 
159         chooseCertificate();
160     }
161 
162     @Override
onNewIntent(Intent intent)163     protected void onNewIntent(Intent intent) {
164         super.onNewIntent(intent);
165         handler.removeCallbacks(mFinishActivity);
166     }
167 
showSnackBar()168     private void showSnackBar() {
169         mFirstIntentReceivedTimeMillis = System.currentTimeMillis();
170         mSnackbar = Snackbar.make(findViewById(R.id.container),
171                 String.format(getResources().getString(R.string.loading_certs_message),
172                         getApplicationLabel()), Snackbar.LENGTH_INDEFINITE);
173         mSnackbar.show();
174     }
175 
finishSnackBar()176     private void finishSnackBar() {
177         if (mSnackbar != null) {
178             mSnackbar.dismiss();
179             mSnackbar = null;
180         } else {
181             handler.removeCallbacks(mShowSnackBar);
182         }
183     }
184 
chooseCertificate()185     private void chooseCertificate() {
186         // Start loading the set of certs to choose from now- if device policy doesn't return an
187         // alias, having aliases loading already will save some time waiting for UI to start.
188         KeyInfoProvider keyInfoProvider = new KeyInfoProvider() {
189             public boolean isUserSelectable(String alias) {
190                 try (KeyChain.KeyChainConnection connection =
191                         KeyChain.bind(KeyChainActivity.this)) {
192                     return connection.getService().isUserSelectable(alias);
193                 }
194                 catch (InterruptedException ignored) {
195                     Log.e(TAG, "interrupted while checking if key is user-selectable", ignored);
196                     Thread.currentThread().interrupt();
197                     return false;
198                 } catch (Exception ignored) {
199                     Log.e(TAG, "error while checking if key is user-selectable", ignored);
200                     return false;
201                 }
202             }
203         };
204 
205         Log.i(TAG, String.format("Requested by app uid %d to provide a private key alias",
206                 mSenderUid));
207 
208         String[] keyTypes = getIntent().getStringArrayExtra(KeyChain.EXTRA_KEY_TYPES);
209         if (keyTypes == null) {
210             keyTypes = new String[]{};
211         }
212         Log.i(TAG, String.format("Key types specified: %s", Arrays.toString(keyTypes)));
213 
214         ArrayList<byte[]> issuers = (ArrayList<byte[]>) getIntent().getSerializableExtra(
215                 KeyChain.EXTRA_ISSUERS);
216         if (issuers == null) {
217             issuers = new ArrayList<byte[]>();
218         } else {
219             Log.i(TAG, "Issuers specified, will be listed later.");
220         }
221 
222         final AliasLoader loader = new AliasLoader(mKeyStore, this, keyInfoProvider,
223                 new CertificateParametersFilter(mKeyStore, keyTypes, issuers));
224         loader.execute();
225 
226         final IKeyChainAliasCallback.Stub callback = new IKeyChainAliasCallback.Stub() {
227             @Override public void alias(String alias) {
228                 Log.i(TAG, String.format("Alias provided by device policy client: %s", alias));
229                 // Use policy-suggested alias if provided or abort further actions if alias is
230                 // KeyChain.KEY_ALIAS_SELECTION_DENIED
231                 if (alias != null) {
232                     finishWithAliasFromPolicy(alias);
233                     return;
234                 }
235 
236                 // No suggested alias - instead finish loading and show UI to pick one
237                 final CertificateAdapter certAdapter;
238                 try {
239                     certAdapter = loader.get();
240                 } catch (InterruptedException | ExecutionException e) {
241                     Log.e(TAG, "Loading certificate aliases interrupted", e);
242                     finish(null);
243                     return;
244                 }
245                 /*
246                  * If there are no keys for the user to choose from, do not display
247                  * the dialog. This is in line with what other operating systems do.
248                  */
249                 if (!certAdapter.hasKeysToChoose()) {
250                     Log.i(TAG, "No keys to choose from");
251                     finish(null);
252                     return;
253                 }
254                 runOnUiThread(() -> {
255                     finishSnackBar();
256                     displayCertChooserDialog(certAdapter);
257                 });
258             }
259         };
260 
261         // Show a snackbar to the user to indicate long-running task.
262         if (mSnackbar == null) {
263             handler.postDelayed(mShowSnackBar, SNACKBAR_DELAY_TIME);
264         }
265         Uri uri = getIntent().getParcelableExtra(KeyChain.EXTRA_URI);
266         String alias = getIntent().getStringExtra(KeyChain.EXTRA_ALIAS);
267 
268         if (isManagedDevice()) {
269             // Give a profile or device owner the chance to intercept the request, if a private key
270             // access listener is registered with the DevicePolicyManagerService.
271             IDevicePolicyManager devicePolicyManager = IDevicePolicyManager.Stub.asInterface(
272                     ServiceManager.getService(Context.DEVICE_POLICY_SERVICE));
273             try {
274                 devicePolicyManager.choosePrivateKeyAlias(mSenderUid, uri, alias, callback);
275             } catch (RemoteException e) {
276                 Log.e(TAG, "Unable to request alias from DevicePolicyManager", e);
277                 // Proceed without a suggested alias.
278                 try {
279                     callback.alias(null);
280                 } catch (RemoteException shouldNeverHappen) {
281                     finish(null);
282                 }
283             }
284         } else {
285             // If the device is unmanaged, check whether the credential management app has provided
286             // an alias for the given uri and calling package name.
287             getAliasFromCredentialManagementApp(uri, callback);
288         }
289     }
290 
isManagedDevice()291     private boolean isManagedDevice() {
292         DevicePolicyManager devicePolicyManager = getSystemService(DevicePolicyManager.class);
293         return devicePolicyManager.getDeviceOwner() != null
294                 || devicePolicyManager.getProfileOwner() != null
295                 || hasManagedProfile();
296     }
297 
hasManagedProfile()298     private boolean hasManagedProfile() {
299         UserManager userManager = getSystemService(UserManager.class);
300         for (final UserInfo userInfo : userManager.getProfiles(getUserId())) {
301             if (userInfo.isManagedProfile()) {
302                 return true;
303             }
304         }
305         return false;
306     }
307 
getAliasFromCredentialManagementApp(Uri uri, IKeyChainAliasCallback.Stub callback)308     private void getAliasFromCredentialManagementApp(Uri uri,
309             IKeyChainAliasCallback.Stub callback) {
310         executor.execute(() -> {
311             try (KeyChain.KeyChainConnection keyChainConnection = KeyChain.bind(this)) {
312                 String chosenAlias = null;
313                 if (keyChainConnection.getService().hasCredentialManagementApp()) {
314                     Log.i(TAG, "There is a credential management app on the device. "
315                             + "Looking for an alias in the policy.");
316                     chosenAlias = keyChainConnection.getService()
317                             .getPredefinedAliasForPackageAndUri(mSenderPackageName, uri);
318                     if (chosenAlias != null) {
319                         keyChainConnection.getService().setGrant(mSenderUid, chosenAlias, true);
320                         Log.w(TAG, String.format("Selected alias %s from the "
321                                 + "credential management app's policy", chosenAlias));
322                         DevicePolicyEventLogger
323                                 .createEvent(DevicePolicyEnums
324                                         .CREDENTIAL_MANAGEMENT_APP_CREDENTIAL_FOUND_IN_POLICY)
325                                 .write();
326                     } else {
327                         Log.i(TAG, "No alias provided from the credential management app");
328                     }
329                 }
330                 callback.alias(chosenAlias);
331             } catch (InterruptedException | RemoteException e) {
332                 Log.e(TAG, "Unable to request find predefined alias from credential "
333                         + "management app policy");
334                 // Proceed without a suggested alias.
335                 try {
336                     callback.alias(null);
337                 } catch (RemoteException shouldNeverHappen) {
338                     finish(null);
339                 } finally {
340                     DevicePolicyEventLogger
341                             .createEvent(DevicePolicyEnums
342                                     .CREDENTIAL_MANAGEMENT_APP_POLICY_LOOKUP_FAILED)
343                             .write();
344                 }
345             }
346         });
347     }
348 
349     @VisibleForTesting
350     public static class CertificateParametersFilter {
351         private final KeyStore mKeyStore;
352         private final List<String> mKeyTypes;
353         private final List<X500Principal> mIssuers;
354 
CertificateParametersFilter(KeyStore keyStore, @NonNull String[] keyTypes, @NonNull ArrayList<byte[]> issuers)355         public CertificateParametersFilter(KeyStore keyStore,
356                 @NonNull String[] keyTypes, @NonNull ArrayList<byte[]> issuers) {
357             mKeyStore = keyStore;
358             mKeyTypes = Arrays.asList(keyTypes);
359             mIssuers = new ArrayList<X500Principal>();
360             for (byte[] issuer : issuers) {
361                 try {
362                     X500Principal issuerPrincipal = new X500Principal(issuer);
363                     Log.i(TAG, "Added issuer: " + issuerPrincipal.getName());
364                     mIssuers.add(new X500Principal(issuer));
365                 } catch (IllegalArgumentException e) {
366                     Log.w(TAG, "Skipping invalid issuer", e);
367                 }
368             }
369         }
370 
shouldPresentCertificate(String alias)371         public boolean shouldPresentCertificate(String alias) {
372             X509Certificate cert = loadCertificate(mKeyStore, alias);
373             // If there's no certificate associated with the alias, skip.
374             if (cert == null) {
375                 Log.i(TAG, String.format("No certificate associated with alias %s", alias));
376                 return false;
377             }
378             List<X509Certificate> certChain = new ArrayList(loadCertificateChain(mKeyStore, alias));
379             Log.i(TAG, String.format("Inspecting certificate %s aliased with %s, chain length %d",
380                         cert.getSubjectDN().getName(), alias, certChain.size()));
381 
382             // If the caller has provided a list of key types to restrict the certificates
383             // offered for selection, skip this alias if the key algorithm is not in that
384             // list.
385             // Note that the end entity (leaf) certificate's public key has to be compatible
386             // with the specified key algorithm, not any one of the chain (see RFC5246
387             // section 7.4.6)
388             String keyAlgorithm = cert.getPublicKey().getAlgorithm();
389             Log.i(TAG, String.format("Certificate key algorithm: %s", keyAlgorithm));
390             if (!mKeyTypes.isEmpty() && !mKeyTypes.contains(keyAlgorithm)) {
391                 return false;
392             }
393 
394             // If the caller has provided a list of issuers to restrict the certificates
395             // offered for selection, skip this alias if none of the issuers in the client
396             // certificate chain is in that list.
397             List<X500Principal> chainIssuers = new ArrayList();
398             chainIssuers.add(cert.getIssuerX500Principal());
399             for (X509Certificate intermediate : certChain) {
400                 X500Principal subject = intermediate.getSubjectX500Principal();
401                 Log.i(TAG, String.format("Subject of intermediate in client certificate chain: %s",
402                             subject.getName()));
403                 // Collect the subjects of all the intermediates, as the RFC specifies that
404                 // "one of the certificates in the certificate chain SHOULD be issued by one of
405                 // the listed CAs."
406                 chainIssuers.add(subject);
407             }
408 
409             if (!mIssuers.isEmpty()) {
410                 for (X500Principal issuer : chainIssuers) {
411                     if (mIssuers.contains(issuer)) {
412                         Log.i(TAG, String.format("Requested issuer found: %s", issuer));
413                         return true;
414                     }
415                 }
416                 return false;
417             }
418 
419             return true;
420         }
421     }
422 
423     @VisibleForTesting
424     static class AliasLoader extends AsyncTask<Void, Void, CertificateAdapter> {
425         private final KeyStore mKeyStore;
426         private final Context mContext;
427         private final KeyInfoProvider mInfoProvider;
428         private final CertificateParametersFilter mCertificateFilter;
429 
AliasLoader(KeyStore keyStore, Context context, KeyInfoProvider infoProvider, CertificateParametersFilter certificateFilter)430         public AliasLoader(KeyStore keyStore, Context context,
431                 KeyInfoProvider infoProvider, CertificateParametersFilter certificateFilter) {
432           mKeyStore = keyStore;
433           mContext = context;
434           mInfoProvider = infoProvider;
435           mCertificateFilter = certificateFilter;
436         }
437 
doInBackground(Void... params)438         @Override protected CertificateAdapter doInBackground(Void... params) {
439             final List<String> rawAliasList = new ArrayList<>();
440             try {
441                 final Enumeration<String> aliases = mKeyStore.aliases();
442                 while (aliases.hasMoreElements()) {
443                     final String alias = aliases.nextElement();
444                     if (mKeyStore.isKeyEntry(alias)) {
445                         rawAliasList.add(alias);
446                     }
447                 }
448             } catch (KeyStoreException e) {
449                 Log.e(TAG, "Error while loading entries from keystore. "
450                         + "List may be empty or incomplete.");
451             }
452 
453             return new CertificateAdapter(mKeyStore, mContext,
454                     rawAliasList.stream().filter(mInfoProvider::isUserSelectable)
455                     .filter(mCertificateFilter::shouldPresentCertificate)
456                     .sorted().collect(Collectors.toList()));
457         }
458     }
459 
displayCertChooserDialog(final CertificateAdapter adapter)460     private void displayCertChooserDialog(final CertificateAdapter adapter) {
461         if (adapter.mAliases.isEmpty()) {
462             Log.w(TAG, "Should not be asked to display the cert chooser without aliases.");
463             finish(null);
464             return;
465         }
466 
467         AlertDialog.Builder builder = new AlertDialog.Builder(this);
468         builder.setNegativeButton(R.string.deny_button, new DialogInterface.OnClickListener() {
469             @Override public void onClick(DialogInterface dialog, int id) {
470                 dialog.cancel(); // will cause OnDismissListener to be called
471             }
472         });
473 
474         int selectedItem = -1;
475         Resources res = getResources();
476         String alias = getIntent().getStringExtra(KeyChain.EXTRA_ALIAS);
477 
478         if (alias != null) {
479             // if alias was requested, set it if found
480             int adapterPosition = adapter.mAliases.indexOf(alias);
481             if (adapterPosition != -1) {
482                 // increase by 1 to account for item 0 being the header.
483                 selectedItem = adapterPosition + 1;
484             }
485         } else if (adapter.mAliases.size() == 1) {
486             // if only one choice, preselect it
487             selectedItem = 1;
488         }
489 
490         builder.setPositiveButton(R.string.allow_button, new DialogInterface.OnClickListener() {
491             @Override public void onClick(DialogInterface dialog, int id) {
492                 if (dialog instanceof AlertDialog) {
493                     ListView lv = ((AlertDialog) dialog).getListView();
494                     int listViewPosition = lv.getCheckedItemPosition();
495                     int adapterPosition = listViewPosition-1;
496                     String alias = ((adapterPosition >= 0)
497                                     ? adapter.getItem(adapterPosition)
498                                     : null);
499                     Log.i(TAG, String.format("User chose: %s", alias));
500                     finish(alias);
501                 } else {
502                     Log.wtf(TAG, "Expected AlertDialog, got " + dialog, new Exception());
503                     finish(null);
504                 }
505             }
506         });
507 
508         builder.setTitle(res.getString(R.string.title_select_cert));
509         builder.setSingleChoiceItems(adapter, selectedItem, null);
510         final AlertDialog dialog = builder.create();
511 
512         // Show text above the list to explain what the certificate will be used for.
513         TextView contextView = (TextView) View.inflate(
514                 this, R.layout.cert_chooser_header, null);
515 
516         final ListView lv = dialog.getListView();
517         lv.addHeaderView(contextView, null, false);
518         lv.setOnItemClickListener(new AdapterView.OnItemClickListener() {
519             public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
520                 if (position == 0) {
521                     // Header. Just text; ignore clicks.
522                     return;
523                 } else {
524                     dialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(true);
525                     lv.setItemChecked(position, true);
526                     adapter.notifyDataSetChanged();
527                 }
528             }
529         });
530 
531         String contextMessage = String.format(res.getString(R.string.requesting_application),
532                 getApplicationLabel());
533         Uri uri = getIntent().getParcelableExtra(KeyChain.EXTRA_URI);
534         if (uri != null) {
535             String hostMessage = String.format(res.getString(R.string.requesting_server),
536                                                uri.getAuthority());
537             if (contextMessage == null) {
538                 contextMessage = hostMessage;
539             } else {
540                 contextMessage += " " + hostMessage;
541             }
542         }
543         contextView.setText(contextMessage);
544 
545         if (selectedItem == -1) {
546             dialog.setOnShowListener(new DialogInterface.OnShowListener() {
547                 @Override
548                 public void onShow(DialogInterface dialogInterface) {
549                      dialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(false);
550                 }
551             });
552         }
553         dialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
554             @Override public void onCancel(DialogInterface dialog) {
555                 finish(null);
556             }
557         });
558         dialog.create();
559         // Prevents screen overlay attack.
560         dialog.getButton(DialogInterface.BUTTON_POSITIVE).setFilterTouchesWhenObscured(true);
561         dialog.show();
562     }
563 
getApplicationLabel()564     private String getApplicationLabel() {
565         PackageManager pm = getPackageManager();
566         try {
567             return pm.getApplicationLabel(pm.getApplicationInfo(mSenderPackageName, 0)).toString();
568         } catch (PackageManager.NameNotFoundException e) {
569             return mSenderPackageName;
570         }
571     }
572 
573     @VisibleForTesting
574     static class CertificateAdapter extends BaseAdapter {
575         private final List<String> mAliases;
576         private final List<String> mSubjects = new ArrayList<String>();
577         private final KeyStore mKeyStore;
578         private final Context mContext;
579 
CertificateAdapter(KeyStore keyStore, Context context, List<String> aliases)580         private CertificateAdapter(KeyStore keyStore, Context context, List<String> aliases) {
581             mAliases = aliases;
582             mSubjects.addAll(Collections.nCopies(aliases.size(), (String) null));
583             mKeyStore = keyStore;
584             mContext = context;
585         }
getCount()586         @Override public int getCount() {
587             return mAliases.size();
588         }
getItem(int adapterPosition)589         @Override public String getItem(int adapterPosition) {
590             return mAliases.get(adapterPosition);
591         }
getItemId(int adapterPosition)592         @Override public long getItemId(int adapterPosition) {
593             return adapterPosition;
594         }
getView(final int adapterPosition, View view, ViewGroup parent)595         @Override public View getView(final int adapterPosition, View view, ViewGroup parent) {
596             ViewHolder holder;
597             if (view == null) {
598                 LayoutInflater inflater = LayoutInflater.from(mContext);
599                 view = inflater.inflate(R.layout.cert_item, parent, false);
600                 holder = new ViewHolder();
601                 holder.mAliasTextView = (TextView) view.findViewById(R.id.cert_item_alias);
602                 holder.mSubjectTextView = (TextView) view.findViewById(R.id.cert_item_subject);
603                 holder.mRadioButton = (RadioButton) view.findViewById(R.id.cert_item_selected);
604                 view.setTag(holder);
605             } else {
606                 holder = (ViewHolder) view.getTag();
607             }
608 
609             String alias = mAliases.get(adapterPosition);
610 
611             holder.mAliasTextView.setText(alias);
612 
613             String subject = mSubjects.get(adapterPosition);
614             if (subject == null) {
615                 new CertLoader(adapterPosition, holder.mSubjectTextView).execute();
616             } else {
617                 holder.mSubjectTextView.setText(subject);
618             }
619 
620             ListView lv = (ListView)parent;
621             int listViewCheckedItemPosition = lv.getCheckedItemPosition();
622             int adapterCheckedItemPosition = listViewCheckedItemPosition-1;
623             holder.mRadioButton.setChecked(adapterPosition == adapterCheckedItemPosition);
624             return view;
625         }
626 
627         /**
628          * Returns true if there are keys to choose from.
629          */
hasKeysToChoose()630         public boolean hasKeysToChoose() {
631             return !mAliases.isEmpty();
632         }
633 
634         private class CertLoader extends AsyncTask<Void, Void, String> {
635             private final int mAdapterPosition;
636             private final TextView mSubjectView;
CertLoader(int adapterPosition, TextView subjectView)637             private CertLoader(int adapterPosition, TextView subjectView) {
638                 mAdapterPosition = adapterPosition;
639                 mSubjectView = subjectView;
640             }
doInBackground(Void... params)641             @Override protected String doInBackground(Void... params) {
642                 String alias = mAliases.get(mAdapterPosition);
643                 X509Certificate cert = loadCertificate(mKeyStore, alias);
644                 if (cert == null) {
645                     return null;
646                 }
647                 // bouncycastle can handle the emailAddress OID of 1.2.840.113549.1.9.1
648                 X500Principal subjectPrincipal = cert.getSubjectX500Principal();
649                 X509Name subjectName = X509Name.getInstance(subjectPrincipal.getEncoded());
650                 return subjectName.toString(true, X509Name.DefaultSymbols);
651             }
onPostExecute(String subjectString)652             @Override protected void onPostExecute(String subjectString) {
653                 mSubjects.set(mAdapterPosition, subjectString);
654                 mSubjectView.setText(subjectString);
655             }
656         }
657     }
658 
659     private static class ViewHolder {
660         TextView mAliasTextView;
661         TextView mSubjectTextView;
662         RadioButton mRadioButton;
663     }
664 
finish(String alias)665     private void finish(String alias) {
666         finish(alias, false);
667     }
668 
finishWithAliasFromPolicy(String alias)669     private void finishWithAliasFromPolicy(String alias) {
670         finish(alias, true);
671     }
672 
finish(String alias, boolean isAliasFromPolicy)673     private void finish(String alias, boolean isAliasFromPolicy) {
674         if (alias == null || alias.equals(KeyChain.KEY_ALIAS_SELECTION_DENIED)) {
675             alias = null;
676             setResult(RESULT_CANCELED);
677         } else {
678             Intent result = new Intent();
679             result.putExtra(Intent.EXTRA_TEXT, alias);
680             setResult(RESULT_OK, result);
681         }
682         IKeyChainAliasCallback keyChainAliasResponse
683                 = IKeyChainAliasCallback.Stub.asInterface(
684                         getIntent().getIBinderExtra(KeyChain.EXTRA_RESPONSE));
685         if (keyChainAliasResponse != null) {
686             new ResponseSender(keyChainAliasResponse, alias, isAliasFromPolicy).execute();
687             return;
688         }
689         finishActivity();
690     }
691 
692     private class ResponseSender extends AsyncTask<Void, Void, Void> {
693         private IKeyChainAliasCallback mKeyChainAliasResponse;
694         private String mAlias;
695         private boolean mFromPolicy;
696 
ResponseSender(IKeyChainAliasCallback keyChainAliasResponse, String alias, boolean isFromPolicy)697         private ResponseSender(IKeyChainAliasCallback keyChainAliasResponse, String alias,
698                 boolean isFromPolicy) {
699             mKeyChainAliasResponse = keyChainAliasResponse;
700             mAlias = alias;
701             mFromPolicy = isFromPolicy;
702         }
doInBackground(Void... unused)703         @Override protected Void doInBackground(Void... unused) {
704             try {
705                 if (mAlias != null) {
706                     KeyChain.KeyChainConnection connection = KeyChain.bind(KeyChainActivity.this);
707                     try {
708                         // This is a safety check to make sure an alias was not somehow chosen by
709                         // the user but is not user-selectable.
710                         // However, if the alias was selected by the Device Owner / Profile Owner
711                         // (by implementing DeviceAdminReceiver), then there's no need to check
712                         // this.
713                         if (!mFromPolicy && (!connection.getService().isUserSelectable(mAlias))) {
714                             Log.w(TAG, String.format("Alias %s not user-selectable.", mAlias));
715                             //TODO: Should we invoke the callback with null here to indicate error?
716                             return null;
717                         }
718                         connection.getService().setGrant(mSenderUid, mAlias, true);
719                     } finally {
720                         connection.close();
721                     }
722                 }
723                 mKeyChainAliasResponse.alias(mAlias);
724             } catch (InterruptedException ignored) {
725                 Thread.currentThread().interrupt();
726                 Log.d(TAG, "interrupted while granting access", ignored);
727             } catch (Exception ignored) {
728                 // don't just catch RemoteException, caller could
729                 // throw back a RuntimeException across processes
730                 // which we should protect against.
731                 Log.e(TAG, "error while granting access", ignored);
732             }
733             return null;
734         }
onPostExecute(Void unused)735         @Override protected void onPostExecute(Void unused) {
736             finishActivity();
737         }
738     }
739 
finishActivity()740     private void finishActivity() {
741         long timeElapsedSinceFirstIntent =
742                 System.currentTimeMillis() - mFirstIntentReceivedTimeMillis;
743         if (mFirstIntentReceivedTimeMillis == 0L
744                 || timeElapsedSinceFirstIntent > SNACKBAR_MIN_TIME) {
745             finishSnackBar();
746             finish();
747         } else {
748             long remainingTimeToShowSnackBar = SNACKBAR_MIN_TIME - timeElapsedSinceFirstIntent;
749             handler.postDelayed(mFinishActivity, remainingTimeToShowSnackBar);
750         }
751     }
752 
onBackPressed()753     @Override public void onBackPressed() {
754         finish(null);
755     }
756 
loadCertificate(KeyStore keyStore, String alias)757     private static X509Certificate loadCertificate(KeyStore keyStore, String alias) {
758         final Certificate cert;
759         try {
760             if (keyStore.isCertificateEntry(alias)) {
761                 return null;
762             }
763             cert = keyStore.getCertificate(alias);
764         } catch (KeyStoreException e) {
765             Log.e(TAG, String.format("Error trying to retrieve certificate for \"%s\".", alias), e);
766             return null;
767         }
768         if (cert != null) {
769             if (cert instanceof X509Certificate) {
770                 return (X509Certificate) cert;
771             } else {
772                 Log.w(TAG, String.format("Certificate associated with alias \"%s\" is not X509.",
773                         alias));
774             }
775         }
776         return null;
777     }
778 
loadCertificateChain(KeyStore keyStore, String alias)779     private static List<X509Certificate> loadCertificateChain(KeyStore keyStore,
780             String alias) {
781         final Certificate[] certs;
782         final boolean isCertificateEntry;
783         try {
784             isCertificateEntry = keyStore.isCertificateEntry(alias);
785             certs = keyStore.getCertificateChain(alias);
786         } catch (KeyStoreException e) {
787             Log.e(TAG, String.format("Error trying to retrieve certificate chain for \"%s\".",
788                     alias), e);
789             return Collections.emptyList();
790         }
791         final List<X509Certificate> result = new ArrayList<>();
792         // If this is a certificate entry we return the single certificate. Otherwise we trim the
793         // leaf and return only the rest of the chain.
794         for (int i = isCertificateEntry ? 0 : 1; i < certs.length; ++i) {
795             if (certs[i] instanceof X509Certificate) {
796                 result.add((X509Certificate) certs[i]);
797             } else {
798                 Log.w(TAG,"A certificate in the chain of alias \""
799                         + alias + "\" is not X509.");
800                 return Collections.emptyList();
801             }
802         }
803         return result;
804     }
805 }
806