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