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