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.pm.PackageManager;
26import android.content.pm.UserInfo;
27import android.content.res.Resources;
28import android.os.AsyncTask;
29import android.os.Bundle;
30import android.os.Process;
31import android.os.RemoteException;
32import android.os.UserHandle;
33import android.os.UserManager;
34import android.security.Credentials;
35import android.security.KeyChain;
36import android.security.KeyChain.KeyChainConnection;
37import android.security.KeyStore;
38import android.text.Editable;
39import android.text.TextUtils;
40import android.text.TextWatcher;
41import android.util.Log;
42import android.view.View;
43import android.widget.Button;
44import android.widget.TextView;
45import android.widget.Toast;
46
47import com.android.internal.widget.LockPatternUtils;
48import com.android.org.bouncycastle.asn1.ASN1InputStream;
49import com.android.org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
50import com.android.settings.password.ChooseLockSettingsHelper;
51import com.android.settings.security.ConfigureKeyGuardDialog;
52import com.android.settings.vpn2.VpnUtils;
53
54import java.io.ByteArrayInputStream;
55import java.io.IOException;
56
57import sun.security.util.ObjectIdentifier;
58import 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 */
96public 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