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