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