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