• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 import com.android.settings.password.ChooseLockSettingsHelper;
51 import com.android.settings.security.ConfigureKeyGuardDialog;
52 import com.android.settings.vpn2.VpnUtils;
53 
54 import java.io.ByteArrayInputStream;
55 import java.io.IOException;
56 
57 import sun.security.util.ObjectIdentifier;
58 import 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  */
96 public 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
onResume()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      */
handleUnlockOrInstall()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      */
ensureKeyGuard()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      */
checkKeyGuardQuality()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 
isHardwareBackedKey(byte[] keyData)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      */
installIfAvailable()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             // The key was prepended USER_PRIVATE_KEY by the CredentialHelper. However,
279             // KeyChain internally uses the raw alias name and only prepends USER_PRIVATE_KEY
280             // to the key name when interfacing with KeyStore.
281             // This is generally a symptom of CredentialStorage and CredentialHelper relying
282             // on internal implementation details of KeyChain and imitating its functionality
283             // rather than delegating to KeyChain for the certificate installation.
284             if (uid == Process.SYSTEM_UID || uid == KeyStore.UID_SELF) {
285                 new MarkKeyAsUserSelectable(
286                         key.replaceFirst("^" + Credentials.USER_PRIVATE_KEY, "")).execute();
287             }
288         }
289 
290         int flags = KeyStore.FLAG_NONE;
291 
292         if (bundle.containsKey(Credentials.EXTRA_USER_CERTIFICATE_NAME)) {
293             String certName = bundle.getString(Credentials.EXTRA_USER_CERTIFICATE_NAME);
294             byte[] certData = bundle.getByteArray(Credentials.EXTRA_USER_CERTIFICATE_DATA);
295 
296             if (!mKeyStore.put(certName, certData, uid, flags)) {
297                 Log.e(TAG, "Failed to install " + certName + " as uid " + uid);
298                 return;
299             }
300         }
301 
302         if (bundle.containsKey(Credentials.EXTRA_CA_CERTIFICATES_NAME)) {
303             String caListName = bundle.getString(Credentials.EXTRA_CA_CERTIFICATES_NAME);
304             byte[] caListData = bundle.getByteArray(Credentials.EXTRA_CA_CERTIFICATES_DATA);
305 
306             if (!mKeyStore.put(caListName, caListData, uid, flags)) {
307                 Log.e(TAG, "Failed to install " + caListName + " as uid " + uid);
308                 return;
309             }
310         }
311 
312         // Send the broadcast.
313         Intent broadcast = new Intent(KeyChain.ACTION_KEYCHAIN_CHANGED);
314         sendBroadcast(broadcast);
315 
316         setResult(RESULT_OK);
317     }
318 
319     /**
320      * Prompt for reset confirmation, resetting on confirmation, finishing otherwise.
321      */
322     private class ResetDialog
323             implements DialogInterface.OnClickListener, DialogInterface.OnDismissListener {
324         private boolean mResetConfirmed;
325 
ResetDialog()326         private ResetDialog() {
327             AlertDialog dialog = new AlertDialog.Builder(CredentialStorage.this)
328                     .setTitle(android.R.string.dialog_alert_title)
329                     .setMessage(R.string.credentials_reset_hint)
330                     .setPositiveButton(android.R.string.ok, this)
331                     .setNegativeButton(android.R.string.cancel, this)
332                     .create();
333             dialog.setOnDismissListener(this);
334             dialog.show();
335         }
336 
337         @Override
onClick(DialogInterface dialog, int button)338         public void onClick(DialogInterface dialog, int button) {
339             mResetConfirmed = (button == DialogInterface.BUTTON_POSITIVE);
340         }
341 
342         @Override
onDismiss(DialogInterface dialog)343         public void onDismiss(DialogInterface dialog) {
344             if (mResetConfirmed) {
345                 mResetConfirmed = false;
346                 if (confirmKeyGuard(CONFIRM_CLEAR_SYSTEM_CREDENTIAL_REQUEST)) {
347                     // will return password value via onActivityResult
348                     return;
349                 }
350             }
351             finish();
352         }
353     }
354 
355     /**
356      * Background task to handle reset of both keystore and user installed CAs.
357      */
358     private class ResetKeyStoreAndKeyChain extends AsyncTask<Void, Void, Boolean> {
359 
360         @Override
doInBackground(Void... unused)361         protected Boolean doInBackground(Void... unused) {
362 
363             // Clear all the users credentials could have been installed in for this user.
364             new LockPatternUtils(CredentialStorage.this).resetKeyStore(UserHandle.myUserId());
365 
366             try {
367                 KeyChainConnection keyChainConnection = KeyChain.bind(CredentialStorage.this);
368                 try {
369                     return keyChainConnection.getService().reset();
370                 } catch (RemoteException e) {
371                     return false;
372                 } finally {
373                     keyChainConnection.close();
374                 }
375             } catch (InterruptedException e) {
376                 Thread.currentThread().interrupt();
377                 return false;
378             }
379         }
380 
381         @Override
onPostExecute(Boolean success)382         protected void onPostExecute(Boolean success) {
383             if (success) {
384                 Toast.makeText(CredentialStorage.this,
385                         R.string.credentials_erased, Toast.LENGTH_SHORT).show();
386                 clearLegacyVpnIfEstablished();
387             } else {
388                 Toast.makeText(CredentialStorage.this,
389                         R.string.credentials_not_erased, Toast.LENGTH_SHORT).show();
390             }
391             finish();
392         }
393     }
394 
clearLegacyVpnIfEstablished()395     private void clearLegacyVpnIfEstablished() {
396         boolean isDone = VpnUtils.disconnectLegacyVpn(getApplicationContext());
397         if (isDone) {
398             Toast.makeText(CredentialStorage.this, R.string.vpn_disconnected,
399                     Toast.LENGTH_SHORT).show();
400         }
401     }
402 
403     /**
404      * Background task to mark a given key alias as user-selectable, so that
405      * it can be selected by users from the Certificate Selection prompt.
406      */
407     private class MarkKeyAsUserSelectable extends AsyncTask<Void, Void, Boolean> {
408         final String mAlias;
409 
MarkKeyAsUserSelectable(String alias)410         public MarkKeyAsUserSelectable(String alias) {
411             mAlias = alias;
412         }
413 
414         @Override
doInBackground(Void... unused)415         protected Boolean doInBackground(Void... unused) {
416             try (KeyChainConnection keyChainConnection = KeyChain.bind(CredentialStorage.this)) {
417                 keyChainConnection.getService().setUserSelectable(mAlias, true);
418                 return true;
419             } catch (RemoteException e) {
420                 Log.w(TAG, "Failed to mark key " + mAlias + " as user-selectable.");
421                 return false;
422             } catch (InterruptedException e) {
423                 Log.w(TAG, "Failed to mark key " + mAlias + " as user-selectable.");
424                 Thread.currentThread().interrupt();
425                 return false;
426             }
427         }
428     }
429 
430     /**
431      * Check that the caller is either certinstaller or Settings running in a profile of this user.
432      */
checkCallerIsCertInstallerOrSelfInProfile()433     private boolean checkCallerIsCertInstallerOrSelfInProfile() {
434         if (TextUtils.equals("com.android.certinstaller", getCallingPackage())) {
435             // CertInstaller is allowed to install credentials if it has the same signature as
436             // Settings package.
437             return getPackageManager().checkSignatures(
438                     getCallingPackage(), getPackageName()) == PackageManager.SIGNATURE_MATCH;
439         }
440 
441         final int launchedFromUserId;
442         try {
443             int launchedFromUid = android.app.ActivityManager.getService()
444                     .getLaunchedFromUid(getActivityToken());
445             if (launchedFromUid == -1) {
446                 Log.e(TAG, ACTION_INSTALL + " must be started with startActivityForResult");
447                 return false;
448             }
449             if (!UserHandle.isSameApp(launchedFromUid, Process.myUid())) {
450                 // Not the same app
451                 return false;
452             }
453             launchedFromUserId = UserHandle.getUserId(launchedFromUid);
454         } catch (RemoteException re) {
455             // Error talking to ActivityManager, just give up
456             return false;
457         }
458 
459         UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE);
460         UserInfo parentInfo = userManager.getProfileParent(launchedFromUserId);
461         if (parentInfo == null || parentInfo.id != UserHandle.myUserId()) {
462             // Caller is not running in a profile of this user
463             return false;
464         }
465         return true;
466     }
467 
468     /**
469      * Confirm existing key guard, returning password via onActivityResult.
470      */
confirmKeyGuard(int requestCode)471     private boolean confirmKeyGuard(int requestCode) {
472         Resources res = getResources();
473         boolean launched = new ChooseLockSettingsHelper(this)
474                 .launchConfirmationActivity(requestCode,
475                         res.getText(R.string.credentials_title), true);
476         return launched;
477     }
478 
479     @Override
onActivityResult(int requestCode, int resultCode, Intent data)480     public void onActivityResult(int requestCode, int resultCode, Intent data) {
481         super.onActivityResult(requestCode, resultCode, data);
482 
483         /**
484          * Receive key guard password initiated by confirmKeyGuard.
485          */
486         if (requestCode == CONFIRM_KEY_GUARD_REQUEST) {
487             if (resultCode == Activity.RESULT_OK) {
488                 String password = data.getStringExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD);
489                 if (!TextUtils.isEmpty(password)) {
490                     // success
491                     mKeyStore.unlock(password);
492                     // return to onResume
493                     return;
494                 }
495             }
496             // failed confirmation, bail
497             finish();
498         } else if (requestCode == CONFIRM_CLEAR_SYSTEM_CREDENTIAL_REQUEST) {
499             if (resultCode == Activity.RESULT_OK) {
500                 new ResetKeyStoreAndKeyChain().execute();
501                 return;
502             }
503             // failed confirmation, bail
504             finish();
505         }
506     }
507 
508     /**
509      * Prompt for unlock with old-style password.
510      *
511      * On successful unlock, ensure migration to key guard before continuing.
512      * On unsuccessful unlock, retry by calling handleUnlockOrInstall.
513      */
514     private class UnlockDialog implements TextWatcher,
515             DialogInterface.OnClickListener, DialogInterface.OnDismissListener {
516         private boolean mUnlockConfirmed;
517 
518         private final Button mButton;
519         private final TextView mOldPassword;
520         private final TextView mError;
521 
UnlockDialog()522         private UnlockDialog() {
523             View view = View.inflate(CredentialStorage.this, R.layout.credentials_dialog, null);
524 
525             CharSequence text;
526             if (mRetriesRemaining == -1) {
527                 text = getResources().getText(R.string.credentials_unlock_hint);
528             } else if (mRetriesRemaining > 3) {
529                 text = getResources().getText(R.string.credentials_wrong_password);
530             } else if (mRetriesRemaining == 1) {
531                 text = getResources().getText(R.string.credentials_reset_warning);
532             } else {
533                 text = getString(R.string.credentials_reset_warning_plural, mRetriesRemaining);
534             }
535 
536             ((TextView) view.findViewById(R.id.hint)).setText(text);
537             mOldPassword = (TextView) view.findViewById(R.id.old_password);
538             mOldPassword.setVisibility(View.VISIBLE);
539             mOldPassword.addTextChangedListener(this);
540             mError = (TextView) view.findViewById(R.id.error);
541 
542             AlertDialog dialog = new AlertDialog.Builder(CredentialStorage.this)
543                     .setView(view)
544                     .setTitle(R.string.credentials_unlock)
545                     .setPositiveButton(android.R.string.ok, this)
546                     .setNegativeButton(android.R.string.cancel, this)
547                     .create();
548             dialog.setOnDismissListener(this);
549             dialog.show();
550             mButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
551             mButton.setEnabled(false);
552         }
553 
554         @Override
afterTextChanged(Editable editable)555         public void afterTextChanged(Editable editable) {
556             mButton.setEnabled(mOldPassword == null || mOldPassword.getText().length() > 0);
557         }
558 
559         @Override
beforeTextChanged(CharSequence s, int start, int count, int after)560         public void beforeTextChanged(CharSequence s, int start, int count, int after) {
561         }
562 
563         @Override
onTextChanged(CharSequence s, int start, int before, int count)564         public void onTextChanged(CharSequence s, int start, int before, int count) {
565         }
566 
567         @Override
onClick(DialogInterface dialog, int button)568         public void onClick(DialogInterface dialog, int button) {
569             mUnlockConfirmed = (button == DialogInterface.BUTTON_POSITIVE);
570         }
571 
572         @Override
onDismiss(DialogInterface dialog)573         public void onDismiss(DialogInterface dialog) {
574             if (mUnlockConfirmed) {
575                 mUnlockConfirmed = false;
576                 mError.setVisibility(View.VISIBLE);
577                 mKeyStore.unlock(mOldPassword.getText().toString());
578                 int error = mKeyStore.getLastError();
579                 if (error == KeyStore.NO_ERROR) {
580                     mRetriesRemaining = -1;
581                     Toast.makeText(CredentialStorage.this,
582                             R.string.credentials_enabled,
583                             Toast.LENGTH_SHORT).show();
584                     // aha, now we are unlocked, switch to key guard.
585                     // we'll end up back in onResume to install
586                     ensureKeyGuard();
587                 } else if (error == KeyStore.UNINITIALIZED) {
588                     mRetriesRemaining = -1;
589                     Toast.makeText(CredentialStorage.this,
590                             R.string.credentials_erased,
591                             Toast.LENGTH_SHORT).show();
592                     // we are reset, we can now set new password with key guard
593                     handleUnlockOrInstall();
594                 } else if (error >= KeyStore.WRONG_PASSWORD) {
595                     // we need to try again
596                     mRetriesRemaining = error - KeyStore.WRONG_PASSWORD + 1;
597                     handleUnlockOrInstall();
598                 }
599                 return;
600             }
601             finish();
602         }
603     }
604 }
605