Home | History | Annotate | Download | only in settings
      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
     67     public int getMetricsCategory() {
     68         return MetricsEvent.USER_CREDENTIALS;
     69     }
     70 
     71     @Override
     72     public void onResume() {
     73         super.onResume();
     74         refreshItems();
     75     }
     76 
     77     @Override
     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
     86     public void onCreate(@Nullable Bundle savedInstanceState) {
     87         super.onCreate(savedInstanceState);
     88         getActivity().setTitle(R.string.user_credentials);
     89     }
     90 
     91     protected void announceRemoval(String alias) {
     92         if (!isAdded()) {
     93             return;
     94         }
     95         getListView().announceForAccessibility(getString(R.string.user_credential_removed, alias));
     96     }
     97 
     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 
    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
    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
    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 
    177             public RemoveCredentialsTask(Context context, Fragment targetFragment) {
    178                 this.context = context;
    179                 this.targetFragment = targetFragment;
    180             }
    181 
    182             @Override
    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 
    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
    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
    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 
    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 
    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
    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 
    340         public CredentialAdapter(List<Credential> items, @Nullable View.OnClickListener listener) {
    341             mItems = items;
    342             mListener = listener;
    343         }
    344 
    345         @Override
    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
    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
    359         public int getItemCount() {
    360             return mItems.size();
    361         }
    362     }
    363 
    364     private static class ViewHolder extends RecyclerView.ViewHolder {
    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 {
    375         credentialViewTypes.put(R.id.contents_userkey, Credential.Type.USER_KEY);
    376         credentialViewTypes.put(R.id.contents_usercrt, Credential.Type.USER_CERTIFICATE);
    377         credentialViewTypes.put(R.id.contents_cacrt, Credential.Type.CA_CERTIFICATE);
    378     }
    379 
    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 
    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 
    442         Credential(final String alias, final int uid) {
    443             this.alias = alias;
    444             this.uid = uid;
    445         }
    446 
    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 
    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 
    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 
    484         public boolean isSystem() {
    485             return UserHandle.getAppId(uid) == Process.SYSTEM_UID;
    486         }
    487     }
    488 }
    489