/* * 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.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.org.bouncycastle.asn1.ASN1InputStream; import com.android.org.bouncycastle.asn1.pkcs.PrivateKeyInfo; import com.android.settings.R; import com.android.settings.password.ChooseLockSettingsHelper; import com.android.settings.vpn2.VpnUtils; import java.io.ByteArrayInputStream; import java.io.IOException; import sun.security.util.ObjectIdentifier; import sun.security.x509.AlgorithmId; /** * 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 final KeyStore mKeyStore = KeyStore.getInstance(); 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); } @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(); } } private boolean isHardwareBackedKey(byte[] keyData) { try { final ASN1InputStream bIn = new ASN1InputStream(new ByteArrayInputStream(keyData)); final PrivateKeyInfo pki = PrivateKeyInfo.getInstance(bIn.readObject()); final String algOid = pki.getPrivateKeyAlgorithm().getAlgorithm().getId(); final String algName = new AlgorithmId(new ObjectIdentifier(algOid)).getName(); return KeyChain.isBoundKeyAlgorithm(algName); } catch (IOException e) { Log.e(TAG, "Failed to parse key data"); return false; } } /** * 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; } boolean shouldFinish = true; if (bundle.containsKey(Credentials.EXTRA_USER_PRIVATE_KEY_NAME)) { final String key = bundle.getString(Credentials.EXTRA_USER_PRIVATE_KEY_NAME); final byte[] value = bundle.getByteArray(Credentials.EXTRA_USER_PRIVATE_KEY_DATA); if (!mKeyStore.importKey(key, value, uid, KeyStore.FLAG_NONE)) { Log.e(TAG, "Failed to install " + key + " as uid " + uid); return true; } // The key was prepended USER_PRIVATE_KEY by the CredentialHelper. However, // KeyChain internally uses the raw alias name and only prepends USER_PRIVATE_KEY // to the key name when interfacing with KeyStore. // This is generally a symptom of CredentialStorage and CredentialHelper relying // on internal implementation details of KeyChain and imitating its functionality // rather than delegating to KeyChain for the certificate installation. if (uid == Process.SYSTEM_UID || uid == KeyStore.UID_SELF) { new MarkKeyAsUserSelectable( key.replaceFirst("^" + Credentials.USER_PRIVATE_KEY, "")).execute(); shouldFinish = false; } } final int flags = KeyStore.FLAG_NONE; if (bundle.containsKey(Credentials.EXTRA_USER_CERTIFICATE_NAME)) { final String certName = bundle.getString(Credentials.EXTRA_USER_CERTIFICATE_NAME); final byte[] certData = bundle.getByteArray(Credentials.EXTRA_USER_CERTIFICATE_DATA); if (!mKeyStore.put(certName, certData, uid, flags)) { Log.e(TAG, "Failed to install " + certName + " as uid " + uid); return shouldFinish; } } if (bundle.containsKey(Credentials.EXTRA_CA_CERTIFICATES_NAME)) { final String caListName = bundle.getString(Credentials.EXTRA_CA_CERTIFICATES_NAME); final byte[] caListData = bundle.getByteArray(Credentials.EXTRA_CA_CERTIFICATES_DATA); if (!mKeyStore.put(caListName, caListData, uid, flags)) { Log.e(TAG, "Failed to install " + caListName + " as uid " + uid); return shouldFinish; } } // Send the broadcast. final Intent broadcast = new Intent(KeyChain.ACTION_KEYCHAIN_CHANGED); sendBroadcast(broadcast); setResult(RESULT_OK); return shouldFinish; } /** * 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 { @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 mark a given key alias as user-selectable, so that * it can be selected by users from the Certificate Selection prompt. */ private class MarkKeyAsUserSelectable extends AsyncTask { final String mAlias; MarkKeyAsUserSelectable(String alias) { mAlias = alias; } @Override protected Boolean doInBackground(Void... unused) { try (KeyChainConnection keyChainConnection = KeyChain.bind(CredentialStorage.this)) { keyChainConnection.getService().setUserSelectable(mAlias, true); return true; } catch (RemoteException e) { Log.w(TAG, "Failed to mark key " + mAlias + " as user-selectable."); return false; } catch (InterruptedException e) { Log.w(TAG, "Failed to mark key " + mAlias + " as user-selectable."); Thread.currentThread().interrupt(); return false; } } @Override protected void onPostExecute(Boolean result) { Log.i(TAG, String.format("Marked alias %s as selectable, success? %s", mAlias, result)); CredentialStorage.this.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(); return new ChooseLockSettingsHelper(this) .launchConfirmationActivity(requestCode, res.getText(R.string.credentials_title), true); } @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(); } } }