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