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