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.annotation.Nullable;
21 import android.app.Dialog;
22 import android.app.settings.SettingsEnums;
23 import android.content.Context;
24 import android.content.DialogInterface;
25 import android.os.AsyncTask;
26 import android.os.Bundle;
27 import android.os.Parcel;
28 import android.os.Parcelable;
29 import android.os.Process;
30 import android.os.RemoteException;
31 import android.os.UserHandle;
32 import android.os.UserManager;
33 import android.security.Credentials;
34 import android.security.IKeyChainService;
35 import android.security.KeyChain;
36 import android.security.KeyChain.KeyChainConnection;
37 import android.security.KeyStore;
38 import android.security.keymaster.KeyCharacteristics;
39 import android.security.keymaster.KeymasterDefs;
40 import android.util.Log;
41 import android.util.SparseArray;
42 import android.view.LayoutInflater;
43 import android.view.View;
44 import android.view.ViewGroup;
45 import android.widget.TextView;
46 
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.internal.widget.LockPatternUtils;
53 import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
54 import com.android.settingslib.RestrictedLockUtils;
55 import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
56 import com.android.settingslib.RestrictedLockUtilsInternal;
57 
58 import java.security.UnrecoverableKeyException;
59 import java.util.ArrayList;
60 import java.util.EnumSet;
61 import java.util.List;
62 import java.util.SortedMap;
63 import java.util.TreeMap;
64 
65 public class UserCredentialsSettings extends SettingsPreferenceFragment
66         implements View.OnClickListener {
67     private static final String TAG = "UserCredentialsSettings";
68 
69     @Override
getMetricsCategory()70     public int getMetricsCategory() {
71         return SettingsEnums.USER_CREDENTIALS;
72     }
73 
74     @Override
onResume()75     public void onResume() {
76         super.onResume();
77         refreshItems();
78     }
79 
80     @Override
onClick(final View view)81     public void onClick(final View view) {
82         final Credential item = (Credential) view.getTag();
83         if (item != null) {
84             CredentialDialogFragment.show(this, item);
85         }
86     }
87 
88     @Override
onCreate(@ullable Bundle savedInstanceState)89     public void onCreate(@Nullable Bundle savedInstanceState) {
90         super.onCreate(savedInstanceState);
91         getActivity().setTitle(R.string.user_credentials);
92     }
93 
announceRemoval(String alias)94     protected void announceRemoval(String alias) {
95         if (!isAdded()) {
96             return;
97         }
98         getListView().announceForAccessibility(getString(R.string.user_credential_removed, alias));
99     }
100 
refreshItems()101     protected void refreshItems() {
102         if (isAdded()) {
103             new AliasLoader().execute();
104         }
105     }
106 
107     public static class CredentialDialogFragment extends InstrumentedDialogFragment {
108         private static final String TAG = "CredentialDialogFragment";
109         private static final String ARG_CREDENTIAL = "credential";
110 
show(Fragment target, Credential item)111         public static void show(Fragment target, Credential item) {
112             final Bundle args = new Bundle();
113             args.putParcelable(ARG_CREDENTIAL, item);
114 
115             if (target.getFragmentManager().findFragmentByTag(TAG) == null) {
116                 final DialogFragment frag = new CredentialDialogFragment();
117                 frag.setTargetFragment(target, /* requestCode */ -1);
118                 frag.setArguments(args);
119                 frag.show(target.getFragmentManager(), TAG);
120             }
121         }
122 
123         @Override
onCreateDialog(Bundle savedInstanceState)124         public Dialog onCreateDialog(Bundle savedInstanceState) {
125             final Credential item = (Credential) getArguments().getParcelable(ARG_CREDENTIAL);
126 
127             View root = getActivity().getLayoutInflater()
128                     .inflate(R.layout.user_credential_dialog, null);
129             ViewGroup infoContainer = (ViewGroup) root.findViewById(R.id.credential_container);
130             View contentView = getCredentialView(item, R.layout.user_credential, null,
131                     infoContainer, /* expanded */ true);
132             infoContainer.addView(contentView);
133 
134             AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
135                     .setView(root)
136                     .setTitle(R.string.user_credential_title)
137                     .setPositiveButton(R.string.done, null);
138 
139             final String restriction = UserManager.DISALLOW_CONFIG_CREDENTIALS;
140             final int myUserId = UserHandle.myUserId();
141             if (!RestrictedLockUtilsInternal.hasBaseUserRestriction(getContext(), restriction,
142                     myUserId)) {
143                 DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
144                     @Override public void onClick(DialogInterface dialog, int id) {
145                         final EnforcedAdmin admin = RestrictedLockUtilsInternal
146                                 .checkIfRestrictionEnforced(getContext(), restriction, myUserId);
147                         if (admin != null) {
148                             RestrictedLockUtils.sendShowAdminSupportDetailsIntent(getContext(),
149                                     admin);
150                         } else {
151                             new RemoveCredentialsTask(getContext(), getTargetFragment())
152                                     .execute(item);
153                         }
154                         dialog.dismiss();
155                     }
156                 };
157                 // TODO: b/127865361
158                 //       a safe means of clearing wifi certificates. Configs refer to aliases
159                 //       directly so deleting certs will break dependent access points.
160                 //       However, Wi-Fi used to remove this certificate from storage if the network
161                 //       was removed, regardless if it is used in more than one network.
162                 //       It has been decided to allow removing certificates from this menu, as we
163                 //       assume that the user who manually adds certificates must have a way to
164                 //       manually remove them.
165                 builder.setNegativeButton(R.string.trusted_credentials_remove_label, listener);
166             }
167             return builder.create();
168         }
169 
170         @Override
getMetricsCategory()171         public int getMetricsCategory() {
172             return SettingsEnums.DIALOG_USER_CREDENTIAL;
173         }
174 
175         /**
176          * Deletes all certificates and keys under a given alias.
177          *
178          * If the {@link Credential} is for a system alias, all active grants to the alias will be
179          * removed using {@link KeyChain}. If the {@link Credential} is for Wi-Fi alias, all
180          * credentials and keys will be removed using {@link KeyStore}.
181          */
182         private class RemoveCredentialsTask extends AsyncTask<Credential, Void, Credential[]> {
183             private Context context;
184             private Fragment targetFragment;
185 
RemoveCredentialsTask(Context context, Fragment targetFragment)186             public RemoveCredentialsTask(Context context, Fragment targetFragment) {
187                 this.context = context;
188                 this.targetFragment = targetFragment;
189             }
190 
191             @Override
doInBackground(Credential... credentials)192             protected Credential[] doInBackground(Credential... credentials) {
193                 for (final Credential credential : credentials) {
194                     if (credential.isSystem()) {
195                         removeGrantsAndDelete(credential);
196                     } else {
197                         deleteWifiCredential(credential);
198                     }
199                 }
200                 return credentials;
201             }
202 
deleteWifiCredential(final Credential credential)203             private void deleteWifiCredential(final Credential credential) {
204                 final KeyStore keyStore = KeyStore.getInstance();
205                 final EnumSet<Credential.Type> storedTypes = credential.getStoredTypes();
206 
207                 // Remove all Wi-Fi credentials
208                 if (storedTypes.contains(Credential.Type.USER_KEY)) {
209                     keyStore.delete(Credentials.USER_PRIVATE_KEY + credential.getAlias(),
210                             Process.WIFI_UID);
211                 }
212                 if (storedTypes.contains(Credential.Type.USER_CERTIFICATE)) {
213                     keyStore.delete(Credentials.USER_CERTIFICATE + credential.getAlias(),
214                             Process.WIFI_UID);
215                 }
216                 if (storedTypes.contains(Credential.Type.CA_CERTIFICATE)) {
217                     keyStore.delete(Credentials.CA_CERTIFICATE + credential.getAlias(),
218                             Process.WIFI_UID);
219                 }
220             }
221 
removeGrantsAndDelete(final Credential credential)222             private void removeGrantsAndDelete(final Credential credential) {
223                 final KeyChainConnection conn;
224                 try {
225                     conn = KeyChain.bind(getContext());
226                 } catch (InterruptedException e) {
227                     Log.w(TAG, "Connecting to KeyChain", e);
228                     return;
229                 }
230 
231                 try {
232                     IKeyChainService keyChain = conn.getService();
233                     keyChain.removeKeyPair(credential.alias);
234                 } catch (RemoteException e) {
235                     Log.w(TAG, "Removing credentials", e);
236                 } finally {
237                     conn.close();
238                 }
239             }
240 
241             @Override
onPostExecute(Credential... credentials)242             protected void onPostExecute(Credential... credentials) {
243                 if (targetFragment instanceof UserCredentialsSettings && targetFragment.isAdded()) {
244                     final UserCredentialsSettings target = (UserCredentialsSettings) targetFragment;
245                     for (final Credential credential : credentials) {
246                         target.announceRemoval(credential.alias);
247                     }
248                     target.refreshItems();
249                 }
250             }
251         }
252     }
253 
254     /**
255      * Opens a background connection to KeyStore to list user credentials.
256      * The credentials are stored in a {@link CredentialAdapter} attached to the main
257      * {@link ListView} in the fragment.
258      */
259     private class AliasLoader extends AsyncTask<Void, Void, List<Credential>> {
260         /**
261          * @return a list of credentials ordered:
262          * <ol>
263          *   <li>first by purpose;</li>
264          *   <li>then by alias.</li>
265          * </ol>
266          */
267         @Override
doInBackground(Void... params)268         protected List<Credential> doInBackground(Void... params) {
269             final KeyStore keyStore = KeyStore.getInstance();
270 
271             // Certificates can be installed into SYSTEM_UID or WIFI_UID through CertInstaller.
272             final int myUserId = UserHandle.myUserId();
273             final int systemUid = UserHandle.getUid(myUserId, Process.SYSTEM_UID);
274             final int wifiUid = UserHandle.getUid(myUserId, Process.WIFI_UID);
275 
276             List<Credential> credentials = new ArrayList<>();
277             credentials.addAll(getCredentialsForUid(keyStore, systemUid).values());
278             credentials.addAll(getCredentialsForUid(keyStore, wifiUid).values());
279             return credentials;
280         }
281 
isAsymmetric(KeyStore keyStore, String alias, int uid)282         private boolean isAsymmetric(KeyStore keyStore, String alias, int uid)
283             throws UnrecoverableKeyException {
284                 KeyCharacteristics keyCharacteristics = new KeyCharacteristics();
285                 int errorCode = keyStore.getKeyCharacteristics(alias, null, null, uid,
286                         keyCharacteristics);
287                 if (errorCode != KeyStore.NO_ERROR) {
288                     throw (UnrecoverableKeyException)
289                             new UnrecoverableKeyException("Failed to obtain information about key")
290                                     .initCause(KeyStore.getKeyStoreException(errorCode));
291                 }
292                 Integer keymasterAlgorithm = keyCharacteristics.getEnum(
293                         KeymasterDefs.KM_TAG_ALGORITHM);
294                 if (keymasterAlgorithm == null) {
295                     throw new UnrecoverableKeyException("Key algorithm unknown");
296                 }
297                 return keymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_RSA ||
298                         keymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_EC;
299         }
300 
getCredentialsForUid(KeyStore keyStore, int uid)301         private SortedMap<String, Credential> getCredentialsForUid(KeyStore keyStore, int uid) {
302             final SortedMap<String, Credential> aliasMap = new TreeMap<>();
303             for (final Credential.Type type : Credential.Type.values()) {
304                 for (final String prefix : type.prefix) {
305                     for (final String alias : keyStore.list(prefix, uid)) {
306                         if (UserHandle.getAppId(uid) == Process.SYSTEM_UID) {
307                             // Do not show work profile keys in user credentials
308                             if (alias.startsWith(LockPatternUtils.PROFILE_KEY_NAME_ENCRYPT) ||
309                                     alias.startsWith(LockPatternUtils.PROFILE_KEY_NAME_DECRYPT)) {
310                                 continue;
311                             }
312                             // Do not show synthetic password keys in user credential
313                             if (alias.startsWith(LockPatternUtils.SYNTHETIC_PASSWORD_KEY_PREFIX)) {
314                                 continue;
315                             }
316                         }
317                         try {
318                             if (type == Credential.Type.USER_KEY &&
319                                     !isAsymmetric(keyStore, prefix + alias, uid)) {
320                                 continue;
321                             }
322                         } catch (UnrecoverableKeyException e) {
323                             Log.e(TAG, "Unable to determine algorithm of key: " + prefix + alias, e);
324                             continue;
325                         }
326                         Credential c = aliasMap.get(alias);
327                         if (c == null) {
328                             c = new Credential(alias, uid);
329                             aliasMap.put(alias, c);
330                         }
331                         c.storedTypes.add(type);
332                     }
333                 }
334             }
335             return aliasMap;
336         }
337 
338         @Override
onPostExecute(List<Credential> credentials)339         protected void onPostExecute(List<Credential> credentials) {
340             if (!isAdded()) {
341                 return;
342             }
343 
344             if (credentials == null || credentials.size() == 0) {
345                 // Create a "no credentials installed" message for the empty case.
346                 TextView emptyTextView = (TextView) getActivity().findViewById(android.R.id.empty);
347                 emptyTextView.setText(R.string.user_credential_none_installed);
348                 setEmptyView(emptyTextView);
349             } else {
350                 setEmptyView(null);
351             }
352 
353             getListView().setAdapter(
354                     new CredentialAdapter(credentials, UserCredentialsSettings.this));
355         }
356     }
357 
358     /**
359      * Helper class to display {@link Credential}s in a list.
360      */
361     private static class CredentialAdapter extends RecyclerView.Adapter<ViewHolder> {
362         private static final int LAYOUT_RESOURCE = R.layout.user_credential_preference;
363 
364         private final List<Credential> mItems;
365         private final View.OnClickListener mListener;
366 
CredentialAdapter(List<Credential> items, @Nullable View.OnClickListener listener)367         public CredentialAdapter(List<Credential> items, @Nullable View.OnClickListener listener) {
368             mItems = items;
369             mListener = listener;
370         }
371 
372         @Override
onCreateViewHolder(ViewGroup parent, int viewType)373         public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
374             final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
375             return new ViewHolder(inflater.inflate(LAYOUT_RESOURCE, parent, false));
376         }
377 
378         @Override
onBindViewHolder(ViewHolder h, int position)379         public void onBindViewHolder(ViewHolder h, int position) {
380             getCredentialView(mItems.get(position), LAYOUT_RESOURCE, h.itemView, null, false);
381             h.itemView.setTag(mItems.get(position));
382             h.itemView.setOnClickListener(mListener);
383         }
384 
385         @Override
getItemCount()386         public int getItemCount() {
387             return mItems.size();
388         }
389     }
390 
391     private static class ViewHolder extends RecyclerView.ViewHolder {
ViewHolder(View item)392         public ViewHolder(View item) {
393             super(item);
394         }
395     }
396 
397     /**
398      * Mapping from View IDs in {@link R} to the types of credentials they describe.
399      */
400     private static final SparseArray<Credential.Type> credentialViewTypes = new SparseArray<>();
401     static {
credentialViewTypes.put(R.id.contents_userkey, Credential.Type.USER_KEY)402         credentialViewTypes.put(R.id.contents_userkey, Credential.Type.USER_KEY);
credentialViewTypes.put(R.id.contents_usercrt, Credential.Type.USER_CERTIFICATE)403         credentialViewTypes.put(R.id.contents_usercrt, Credential.Type.USER_CERTIFICATE);
credentialViewTypes.put(R.id.contents_cacrt, Credential.Type.CA_CERTIFICATE)404         credentialViewTypes.put(R.id.contents_cacrt, Credential.Type.CA_CERTIFICATE);
405     }
406 
getCredentialView(Credential item, @LayoutRes int layoutResource, @Nullable View view, ViewGroup parent, boolean expanded)407     protected static View getCredentialView(Credential item, @LayoutRes int layoutResource,
408             @Nullable View view, ViewGroup parent, boolean expanded) {
409         if (view == null) {
410             view = LayoutInflater.from(parent.getContext()).inflate(layoutResource, parent, false);
411         }
412 
413         ((TextView) view.findViewById(R.id.alias)).setText(item.alias);
414         ((TextView) view.findViewById(R.id.purpose)).setText(item.isSystem()
415                 ? R.string.credential_for_vpn_and_apps
416                 : R.string.credential_for_wifi);
417 
418         view.findViewById(R.id.contents).setVisibility(expanded ? View.VISIBLE : View.GONE);
419         if (expanded) {
420             for (int i = 0; i < credentialViewTypes.size(); i++) {
421                 final View detail = view.findViewById(credentialViewTypes.keyAt(i));
422                 detail.setVisibility(item.storedTypes.contains(credentialViewTypes.valueAt(i))
423                         ? View.VISIBLE : View.GONE);
424             }
425         }
426         return view;
427     }
428 
429     static class AliasEntry {
430         public String alias;
431         public int uid;
432     }
433 
434     static class Credential implements Parcelable {
435         static enum Type {
436             CA_CERTIFICATE (Credentials.CA_CERTIFICATE),
437             USER_CERTIFICATE (Credentials.USER_CERTIFICATE),
438             USER_KEY(Credentials.USER_PRIVATE_KEY, Credentials.USER_SECRET_KEY);
439 
440             final String[] prefix;
441 
Type(String... prefix)442             Type(String... prefix) {
443                 this.prefix = prefix;
444             }
445         }
446 
447         /**
448          * Main part of the credential's alias. To fetch an item from KeyStore, prepend one of the
449          * prefixes from {@link CredentialItem.storedTypes}.
450          */
451         final String alias;
452 
453         /**
454          * UID under which this credential is stored. Typically {@link Process#SYSTEM_UID} but can
455          * also be {@link Process#WIFI_UID} for credentials installed as wifi certificates.
456          */
457         final int uid;
458 
459         /**
460          * Should contain some non-empty subset of:
461          * <ul>
462          *   <li>{@link Credentials.CA_CERTIFICATE}</li>
463          *   <li>{@link Credentials.USER_CERTIFICATE}</li>
464          *   <li>{@link Credentials.USER_KEY}</li>
465          * </ul>
466          */
467         final EnumSet<Type> storedTypes = EnumSet.noneOf(Type.class);
468 
Credential(final String alias, final int uid)469         Credential(final String alias, final int uid) {
470             this.alias = alias;
471             this.uid = uid;
472         }
473 
Credential(Parcel in)474         Credential(Parcel in) {
475             this(in.readString(), in.readInt());
476 
477             long typeBits = in.readLong();
478             for (Type i : Type.values()) {
479                 if ((typeBits & (1L << i.ordinal())) != 0L) {
480                     storedTypes.add(i);
481                 }
482             }
483         }
484 
writeToParcel(Parcel out, int flags)485         public void writeToParcel(Parcel out, int flags) {
486             out.writeString(alias);
487             out.writeInt(uid);
488 
489             long typeBits = 0;
490             for (Type i : storedTypes) {
491                 typeBits |= 1L << i.ordinal();
492             }
493             out.writeLong(typeBits);
494         }
495 
describeContents()496         public int describeContents() {
497             return 0;
498         }
499 
500         public static final Parcelable.Creator<Credential> CREATOR
501                 = new Parcelable.Creator<Credential>() {
502             public Credential createFromParcel(Parcel in) {
503                 return new Credential(in);
504             }
505 
506             public Credential[] newArray(int size) {
507                 return new Credential[size];
508             }
509         };
510 
isSystem()511         public boolean isSystem() {
512             return UserHandle.getAppId(uid) == Process.SYSTEM_UID;
513         }
514 
getAlias()515         public String getAlias() { return alias; }
516 
getStoredTypes()517         public EnumSet<Type> getStoredTypes() {
518             return storedTypes;
519         }
520     }
521 }
522