| /* |
| * Copyright (C) 2011 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.settings.security; |
| |
| import android.app.Activity; |
| import android.app.admin.DevicePolicyManager; |
| import android.content.Context; |
| import android.content.DialogInterface; |
| import android.content.Intent; |
| import android.content.pm.PackageManager; |
| import android.content.pm.UserInfo; |
| import android.content.res.Resources; |
| import android.os.AsyncTask; |
| import android.os.Bundle; |
| import android.os.Process; |
| import android.os.RemoteException; |
| import android.os.UserHandle; |
| import android.os.UserManager; |
| import android.security.Credentials; |
| import android.security.IKeyChainService; |
| import android.security.KeyChain; |
| import android.security.KeyChain.KeyChainConnection; |
| import android.security.KeyStore; |
| import android.text.TextUtils; |
| import android.util.Log; |
| import android.widget.Toast; |
| |
| import androidx.appcompat.app.AlertDialog; |
| import androidx.fragment.app.FragmentActivity; |
| |
| import com.android.internal.widget.LockPatternUtils; |
| import com.android.settings.R; |
| import com.android.settings.password.ChooseLockSettingsHelper; |
| import com.android.settings.vpn2.VpnUtils; |
| import com.android.settingslib.core.lifecycle.HideNonSystemOverlayMixin; |
| |
| /** |
| * CredentialStorage handles resetting and installing keys into KeyStore. |
| */ |
| public final class CredentialStorage extends FragmentActivity { |
| |
| private static final String TAG = "CredentialStorage"; |
| |
| public static final String ACTION_INSTALL = "com.android.credentials.INSTALL"; |
| public static final String ACTION_RESET = "com.android.credentials.RESET"; |
| |
| // This is the minimum acceptable password quality. If the current password quality is |
| // lower than this, keystore should not be activated. |
| public static final int MIN_PASSWORD_QUALITY = DevicePolicyManager.PASSWORD_QUALITY_SOMETHING; |
| |
| private static final int CONFIRM_CLEAR_SYSTEM_CREDENTIAL_REQUEST = 1; |
| |
| private LockPatternUtils mUtils; |
| |
| /** |
| * When non-null, the bundle containing credentials to install. |
| */ |
| private Bundle mInstallBundle; |
| |
| @Override |
| protected void onCreate(Bundle savedState) { |
| super.onCreate(savedState); |
| mUtils = new LockPatternUtils(this); |
| getLifecycle().addObserver(new HideNonSystemOverlayMixin(this)); |
| } |
| |
| @Override |
| protected void onResume() { |
| super.onResume(); |
| |
| final Intent intent = getIntent(); |
| final String action = intent.getAction(); |
| final UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE); |
| if (!userManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_CREDENTIALS)) { |
| if (ACTION_RESET.equals(action) && checkCallerIsSelf()) { |
| new ResetDialog(); |
| } else { |
| if (ACTION_INSTALL.equals(action) && checkCallerIsCertInstallerOrSelfInProfile()) { |
| mInstallBundle = intent.getExtras(); |
| } |
| handleInstall(); |
| } |
| } else { |
| finish(); |
| } |
| } |
| |
| /** |
| * Install credentials from mInstallBundle into Keystore. |
| */ |
| private void handleInstall() { |
| // something already decided we are done, do not proceed |
| if (isFinishing()) { |
| return; |
| } |
| if (installIfAvailable()) { |
| finish(); |
| } |
| } |
| |
| /** |
| * Install credentials if available, otherwise do nothing. |
| * |
| * @return true if the installation is done and the activity should be finished, false if |
| * an asynchronous task is pending and will finish the activity when it's done. |
| */ |
| private boolean installIfAvailable() { |
| if (mInstallBundle == null || mInstallBundle.isEmpty()) { |
| return true; |
| } |
| |
| final Bundle bundle = mInstallBundle; |
| mInstallBundle = null; |
| |
| final int uid = bundle.getInt(Credentials.EXTRA_INSTALL_AS_UID, KeyStore.UID_SELF); |
| |
| if (uid != KeyStore.UID_SELF && !UserHandle.isSameUser(uid, Process.myUid())) { |
| final int dstUserId = UserHandle.getUserId(uid); |
| |
| // Restrict install target to the wifi uid. |
| if (uid != Process.WIFI_UID) { |
| Log.e(TAG, "Failed to install credentials as uid " + uid + ": cross-user installs" |
| + " may only target wifi uids"); |
| return true; |
| } |
| |
| final Intent installIntent = new Intent(ACTION_INSTALL) |
| .setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT) |
| .putExtras(bundle); |
| startActivityAsUser(installIntent, new UserHandle(dstUserId)); |
| return true; |
| } |
| |
| String alias = bundle.getString(Credentials.EXTRA_USER_KEY_ALIAS, null); |
| if (TextUtils.isEmpty(alias)) { |
| Log.e(TAG, "Cannot install key without an alias"); |
| return true; |
| } |
| |
| final byte[] privateKeyData = bundle.getByteArray(Credentials.EXTRA_USER_PRIVATE_KEY_DATA); |
| final byte[] certData = bundle.getByteArray(Credentials.EXTRA_USER_CERTIFICATE_DATA); |
| final byte[] caListData = bundle.getByteArray(Credentials.EXTRA_CA_CERTIFICATES_DATA); |
| new InstallKeyInKeyChain(alias, privateKeyData, certData, caListData, uid).execute(); |
| |
| return false; |
| } |
| |
| /** |
| * Prompt for reset confirmation, resetting on confirmation, finishing otherwise. |
| */ |
| private class ResetDialog |
| implements DialogInterface.OnClickListener, DialogInterface.OnDismissListener { |
| private boolean mResetConfirmed; |
| |
| private ResetDialog() { |
| final AlertDialog dialog = new AlertDialog.Builder(CredentialStorage.this) |
| .setTitle(android.R.string.dialog_alert_title) |
| .setMessage(R.string.credentials_reset_hint) |
| .setPositiveButton(android.R.string.ok, this) |
| .setNegativeButton(android.R.string.cancel, this) |
| .create(); |
| dialog.setOnDismissListener(this); |
| dialog.show(); |
| } |
| |
| @Override |
| public void onClick(DialogInterface dialog, int button) { |
| mResetConfirmed = (button == DialogInterface.BUTTON_POSITIVE); |
| } |
| |
| @Override |
| public void onDismiss(DialogInterface dialog) { |
| if (!mResetConfirmed) { |
| finish(); |
| return; |
| } |
| |
| mResetConfirmed = false; |
| if (!mUtils.isSecure(UserHandle.myUserId())) { |
| // This task will call finish() in the end. |
| new ResetKeyStoreAndKeyChain().execute(); |
| } else if (!confirmKeyGuard(CONFIRM_CLEAR_SYSTEM_CREDENTIAL_REQUEST)) { |
| Log.w(TAG, "Failed to launch credential confirmation for a secure user."); |
| finish(); |
| } |
| // Confirmation result will be handled in onActivityResult if needed. |
| } |
| } |
| |
| /** |
| * Background task to handle reset of both keystore and user installed CAs. |
| */ |
| private class ResetKeyStoreAndKeyChain extends AsyncTask<Void, Void, Boolean> { |
| |
| @Override |
| protected Boolean doInBackground(Void... unused) { |
| |
| // Clear all the users credentials could have been installed in for this user. |
| mUtils.resetKeyStore(UserHandle.myUserId()); |
| |
| try { |
| final KeyChainConnection keyChainConnection = KeyChain.bind(CredentialStorage.this); |
| try { |
| return keyChainConnection.getService().reset(); |
| } catch (RemoteException e) { |
| return false; |
| } finally { |
| keyChainConnection.close(); |
| } |
| } catch (InterruptedException e) { |
| Thread.currentThread().interrupt(); |
| return false; |
| } |
| } |
| |
| @Override |
| protected void onPostExecute(Boolean success) { |
| if (success) { |
| Toast.makeText(CredentialStorage.this, |
| R.string.credentials_erased, Toast.LENGTH_SHORT).show(); |
| clearLegacyVpnIfEstablished(); |
| } else { |
| Toast.makeText(CredentialStorage.this, |
| R.string.credentials_not_erased, Toast.LENGTH_SHORT).show(); |
| } |
| finish(); |
| } |
| } |
| |
| private void clearLegacyVpnIfEstablished() { |
| final boolean isDone = VpnUtils.disconnectLegacyVpn(getApplicationContext()); |
| if (isDone) { |
| Toast.makeText(CredentialStorage.this, R.string.vpn_disconnected, |
| Toast.LENGTH_SHORT).show(); |
| } |
| } |
| |
| /** |
| * Background task to install a certificate into KeyChain or the WiFi Keystore. |
| */ |
| private class InstallKeyInKeyChain extends AsyncTask<Void, Void, Boolean> { |
| final String mAlias; |
| private final byte[] mKeyData; |
| private final byte[] mCertData; |
| private final byte[] mCaListData; |
| private final int mUid; |
| |
| InstallKeyInKeyChain(String alias, byte[] keyData, byte[] certData, byte[] caListData, |
| int uid) { |
| mAlias = alias; |
| mKeyData = keyData; |
| mCertData = certData; |
| mCaListData = caListData; |
| mUid = uid; |
| } |
| |
| @Override |
| protected Boolean doInBackground(Void... unused) { |
| try (KeyChainConnection keyChainConnection = KeyChain.bind(CredentialStorage.this)) { |
| IKeyChainService service = keyChainConnection.getService(); |
| if (!service.installKeyPair(mKeyData, mCertData, mCaListData, mAlias, mUid)) { |
| Log.w(TAG, String.format("Failed installing key %s", mAlias)); |
| return false; |
| } |
| |
| // If this is not a WiFi key, mark it as user-selectable, so that it can be |
| // selected by users from the Certificate Selection prompt. |
| if (mUid == Process.SYSTEM_UID || mUid == KeyStore.UID_SELF) { |
| service.setUserSelectable(mAlias, true); |
| } |
| |
| return true; |
| } catch (RemoteException e) { |
| Log.w(TAG, String.format("Failed to install key %s to uid %d", mAlias, mUid), e); |
| return false; |
| } catch (InterruptedException e) { |
| Log.w(TAG, String.format("Interrupted while installing key %s", mAlias), e); |
| Thread.currentThread().interrupt(); |
| return false; |
| } |
| } |
| |
| @Override |
| protected void onPostExecute(Boolean result) { |
| CredentialStorage.this.onKeyInstalled(mAlias, mUid, result); |
| } |
| } |
| |
| private void onKeyInstalled(String alias, int uid, boolean result) { |
| if (!result) { |
| Log.w(TAG, String.format("Error installing alias %s for uid %d", alias, uid)); |
| finish(); |
| return; |
| } |
| |
| Log.i(TAG, String.format("Successfully installed alias %s to uid %d.", |
| alias, uid)); |
| |
| // Send the broadcast. |
| final Intent broadcast = new Intent(KeyChain.ACTION_KEYCHAIN_CHANGED); |
| sendBroadcast(broadcast); |
| setResult(RESULT_OK); |
| |
| finish(); |
| } |
| |
| /** |
| * Check that the caller is Settings. |
| */ |
| private boolean checkCallerIsSelf() { |
| try { |
| return Process.myUid() == android.app.ActivityManager.getService() |
| .getLaunchedFromUid(getActivityToken()); |
| } catch (RemoteException re) { |
| // Error talking to ActivityManager, just give up |
| return false; |
| } |
| } |
| |
| /** |
| * Check that the caller is either certinstaller or Settings running in a profile of this user. |
| */ |
| private boolean checkCallerIsCertInstallerOrSelfInProfile() { |
| if (TextUtils.equals("com.android.certinstaller", getCallingPackage())) { |
| // CertInstaller is allowed to install credentials if it has the same signature as |
| // Settings package. |
| return getPackageManager().checkSignatures( |
| getCallingPackage(), getPackageName()) == PackageManager.SIGNATURE_MATCH; |
| } |
| |
| final int launchedFromUserId; |
| try { |
| final int launchedFromUid = android.app.ActivityManager.getService() |
| .getLaunchedFromUid(getActivityToken()); |
| if (launchedFromUid == -1) { |
| Log.e(TAG, ACTION_INSTALL + " must be started with startActivityForResult"); |
| return false; |
| } |
| if (!UserHandle.isSameApp(launchedFromUid, Process.myUid())) { |
| // Not the same app |
| return false; |
| } |
| launchedFromUserId = UserHandle.getUserId(launchedFromUid); |
| } catch (RemoteException re) { |
| // Error talking to ActivityManager, just give up |
| return false; |
| } |
| |
| final UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE); |
| final UserInfo parentInfo = userManager.getProfileParent(launchedFromUserId); |
| // Caller is running in a profile of this user |
| return ((parentInfo != null) && (parentInfo.id == UserHandle.myUserId())); |
| } |
| |
| /** |
| * Confirm existing key guard, returning password via onActivityResult. |
| */ |
| private boolean confirmKeyGuard(int requestCode) { |
| final Resources res = getResources(); |
| final ChooseLockSettingsHelper.Builder builder = |
| new ChooseLockSettingsHelper.Builder(this); |
| return builder.setRequestCode(requestCode) |
| .setTitle(res.getText(R.string.credentials_title)) |
| .show(); |
| } |
| |
| @Override |
| public void onActivityResult(int requestCode, int resultCode, Intent data) { |
| super.onActivityResult(requestCode, resultCode, data); |
| if (requestCode == CONFIRM_CLEAR_SYSTEM_CREDENTIAL_REQUEST) { |
| if (resultCode == Activity.RESULT_OK) { |
| new ResetKeyStoreAndKeyChain().execute(); |
| return; |
| } |
| // failed confirmation, bail |
| finish(); |
| } |
| } |
| } |