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