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