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