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