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.Context;
23import android.content.DialogInterface;
24import android.content.Intent;
25import android.content.res.Resources;
26import android.os.AsyncTask;
27import android.os.Bundle;
28import android.os.RemoteException;
29import android.os.Process;
30import android.os.UserManager;
31import android.security.Credentials;
32import android.security.KeyChain.KeyChainConnection;
33import android.security.KeyChain;
34import android.security.KeyStore;
35import android.text.Editable;
36import android.text.TextUtils;
37import android.text.TextWatcher;
38import android.util.Log;
39import android.view.View;
40import android.widget.Button;
41import android.widget.TextView;
42import android.widget.Toast;
43
44import com.android.internal.widget.LockPatternUtils;
45import com.android.org.bouncycastle.asn1.ASN1InputStream;
46import com.android.org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
47
48import org.apache.harmony.security.utils.AlgNameMapper;
49
50import java.io.ByteArrayInputStream;
51import 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 */
89public 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