/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.admin.DevicePolicyManager;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.Resources;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.UserHandle;
import android.security.Credentials;
import android.security.KeyChain.KeyChainConnection;
import android.security.KeyChain;
import android.security.KeyStore;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import com.android.internal.widget.LockPatternUtils;
/**
* CredentialStorage handles KeyStore reset, unlock, and install.
*
* CredentialStorage has a pretty convoluted state machine to migrate
* from the old style separate keystore password to a new key guard
* based password, as well as to deal with setting up the key guard if
* necessary.
*
* KeyStore: UNINITALIZED
* KeyGuard: OFF
* Action: set up key guard
* Notes: factory state
*
* KeyStore: UNINITALIZED
* KeyGuard: ON
* Action: confirm key guard
* Notes: user had key guard but no keystore and upgraded from pre-ICS
* OR user had key guard and pre-ICS keystore password which was then reset
*
* KeyStore: LOCKED
* KeyGuard: OFF/ON
* Action: old unlock dialog
* Notes: assume old password, need to use it to unlock.
* if unlock, ensure key guard before install.
* if reset, treat as UNINITALIZED/OFF
*
* KeyStore: UNLOCKED
* KeyGuard: OFF
* Action: set up key guard
* Notes: ensure key guard, then proceed
*
* KeyStore: UNLOCKED
* keyguard: ON
* Action: normal unlock/install
* Notes: this is the common case
*/
public final class CredentialStorage extends Activity {
private static final String TAG = "CredentialStorage";
public static final String ACTION_UNLOCK = "com.android.credentials.UNLOCK";
public static final String ACTION_INSTALL = "com.android.credentials.INSTALL";
public static final String ACTION_RESET = "com.android.credentials.RESET";
// This is the minimum acceptable password quality. If the current password quality is
// lower than this, keystore should not be activated.
static final int MIN_PASSWORD_QUALITY = DevicePolicyManager.PASSWORD_QUALITY_SOMETHING;
private static final int CONFIRM_KEY_GUARD_REQUEST = 1;
private final KeyStore mKeyStore = KeyStore.getInstance();
/**
* When non-null, the bundle containing credentials to install.
*/
private Bundle mInstallBundle;
/**
* After unsuccessful KeyStore.unlock, the number of unlock
* attempts remaining before the KeyStore will reset itself.
*
* Reset to -1 on successful unlock or reset.
*/
private int mRetriesRemaining = -1;
@Override
protected void onResume() {
super.onResume();
if (UserHandle.myUserId() != UserHandle.USER_OWNER) {
Log.i(TAG, "Cannot install to CredentialStorage as non-primary user");
finish();
return;
}
Intent intent = getIntent();
String action = intent.getAction();
if (ACTION_RESET.equals(action)) {
new ResetDialog();
} else {
if (ACTION_INSTALL.equals(action)
&& "com.android.certinstaller".equals(getCallingPackage())) {
mInstallBundle = intent.getExtras();
}
// ACTION_UNLOCK also handled here in addition to ACTION_INSTALL
handleUnlockOrInstall();
}
}
/**
* Based on the current state of the KeyStore and key guard, try to
* make progress on unlocking or installing to the keystore.
*/
private void handleUnlockOrInstall() {
// something already decided we are done, do not proceed
if (isFinishing()) {
return;
}
switch (mKeyStore.state()) {
case UNINITIALIZED: {
ensureKeyGuard();
return;
}
case LOCKED: {
new UnlockDialog();
return;
}
case UNLOCKED: {
if (!checkKeyGuardQuality()) {
new ConfigureKeyGuardDialog();
return;
}
installIfAvailable();
finish();
return;
}
}
}
/**
* Make sure the user enters the key guard to set or change the
* keystore password. This can be used in UNINITIALIZED to set the
* keystore password or UNLOCKED to change the password (as is the
* case after unlocking with an old-style password).
*/
private void ensureKeyGuard() {
if (!checkKeyGuardQuality()) {
// key guard not setup, doing so will initialize keystore
new ConfigureKeyGuardDialog();
// will return to onResume after Activity
return;
}
// force key guard confirmation
if (confirmKeyGuard()) {
// will return password value via onActivityResult
return;
}
finish();
}
/**
* Returns true if the currently set key guard matches our minimum quality requirements.
*/
private boolean checkKeyGuardQuality() {
int quality = new LockPatternUtils(this).getActivePasswordQuality();
return (quality >= MIN_PASSWORD_QUALITY);
}
/**
* Install credentials if available, otherwise do nothing.
*/
private void installIfAvailable() {
if (mInstallBundle != null && !mInstallBundle.isEmpty()) {
Bundle bundle = mInstallBundle;
mInstallBundle = null;
if (bundle.containsKey(Credentials.EXTRA_USER_PRIVATE_KEY_NAME)) {
String key = bundle.getString(Credentials.EXTRA_USER_PRIVATE_KEY_NAME);
byte[] value = bundle.getByteArray(Credentials.EXTRA_USER_PRIVATE_KEY_DATA);
if (!mKeyStore.importKey(key, value)) {
Log.e(TAG, "Failed to install " + key);
return;
}
}
if (bundle.containsKey(Credentials.EXTRA_USER_CERTIFICATE_NAME)) {
String certName = bundle.getString(Credentials.EXTRA_USER_CERTIFICATE_NAME);
byte[] certData = bundle.getByteArray(Credentials.EXTRA_USER_CERTIFICATE_DATA);
if (!mKeyStore.put(certName, certData)) {
Log.e(TAG, "Failed to install " + certName);
return;
}
}
if (bundle.containsKey(Credentials.EXTRA_CA_CERTIFICATES_NAME)) {
String caListName = bundle.getString(Credentials.EXTRA_CA_CERTIFICATES_NAME);
byte[] caListData = bundle.getByteArray(Credentials.EXTRA_CA_CERTIFICATES_DATA);
if (!mKeyStore.put(caListName, caListData)) {
Log.e(TAG, "Failed to install " + caListName);
return;
}
}
setResult(RESULT_OK);
}
}
/**
* Prompt for reset confirmation, resetting on confirmation, finishing otherwise.
*/
private class ResetDialog
implements DialogInterface.OnClickListener, DialogInterface.OnDismissListener
{
private boolean mResetConfirmed;
private ResetDialog() {
AlertDialog dialog = new AlertDialog.Builder(CredentialStorage.this)
.setTitle(android.R.string.dialog_alert_title)
.setIconAttribute(android.R.attr.alertDialogIcon)
.setMessage(R.string.credentials_reset_hint)
.setPositiveButton(android.R.string.ok, this)
.setNegativeButton(android.R.string.cancel, this)
.create();
dialog.setOnDismissListener(this);
dialog.show();
}
@Override public void onClick(DialogInterface dialog, int button) {
mResetConfirmed = (button == DialogInterface.BUTTON_POSITIVE);
}
@Override public void onDismiss(DialogInterface dialog) {
if (mResetConfirmed) {
mResetConfirmed = false;
new ResetKeyStoreAndKeyChain().execute();
return;
}
finish();
}
}
/**
* Background task to handle reset of both keystore and user installed CAs.
*/
private class ResetKeyStoreAndKeyChain extends AsyncTask<Void, Void, Boolean> {
@Override protected Boolean doInBackground(Void... unused) {
mKeyStore.reset();
try {
KeyChainConnection keyChainConnection = KeyChain.bind(CredentialStorage.this);
try {
return keyChainConnection.getService().reset();
} catch (RemoteException e) {
return false;
} finally {
keyChainConnection.close();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
@Override protected void onPostExecute(Boolean success) {
if (success) {
Toast.makeText(CredentialStorage.this,
R.string.credentials_erased, Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(CredentialStorage.this,
R.string.credentials_not_erased, Toast.LENGTH_SHORT).show();
}
finish();
}
}
/**
* Prompt for key guard configuration confirmation.
*/
private class ConfigureKeyGuardDialog
implements DialogInterface.OnClickListener, DialogInterface.OnDismissListener
{
private boolean mConfigureConfirmed;
private ConfigureKeyGuardDialog() {
AlertDialog dialog = new AlertDialog.Builder(CredentialStorage.this)
.setTitle(android.R.string.dialog_alert_title)
.setIconAttribute(android.R.attr.alertDialogIcon)
.setMessage(R.string.credentials_configure_lock_screen_hint)
.setPositiveButton(android.R.string.ok, this)
.setNegativeButton(android.R.string.cancel, this)
.create();
dialog.setOnDismissListener(this);
dialog.show();
}
@Override public void onClick(DialogInterface dialog, int button) {
mConfigureConfirmed = (button == DialogInterface.BUTTON_POSITIVE);
}
@Override public void onDismiss(DialogInterface dialog) {
if (mConfigureConfirmed) {
mConfigureConfirmed = false;
Intent intent = new Intent(DevicePolicyManager.ACTION_SET_NEW_PASSWORD);
intent.putExtra(ChooseLockGeneric.ChooseLockGenericFragment.MINIMUM_QUALITY_KEY,
MIN_PASSWORD_QUALITY);
startActivity(intent);
return;
}
finish();
}
}
/**
* Confirm existing key guard, returning password via onActivityResult.
*/
private boolean confirmKeyGuard() {
Resources res = getResources();
boolean launched = new ChooseLockSettingsHelper(this)
.launchConfirmationActivity(CONFIRM_KEY_GUARD_REQUEST,
res.getText(R.string.credentials_install_gesture_prompt),
res.getText(R.string.credentials_install_gesture_explanation));
return launched;
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
/**
* Receive key guard password initiated by confirmKeyGuard.
*/
if (requestCode == CONFIRM_KEY_GUARD_REQUEST) {
if (resultCode == Activity.RESULT_OK) {
String password = data.getStringExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD);
if (!TextUtils.isEmpty(password)) {
// success
mKeyStore.password(password);
// return to onResume
return;
}
}
// failed confirmation, bail
finish();
}
}
/**
* Prompt for unlock with old-style password.
*
* On successful unlock, ensure migration to key guard before continuing.
* On unsuccessful unlock, retry by calling handleUnlockOrInstall.
*/
private class UnlockDialog implements TextWatcher,
DialogInterface.OnClickListener, DialogInterface.OnDismissListener
{
private boolean mUnlockConfirmed;
private final Button mButton;
private final TextView mOldPassword;
private final TextView mError;
private UnlockDialog() {
View view = View.inflate(CredentialStorage.this, R.layout.credentials_dialog, null);
CharSequence text;
if (mRetriesRemaining == -1) {
text = getResources().getText(R.string.credentials_unlock_hint);
} else if (mRetriesRemaining > 3) {
text = getResources().getText(R.string.credentials_wrong_password);
} else if (mRetriesRemaining == 1) {
text = getResources().getText(R.string.credentials_reset_warning);
} else {
text = getString(R.string.credentials_reset_warning_plural, mRetriesRemaining);
}
((TextView) view.findViewById(R.id.hint)).setText(text);
mOldPassword = (TextView) view.findViewById(R.id.old_password);
mOldPassword.setVisibility(View.VISIBLE);
mOldPassword.addTextChangedListener(this);
mError = (TextView) view.findViewById(R.id.error);
AlertDialog dialog = new AlertDialog.Builder(CredentialStorage.this)
.setView(view)
.setTitle(R.string.credentials_unlock)
.setPositiveButton(android.R.string.ok, this)
.setNegativeButton(android.R.string.cancel, this)
.create();
dialog.setOnDismissListener(this);
dialog.show();
mButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
mButton.setEnabled(false);
}
@Override public void afterTextChanged(Editable editable) {
mButton.setEnabled(mOldPassword == null || mOldPassword.getText().length() > 0);
}
@Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override public void onTextChanged(CharSequence s,int start, int before, int count) {
}
@Override public void onClick(DialogInterface dialog, int button) {
mUnlockConfirmed = (button == DialogInterface.BUTTON_POSITIVE);
}
@Override public void onDismiss(DialogInterface dialog) {
if (mUnlockConfirmed) {
mUnlockConfirmed = false;
mError.setVisibility(View.VISIBLE);
mKeyStore.unlock(mOldPassword.getText().toString());
int error = mKeyStore.getLastError();
if (error == KeyStore.NO_ERROR) {
mRetriesRemaining = -1;
Toast.makeText(CredentialStorage.this,
R.string.credentials_enabled,
Toast.LENGTH_SHORT).show();
// aha, now we are unlocked, switch to key guard.
// we'll end up back in onResume to install
ensureKeyGuard();
} else if (error == KeyStore.UNINITIALIZED) {
mRetriesRemaining = -1;
Toast.makeText(CredentialStorage.this,
R.string.credentials_erased,
Toast.LENGTH_SHORT).show();
// we are reset, we can now set new password with key guard
handleUnlockOrInstall();
} else if (error >= KeyStore.WRONG_PASSWORD) {
// we need to try again
mRetriesRemaining = error - KeyStore.WRONG_PASSWORD + 1;
handleUnlockOrInstall();
}
return;
}
finish();
}
}
}