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