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