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