1 /*
2  * Copyright (C) 2015 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.settings;
18 
19 import android.annotation.LayoutRes;
20 import android.app.Dialog;
21 import android.app.settings.SettingsEnums;
22 import android.content.Context;
23 import android.content.DialogInterface;
24 import android.os.AsyncTask;
25 import android.os.Bundle;
26 import android.os.Parcel;
27 import android.os.Parcelable;
28 import android.os.Process;
29 import android.os.RemoteException;
30 import android.os.UserHandle;
31 import android.os.UserManager;
32 import android.security.Credentials;
33 import android.security.IKeyChainService;
34 import android.security.KeyChain;
35 import android.security.KeyChain.KeyChainConnection;
36 import android.security.keystore.KeyProperties;
37 import android.security.keystore2.AndroidKeyStoreLoadStoreParameter;
38 import android.util.Log;
39 import android.util.SparseArray;
40 import android.view.LayoutInflater;
41 import android.view.View;
42 import android.view.ViewGroup;
43 import android.widget.TextView;
44 
45 import androidx.annotation.Nullable;
46 import androidx.annotation.VisibleForTesting;
47 import androidx.appcompat.app.AlertDialog;
48 import androidx.fragment.app.DialogFragment;
49 import androidx.fragment.app.Fragment;
50 import androidx.recyclerview.widget.RecyclerView;
51 
52 import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
53 import com.android.settings.wifi.helper.SavedWifiHelper;
54 import com.android.settingslib.RestrictedLockUtils;
55 import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
56 import com.android.settingslib.RestrictedLockUtilsInternal;
57 
58 import java.security.Key;
59 import java.security.KeyStore;
60 import java.security.KeyStoreException;
61 import java.security.NoSuchAlgorithmException;
62 import java.security.UnrecoverableKeyException;
63 import java.security.cert.Certificate;
64 import java.util.ArrayList;
65 import java.util.EnumSet;
66 import java.util.Enumeration;
67 import java.util.List;
68 import java.util.SortedMap;
69 import java.util.TreeMap;
70 
71 import javax.crypto.SecretKey;
72 
73 public class UserCredentialsSettings extends SettingsPreferenceFragment
74         implements View.OnClickListener {
75     private static final String TAG = "UserCredentialsSettings";
76 
77     private static final String KEYSTORE_PROVIDER = "AndroidKeyStore";
78 
79     @VisibleForTesting
80     protected SavedWifiHelper mSavedWifiHelper;
81 
82     @Override
getMetricsCategory()83     public int getMetricsCategory() {
84         return SettingsEnums.USER_CREDENTIALS;
85     }
86 
87     @Override
onResume()88     public void onResume() {
89         super.onResume();
90         refreshItems();
91     }
92 
93     @Override
onClick(final View view)94     public void onClick(final View view) {
95         final Credential item = (Credential) view.getTag();
96         if (item == null) return;
97         if (item.isInUse()) {
98             item.setUsedByNames(mSavedWifiHelper.getCertificateNetworkNames(item.alias));
99         }
100         showCredentialDialogFragment(item);
101     }
102 
103     @Override
onCreate(@ullable Bundle savedInstanceState)104     public void onCreate(@Nullable Bundle savedInstanceState) {
105         super.onCreate(savedInstanceState);
106         getActivity().setTitle(R.string.user_credentials);
107         mSavedWifiHelper = SavedWifiHelper.getInstance(getContext(), getSettingsLifecycle());
108     }
109 
110     @VisibleForTesting
showCredentialDialogFragment(Credential item)111     protected void showCredentialDialogFragment(Credential item) {
112         CredentialDialogFragment.show(this, item);
113     }
114 
announceRemoval(String alias)115     protected void announceRemoval(String alias) {
116         if (!isAdded()) {
117             return;
118         }
119         getListView().announceForAccessibility(getString(R.string.user_credential_removed, alias));
120     }
121 
refreshItems()122     protected void refreshItems() {
123         if (isAdded()) {
124             new AliasLoader().execute();
125         }
126     }
127 
128     /** The fragment to show the credential information. */
129     public static class CredentialDialogFragment extends InstrumentedDialogFragment
130             implements DialogInterface.OnShowListener {
131         private static final String TAG = "CredentialDialogFragment";
132         private static final String ARG_CREDENTIAL = "credential";
133 
show(Fragment target, Credential item)134         public static void show(Fragment target, Credential item) {
135             final Bundle args = new Bundle();
136             args.putParcelable(ARG_CREDENTIAL, item);
137 
138             if (target.getFragmentManager().findFragmentByTag(TAG) == null) {
139                 final DialogFragment frag = new CredentialDialogFragment();
140                 frag.setTargetFragment(target, /* requestCode */ -1);
141                 frag.setArguments(args);
142                 frag.show(target.getFragmentManager(), TAG);
143             }
144         }
145 
146         @Override
onCreateDialog(Bundle savedInstanceState)147         public Dialog onCreateDialog(Bundle savedInstanceState) {
148             final Credential item = (Credential) getArguments().getParcelable(ARG_CREDENTIAL);
149 
150             View root = getActivity().getLayoutInflater()
151                     .inflate(R.layout.user_credential_dialog, null);
152             ViewGroup infoContainer = (ViewGroup) root.findViewById(R.id.credential_container);
153             View contentView = getCredentialView(item, R.layout.user_credential, null,
154                     infoContainer, /* expanded */ true);
155             infoContainer.addView(contentView);
156 
157             AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
158                     .setView(root)
159                     .setTitle(R.string.user_credential_title)
160                     .setPositiveButton(R.string.done, null);
161 
162             final String restriction = UserManager.DISALLOW_CONFIG_CREDENTIALS;
163             final int myUserId = UserHandle.myUserId();
164             if (!RestrictedLockUtilsInternal.hasBaseUserRestriction(getContext(), restriction,
165                     myUserId)) {
166                 DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
167                     @Override public void onClick(DialogInterface dialog, int id) {
168                         final EnforcedAdmin admin = RestrictedLockUtilsInternal
169                                 .checkIfRestrictionEnforced(getContext(), restriction, myUserId);
170                         if (admin != null) {
171                             RestrictedLockUtils.sendShowAdminSupportDetailsIntent(getContext(),
172                                     admin);
173                         } else {
174                             new RemoveCredentialsTask(getContext(), getTargetFragment())
175                                     .execute(item);
176                         }
177                         dialog.dismiss();
178                     }
179                 };
180                 builder.setNegativeButton(R.string.trusted_credentials_remove_label, listener);
181             }
182             AlertDialog dialog = builder.create();
183             dialog.setOnShowListener(this);
184             return dialog;
185         }
186 
187         /**
188          * Override for the negative button enablement on demand.
189          */
190         @Override
onShow(DialogInterface dialogInterface)191         public void onShow(DialogInterface dialogInterface) {
192             final Credential item = (Credential) getArguments().getParcelable(ARG_CREDENTIAL);
193             if (item.isInUse()) {
194                 ((AlertDialog) getDialog()).getButton(AlertDialog.BUTTON_NEGATIVE)
195                         .setEnabled(false);
196             }
197         }
198 
199         @Override
getMetricsCategory()200         public int getMetricsCategory() {
201             return SettingsEnums.DIALOG_USER_CREDENTIAL;
202         }
203 
204         /**
205          * Deletes all certificates and keys under a given alias.
206          *
207          * If the {@link Credential} is for a system alias, all active grants to the alias will be
208          * removed using {@link KeyChain}. If the {@link Credential} is for Wi-Fi alias, all
209          * credentials and keys will be removed using {@link KeyStore}.
210          */
211         private class RemoveCredentialsTask extends AsyncTask<Credential, Void, Credential[]> {
212             private Context context;
213             private Fragment targetFragment;
214 
RemoveCredentialsTask(Context context, Fragment targetFragment)215             public RemoveCredentialsTask(Context context, Fragment targetFragment) {
216                 this.context = context;
217                 this.targetFragment = targetFragment;
218             }
219 
220             @Override
doInBackground(Credential... credentials)221             protected Credential[] doInBackground(Credential... credentials) {
222                 for (final Credential credential : credentials) {
223                     if (credential.isSystem()) {
224                         removeGrantsAndDelete(credential);
225                     } else {
226                         deleteWifiCredential(credential);
227                     }
228                 }
229                 return credentials;
230             }
231 
deleteWifiCredential(final Credential credential)232             private void deleteWifiCredential(final Credential credential) {
233                 try {
234                     final KeyStore keyStore = KeyStore.getInstance(KEYSTORE_PROVIDER);
235                     keyStore.load(
236                             new AndroidKeyStoreLoadStoreParameter(
237                                     KeyProperties.NAMESPACE_WIFI));
238                     keyStore.deleteEntry(credential.getAlias());
239                 } catch (Exception e) {
240                     throw new RuntimeException("Failed to delete keys from keystore.");
241                 }
242             }
243 
removeGrantsAndDelete(final Credential credential)244             private void removeGrantsAndDelete(final Credential credential) {
245                 final KeyChainConnection conn;
246                 try {
247                     conn = KeyChain.bind(getContext());
248                 } catch (InterruptedException e) {
249                     Log.w(TAG, "Connecting to KeyChain", e);
250                     return;
251                 }
252 
253                 try {
254                     IKeyChainService keyChain = conn.getService();
255                     keyChain.removeKeyPair(credential.alias);
256                 } catch (RemoteException e) {
257                     Log.w(TAG, "Removing credentials", e);
258                 } finally {
259                     conn.close();
260                 }
261             }
262 
263             @Override
onPostExecute(Credential... credentials)264             protected void onPostExecute(Credential... credentials) {
265                 if (targetFragment instanceof UserCredentialsSettings && targetFragment.isAdded()) {
266                     final UserCredentialsSettings target = (UserCredentialsSettings) targetFragment;
267                     for (final Credential credential : credentials) {
268                         target.announceRemoval(credential.alias);
269                     }
270                     target.refreshItems();
271                 }
272             }
273         }
274     }
275 
276     /**
277      * Opens a background connection to KeyStore to list user credentials.
278      * The credentials are stored in a {@link CredentialAdapter} attached to the main
279      * {@link ListView} in the fragment.
280      */
281     private class AliasLoader extends AsyncTask<Void, Void, List<Credential>> {
282         /**
283          * @return a list of credentials ordered:
284          * <ol>
285          *   <li>first by purpose;</li>
286          *   <li>then by alias.</li>
287          * </ol>
288          */
289         @Override
doInBackground(Void... params)290         protected List<Credential> doInBackground(Void... params) {
291             // Certificates can be installed into SYSTEM_UID or WIFI_UID through CertInstaller.
292             final int myUserId = UserHandle.myUserId();
293             final int systemUid = UserHandle.getUid(myUserId, Process.SYSTEM_UID);
294             final int wifiUid = UserHandle.getUid(myUserId, Process.WIFI_UID);
295 
296             try {
297                 KeyStore processKeystore = KeyStore.getInstance(KEYSTORE_PROVIDER);
298                 processKeystore.load(null);
299                 KeyStore wifiKeystore = null;
300                 if (myUserId == 0) {
301                     wifiKeystore = KeyStore.getInstance(KEYSTORE_PROVIDER);
302                     wifiKeystore.load(new AndroidKeyStoreLoadStoreParameter(
303                             KeyProperties.NAMESPACE_WIFI));
304                 }
305 
306                 List<Credential> credentials = new ArrayList<>();
307                 credentials.addAll(getCredentialsForUid(processKeystore, systemUid).values());
308                 if (wifiKeystore != null) {
309                     credentials.addAll(getCredentialsForUid(wifiKeystore, wifiUid).values());
310                 }
311                 return credentials;
312             } catch (Exception e) {
313                 throw new RuntimeException("Failed to load credentials from Keystore.", e);
314             }
315         }
316 
getCredentialsForUid(KeyStore keyStore, int uid)317         private SortedMap<String, Credential> getCredentialsForUid(KeyStore keyStore, int uid) {
318             try {
319                 final SortedMap<String, Credential> aliasMap = new TreeMap<>();
320                 Enumeration<String> aliases = keyStore.aliases();
321                 while (aliases.hasMoreElements()) {
322                     String alias = aliases.nextElement();
323                     Credential c = new Credential(alias, uid);
324                     if (!c.isSystem()) {
325                         c.setInUse(mSavedWifiHelper.isCertificateInUse(alias));
326                     }
327                     Key key = null;
328                     try {
329                         key = keyStore.getKey(alias, null);
330                     } catch (NoSuchAlgorithmException | UnrecoverableKeyException e) {
331                         Log.e(TAG, "Error tying to retrieve key: " + alias, e);
332                         continue;
333                     }
334                     if (key != null) {
335                         // So we have a key
336                         if (key instanceof SecretKey) {
337                             // We don't display any symmetric key entries.
338                             continue;
339                         }
340                         // At this point we have determined that we have an asymmetric key.
341                         // so we have at least a USER_KEY and USER_CERTIFICATE.
342                         c.storedTypes.add(Credential.Type.USER_KEY);
343 
344                         Certificate[] certs =  keyStore.getCertificateChain(alias);
345                         if (certs != null) {
346                             c.storedTypes.add(Credential.Type.USER_CERTIFICATE);
347                             if (certs.length > 1) {
348                                 c.storedTypes.add(Credential.Type.CA_CERTIFICATE);
349                             }
350                         }
351                     } else {
352                         // So there is no key but we have an alias. This must mean that we have
353                         // some certificate.
354                         if (keyStore.isCertificateEntry(alias)) {
355                             c.storedTypes.add(Credential.Type.CA_CERTIFICATE);
356                         } else {
357                             // This is a weired inconsistent case that should not exist.
358                             // Pure trusted certificate entries should be stored in CA_CERTIFICATE,
359                             // but if isCErtificateEntry returns null this means that only the
360                             // USER_CERTIFICATE is populated which should never be the case without
361                             // a private key. It can still be retrieved with
362                             // keystore.getCertificate().
363                             c.storedTypes.add(Credential.Type.USER_CERTIFICATE);
364                         }
365                     }
366                     aliasMap.put(alias, c);
367                 }
368                 return aliasMap;
369             } catch (KeyStoreException e) {
370                 throw new RuntimeException("Failed to load credential from Android Keystore.", e);
371             }
372         }
373 
374         @Override
onPostExecute(List<Credential> credentials)375         protected void onPostExecute(List<Credential> credentials) {
376             if (!isAdded()) {
377                 return;
378             }
379 
380             if (credentials == null || credentials.size() == 0) {
381                 // Create a "no credentials installed" message for the empty case.
382                 TextView emptyTextView = (TextView) getActivity().findViewById(android.R.id.empty);
383                 emptyTextView.setText(R.string.user_credential_none_installed);
384                 setEmptyView(emptyTextView);
385             } else {
386                 setEmptyView(null);
387             }
388 
389             getListView().setAdapter(
390                     new CredentialAdapter(credentials, UserCredentialsSettings.this));
391         }
392     }
393 
394     /**
395      * Helper class to display {@link Credential}s in a list.
396      */
397     private static class CredentialAdapter extends RecyclerView.Adapter<ViewHolder> {
398         private static final int LAYOUT_RESOURCE = R.layout.user_credential_preference;
399 
400         private final List<Credential> mItems;
401         private final View.OnClickListener mListener;
402 
CredentialAdapter(List<Credential> items, @Nullable View.OnClickListener listener)403         public CredentialAdapter(List<Credential> items, @Nullable View.OnClickListener listener) {
404             mItems = items;
405             mListener = listener;
406         }
407 
408         @Override
onCreateViewHolder(ViewGroup parent, int viewType)409         public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
410             final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
411             return new ViewHolder(inflater.inflate(LAYOUT_RESOURCE, parent, false));
412         }
413 
414         @Override
onBindViewHolder(ViewHolder h, int position)415         public void onBindViewHolder(ViewHolder h, int position) {
416             getCredentialView(mItems.get(position), LAYOUT_RESOURCE, h.itemView, null, false);
417             h.itemView.setTag(mItems.get(position));
418             h.itemView.setOnClickListener(mListener);
419         }
420 
421         @Override
getItemCount()422         public int getItemCount() {
423             return mItems.size();
424         }
425     }
426 
427     private static class ViewHolder extends RecyclerView.ViewHolder {
ViewHolder(View item)428         public ViewHolder(View item) {
429             super(item);
430         }
431     }
432 
433     /**
434      * Mapping from View IDs in {@link R} to the types of credentials they describe.
435      */
436     private static final SparseArray<Credential.Type> credentialViewTypes = new SparseArray<>();
437     static {
credentialViewTypes.put(R.id.contents_userkey, Credential.Type.USER_KEY)438         credentialViewTypes.put(R.id.contents_userkey, Credential.Type.USER_KEY);
credentialViewTypes.put(R.id.contents_usercrt, Credential.Type.USER_CERTIFICATE)439         credentialViewTypes.put(R.id.contents_usercrt, Credential.Type.USER_CERTIFICATE);
credentialViewTypes.put(R.id.contents_cacrt, Credential.Type.CA_CERTIFICATE)440         credentialViewTypes.put(R.id.contents_cacrt, Credential.Type.CA_CERTIFICATE);
441     }
442 
getCredentialView(Credential item, @LayoutRes int layoutResource, @Nullable View view, ViewGroup parent, boolean expanded)443     protected static View getCredentialView(Credential item, @LayoutRes int layoutResource,
444             @Nullable View view, ViewGroup parent, boolean expanded) {
445         if (view == null) {
446             view = LayoutInflater.from(parent.getContext()).inflate(layoutResource, parent, false);
447         }
448 
449         ((TextView) view.findViewById(R.id.alias)).setText(item.alias);
450         updatePurposeView(view.findViewById(R.id.purpose), item);
451 
452         view.findViewById(R.id.contents).setVisibility(expanded ? View.VISIBLE : View.GONE);
453         if (expanded) {
454             updateUsedByViews(view.findViewById(R.id.credential_being_used_by_title),
455                     view.findViewById(R.id.credential_being_used_by_content), item);
456 
457             for (int i = 0; i < credentialViewTypes.size(); i++) {
458                 final View detail = view.findViewById(credentialViewTypes.keyAt(i));
459                 detail.setVisibility(item.storedTypes.contains(credentialViewTypes.valueAt(i))
460                         ? View.VISIBLE : View.GONE);
461             }
462         }
463         return view;
464     }
465 
466     @VisibleForTesting
updatePurposeView(TextView purpose, Credential item)467     protected static void updatePurposeView(TextView purpose, Credential item) {
468         int subTextResId = R.string.credential_for_vpn_and_apps;
469         if (!item.isSystem()) {
470             subTextResId = (item.isInUse())
471                     ? R.string.credential_for_wifi_in_use
472                     : R.string.credential_for_wifi;
473         }
474         purpose.setText(subTextResId);
475     }
476 
477     @VisibleForTesting
updateUsedByViews(TextView title, TextView content, Credential item)478     protected static void updateUsedByViews(TextView title, TextView content, Credential item) {
479         List<String> usedByNames = item.getUsedByNames();
480         if (usedByNames.size() > 0) {
481             title.setVisibility(View.VISIBLE);
482             content.setText(String.join("\n", usedByNames));
483             content.setVisibility(View.VISIBLE);
484         } else {
485             title.setVisibility(View.GONE);
486             content.setVisibility(View.GONE);
487         }
488     }
489 
490     static class AliasEntry {
491         public String alias;
492         public int uid;
493     }
494 
495     static class Credential implements Parcelable {
496         static enum Type {
497             CA_CERTIFICATE (Credentials.CA_CERTIFICATE),
498             USER_CERTIFICATE (Credentials.USER_CERTIFICATE),
499             USER_KEY(Credentials.USER_PRIVATE_KEY, Credentials.USER_SECRET_KEY);
500 
501             final String[] prefix;
502 
Type(String... prefix)503             Type(String... prefix) {
504                 this.prefix = prefix;
505             }
506         }
507 
508         /**
509          * Main part of the credential's alias. To fetch an item from KeyStore, prepend one of the
510          * prefixes from {@link CredentialItem.storedTypes}.
511          */
512         final String alias;
513 
514         /**
515          * UID under which this credential is stored. Typically {@link Process#SYSTEM_UID} but can
516          * also be {@link Process#WIFI_UID} for credentials installed as wifi certificates.
517          */
518         final int uid;
519 
520         /**
521          * Indicate whether or not this credential is in use.
522          */
523         boolean mIsInUse;
524 
525         /**
526          * The list of networks which use this credential.
527          */
528         List<String> mUsedByNames = new ArrayList<>();
529 
530         /**
531          * Should contain some non-empty subset of:
532          * <ul>
533          *   <li>{@link Credentials.CA_CERTIFICATE}</li>
534          *   <li>{@link Credentials.USER_CERTIFICATE}</li>
535          *   <li>{@link Credentials.USER_KEY}</li>
536          * </ul>
537          */
538         final EnumSet<Type> storedTypes = EnumSet.noneOf(Type.class);
539 
Credential(final String alias, final int uid)540         Credential(final String alias, final int uid) {
541             this.alias = alias;
542             this.uid = uid;
543         }
544 
Credential(Parcel in)545         Credential(Parcel in) {
546             this(in.readString(), in.readInt());
547 
548             long typeBits = in.readLong();
549             for (Type i : Type.values()) {
550                 if ((typeBits & (1L << i.ordinal())) != 0L) {
551                     storedTypes.add(i);
552                 }
553             }
554         }
555 
writeToParcel(Parcel out, int flags)556         public void writeToParcel(Parcel out, int flags) {
557             out.writeString(alias);
558             out.writeInt(uid);
559 
560             long typeBits = 0;
561             for (Type i : storedTypes) {
562                 typeBits |= 1L << i.ordinal();
563             }
564             out.writeLong(typeBits);
565         }
566 
describeContents()567         public int describeContents() {
568             return 0;
569         }
570 
571         public static final Parcelable.Creator<Credential> CREATOR
572                 = new Parcelable.Creator<Credential>() {
573             public Credential createFromParcel(Parcel in) {
574                 return new Credential(in);
575             }
576 
577             public Credential[] newArray(int size) {
578                 return new Credential[size];
579             }
580         };
581 
isSystem()582         public boolean isSystem() {
583             return UserHandle.getAppId(uid) == Process.SYSTEM_UID;
584         }
585 
getAlias()586         public String getAlias() {
587             return alias;
588         }
589 
getStoredTypes()590         public EnumSet<Type> getStoredTypes() {
591             return storedTypes;
592         }
593 
setInUse(boolean inUse)594         public void setInUse(boolean inUse) {
595             mIsInUse = inUse;
596         }
597 
isInUse()598         public boolean isInUse() {
599             return mIsInUse;
600         }
601 
setUsedByNames(List<String> names)602         public void setUsedByNames(List<String> names) {
603             mUsedByNames = new ArrayList<>(names);
604         }
605 
getUsedByNames()606         public List<String> getUsedByNames() {
607             return new ArrayList<String>(mUsedByNames);
608         }
609     }
610 }
611