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.res.Resources; 26 import android.os.AsyncTask; 27 import android.os.Bundle; 28 import android.os.RemoteException; 29 import android.os.Process; 30 import android.os.UserManager; 31 import android.security.Credentials; 32 import android.security.KeyChain.KeyChainConnection; 33 import android.security.KeyChain; 34 import android.security.KeyStore; 35 import android.text.Editable; 36 import android.text.TextUtils; 37 import android.text.TextWatcher; 38 import android.util.Log; 39 import android.view.View; 40 import android.widget.Button; 41 import android.widget.TextView; 42 import android.widget.Toast; 43 44 import com.android.internal.widget.LockPatternUtils; 45 import com.android.org.bouncycastle.asn1.ASN1InputStream; 46 import com.android.org.bouncycastle.asn1.pkcs.PrivateKeyInfo; 47 48 import org.apache.harmony.security.utils.AlgNameMapper; 49 50 import java.io.ByteArrayInputStream; 51 import java.io.IOException; 52 53 /** 54 * CredentialStorage handles KeyStore reset, unlock, and install. 55 * 56 * CredentialStorage has a pretty convoluted state machine to migrate 57 * from the old style separate keystore password to a new key guard 58 * based password, as well as to deal with setting up the key guard if 59 * necessary. 60 * 61 * KeyStore: UNINITALIZED 62 * KeyGuard: OFF 63 * Action: set up key guard 64 * Notes: factory state 65 * 66 * KeyStore: UNINITALIZED 67 * KeyGuard: ON 68 * Action: confirm key guard 69 * Notes: user had key guard but no keystore and upgraded from pre-ICS 70 * OR user had key guard and pre-ICS keystore password which was then reset 71 * 72 * KeyStore: LOCKED 73 * KeyGuard: OFF/ON 74 * Action: old unlock dialog 75 * Notes: assume old password, need to use it to unlock. 76 * if unlock, ensure key guard before install. 77 * if reset, treat as UNINITALIZED/OFF 78 * 79 * KeyStore: UNLOCKED 80 * KeyGuard: OFF 81 * Action: set up key guard 82 * Notes: ensure key guard, then proceed 83 * 84 * KeyStore: UNLOCKED 85 * keyguard: ON 86 * Action: normal unlock/install 87 * Notes: this is the common case 88 */ 89 public final class CredentialStorage extends Activity { 90 91 private static final String TAG = "CredentialStorage"; 92 93 public static final String ACTION_UNLOCK = "com.android.credentials.UNLOCK"; 94 public static final String ACTION_INSTALL = "com.android.credentials.INSTALL"; 95 public static final String ACTION_RESET = "com.android.credentials.RESET"; 96 97 // This is the minimum acceptable password quality. If the current password quality is 98 // lower than this, keystore should not be activated. 99 static final int MIN_PASSWORD_QUALITY = DevicePolicyManager.PASSWORD_QUALITY_SOMETHING; 100 101 private static final int CONFIRM_KEY_GUARD_REQUEST = 1; 102 103 private final KeyStore mKeyStore = KeyStore.getInstance(); 104 105 /** 106 * When non-null, the bundle containing credentials to install. 107 */ 108 private Bundle mInstallBundle; 109 110 /** 111 * After unsuccessful KeyStore.unlock, the number of unlock 112 * attempts remaining before the KeyStore will reset itself. 113 * 114 * Reset to -1 on successful unlock or reset. 115 */ 116 private int mRetriesRemaining = -1; 117 118 @Override 119 protected void onResume() { 120 super.onResume(); 121 122 Intent intent = getIntent(); 123 String action = intent.getAction(); 124 UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE); 125 if (!userManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_CREDENTIALS)) { 126 if (ACTION_RESET.equals(action)) { 127 new ResetDialog(); 128 } else { 129 if (ACTION_INSTALL.equals(action) 130 && "com.android.certinstaller".equals(getCallingPackage())) { 131 mInstallBundle = intent.getExtras(); 132 } 133 // ACTION_UNLOCK also handled here in addition to ACTION_INSTALL 134 handleUnlockOrInstall(); 135 } 136 } else { 137 finish(); 138 } 139 } 140 141 /** 142 * Based on the current state of the KeyStore and key guard, try to 143 * make progress on unlocking or installing to the keystore. 144 */ 145 private void handleUnlockOrInstall() { 146 // something already decided we are done, do not proceed 147 if (isFinishing()) { 148 return; 149 } 150 switch (mKeyStore.state()) { 151 case UNINITIALIZED: { 152 ensureKeyGuard(); 153 return; 154 } 155 case LOCKED: { 156 new UnlockDialog(); 157 return; 158 } 159 case UNLOCKED: { 160 if (!checkKeyGuardQuality()) { 161 new ConfigureKeyGuardDialog(); 162 return; 163 } 164 installIfAvailable(); 165 finish(); 166 return; 167 } 168 } 169 } 170 171 /** 172 * Make sure the user enters the key guard to set or change the 173 * keystore password. This can be used in UNINITIALIZED to set the 174 * keystore password or UNLOCKED to change the password (as is the 175 * case after unlocking with an old-style password). 176 */ 177 private void ensureKeyGuard() { 178 if (!checkKeyGuardQuality()) { 179 // key guard not setup, doing so will initialize keystore 180 new ConfigureKeyGuardDialog(); 181 // will return to onResume after Activity 182 return; 183 } 184 // force key guard confirmation 185 if (confirmKeyGuard()) { 186 // will return password value via onActivityResult 187 return; 188 } 189 finish(); 190 } 191 192 /** 193 * Returns true if the currently set key guard matches our minimum quality requirements. 194 */ 195 private boolean checkKeyGuardQuality() { 196 int quality = new LockPatternUtils(this).getActivePasswordQuality(); 197 return (quality >= MIN_PASSWORD_QUALITY); 198 } 199 200 private boolean isHardwareBackedKey(byte[] keyData) { 201 try { 202 ASN1InputStream bIn = new ASN1InputStream(new ByteArrayInputStream(keyData)); 203 PrivateKeyInfo pki = PrivateKeyInfo.getInstance(bIn.readObject()); 204 String algId = pki.getAlgorithmId().getAlgorithm().getId(); 205 String algName = AlgNameMapper.map2AlgName(algId); 206 207 return KeyChain.isBoundKeyAlgorithm(algName); 208 } catch (IOException e) { 209 Log.e(TAG, "Failed to parse key data"); 210 return false; 211 } 212 } 213 214 /** 215 * Install credentials if available, otherwise do nothing. 216 */ 217 private void installIfAvailable() { 218 if (mInstallBundle != null && !mInstallBundle.isEmpty()) { 219 Bundle bundle = mInstallBundle; 220 mInstallBundle = null; 221 222 final int uid = bundle.getInt(Credentials.EXTRA_INSTALL_AS_UID, -1); 223 224 if (bundle.containsKey(Credentials.EXTRA_USER_PRIVATE_KEY_NAME)) { 225 String key = bundle.getString(Credentials.EXTRA_USER_PRIVATE_KEY_NAME); 226 byte[] value = bundle.getByteArray(Credentials.EXTRA_USER_PRIVATE_KEY_DATA); 227 228 int flags = KeyStore.FLAG_ENCRYPTED; 229 if (uid == Process.WIFI_UID && isHardwareBackedKey(value)) { 230 // Hardware backed keystore is secure enough to allow for WIFI stack 231 // to enable access to secure networks without user intervention 232 Log.d(TAG, "Saving private key with FLAG_NONE for WIFI_UID"); 233 flags = KeyStore.FLAG_NONE; 234 } 235 236 if (!mKeyStore.importKey(key, value, uid, flags)) { 237 Log.e(TAG, "Failed to install " + key + " as user " + uid); 238 return; 239 } 240 } 241 242 int flags = (uid == Process.WIFI_UID) ? KeyStore.FLAG_NONE : KeyStore.FLAG_ENCRYPTED; 243 244 if (bundle.containsKey(Credentials.EXTRA_USER_CERTIFICATE_NAME)) { 245 String certName = bundle.getString(Credentials.EXTRA_USER_CERTIFICATE_NAME); 246 byte[] certData = bundle.getByteArray(Credentials.EXTRA_USER_CERTIFICATE_DATA); 247 248 if (!mKeyStore.put(certName, certData, uid, flags)) { 249 Log.e(TAG, "Failed to install " + certName + " as user " + uid); 250 return; 251 } 252 } 253 254 if (bundle.containsKey(Credentials.EXTRA_CA_CERTIFICATES_NAME)) { 255 String caListName = bundle.getString(Credentials.EXTRA_CA_CERTIFICATES_NAME); 256 byte[] caListData = bundle.getByteArray(Credentials.EXTRA_CA_CERTIFICATES_DATA); 257 258 if (!mKeyStore.put(caListName, caListData, uid, flags)) { 259 Log.e(TAG, "Failed to install " + caListName + " as user " + uid); 260 return; 261 } 262 } 263 264 setResult(RESULT_OK); 265 } 266 } 267 268 /** 269 * Prompt for reset confirmation, resetting on confirmation, finishing otherwise. 270 */ 271 private class ResetDialog 272 implements DialogInterface.OnClickListener, DialogInterface.OnDismissListener 273 { 274 private boolean mResetConfirmed; 275 276 private ResetDialog() { 277 AlertDialog dialog = new AlertDialog.Builder(CredentialStorage.this) 278 .setTitle(android.R.string.dialog_alert_title) 279 .setMessage(R.string.credentials_reset_hint) 280 .setPositiveButton(android.R.string.ok, this) 281 .setNegativeButton(android.R.string.cancel, this) 282 .create(); 283 dialog.setOnDismissListener(this); 284 dialog.show(); 285 } 286 287 @Override public void onClick(DialogInterface dialog, int button) { 288 mResetConfirmed = (button == DialogInterface.BUTTON_POSITIVE); 289 } 290 291 @Override public void onDismiss(DialogInterface dialog) { 292 if (mResetConfirmed) { 293 mResetConfirmed = false; 294 new ResetKeyStoreAndKeyChain().execute(); 295 return; 296 } 297 finish(); 298 } 299 } 300 301 /** 302 * Background task to handle reset of both keystore and user installed CAs. 303 */ 304 private class ResetKeyStoreAndKeyChain extends AsyncTask<Void, Void, Boolean> { 305 306 @Override protected Boolean doInBackground(Void... unused) { 307 308 mKeyStore.reset(); 309 310 try { 311 KeyChainConnection keyChainConnection = KeyChain.bind(CredentialStorage.this); 312 try { 313 return keyChainConnection.getService().reset(); 314 } catch (RemoteException e) { 315 return false; 316 } finally { 317 keyChainConnection.close(); 318 } 319 } catch (InterruptedException e) { 320 Thread.currentThread().interrupt(); 321 return false; 322 } 323 } 324 325 @Override protected void onPostExecute(Boolean success) { 326 if (success) { 327 Toast.makeText(CredentialStorage.this, 328 R.string.credentials_erased, Toast.LENGTH_SHORT).show(); 329 } else { 330 Toast.makeText(CredentialStorage.this, 331 R.string.credentials_not_erased, Toast.LENGTH_SHORT).show(); 332 } 333 finish(); 334 } 335 } 336 337 /** 338 * Prompt for key guard configuration confirmation. 339 */ 340 private class ConfigureKeyGuardDialog 341 implements DialogInterface.OnClickListener, DialogInterface.OnDismissListener 342 { 343 private boolean mConfigureConfirmed; 344 345 private ConfigureKeyGuardDialog() { 346 AlertDialog dialog = new AlertDialog.Builder(CredentialStorage.this) 347 .setTitle(android.R.string.dialog_alert_title) 348 .setMessage(R.string.credentials_configure_lock_screen_hint) 349 .setPositiveButton(android.R.string.ok, this) 350 .setNegativeButton(android.R.string.cancel, this) 351 .create(); 352 dialog.setOnDismissListener(this); 353 dialog.show(); 354 } 355 356 @Override public void onClick(DialogInterface dialog, int button) { 357 mConfigureConfirmed = (button == DialogInterface.BUTTON_POSITIVE); 358 } 359 360 @Override public void onDismiss(DialogInterface dialog) { 361 if (mConfigureConfirmed) { 362 mConfigureConfirmed = false; 363 Intent intent = new Intent(DevicePolicyManager.ACTION_SET_NEW_PASSWORD); 364 intent.putExtra(ChooseLockGeneric.ChooseLockGenericFragment.MINIMUM_QUALITY_KEY, 365 MIN_PASSWORD_QUALITY); 366 startActivity(intent); 367 return; 368 } 369 finish(); 370 } 371 } 372 373 /** 374 * Confirm existing key guard, returning password via onActivityResult. 375 */ 376 private boolean confirmKeyGuard() { 377 Resources res = getResources(); 378 boolean launched = new ChooseLockSettingsHelper(this) 379 .launchConfirmationActivity(CONFIRM_KEY_GUARD_REQUEST, 380 res.getText(R.string.credentials_install_gesture_prompt), 381 res.getText(R.string.credentials_install_gesture_explanation), 382 true); 383 return launched; 384 } 385 386 @Override 387 public void onActivityResult(int requestCode, int resultCode, Intent data) { 388 super.onActivityResult(requestCode, resultCode, data); 389 390 /** 391 * Receive key guard password initiated by confirmKeyGuard. 392 */ 393 if (requestCode == CONFIRM_KEY_GUARD_REQUEST) { 394 if (resultCode == Activity.RESULT_OK) { 395 String password = data.getStringExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD); 396 if (!TextUtils.isEmpty(password)) { 397 // success 398 mKeyStore.password(password); 399 // return to onResume 400 return; 401 } 402 } 403 // failed confirmation, bail 404 finish(); 405 } 406 } 407 408 /** 409 * Prompt for unlock with old-style password. 410 * 411 * On successful unlock, ensure migration to key guard before continuing. 412 * On unsuccessful unlock, retry by calling handleUnlockOrInstall. 413 */ 414 private class UnlockDialog implements TextWatcher, 415 DialogInterface.OnClickListener, DialogInterface.OnDismissListener 416 { 417 private boolean mUnlockConfirmed; 418 419 private final Button mButton; 420 private final TextView mOldPassword; 421 private final TextView mError; 422 423 private UnlockDialog() { 424 View view = View.inflate(CredentialStorage.this, R.layout.credentials_dialog, null); 425 426 CharSequence text; 427 if (mRetriesRemaining == -1) { 428 text = getResources().getText(R.string.credentials_unlock_hint); 429 } else if (mRetriesRemaining > 3) { 430 text = getResources().getText(R.string.credentials_wrong_password); 431 } else if (mRetriesRemaining == 1) { 432 text = getResources().getText(R.string.credentials_reset_warning); 433 } else { 434 text = getString(R.string.credentials_reset_warning_plural, mRetriesRemaining); 435 } 436 437 ((TextView) view.findViewById(R.id.hint)).setText(text); 438 mOldPassword = (TextView) view.findViewById(R.id.old_password); 439 mOldPassword.setVisibility(View.VISIBLE); 440 mOldPassword.addTextChangedListener(this); 441 mError = (TextView) view.findViewById(R.id.error); 442 443 AlertDialog dialog = new AlertDialog.Builder(CredentialStorage.this) 444 .setView(view) 445 .setTitle(R.string.credentials_unlock) 446 .setPositiveButton(android.R.string.ok, this) 447 .setNegativeButton(android.R.string.cancel, this) 448 .create(); 449 dialog.setOnDismissListener(this); 450 dialog.show(); 451 mButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE); 452 mButton.setEnabled(false); 453 } 454 455 @Override public void afterTextChanged(Editable editable) { 456 mButton.setEnabled(mOldPassword == null || mOldPassword.getText().length() > 0); 457 } 458 459 @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { 460 } 461 462 @Override public void onTextChanged(CharSequence s,int start, int before, int count) { 463 } 464 465 @Override public void onClick(DialogInterface dialog, int button) { 466 mUnlockConfirmed = (button == DialogInterface.BUTTON_POSITIVE); 467 } 468 469 @Override public void onDismiss(DialogInterface dialog) { 470 if (mUnlockConfirmed) { 471 mUnlockConfirmed = false; 472 mError.setVisibility(View.VISIBLE); 473 mKeyStore.unlock(mOldPassword.getText().toString()); 474 int error = mKeyStore.getLastError(); 475 if (error == KeyStore.NO_ERROR) { 476 mRetriesRemaining = -1; 477 Toast.makeText(CredentialStorage.this, 478 R.string.credentials_enabled, 479 Toast.LENGTH_SHORT).show(); 480 // aha, now we are unlocked, switch to key guard. 481 // we'll end up back in onResume to install 482 ensureKeyGuard(); 483 } else if (error == KeyStore.UNINITIALIZED) { 484 mRetriesRemaining = -1; 485 Toast.makeText(CredentialStorage.this, 486 R.string.credentials_erased, 487 Toast.LENGTH_SHORT).show(); 488 // we are reset, we can now set new password with key guard 489 handleUnlockOrInstall(); 490 } else if (error >= KeyStore.WRONG_PASSWORD) { 491 // we need to try again 492 mRetriesRemaining = error - KeyStore.WRONG_PASSWORD + 1; 493 handleUnlockOrInstall(); 494 } 495 return; 496 } 497 finish(); 498 } 499 } 500 } 501