2 * Copyright (C) 2011 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.settings;
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.pm.UserInfo;
26 import android.content.res.Resources;
27 import android.os.AsyncTask;
28 import android.os.Bundle;
29 import android.os.RemoteException;
30 import android.os.Process;
31 import android.os.UserHandle;
32 import android.os.UserManager;
33 import android.security.Credentials;
34 import android.security.KeyChain.KeyChainConnection;
35 import android.security.KeyChain;
36 import android.security.KeyStore;
37 import android.text.Editable;
38 import android.text.TextUtils;
39 import android.text.TextWatcher;
40 import android.util.Log;
41 import android.view.View;
42 import android.widget.Button;
43 import android.widget.TextView;
44 import android.widget.Toast;
46 import com.android.internal.widget.LockPatternUtils;
47 import com.android.org.bouncycastle.asn1.ASN1InputStream;
48 import com.android.org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
50 import org.apache.harmony.security.utils.AlgNameMapper;
52 import java.io.ByteArrayInputStream;
53 import java.io.IOException;
56 * CredentialStorage handles KeyStore reset, unlock, and install.
58 * CredentialStorage has a pretty convoluted state machine to migrate
59 * from the old style separate keystore password to a new key guard
60 * based password, as well as to deal with setting up the key guard if
63 * KeyStore: UNINITALIZED
65 * Action: set up key guard
66 * Notes: factory state
68 * KeyStore: UNINITALIZED
70 * Action: confirm key guard
71 * Notes: user had key guard but no keystore and upgraded from pre-ICS
72 * OR user had key guard and pre-ICS keystore password which was then reset
76 * Action: old unlock dialog
77 * Notes: assume old password, need to use it to unlock.
78 * if unlock, ensure key guard before install.
79 * if reset, treat as UNINITALIZED/OFF
83 * Action: set up key guard
84 * Notes: ensure key guard, then proceed
88 * Action: normal unlock/install
89 * Notes: this is the common case
91 public final class CredentialStorage extends Activity {
93 private static final String TAG = "CredentialStorage";
95 public static final String ACTION_UNLOCK = "com.android.credentials.UNLOCK";
96 public static final String ACTION_INSTALL = "com.android.credentials.INSTALL";
97 public static final String ACTION_RESET = "com.android.credentials.RESET";
99 // This is the minimum acceptable password quality. If the current password quality is
100 // lower than this, keystore should not be activated.
101 static final int MIN_PASSWORD_QUALITY = DevicePolicyManager.PASSWORD_QUALITY_SOMETHING;
103 private static final int CONFIRM_KEY_GUARD_REQUEST = 1;
105 private final KeyStore mKeyStore = KeyStore.getInstance();
108 * When non-null, the bundle containing credentials to install.
110 private Bundle mInstallBundle;
113 * After unsuccessful KeyStore.unlock, the number of unlock
114 * attempts remaining before the KeyStore will reset itself.
116 * Reset to -1 on successful unlock or reset.
118 private int mRetriesRemaining = -1;
121 protected void onResume() {
124 Intent intent = getIntent();
125 String action = intent.getAction();
126 UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE);
127 if (!userManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_CREDENTIALS)) {
128 if (ACTION_RESET.equals(action)) {
131 if (ACTION_INSTALL.equals(action) && checkCallerIsCertInstallerOrSelfInProfile()) {
132 mInstallBundle = intent.getExtras();
134 // ACTION_UNLOCK also handled here in addition to ACTION_INSTALL
135 handleUnlockOrInstall();
143 * Based on the current state of the KeyStore and key guard, try to
144 * make progress on unlocking or installing to the keystore.
146 private void handleUnlockOrInstall() {
147 // something already decided we are done, do not proceed
151 switch (mKeyStore.state()) {
152 case UNINITIALIZED: {
161 if (!checkKeyGuardQuality()) {
162 new ConfigureKeyGuardDialog();
165 installIfAvailable();
173 * Make sure the user enters the key guard to set or change the
174 * keystore password. This can be used in UNINITIALIZED to set the
175 * keystore password or UNLOCKED to change the password (as is the
176 * case after unlocking with an old-style password).
178 private void ensureKeyGuard() {
179 if (!checkKeyGuardQuality()) {
180 // key guard not setup, doing so will initialize keystore
181 new ConfigureKeyGuardDialog();
182 // will return to onResume after Activity
185 // force key guard confirmation
186 if (confirmKeyGuard()) {
187 // will return password value via onActivityResult
194 * Returns true if the currently set key guard matches our minimum quality requirements.
196 private boolean checkKeyGuardQuality() {
197 int quality = new LockPatternUtils(this).getActivePasswordQuality();
198 return (quality >= MIN_PASSWORD_QUALITY);
201 private boolean isHardwareBackedKey(byte[] keyData) {
203 ASN1InputStream bIn = new ASN1InputStream(new ByteArrayInputStream(keyData));
204 PrivateKeyInfo pki = PrivateKeyInfo.getInstance(bIn.readObject());
205 String algId = pki.getAlgorithmId().getAlgorithm().getId();
206 String algName = AlgNameMapper.map2AlgName(algId);
208 return KeyChain.isBoundKeyAlgorithm(algName);
209 } catch (IOException e) {
210 Log.e(TAG, "Failed to parse key data");
216 * Install credentials if available, otherwise do nothing.
218 private void installIfAvailable() {
219 if (mInstallBundle == null || mInstallBundle.isEmpty()) {
223 Bundle bundle = mInstallBundle;
224 mInstallBundle = null;
226 final int uid = bundle.getInt(Credentials.EXTRA_INSTALL_AS_UID, KeyStore.UID_SELF);
228 if (uid != KeyStore.UID_SELF && !UserHandle.isSameUser(uid, Process.myUid())) {
229 int dstUserId = UserHandle.getUserId(uid);
230 int myUserId = UserHandle.myUserId();
232 // Restrict install target to the wifi uid.
233 if (uid != Process.WIFI_UID) {
234 Log.e(TAG, "Failed to install credentials as uid " + uid + ": cross-user installs"
235 + " may only target wifi uids");
239 Intent installIntent = new Intent(ACTION_INSTALL)
240 .setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT)
242 startActivityAsUser(installIntent, new UserHandle(dstUserId));
246 if (bundle.containsKey(Credentials.EXTRA_USER_PRIVATE_KEY_NAME)) {
247 String key = bundle.getString(Credentials.EXTRA_USER_PRIVATE_KEY_NAME);
248 byte[] value = bundle.getByteArray(Credentials.EXTRA_USER_PRIVATE_KEY_DATA);
250 int flags = KeyStore.FLAG_ENCRYPTED;
251 if (uid == Process.WIFI_UID && isHardwareBackedKey(value)) {
252 // Hardware backed keystore is secure enough to allow for WIFI stack
253 // to enable access to secure networks without user intervention
254 Log.d(TAG, "Saving private key with FLAG_NONE for WIFI_UID");
255 flags = KeyStore.FLAG_NONE;
258 if (!mKeyStore.importKey(key, value, uid, flags)) {
259 Log.e(TAG, "Failed to install " + key + " as uid " + uid);
264 int flags = (uid == Process.WIFI_UID) ? KeyStore.FLAG_NONE : KeyStore.FLAG_ENCRYPTED;
266 if (bundle.containsKey(Credentials.EXTRA_USER_CERTIFICATE_NAME)) {
267 String certName = bundle.getString(Credentials.EXTRA_USER_CERTIFICATE_NAME);
268 byte[] certData = bundle.getByteArray(Credentials.EXTRA_USER_CERTIFICATE_DATA);
270 if (!mKeyStore.put(certName, certData, uid, flags)) {
271 Log.e(TAG, "Failed to install " + certName + " as uid " + uid);
276 if (bundle.containsKey(Credentials.EXTRA_CA_CERTIFICATES_NAME)) {
277 String caListName = bundle.getString(Credentials.EXTRA_CA_CERTIFICATES_NAME);
278 byte[] caListData = bundle.getByteArray(Credentials.EXTRA_CA_CERTIFICATES_DATA);
280 if (!mKeyStore.put(caListName, caListData, uid, flags)) {
281 Log.e(TAG, "Failed to install " + caListName + " as uid " + uid);
286 setResult(RESULT_OK);
290 * Prompt for reset confirmation, resetting on confirmation, finishing otherwise.
292 private class ResetDialog
293 implements DialogInterface.OnClickListener, DialogInterface.OnDismissListener
295 private boolean mResetConfirmed;
297 private ResetDialog() {
298 AlertDialog dialog = new AlertDialog.Builder(CredentialStorage.this)
299 .setTitle(android.R.string.dialog_alert_title)
300 .setMessage(R.string.credentials_reset_hint)
301 .setPositiveButton(android.R.string.ok, this)
302 .setNegativeButton(android.R.string.cancel, this)
304 dialog.setOnDismissListener(this);
308 @Override public void onClick(DialogInterface dialog, int button) {
309 mResetConfirmed = (button == DialogInterface.BUTTON_POSITIVE);
312 @Override public void onDismiss(DialogInterface dialog) {
313 if (mResetConfirmed) {
314 mResetConfirmed = false;
315 new ResetKeyStoreAndKeyChain().execute();
323 * Background task to handle reset of both keystore and user installed CAs.
325 private class ResetKeyStoreAndKeyChain extends AsyncTask<Void, Void, Boolean> {
327 @Override protected Boolean doInBackground(Void... unused) {
332 KeyChainConnection keyChainConnection = KeyChain.bind(CredentialStorage.this);
334 return keyChainConnection.getService().reset();
335 } catch (RemoteException e) {
338 keyChainConnection.close();
340 } catch (InterruptedException e) {
341 Thread.currentThread().interrupt();
346 @Override protected void onPostExecute(Boolean success) {
348 Toast.makeText(CredentialStorage.this,
349 R.string.credentials_erased, Toast.LENGTH_SHORT).show();
351 Toast.makeText(CredentialStorage.this,
352 R.string.credentials_not_erased, Toast.LENGTH_SHORT).show();
359 * Prompt for key guard configuration confirmation.
361 private class ConfigureKeyGuardDialog
362 implements DialogInterface.OnClickListener, DialogInterface.OnDismissListener
364 private boolean mConfigureConfirmed;
366 private ConfigureKeyGuardDialog() {
367 AlertDialog dialog = new AlertDialog.Builder(CredentialStorage.this)
368 .setTitle(android.R.string.dialog_alert_title)
369 .setMessage(R.string.credentials_configure_lock_screen_hint)
370 .setPositiveButton(android.R.string.ok, this)
371 .setNegativeButton(android.R.string.cancel, this)
373 dialog.setOnDismissListener(this);
377 @Override public void onClick(DialogInterface dialog, int button) {
378 mConfigureConfirmed = (button == DialogInterface.BUTTON_POSITIVE);
381 @Override public void onDismiss(DialogInterface dialog) {
382 if (mConfigureConfirmed) {
383 mConfigureConfirmed = false;
384 Intent intent = new Intent(DevicePolicyManager.ACTION_SET_NEW_PASSWORD);
385 intent.putExtra(ChooseLockGeneric.ChooseLockGenericFragment.MINIMUM_QUALITY_KEY,
386 MIN_PASSWORD_QUALITY);
387 startActivity(intent);
395 * Check that the caller is either certinstaller or Settings running in a profile of this user.
397 private boolean checkCallerIsCertInstallerOrSelfInProfile() {
398 if (TextUtils.equals("com.android.certinstaller", getCallingPackage())) {
399 // CertInstaller is allowed to install credentials
403 final int launchedFromUserId;
405 int launchedFromUid = android.app.ActivityManagerNative.getDefault()
406 .getLaunchedFromUid(getActivityToken());
407 if (launchedFromUid == -1) {
408 Log.e(TAG, ACTION_INSTALL + " must be started with startActivityForResult");
411 if (!UserHandle.isSameApp(launchedFromUid, Process.myUid())) {
415 launchedFromUserId = UserHandle.getUserId(launchedFromUid);
416 } catch (RemoteException re) {
417 // Error talking to ActivityManager, just give up
421 UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE);
422 UserInfo parentInfo = userManager.getProfileParent(launchedFromUserId);
423 if (parentInfo == null || parentInfo.id != UserHandle.myUserId()) {
424 // Caller is not running in a profile of this user
431 * Confirm existing key guard, returning password via onActivityResult.
433 private boolean confirmKeyGuard() {
434 Resources res = getResources();
435 boolean launched = new ChooseLockSettingsHelper(this)
436 .launchConfirmationActivity(CONFIRM_KEY_GUARD_REQUEST, null,
437 res.getText(R.string.credentials_install_gesture_explanation),
443 public void onActivityResult(int requestCode, int resultCode, Intent data) {
444 super.onActivityResult(requestCode, resultCode, data);
447 * Receive key guard password initiated by confirmKeyGuard.
449 if (requestCode == CONFIRM_KEY_GUARD_REQUEST) {
450 if (resultCode == Activity.RESULT_OK) {
451 String password = data.getStringExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD);
452 if (!TextUtils.isEmpty(password)) {
454 mKeyStore.password(password);
455 // return to onResume
459 // failed confirmation, bail
465 * Prompt for unlock with old-style password.
467 * On successful unlock, ensure migration to key guard before continuing.
468 * On unsuccessful unlock, retry by calling handleUnlockOrInstall.
470 private class UnlockDialog implements TextWatcher,
471 DialogInterface.OnClickListener, DialogInterface.OnDismissListener
473 private boolean mUnlockConfirmed;
475 private final Button mButton;
476 private final TextView mOldPassword;
477 private final TextView mError;
479 private UnlockDialog() {
480 View view = View.inflate(CredentialStorage.this, R.layout.credentials_dialog, null);
483 if (mRetriesRemaining == -1) {
484 text = getResources().getText(R.string.credentials_unlock_hint);
485 } else if (mRetriesRemaining > 3) {
486 text = getResources().getText(R.string.credentials_wrong_password);
487 } else if (mRetriesRemaining == 1) {
488 text = getResources().getText(R.string.credentials_reset_warning);
490 text = getString(R.string.credentials_reset_warning_plural, mRetriesRemaining);
493 ((TextView) view.findViewById(R.id.hint)).setText(text);
494 mOldPassword = (TextView) view.findViewById(R.id.old_password);
495 mOldPassword.setVisibility(View.VISIBLE);
496 mOldPassword.addTextChangedListener(this);
497 mError = (TextView) view.findViewById(R.id.error);
499 AlertDialog dialog = new AlertDialog.Builder(CredentialStorage.this)
501 .setTitle(R.string.credentials_unlock)
502 .setPositiveButton(android.R.string.ok, this)
503 .setNegativeButton(android.R.string.cancel, this)
505 dialog.setOnDismissListener(this);
507 mButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
508 mButton.setEnabled(false);
511 @Override public void afterTextChanged(Editable editable) {
512 mButton.setEnabled(mOldPassword == null || mOldPassword.getText().length() > 0);
515 @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {
518 @Override public void onTextChanged(CharSequence s,int start, int before, int count) {
521 @Override public void onClick(DialogInterface dialog, int button) {
522 mUnlockConfirmed = (button == DialogInterface.BUTTON_POSITIVE);
525 @Override public void onDismiss(DialogInterface dialog) {
526 if (mUnlockConfirmed) {
527 mUnlockConfirmed = false;
528 mError.setVisibility(View.VISIBLE);
529 mKeyStore.unlock(mOldPassword.getText().toString());
530 int error = mKeyStore.getLastError();
531 if (error == KeyStore.NO_ERROR) {
532 mRetriesRemaining = -1;
533 Toast.makeText(CredentialStorage.this,
534 R.string.credentials_enabled,
535 Toast.LENGTH_SHORT).show();
536 // aha, now we are unlocked, switch to key guard.
537 // we'll end up back in onResume to install
539 } else if (error == KeyStore.UNINITIALIZED) {
540 mRetriesRemaining = -1;
541 Toast.makeText(CredentialStorage.this,
542 R.string.credentials_erased,
543 Toast.LENGTH_SHORT).show();
544 // we are reset, we can now set new password with key guard
545 handleUnlockOrInstall();
546 } else if (error >= KeyStore.WRONG_PASSWORD) {
547 // we need to try again
548 mRetriesRemaining = error - KeyStore.WRONG_PASSWORD + 1;
549 handleUnlockOrInstall();