Home | History | Annotate | Download | only in settings
      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