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