1 /* 2 * Copyright (C) 2011 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.app.Activity; 20 import android.app.AlertDialog; 21 import android.app.admin.DevicePolicyManager; 22 import android.content.Context; 23 import android.content.DialogInterface; 24 import android.content.Intent; 25 import android.content.pm.PackageManager; 26 import android.content.pm.UserInfo; 27 import android.content.res.Resources; 28 import android.os.AsyncTask; 29 import android.os.Bundle; 30 import android.os.Process; 31 import android.os.RemoteException; 32 import android.os.UserHandle; 33 import android.os.UserManager; 34 import android.security.Credentials; 35 import android.security.KeyChain; 36 import android.security.KeyChain.KeyChainConnection; 37 import android.security.KeyStore; 38 import android.text.Editable; 39 import android.text.TextUtils; 40 import android.text.TextWatcher; 41 import android.util.Log; 42 import android.view.View; 43 import android.widget.Button; 44 import android.widget.TextView; 45 import android.widget.Toast; 46 47 import com.android.internal.widget.LockPatternUtils; 48 import com.android.org.bouncycastle.asn1.ASN1InputStream; 49 import com.android.org.bouncycastle.asn1.pkcs.PrivateKeyInfo; 50 import com.android.settings.password.ChooseLockSettingsHelper; 51 import com.android.settings.security.ConfigureKeyGuardDialog; 52 import com.android.settings.vpn2.VpnUtils; 53 54 import java.io.ByteArrayInputStream; 55 import java.io.IOException; 56 57 import sun.security.util.ObjectIdentifier; 58 import sun.security.x509.AlgorithmId; 59 60 /** 61 * CredentialStorage handles KeyStore reset, unlock, and install. 62 * 63 * CredentialStorage has a pretty convoluted state machine to migrate 64 * from the old style separate keystore password to a new key guard 65 * based password, as well as to deal with setting up the key guard if 66 * necessary. 67 * 68 * KeyStore: UNINITALIZED 69 * KeyGuard: OFF 70 * Action: set up key guard 71 * Notes: factory state 72 * 73 * KeyStore: UNINITALIZED 74 * KeyGuard: ON 75 * Action: confirm key guard 76 * Notes: user had key guard but no keystore and upgraded from pre-ICS 77 * OR user had key guard and pre-ICS keystore password which was then reset 78 * 79 * KeyStore: LOCKED 80 * KeyGuard: OFF/ON 81 * Action: old unlock dialog 82 * Notes: assume old password, need to use it to unlock. 83 * if unlock, ensure key guard before install. 84 * if reset, treat as UNINITALIZED/OFF 85 * 86 * KeyStore: UNLOCKED 87 * KeyGuard: OFF 88 * Action: set up key guard 89 * Notes: ensure key guard, then proceed 90 * 91 * KeyStore: UNLOCKED 92 * keyguard: ON 93 * Action: normal unlock/install 94 * Notes: this is the common case 95 */ 96 public final class CredentialStorage extends Activity { 97 98 private static final String TAG = "CredentialStorage"; 99 100 public static final String ACTION_UNLOCK = "com.android.credentials.UNLOCK"; 101 public static final String ACTION_INSTALL = "com.android.credentials.INSTALL"; 102 public static final String ACTION_RESET = "com.android.credentials.RESET"; 103 104 // This is the minimum acceptable password quality. If the current password quality is 105 // lower than this, keystore should not be activated. 106 public static final int MIN_PASSWORD_QUALITY = DevicePolicyManager.PASSWORD_QUALITY_SOMETHING; 107 108 private static final int CONFIRM_KEY_GUARD_REQUEST = 1; 109 private static final int CONFIRM_CLEAR_SYSTEM_CREDENTIAL_REQUEST = 2; 110 111 private final KeyStore mKeyStore = KeyStore.getInstance(); 112 113 /** 114 * When non-null, the bundle containing credentials to install. 115 */ 116 private Bundle mInstallBundle; 117 118 /** 119 * After unsuccessful KeyStore.unlock, the number of unlock 120 * attempts remaining before the KeyStore will reset itself. 121 * 122 * Reset to -1 on successful unlock or reset. 123 */ 124 private int mRetriesRemaining = -1; 125 126 @Override onResume()127 protected void onResume() { 128 super.onResume(); 129 130 Intent intent = getIntent(); 131 String action = intent.getAction(); 132 UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE); 133 if (!userManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_CREDENTIALS)) { 134 if (ACTION_RESET.equals(action)) { 135 new ResetDialog(); 136 } else { 137 if (ACTION_INSTALL.equals(action) && checkCallerIsCertInstallerOrSelfInProfile()) { 138 mInstallBundle = intent.getExtras(); 139 } 140 // ACTION_UNLOCK also handled here in addition to ACTION_INSTALL 141 handleUnlockOrInstall(); 142 } 143 } else { 144 // Users can set a screen lock if there is none even if they can't modify the 145 // credentials store. 146 if (ACTION_UNLOCK.equals(action) && mKeyStore.state() == KeyStore.State.UNINITIALIZED) { 147 ensureKeyGuard(); 148 } else { 149 finish(); 150 } 151 } 152 } 153 154 /** 155 * Based on the current state of the KeyStore and key guard, try to 156 * make progress on unlocking or installing to the keystore. 157 */ handleUnlockOrInstall()158 private void handleUnlockOrInstall() { 159 // something already decided we are done, do not proceed 160 if (isFinishing()) { 161 return; 162 } 163 switch (mKeyStore.state()) { 164 case UNINITIALIZED: { 165 ensureKeyGuard(); 166 return; 167 } 168 case LOCKED: { 169 new UnlockDialog(); 170 return; 171 } 172 case UNLOCKED: { 173 if (!checkKeyGuardQuality()) { 174 final ConfigureKeyGuardDialog dialog = new ConfigureKeyGuardDialog(); 175 dialog.show(getFragmentManager(), ConfigureKeyGuardDialog.TAG); 176 return; 177 } 178 installIfAvailable(); 179 finish(); 180 return; 181 } 182 } 183 } 184 185 /** 186 * Make sure the user enters the key guard to set or change the 187 * keystore password. This can be used in UNINITIALIZED to set the 188 * keystore password or UNLOCKED to change the password (as is the 189 * case after unlocking with an old-style password). 190 */ ensureKeyGuard()191 private void ensureKeyGuard() { 192 if (!checkKeyGuardQuality()) { 193 // key guard not setup, doing so will initialize keystore 194 final ConfigureKeyGuardDialog dialog = new ConfigureKeyGuardDialog(); 195 dialog.show(getFragmentManager(), ConfigureKeyGuardDialog.TAG); 196 // will return to onResume after Activity 197 return; 198 } 199 // force key guard confirmation 200 if (confirmKeyGuard(CONFIRM_KEY_GUARD_REQUEST)) { 201 // will return password value via onActivityResult 202 return; 203 } 204 finish(); 205 } 206 207 /** 208 * Returns true if the currently set key guard matches our minimum quality requirements. 209 */ checkKeyGuardQuality()210 private boolean checkKeyGuardQuality() { 211 int credentialOwner = 212 UserManager.get(this).getCredentialOwnerProfile(UserHandle.myUserId()); 213 int quality = new LockPatternUtils(this).getActivePasswordQuality(credentialOwner); 214 return (quality >= MIN_PASSWORD_QUALITY); 215 } 216 isHardwareBackedKey(byte[] keyData)217 private boolean isHardwareBackedKey(byte[] keyData) { 218 try { 219 ASN1InputStream bIn = new ASN1InputStream(new ByteArrayInputStream(keyData)); 220 PrivateKeyInfo pki = PrivateKeyInfo.getInstance(bIn.readObject()); 221 String algOid = pki.getAlgorithmId().getAlgorithm().getId(); 222 String algName = new AlgorithmId(new ObjectIdentifier(algOid)).getName(); 223 224 return KeyChain.isBoundKeyAlgorithm(algName); 225 } catch (IOException e) { 226 Log.e(TAG, "Failed to parse key data"); 227 return false; 228 } 229 } 230 231 /** 232 * Install credentials if available, otherwise do nothing. 233 */ installIfAvailable()234 private void installIfAvailable() { 235 if (mInstallBundle == null || mInstallBundle.isEmpty()) { 236 return; 237 } 238 239 Bundle bundle = mInstallBundle; 240 mInstallBundle = null; 241 242 final int uid = bundle.getInt(Credentials.EXTRA_INSTALL_AS_UID, KeyStore.UID_SELF); 243 244 if (uid != KeyStore.UID_SELF && !UserHandle.isSameUser(uid, Process.myUid())) { 245 int dstUserId = UserHandle.getUserId(uid); 246 int myUserId = UserHandle.myUserId(); 247 248 // Restrict install target to the wifi uid. 249 if (uid != Process.WIFI_UID) { 250 Log.e(TAG, "Failed to install credentials as uid " + uid + ": cross-user installs" 251 + " may only target wifi uids"); 252 return; 253 } 254 255 Intent installIntent = new Intent(ACTION_INSTALL) 256 .setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT) 257 .putExtras(bundle); 258 startActivityAsUser(installIntent, new UserHandle(dstUserId)); 259 return; 260 } 261 262 if (bundle.containsKey(Credentials.EXTRA_USER_PRIVATE_KEY_NAME)) { 263 String key = bundle.getString(Credentials.EXTRA_USER_PRIVATE_KEY_NAME); 264 byte[] value = bundle.getByteArray(Credentials.EXTRA_USER_PRIVATE_KEY_DATA); 265 266 int flags = KeyStore.FLAG_ENCRYPTED; 267 if (uid == Process.WIFI_UID && isHardwareBackedKey(value)) { 268 // Hardware backed keystore is secure enough to allow for WIFI stack 269 // to enable access to secure networks without user intervention 270 Log.d(TAG, "Saving private key with FLAG_NONE for WIFI_UID"); 271 flags = KeyStore.FLAG_NONE; 272 } 273 274 if (!mKeyStore.importKey(key, value, uid, flags)) { 275 Log.e(TAG, "Failed to install " + key + " as uid " + uid); 276 return; 277 } 278 // The key was prepended USER_PRIVATE_KEY by the CredentialHelper. However, 279 // KeyChain internally uses the raw alias name and only prepends USER_PRIVATE_KEY 280 // to the key name when interfacing with KeyStore. 281 // This is generally a symptom of CredentialStorage and CredentialHelper relying 282 // on internal implementation details of KeyChain and imitating its functionality 283 // rather than delegating to KeyChain for the certificate installation. 284 if (uid == Process.SYSTEM_UID || uid == KeyStore.UID_SELF) { 285 new MarkKeyAsUserSelectable( 286 key.replaceFirst("^" + Credentials.USER_PRIVATE_KEY, "")).execute(); 287 } 288 } 289 290 int flags = KeyStore.FLAG_NONE; 291 292 if (bundle.containsKey(Credentials.EXTRA_USER_CERTIFICATE_NAME)) { 293 String certName = bundle.getString(Credentials.EXTRA_USER_CERTIFICATE_NAME); 294 byte[] certData = bundle.getByteArray(Credentials.EXTRA_USER_CERTIFICATE_DATA); 295 296 if (!mKeyStore.put(certName, certData, uid, flags)) { 297 Log.e(TAG, "Failed to install " + certName + " as uid " + uid); 298 return; 299 } 300 } 301 302 if (bundle.containsKey(Credentials.EXTRA_CA_CERTIFICATES_NAME)) { 303 String caListName = bundle.getString(Credentials.EXTRA_CA_CERTIFICATES_NAME); 304 byte[] caListData = bundle.getByteArray(Credentials.EXTRA_CA_CERTIFICATES_DATA); 305 306 if (!mKeyStore.put(caListName, caListData, uid, flags)) { 307 Log.e(TAG, "Failed to install " + caListName + " as uid " + uid); 308 return; 309 } 310 } 311 312 // Send the broadcast. 313 Intent broadcast = new Intent(KeyChain.ACTION_KEYCHAIN_CHANGED); 314 sendBroadcast(broadcast); 315 316 setResult(RESULT_OK); 317 } 318 319 /** 320 * Prompt for reset confirmation, resetting on confirmation, finishing otherwise. 321 */ 322 private class ResetDialog 323 implements DialogInterface.OnClickListener, DialogInterface.OnDismissListener { 324 private boolean mResetConfirmed; 325 ResetDialog()326 private ResetDialog() { 327 AlertDialog dialog = new AlertDialog.Builder(CredentialStorage.this) 328 .setTitle(android.R.string.dialog_alert_title) 329 .setMessage(R.string.credentials_reset_hint) 330 .setPositiveButton(android.R.string.ok, this) 331 .setNegativeButton(android.R.string.cancel, this) 332 .create(); 333 dialog.setOnDismissListener(this); 334 dialog.show(); 335 } 336 337 @Override onClick(DialogInterface dialog, int button)338 public void onClick(DialogInterface dialog, int button) { 339 mResetConfirmed = (button == DialogInterface.BUTTON_POSITIVE); 340 } 341 342 @Override onDismiss(DialogInterface dialog)343 public void onDismiss(DialogInterface dialog) { 344 if (mResetConfirmed) { 345 mResetConfirmed = false; 346 if (confirmKeyGuard(CONFIRM_CLEAR_SYSTEM_CREDENTIAL_REQUEST)) { 347 // will return password value via onActivityResult 348 return; 349 } 350 } 351 finish(); 352 } 353 } 354 355 /** 356 * Background task to handle reset of both keystore and user installed CAs. 357 */ 358 private class ResetKeyStoreAndKeyChain extends AsyncTask<Void, Void, Boolean> { 359 360 @Override doInBackground(Void... unused)361 protected Boolean doInBackground(Void... unused) { 362 363 // Clear all the users credentials could have been installed in for this user. 364 new LockPatternUtils(CredentialStorage.this).resetKeyStore(UserHandle.myUserId()); 365 366 try { 367 KeyChainConnection keyChainConnection = KeyChain.bind(CredentialStorage.this); 368 try { 369 return keyChainConnection.getService().reset(); 370 } catch (RemoteException e) { 371 return false; 372 } finally { 373 keyChainConnection.close(); 374 } 375 } catch (InterruptedException e) { 376 Thread.currentThread().interrupt(); 377 return false; 378 } 379 } 380 381 @Override onPostExecute(Boolean success)382 protected void onPostExecute(Boolean success) { 383 if (success) { 384 Toast.makeText(CredentialStorage.this, 385 R.string.credentials_erased, Toast.LENGTH_SHORT).show(); 386 clearLegacyVpnIfEstablished(); 387 } else { 388 Toast.makeText(CredentialStorage.this, 389 R.string.credentials_not_erased, Toast.LENGTH_SHORT).show(); 390 } 391 finish(); 392 } 393 } 394 clearLegacyVpnIfEstablished()395 private void clearLegacyVpnIfEstablished() { 396 boolean isDone = VpnUtils.disconnectLegacyVpn(getApplicationContext()); 397 if (isDone) { 398 Toast.makeText(CredentialStorage.this, R.string.vpn_disconnected, 399 Toast.LENGTH_SHORT).show(); 400 } 401 } 402 403 /** 404 * Background task to mark a given key alias as user-selectable, so that 405 * it can be selected by users from the Certificate Selection prompt. 406 */ 407 private class MarkKeyAsUserSelectable extends AsyncTask<Void, Void, Boolean> { 408 final String mAlias; 409 MarkKeyAsUserSelectable(String alias)410 public MarkKeyAsUserSelectable(String alias) { 411 mAlias = alias; 412 } 413 414 @Override doInBackground(Void... unused)415 protected Boolean doInBackground(Void... unused) { 416 try (KeyChainConnection keyChainConnection = KeyChain.bind(CredentialStorage.this)) { 417 keyChainConnection.getService().setUserSelectable(mAlias, true); 418 return true; 419 } catch (RemoteException e) { 420 Log.w(TAG, "Failed to mark key " + mAlias + " as user-selectable."); 421 return false; 422 } catch (InterruptedException e) { 423 Log.w(TAG, "Failed to mark key " + mAlias + " as user-selectable."); 424 Thread.currentThread().interrupt(); 425 return false; 426 } 427 } 428 } 429 430 /** 431 * Check that the caller is either certinstaller or Settings running in a profile of this user. 432 */ checkCallerIsCertInstallerOrSelfInProfile()433 private boolean checkCallerIsCertInstallerOrSelfInProfile() { 434 if (TextUtils.equals("com.android.certinstaller", getCallingPackage())) { 435 // CertInstaller is allowed to install credentials if it has the same signature as 436 // Settings package. 437 return getPackageManager().checkSignatures( 438 getCallingPackage(), getPackageName()) == PackageManager.SIGNATURE_MATCH; 439 } 440 441 final int launchedFromUserId; 442 try { 443 int launchedFromUid = android.app.ActivityManager.getService() 444 .getLaunchedFromUid(getActivityToken()); 445 if (launchedFromUid == -1) { 446 Log.e(TAG, ACTION_INSTALL + " must be started with startActivityForResult"); 447 return false; 448 } 449 if (!UserHandle.isSameApp(launchedFromUid, Process.myUid())) { 450 // Not the same app 451 return false; 452 } 453 launchedFromUserId = UserHandle.getUserId(launchedFromUid); 454 } catch (RemoteException re) { 455 // Error talking to ActivityManager, just give up 456 return false; 457 } 458 459 UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE); 460 UserInfo parentInfo = userManager.getProfileParent(launchedFromUserId); 461 if (parentInfo == null || parentInfo.id != UserHandle.myUserId()) { 462 // Caller is not running in a profile of this user 463 return false; 464 } 465 return true; 466 } 467 468 /** 469 * Confirm existing key guard, returning password via onActivityResult. 470 */ confirmKeyGuard(int requestCode)471 private boolean confirmKeyGuard(int requestCode) { 472 Resources res = getResources(); 473 boolean launched = new ChooseLockSettingsHelper(this) 474 .launchConfirmationActivity(requestCode, 475 res.getText(R.string.credentials_title), true); 476 return launched; 477 } 478 479 @Override onActivityResult(int requestCode, int resultCode, Intent data)480 public void onActivityResult(int requestCode, int resultCode, Intent data) { 481 super.onActivityResult(requestCode, resultCode, data); 482 483 /** 484 * Receive key guard password initiated by confirmKeyGuard. 485 */ 486 if (requestCode == CONFIRM_KEY_GUARD_REQUEST) { 487 if (resultCode == Activity.RESULT_OK) { 488 String password = data.getStringExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD); 489 if (!TextUtils.isEmpty(password)) { 490 // success 491 mKeyStore.unlock(password); 492 // return to onResume 493 return; 494 } 495 } 496 // failed confirmation, bail 497 finish(); 498 } else if (requestCode == CONFIRM_CLEAR_SYSTEM_CREDENTIAL_REQUEST) { 499 if (resultCode == Activity.RESULT_OK) { 500 new ResetKeyStoreAndKeyChain().execute(); 501 return; 502 } 503 // failed confirmation, bail 504 finish(); 505 } 506 } 507 508 /** 509 * Prompt for unlock with old-style password. 510 * 511 * On successful unlock, ensure migration to key guard before continuing. 512 * On unsuccessful unlock, retry by calling handleUnlockOrInstall. 513 */ 514 private class UnlockDialog implements TextWatcher, 515 DialogInterface.OnClickListener, DialogInterface.OnDismissListener { 516 private boolean mUnlockConfirmed; 517 518 private final Button mButton; 519 private final TextView mOldPassword; 520 private final TextView mError; 521 UnlockDialog()522 private UnlockDialog() { 523 View view = View.inflate(CredentialStorage.this, R.layout.credentials_dialog, null); 524 525 CharSequence text; 526 if (mRetriesRemaining == -1) { 527 text = getResources().getText(R.string.credentials_unlock_hint); 528 } else if (mRetriesRemaining > 3) { 529 text = getResources().getText(R.string.credentials_wrong_password); 530 } else if (mRetriesRemaining == 1) { 531 text = getResources().getText(R.string.credentials_reset_warning); 532 } else { 533 text = getString(R.string.credentials_reset_warning_plural, mRetriesRemaining); 534 } 535 536 ((TextView) view.findViewById(R.id.hint)).setText(text); 537 mOldPassword = (TextView) view.findViewById(R.id.old_password); 538 mOldPassword.setVisibility(View.VISIBLE); 539 mOldPassword.addTextChangedListener(this); 540 mError = (TextView) view.findViewById(R.id.error); 541 542 AlertDialog dialog = new AlertDialog.Builder(CredentialStorage.this) 543 .setView(view) 544 .setTitle(R.string.credentials_unlock) 545 .setPositiveButton(android.R.string.ok, this) 546 .setNegativeButton(android.R.string.cancel, this) 547 .create(); 548 dialog.setOnDismissListener(this); 549 dialog.show(); 550 mButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE); 551 mButton.setEnabled(false); 552 } 553 554 @Override afterTextChanged(Editable editable)555 public void afterTextChanged(Editable editable) { 556 mButton.setEnabled(mOldPassword == null || mOldPassword.getText().length() > 0); 557 } 558 559 @Override beforeTextChanged(CharSequence s, int start, int count, int after)560 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 561 } 562 563 @Override onTextChanged(CharSequence s, int start, int before, int count)564 public void onTextChanged(CharSequence s, int start, int before, int count) { 565 } 566 567 @Override onClick(DialogInterface dialog, int button)568 public void onClick(DialogInterface dialog, int button) { 569 mUnlockConfirmed = (button == DialogInterface.BUTTON_POSITIVE); 570 } 571 572 @Override onDismiss(DialogInterface dialog)573 public void onDismiss(DialogInterface dialog) { 574 if (mUnlockConfirmed) { 575 mUnlockConfirmed = false; 576 mError.setVisibility(View.VISIBLE); 577 mKeyStore.unlock(mOldPassword.getText().toString()); 578 int error = mKeyStore.getLastError(); 579 if (error == KeyStore.NO_ERROR) { 580 mRetriesRemaining = -1; 581 Toast.makeText(CredentialStorage.this, 582 R.string.credentials_enabled, 583 Toast.LENGTH_SHORT).show(); 584 // aha, now we are unlocked, switch to key guard. 585 // we'll end up back in onResume to install 586 ensureKeyGuard(); 587 } else if (error == KeyStore.UNINITIALIZED) { 588 mRetriesRemaining = -1; 589 Toast.makeText(CredentialStorage.this, 590 R.string.credentials_erased, 591 Toast.LENGTH_SHORT).show(); 592 // we are reset, we can now set new password with key guard 593 handleUnlockOrInstall(); 594 } else if (error >= KeyStore.WRONG_PASSWORD) { 595 // we need to try again 596 mRetriesRemaining = error - KeyStore.WRONG_PASSWORD + 1; 597 handleUnlockOrInstall(); 598 } 599 return; 600 } 601 finish(); 602 } 603 } 604 } 605