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