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