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