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.security; 18 19 import android.app.Activity; 20 import android.app.admin.DevicePolicyManager; 21 import android.content.Context; 22 import android.content.DialogInterface; 23 import android.content.Intent; 24 import android.content.pm.PackageManager; 25 import android.content.pm.UserInfo; 26 import android.content.res.Resources; 27 import android.os.AsyncTask; 28 import android.os.Bundle; 29 import android.os.Process; 30 import android.os.RemoteException; 31 import android.os.UserHandle; 32 import android.os.UserManager; 33 import android.security.Credentials; 34 import android.security.KeyChain; 35 import android.security.KeyChain.KeyChainConnection; 36 import android.security.KeyStore; 37 import android.text.TextUtils; 38 import android.util.Log; 39 import android.widget.Toast; 40 41 import androidx.appcompat.app.AlertDialog; 42 import androidx.fragment.app.FragmentActivity; 43 44 import com.android.internal.widget.LockPatternUtils; 45 import com.android.org.bouncycastle.asn1.ASN1InputStream; 46 import com.android.org.bouncycastle.asn1.pkcs.PrivateKeyInfo; 47 import com.android.settings.R; 48 import com.android.settings.password.ChooseLockSettingsHelper; 49 import com.android.settings.vpn2.VpnUtils; 50 51 import java.io.ByteArrayInputStream; 52 import java.io.IOException; 53 54 import sun.security.util.ObjectIdentifier; 55 import sun.security.x509.AlgorithmId; 56 57 /** 58 * CredentialStorage handles resetting and installing keys into KeyStore. 59 */ 60 public final class CredentialStorage extends FragmentActivity { 61 62 private static final String TAG = "CredentialStorage"; 63 64 public static final String ACTION_INSTALL = "com.android.credentials.INSTALL"; 65 public static final String ACTION_RESET = "com.android.credentials.RESET"; 66 67 // This is the minimum acceptable password quality. If the current password quality is 68 // lower than this, keystore should not be activated. 69 public static final int MIN_PASSWORD_QUALITY = DevicePolicyManager.PASSWORD_QUALITY_SOMETHING; 70 71 private static final int CONFIRM_CLEAR_SYSTEM_CREDENTIAL_REQUEST = 1; 72 73 private final KeyStore mKeyStore = KeyStore.getInstance(); 74 private LockPatternUtils mUtils; 75 76 /** 77 * When non-null, the bundle containing credentials to install. 78 */ 79 private Bundle mInstallBundle; 80 81 @Override onCreate(Bundle savedState)82 protected void onCreate(Bundle savedState) { 83 super.onCreate(savedState); 84 mUtils = new LockPatternUtils(this); 85 } 86 87 @Override onResume()88 protected void onResume() { 89 super.onResume(); 90 91 final Intent intent = getIntent(); 92 final String action = intent.getAction(); 93 final UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE); 94 if (!userManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_CREDENTIALS)) { 95 if (ACTION_RESET.equals(action)) { 96 new ResetDialog(); 97 } else { 98 if (ACTION_INSTALL.equals(action) && checkCallerIsCertInstallerOrSelfInProfile()) { 99 mInstallBundle = intent.getExtras(); 100 } 101 handleInstall(); 102 } 103 } else { 104 finish(); 105 } 106 } 107 108 /** 109 * Install credentials from mInstallBundle into Keystore. 110 */ handleInstall()111 private void handleInstall() { 112 // something already decided we are done, do not proceed 113 if (isFinishing()) { 114 return; 115 } 116 if (installIfAvailable()) { 117 finish(); 118 } 119 } 120 isHardwareBackedKey(byte[] keyData)121 private boolean isHardwareBackedKey(byte[] keyData) { 122 try { 123 final ASN1InputStream bIn = new ASN1InputStream(new ByteArrayInputStream(keyData)); 124 final PrivateKeyInfo pki = PrivateKeyInfo.getInstance(bIn.readObject()); 125 final String algOid = pki.getPrivateKeyAlgorithm().getAlgorithm().getId(); 126 final String algName = new AlgorithmId(new ObjectIdentifier(algOid)).getName(); 127 128 return KeyChain.isBoundKeyAlgorithm(algName); 129 } catch (IOException e) { 130 Log.e(TAG, "Failed to parse key data"); 131 return false; 132 } 133 } 134 135 /** 136 * Install credentials if available, otherwise do nothing. 137 * 138 * @return true if the installation is done and the activity should be finished, false if 139 * an asynchronous task is pending and will finish the activity when it's done. 140 */ installIfAvailable()141 private boolean installIfAvailable() { 142 if (mInstallBundle == null || mInstallBundle.isEmpty()) { 143 return true; 144 } 145 146 final Bundle bundle = mInstallBundle; 147 mInstallBundle = null; 148 149 final int uid = bundle.getInt(Credentials.EXTRA_INSTALL_AS_UID, KeyStore.UID_SELF); 150 151 if (uid != KeyStore.UID_SELF && !UserHandle.isSameUser(uid, Process.myUid())) { 152 final int dstUserId = UserHandle.getUserId(uid); 153 154 // Restrict install target to the wifi uid. 155 if (uid != Process.WIFI_UID) { 156 Log.e(TAG, "Failed to install credentials as uid " + uid + ": cross-user installs" 157 + " may only target wifi uids"); 158 return true; 159 } 160 161 final Intent installIntent = new Intent(ACTION_INSTALL) 162 .setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT) 163 .putExtras(bundle); 164 startActivityAsUser(installIntent, new UserHandle(dstUserId)); 165 return true; 166 } 167 168 boolean shouldFinish = true; 169 if (bundle.containsKey(Credentials.EXTRA_USER_PRIVATE_KEY_NAME)) { 170 final String key = bundle.getString(Credentials.EXTRA_USER_PRIVATE_KEY_NAME); 171 final byte[] value = bundle.getByteArray(Credentials.EXTRA_USER_PRIVATE_KEY_DATA); 172 173 if (!mKeyStore.importKey(key, value, uid, KeyStore.FLAG_NONE)) { 174 Log.e(TAG, "Failed to install " + key + " as uid " + uid); 175 return true; 176 } 177 // The key was prepended USER_PRIVATE_KEY by the CredentialHelper. However, 178 // KeyChain internally uses the raw alias name and only prepends USER_PRIVATE_KEY 179 // to the key name when interfacing with KeyStore. 180 // This is generally a symptom of CredentialStorage and CredentialHelper relying 181 // on internal implementation details of KeyChain and imitating its functionality 182 // rather than delegating to KeyChain for the certificate installation. 183 if (uid == Process.SYSTEM_UID || uid == KeyStore.UID_SELF) { 184 new MarkKeyAsUserSelectable( 185 key.replaceFirst("^" + Credentials.USER_PRIVATE_KEY, "")).execute(); 186 shouldFinish = false; 187 } 188 } 189 190 final int flags = KeyStore.FLAG_NONE; 191 192 if (bundle.containsKey(Credentials.EXTRA_USER_CERTIFICATE_NAME)) { 193 final String certName = bundle.getString(Credentials.EXTRA_USER_CERTIFICATE_NAME); 194 final byte[] certData = bundle.getByteArray(Credentials.EXTRA_USER_CERTIFICATE_DATA); 195 196 if (!mKeyStore.put(certName, certData, uid, flags)) { 197 Log.e(TAG, "Failed to install " + certName + " as uid " + uid); 198 return shouldFinish; 199 } 200 } 201 202 if (bundle.containsKey(Credentials.EXTRA_CA_CERTIFICATES_NAME)) { 203 final String caListName = bundle.getString(Credentials.EXTRA_CA_CERTIFICATES_NAME); 204 final byte[] caListData = bundle.getByteArray(Credentials.EXTRA_CA_CERTIFICATES_DATA); 205 206 if (!mKeyStore.put(caListName, caListData, uid, flags)) { 207 Log.e(TAG, "Failed to install " + caListName + " as uid " + uid); 208 return shouldFinish; 209 } 210 } 211 212 // Send the broadcast. 213 final Intent broadcast = new Intent(KeyChain.ACTION_KEYCHAIN_CHANGED); 214 sendBroadcast(broadcast); 215 216 setResult(RESULT_OK); 217 return shouldFinish; 218 } 219 220 /** 221 * Prompt for reset confirmation, resetting on confirmation, finishing otherwise. 222 */ 223 private class ResetDialog 224 implements DialogInterface.OnClickListener, DialogInterface.OnDismissListener { 225 private boolean mResetConfirmed; 226 ResetDialog()227 private ResetDialog() { 228 final AlertDialog dialog = new AlertDialog.Builder(CredentialStorage.this) 229 .setTitle(android.R.string.dialog_alert_title) 230 .setMessage(R.string.credentials_reset_hint) 231 .setPositiveButton(android.R.string.ok, this) 232 .setNegativeButton(android.R.string.cancel, this) 233 .create(); 234 dialog.setOnDismissListener(this); 235 dialog.show(); 236 } 237 238 @Override onClick(DialogInterface dialog, int button)239 public void onClick(DialogInterface dialog, int button) { 240 mResetConfirmed = (button == DialogInterface.BUTTON_POSITIVE); 241 } 242 243 @Override onDismiss(DialogInterface dialog)244 public void onDismiss(DialogInterface dialog) { 245 if (!mResetConfirmed) { 246 finish(); 247 return; 248 } 249 250 mResetConfirmed = false; 251 if (!mUtils.isSecure(UserHandle.myUserId())) { 252 // This task will call finish() in the end. 253 new ResetKeyStoreAndKeyChain().execute(); 254 } else if (!confirmKeyGuard(CONFIRM_CLEAR_SYSTEM_CREDENTIAL_REQUEST)) { 255 Log.w(TAG, "Failed to launch credential confirmation for a secure user."); 256 finish(); 257 } 258 // Confirmation result will be handled in onActivityResult if needed. 259 } 260 } 261 262 /** 263 * Background task to handle reset of both keystore and user installed CAs. 264 */ 265 private class ResetKeyStoreAndKeyChain extends AsyncTask<Void, Void, Boolean> { 266 267 @Override doInBackground(Void... unused)268 protected Boolean doInBackground(Void... unused) { 269 270 // Clear all the users credentials could have been installed in for this user. 271 mUtils.resetKeyStore(UserHandle.myUserId()); 272 273 try { 274 final KeyChainConnection keyChainConnection = KeyChain.bind(CredentialStorage.this); 275 try { 276 return keyChainConnection.getService().reset(); 277 } catch (RemoteException e) { 278 return false; 279 } finally { 280 keyChainConnection.close(); 281 } 282 } catch (InterruptedException e) { 283 Thread.currentThread().interrupt(); 284 return false; 285 } 286 } 287 288 @Override onPostExecute(Boolean success)289 protected void onPostExecute(Boolean success) { 290 if (success) { 291 Toast.makeText(CredentialStorage.this, 292 R.string.credentials_erased, Toast.LENGTH_SHORT).show(); 293 clearLegacyVpnIfEstablished(); 294 } else { 295 Toast.makeText(CredentialStorage.this, 296 R.string.credentials_not_erased, Toast.LENGTH_SHORT).show(); 297 } 298 finish(); 299 } 300 } 301 clearLegacyVpnIfEstablished()302 private void clearLegacyVpnIfEstablished() { 303 final boolean isDone = VpnUtils.disconnectLegacyVpn(getApplicationContext()); 304 if (isDone) { 305 Toast.makeText(CredentialStorage.this, R.string.vpn_disconnected, 306 Toast.LENGTH_SHORT).show(); 307 } 308 } 309 310 /** 311 * Background task to mark a given key alias as user-selectable, so that 312 * it can be selected by users from the Certificate Selection prompt. 313 */ 314 private class MarkKeyAsUserSelectable extends AsyncTask<Void, Void, Boolean> { 315 final String mAlias; 316 MarkKeyAsUserSelectable(String alias)317 MarkKeyAsUserSelectable(String alias) { 318 mAlias = alias; 319 } 320 321 @Override doInBackground(Void... unused)322 protected Boolean doInBackground(Void... unused) { 323 try (KeyChainConnection keyChainConnection = KeyChain.bind(CredentialStorage.this)) { 324 keyChainConnection.getService().setUserSelectable(mAlias, true); 325 return true; 326 } catch (RemoteException e) { 327 Log.w(TAG, "Failed to mark key " + mAlias + " as user-selectable."); 328 return false; 329 } catch (InterruptedException e) { 330 Log.w(TAG, "Failed to mark key " + mAlias + " as user-selectable."); 331 Thread.currentThread().interrupt(); 332 return false; 333 } 334 } 335 336 @Override onPostExecute(Boolean result)337 protected void onPostExecute(Boolean result) { 338 Log.i(TAG, String.format("Marked alias %s as selectable, success? %s", 339 mAlias, result)); 340 CredentialStorage.this.finish(); 341 } 342 } 343 344 /** 345 * Check that the caller is either certinstaller or Settings running in a profile of this user. 346 */ checkCallerIsCertInstallerOrSelfInProfile()347 private boolean checkCallerIsCertInstallerOrSelfInProfile() { 348 if (TextUtils.equals("com.android.certinstaller", getCallingPackage())) { 349 // CertInstaller is allowed to install credentials if it has the same signature as 350 // Settings package. 351 return getPackageManager().checkSignatures( 352 getCallingPackage(), getPackageName()) == PackageManager.SIGNATURE_MATCH; 353 } 354 355 final int launchedFromUserId; 356 try { 357 final int launchedFromUid = android.app.ActivityManager.getService() 358 .getLaunchedFromUid(getActivityToken()); 359 if (launchedFromUid == -1) { 360 Log.e(TAG, ACTION_INSTALL + " must be started with startActivityForResult"); 361 return false; 362 } 363 if (!UserHandle.isSameApp(launchedFromUid, Process.myUid())) { 364 // Not the same app 365 return false; 366 } 367 launchedFromUserId = UserHandle.getUserId(launchedFromUid); 368 } catch (RemoteException re) { 369 // Error talking to ActivityManager, just give up 370 return false; 371 } 372 373 final UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE); 374 final UserInfo parentInfo = userManager.getProfileParent(launchedFromUserId); 375 // Caller is running in a profile of this user 376 return ((parentInfo != null) && (parentInfo.id == UserHandle.myUserId())); 377 } 378 379 /** 380 * Confirm existing key guard, returning password via onActivityResult. 381 */ confirmKeyGuard(int requestCode)382 private boolean confirmKeyGuard(int requestCode) { 383 final Resources res = getResources(); 384 return new ChooseLockSettingsHelper(this) 385 .launchConfirmationActivity(requestCode, 386 res.getText(R.string.credentials_title), true); 387 } 388 389 @Override onActivityResult(int requestCode, int resultCode, Intent data)390 public void onActivityResult(int requestCode, int resultCode, Intent data) { 391 super.onActivityResult(requestCode, resultCode, data); 392 if (requestCode == CONFIRM_CLEAR_SYSTEM_CREDENTIAL_REQUEST) { 393 if (resultCode == Activity.RESULT_OK) { 394 new ResetKeyStoreAndKeyChain().execute(); 395 return; 396 } 397 // failed confirmation, bail 398 finish(); 399 } 400 } 401 } 402