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.keychain; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.app.ActivityManager; 22 import android.app.AlertDialog; 23 import android.app.PendingIntent; 24 import android.app.admin.DevicePolicyEventLogger; 25 import android.app.admin.DevicePolicyManager; 26 import android.app.admin.IDevicePolicyManager; 27 import android.content.Context; 28 import android.content.DialogInterface; 29 import android.content.Intent; 30 import android.content.pm.PackageManager; 31 import android.content.pm.UserInfo; 32 import android.content.res.Resources; 33 import android.net.Uri; 34 import android.os.AsyncTask; 35 import android.os.Bundle; 36 import android.os.Handler; 37 import android.os.IBinder; 38 import android.os.Looper; 39 import android.os.RemoteException; 40 import android.os.ServiceManager; 41 import android.os.UserManager; 42 import android.security.IKeyChainAliasCallback; 43 import android.security.KeyChain; 44 import android.stats.devicepolicy.DevicePolicyEnums; 45 import android.util.Log; 46 import android.view.LayoutInflater; 47 import android.view.View; 48 import android.view.ViewGroup; 49 import android.widget.AdapterView; 50 import android.widget.BaseAdapter; 51 import android.widget.ListView; 52 import android.widget.RadioButton; 53 import android.widget.TextView; 54 55 import androidx.appcompat.app.AppCompatActivity; 56 57 import com.android.internal.annotations.VisibleForTesting; 58 import com.android.keychain.internal.KeyInfoProvider; 59 60 import com.google.android.material.snackbar.Snackbar; 61 62 import org.bouncycastle.asn1.x509.X509Name; 63 64 import java.io.IOException; 65 import java.security.KeyStore; 66 import java.security.KeyStoreException; 67 import java.security.NoSuchAlgorithmException; 68 import java.security.cert.Certificate; 69 import java.security.cert.CertificateException; 70 import java.security.cert.X509Certificate; 71 import java.util.ArrayList; 72 import java.util.Arrays; 73 import java.util.Collections; 74 import java.util.Enumeration; 75 import java.util.List; 76 import java.util.concurrent.ExecutionException; 77 import java.util.concurrent.ExecutorService; 78 import java.util.concurrent.Executors; 79 import java.util.stream.Collectors; 80 81 import javax.security.auth.x500.X500Principal; 82 83 import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; 84 85 public class KeyChainActivity extends AppCompatActivity { 86 private static final String TAG = "KeyChain"; 87 88 // The amount of time to delay showing a snackbar. If the alias is received before the snackbar 89 // is shown, the activity will finish. If the certificate selection dialog is shown before the 90 // snackbar, no snackbar will be shown. 91 private static final long SNACKBAR_DELAY_TIME = 2000; 92 // The minimum amount of time to display a snackbar while loading certificates. 93 private static final long SNACKBAR_MIN_TIME = 1000; 94 95 private int mSenderUid; 96 private String mSenderPackageName; 97 98 // beware that some of these KeyStore operations such as saw and 99 // get do file I/O in the remote keystore process and while they 100 // do not cause StrictMode violations, they logically should not 101 // be done on the UI thread. 102 private final KeyStore mKeyStore = getKeyStore(); 103 getKeyStore()104 private static KeyStore getKeyStore() { 105 try { 106 final KeyStore keystore = KeyStore.getInstance("AndroidKeyStore"); 107 keystore.load(null); 108 return keystore; 109 } catch (KeyStoreException | IOException | NoSuchAlgorithmException 110 | CertificateException e) { 111 Log.e(TAG, "Error opening AndroidKeyStore.", e); 112 throw new RuntimeException("Error opening AndroidKeyStore.", e); 113 } 114 } 115 116 // A snackbar to show the user while the KeyChain Activity is loading the certificates. 117 private Snackbar mSnackbar; 118 119 // A remote service may call {@link android.security.KeyChain#choosePrivateKeyAlias} multiple 120 // times, which will result in multiple intents being sent to KeyChainActivity. The time of the 121 // first received intent is recorded in order to ensure the snackbar is displayed for a 122 // minimum amount of time after receiving the first intent. 123 private long mFirstIntentReceivedTimeMillis = 0L; 124 125 private ExecutorService executor = Executors.newSingleThreadExecutor(); 126 private Handler handler = new Handler(Looper.getMainLooper()); 127 private final Runnable mFinishActivity = KeyChainActivity.this::finish; 128 private final Runnable mShowSnackBar = this::showSnackBar; 129 130 @Override onCreate(Bundle savedState)131 protected void onCreate(Bundle savedState) { 132 super.onCreate(savedState); 133 setContentView(R.layout.keychain_activity); 134 getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); 135 } 136 137 /** 138 * Returns the package name which the activity with {@code activityToken} is launched from. 139 */ 140 @Nullable getCallingAppPackageName(IBinder activityToken)141 private static String getCallingAppPackageName(IBinder activityToken) { 142 String pkg = null; 143 try { 144 pkg = ActivityManager.getService().getLaunchedFromPackage(activityToken); 145 } catch (RemoteException e) { 146 Log.v(TAG, "Could not talk to activity manager.", e); 147 } 148 return pkg; 149 } 150 151 @Override onResume()152 public void onResume() { 153 super.onResume(); 154 155 final IBinder activityToken = getActivityToken(); 156 mSenderPackageName = getCallingAppPackageName(activityToken); 157 if (mSenderPackageName == null) { 158 //if no sender, bail, we need to identify the app to the user securely. 159 finish(null); 160 return; 161 } 162 try { 163 mSenderUid = getPackageManager().getPackageInfo( 164 mSenderPackageName, 0).applicationInfo.uid; 165 } catch (PackageManager.NameNotFoundException e) { 166 // if unable to find the sender package info bail, 167 // we need to identify the app to the user securely. 168 finish(null); 169 return; 170 } 171 172 chooseCertificate(); 173 } 174 175 @Override onNewIntent(Intent intent)176 protected void onNewIntent(Intent intent) { 177 super.onNewIntent(intent); 178 handler.removeCallbacks(mFinishActivity); 179 } 180 showSnackBar()181 private void showSnackBar() { 182 mFirstIntentReceivedTimeMillis = System.currentTimeMillis(); 183 mSnackbar = Snackbar.make(findViewById(R.id.container), 184 String.format(getResources().getString(R.string.loading_certs_message), 185 getApplicationLabel()), Snackbar.LENGTH_INDEFINITE); 186 mSnackbar.show(); 187 } 188 finishSnackBar()189 private void finishSnackBar() { 190 if (mSnackbar != null) { 191 mSnackbar.dismiss(); 192 mSnackbar = null; 193 } else { 194 handler.removeCallbacks(mShowSnackBar); 195 } 196 } 197 chooseCertificate()198 private void chooseCertificate() { 199 // Start loading the set of certs to choose from now- if device policy doesn't return an 200 // alias, having aliases loading already will save some time waiting for UI to start. 201 KeyInfoProvider keyInfoProvider = new KeyInfoProvider() { 202 public boolean isUserSelectable(String alias) { 203 try (KeyChain.KeyChainConnection connection = 204 KeyChain.bind(KeyChainActivity.this)) { 205 return connection.getService().isUserSelectable(alias); 206 } 207 catch (InterruptedException ignored) { 208 Log.e(TAG, "interrupted while checking if key is user-selectable", ignored); 209 Thread.currentThread().interrupt(); 210 return false; 211 } catch (Exception | AssertionError ignored) { 212 Log.e(TAG, "error while checking if key is user-selectable", ignored); 213 return false; 214 } 215 } 216 }; 217 218 Log.i(TAG, String.format("Requested by app uid %d to provide a private key alias", 219 mSenderUid)); 220 221 String[] keyTypes = getIntent().getStringArrayExtra(KeyChain.EXTRA_KEY_TYPES); 222 if (keyTypes == null) { 223 keyTypes = new String[]{}; 224 } 225 Log.i(TAG, String.format("Key types specified: %s", Arrays.toString(keyTypes))); 226 227 ArrayList<byte[]> issuers = (ArrayList<byte[]>) getIntent().getSerializableExtra( 228 KeyChain.EXTRA_ISSUERS); 229 if (issuers == null) { 230 issuers = new ArrayList<byte[]>(); 231 } else { 232 Log.i(TAG, "Issuers specified, will be listed later."); 233 } 234 235 final AliasLoader loader = new AliasLoader(mKeyStore, this, keyInfoProvider, 236 new CertificateParametersFilter(mKeyStore, keyTypes, issuers)); 237 loader.execute(); 238 239 final IKeyChainAliasCallback.Stub callback = new IKeyChainAliasCallback.Stub() { 240 @Override public void alias(String alias) { 241 Log.i(TAG, String.format("Alias provided by device policy client: %s", alias)); 242 // Use policy-suggested alias if provided or abort further actions if alias is 243 // KeyChain.KEY_ALIAS_SELECTION_DENIED 244 if (alias != null) { 245 finishWithAliasFromPolicy(alias); 246 return; 247 } 248 249 // No suggested alias - instead finish loading and show UI to pick one 250 final CertificateAdapter certAdapter; 251 try { 252 certAdapter = loader.get(); 253 } catch (InterruptedException | ExecutionException e) { 254 Log.e(TAG, "Loading certificate aliases interrupted", e); 255 finish(null); 256 return; 257 } 258 /* 259 * If there are no keys for the user to choose from, do not display 260 * the dialog. This is in line with what other operating systems do. 261 */ 262 if (!certAdapter.hasKeysToChoose()) { 263 Log.i(TAG, "No keys to choose from"); 264 finish(null); 265 return; 266 } 267 runOnUiThread(() -> { 268 finishSnackBar(); 269 displayCertChooserDialog(certAdapter); 270 }); 271 } 272 }; 273 274 // Show a snackbar to the user to indicate long-running task. 275 if (mSnackbar == null) { 276 handler.postDelayed(mShowSnackBar, SNACKBAR_DELAY_TIME); 277 } 278 Uri uri = getIntent().getParcelableExtra(KeyChain.EXTRA_URI); 279 String alias = getIntent().getStringExtra(KeyChain.EXTRA_ALIAS); 280 281 if (isManagedDevice()) { 282 // Give a profile or device owner the chance to intercept the request, if a private key 283 // access listener is registered with the DevicePolicyManagerService. 284 IDevicePolicyManager devicePolicyManager = IDevicePolicyManager.Stub.asInterface( 285 ServiceManager.getService(Context.DEVICE_POLICY_SERVICE)); 286 try { 287 devicePolicyManager.choosePrivateKeyAlias(mSenderUid, uri, alias, callback); 288 } catch (RemoteException e) { 289 Log.e(TAG, "Unable to request alias from DevicePolicyManager", e); 290 // Proceed without a suggested alias. 291 try { 292 callback.alias(null); 293 } catch (RemoteException shouldNeverHappen) { 294 finish(null); 295 } 296 } 297 } else { 298 // If the device is unmanaged, check whether the credential management app has provided 299 // an alias for the given uri and calling package name. 300 getAliasFromCredentialManagementApp(uri, callback); 301 } 302 } 303 isManagedDevice()304 private boolean isManagedDevice() { 305 DevicePolicyManager devicePolicyManager = getSystemService(DevicePolicyManager.class); 306 return devicePolicyManager.getDeviceOwner() != null 307 || devicePolicyManager.getProfileOwner() != null 308 || hasManagedProfile(); 309 } 310 hasManagedProfile()311 private boolean hasManagedProfile() { 312 UserManager userManager = getSystemService(UserManager.class); 313 for (final UserInfo userInfo : userManager.getProfiles(getUserId())) { 314 if (userInfo.isManagedProfile()) { 315 return true; 316 } 317 } 318 return false; 319 } 320 getAliasFromCredentialManagementApp(Uri uri, IKeyChainAliasCallback.Stub callback)321 private void getAliasFromCredentialManagementApp(Uri uri, 322 IKeyChainAliasCallback.Stub callback) { 323 executor.execute(() -> { 324 try (KeyChain.KeyChainConnection keyChainConnection = KeyChain.bind(this)) { 325 String chosenAlias = null; 326 if (keyChainConnection.getService().hasCredentialManagementApp()) { 327 Log.i(TAG, "There is a credential management app on the device. " 328 + "Looking for an alias in the policy."); 329 chosenAlias = keyChainConnection.getService() 330 .getPredefinedAliasForPackageAndUri(mSenderPackageName, uri); 331 if (chosenAlias != null) { 332 keyChainConnection.getService().setGrant(mSenderUid, chosenAlias, true); 333 Log.w(TAG, String.format("Selected alias %s from the " 334 + "credential management app's policy", chosenAlias)); 335 DevicePolicyEventLogger 336 .createEvent(DevicePolicyEnums 337 .CREDENTIAL_MANAGEMENT_APP_CREDENTIAL_FOUND_IN_POLICY) 338 .write(); 339 } else { 340 Log.i(TAG, "No alias provided from the credential management app"); 341 } 342 } 343 callback.alias(chosenAlias); 344 } catch (InterruptedException | RemoteException | AssertionError e) { 345 Log.e(TAG, "Unable to request find predefined alias from credential " 346 + "management app policy"); 347 // Proceed without a suggested alias. 348 try { 349 callback.alias(null); 350 } catch (RemoteException shouldNeverHappen) { 351 finish(null); 352 } finally { 353 DevicePolicyEventLogger 354 .createEvent(DevicePolicyEnums 355 .CREDENTIAL_MANAGEMENT_APP_POLICY_LOOKUP_FAILED) 356 .write(); 357 } 358 } 359 }); 360 } 361 362 @VisibleForTesting 363 public static class CertificateParametersFilter { 364 private final KeyStore mKeyStore; 365 private final List<String> mKeyTypes; 366 private final List<X500Principal> mIssuers; 367 CertificateParametersFilter(KeyStore keyStore, @NonNull String[] keyTypes, @NonNull ArrayList<byte[]> issuers)368 public CertificateParametersFilter(KeyStore keyStore, 369 @NonNull String[] keyTypes, @NonNull ArrayList<byte[]> issuers) { 370 mKeyStore = keyStore; 371 mKeyTypes = Arrays.asList(keyTypes); 372 mIssuers = new ArrayList<X500Principal>(); 373 for (byte[] issuer : issuers) { 374 try { 375 X500Principal issuerPrincipal = new X500Principal(issuer); 376 Log.i(TAG, "Added issuer: " + issuerPrincipal.getName()); 377 mIssuers.add(new X500Principal(issuer)); 378 } catch (IllegalArgumentException e) { 379 Log.w(TAG, "Skipping invalid issuer", e); 380 } 381 } 382 } 383 shouldPresentCertificate(String alias)384 public boolean shouldPresentCertificate(String alias) { 385 X509Certificate cert = loadCertificate(mKeyStore, alias); 386 // If there's no certificate associated with the alias, skip. 387 if (cert == null) { 388 Log.i(TAG, String.format("No certificate associated with alias %s", alias)); 389 return false; 390 } 391 List<X509Certificate> certChain = new ArrayList(loadCertificateChain(mKeyStore, alias)); 392 Log.i(TAG, String.format("Inspecting certificate %s aliased with %s, chain length %d", 393 cert.getSubjectDN().getName(), alias, certChain.size())); 394 395 // If the caller has provided a list of key types to restrict the certificates 396 // offered for selection, skip this alias if the key algorithm is not in that 397 // list. 398 // Note that the end entity (leaf) certificate's public key has to be compatible 399 // with the specified key algorithm, not any one of the chain (see RFC5246 400 // section 7.4.6) 401 String keyAlgorithm = cert.getPublicKey().getAlgorithm(); 402 Log.i(TAG, String.format("Certificate key algorithm: %s", keyAlgorithm)); 403 if (!mKeyTypes.isEmpty() && !mKeyTypes.contains(keyAlgorithm)) { 404 return false; 405 } 406 407 // If the caller has provided a list of issuers to restrict the certificates 408 // offered for selection, skip this alias if none of the issuers in the client 409 // certificate chain is in that list. 410 List<X500Principal> chainIssuers = new ArrayList(); 411 chainIssuers.add(cert.getIssuerX500Principal()); 412 for (X509Certificate intermediate : certChain) { 413 X500Principal subject = intermediate.getSubjectX500Principal(); 414 Log.i(TAG, String.format("Subject of intermediate in client certificate chain: %s", 415 subject.getName())); 416 // Collect the subjects of all the intermediates, as the RFC specifies that 417 // "one of the certificates in the certificate chain SHOULD be issued by one of 418 // the listed CAs." 419 chainIssuers.add(subject); 420 } 421 422 if (!mIssuers.isEmpty()) { 423 for (X500Principal issuer : chainIssuers) { 424 if (mIssuers.contains(issuer)) { 425 Log.i(TAG, String.format("Requested issuer found: %s", issuer)); 426 return true; 427 } 428 } 429 return false; 430 } 431 432 return true; 433 } 434 } 435 436 @VisibleForTesting 437 static class AliasLoader extends AsyncTask<Void, Void, CertificateAdapter> { 438 private final KeyStore mKeyStore; 439 private final Context mContext; 440 private final KeyInfoProvider mInfoProvider; 441 private final CertificateParametersFilter mCertificateFilter; 442 AliasLoader(KeyStore keyStore, Context context, KeyInfoProvider infoProvider, CertificateParametersFilter certificateFilter)443 public AliasLoader(KeyStore keyStore, Context context, 444 KeyInfoProvider infoProvider, CertificateParametersFilter certificateFilter) { 445 mKeyStore = keyStore; 446 mContext = context; 447 mInfoProvider = infoProvider; 448 mCertificateFilter = certificateFilter; 449 } 450 doInBackground(Void... params)451 @Override protected CertificateAdapter doInBackground(Void... params) { 452 final List<String> rawAliasList = new ArrayList<>(); 453 try { 454 final Enumeration<String> aliases = mKeyStore.aliases(); 455 while (aliases.hasMoreElements()) { 456 final String alias = aliases.nextElement(); 457 if (mKeyStore.isKeyEntry(alias)) { 458 rawAliasList.add(alias); 459 } 460 } 461 } catch (KeyStoreException e) { 462 Log.e(TAG, "Error while loading entries from keystore. " 463 + "List may be empty or incomplete."); 464 } 465 466 return new CertificateAdapter(mKeyStore, mContext, 467 rawAliasList.stream().filter(mInfoProvider::isUserSelectable) 468 .filter(mCertificateFilter::shouldPresentCertificate) 469 .sorted().collect(Collectors.toList())); 470 } 471 } 472 displayCertChooserDialog(final CertificateAdapter adapter)473 private void displayCertChooserDialog(final CertificateAdapter adapter) { 474 if (adapter.mAliases.isEmpty()) { 475 Log.w(TAG, "Should not be asked to display the cert chooser without aliases."); 476 finish(null); 477 return; 478 } 479 480 AlertDialog.Builder builder = new AlertDialog.Builder(this); 481 builder.setNegativeButton(R.string.deny_button, new DialogInterface.OnClickListener() { 482 @Override public void onClick(DialogInterface dialog, int id) { 483 dialog.cancel(); // will cause OnDismissListener to be called 484 } 485 }); 486 487 int selectedItem = -1; 488 Resources res = getResources(); 489 String alias = getIntent().getStringExtra(KeyChain.EXTRA_ALIAS); 490 491 if (alias != null) { 492 // if alias was requested, set it if found 493 int adapterPosition = adapter.mAliases.indexOf(alias); 494 if (adapterPosition != -1) { 495 // increase by 1 to account for item 0 being the header. 496 selectedItem = adapterPosition + 1; 497 } 498 } else if (adapter.mAliases.size() == 1) { 499 // if only one choice, preselect it 500 selectedItem = 1; 501 } 502 503 builder.setPositiveButton(R.string.allow_button, new DialogInterface.OnClickListener() { 504 @Override public void onClick(DialogInterface dialog, int id) { 505 if (dialog instanceof AlertDialog) { 506 ListView lv = ((AlertDialog) dialog).getListView(); 507 int listViewPosition = lv.getCheckedItemPosition(); 508 int adapterPosition = listViewPosition-1; 509 String alias = ((adapterPosition >= 0) 510 ? adapter.getItem(adapterPosition) 511 : null); 512 Log.i(TAG, String.format("User chose: %s", alias)); 513 finish(alias); 514 } else { 515 Log.wtf(TAG, "Expected AlertDialog, got " + dialog, new Exception()); 516 finish(null); 517 } 518 } 519 }); 520 521 builder.setTitle(res.getString(R.string.title_select_cert)); 522 builder.setSingleChoiceItems(adapter, selectedItem, null); 523 final AlertDialog dialog = builder.create(); 524 525 // Show text above the list to explain what the certificate will be used for. 526 TextView contextView = (TextView) View.inflate( 527 this, R.layout.cert_chooser_header, null); 528 529 final ListView lv = dialog.getListView(); 530 lv.addHeaderView(contextView, null, false); 531 lv.setOnItemClickListener(new AdapterView.OnItemClickListener() { 532 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 533 if (position == 0) { 534 // Header. Just text; ignore clicks. 535 return; 536 } else { 537 dialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(true); 538 lv.setItemChecked(position, true); 539 adapter.notifyDataSetChanged(); 540 } 541 } 542 }); 543 544 String contextMessage = String.format(res.getString(R.string.requesting_application), 545 getApplicationLabel()); 546 Uri uri = getIntent().getParcelableExtra(KeyChain.EXTRA_URI); 547 if (uri != null) { 548 String hostMessage = String.format(res.getString(R.string.requesting_server), 549 Uri.encode(uri.getAuthority(), "$,;:@&=+")); 550 if (contextMessage == null) { 551 contextMessage = hostMessage; 552 } else { 553 contextMessage += " " + hostMessage; 554 } 555 } 556 contextView.setText(contextMessage); 557 558 if (selectedItem == -1) { 559 dialog.setOnShowListener(new DialogInterface.OnShowListener() { 560 @Override 561 public void onShow(DialogInterface dialogInterface) { 562 dialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(false); 563 } 564 }); 565 } 566 dialog.setOnCancelListener(new DialogInterface.OnCancelListener() { 567 @Override public void onCancel(DialogInterface dialog) { 568 finish(null); 569 } 570 }); 571 dialog.create(); 572 // Prevents screen overlay attack. 573 dialog.getButton(DialogInterface.BUTTON_POSITIVE).setFilterTouchesWhenObscured(true); 574 dialog.show(); 575 } 576 getApplicationLabel()577 private String getApplicationLabel() { 578 PackageManager pm = getPackageManager(); 579 try { 580 return pm.getApplicationLabel(pm.getApplicationInfo(mSenderPackageName, 0)).toString(); 581 } catch (PackageManager.NameNotFoundException e) { 582 return mSenderPackageName; 583 } 584 } 585 586 @VisibleForTesting 587 static class CertificateAdapter extends BaseAdapter { 588 private final List<String> mAliases; 589 private final List<String> mSubjects = new ArrayList<String>(); 590 private final KeyStore mKeyStore; 591 private final Context mContext; 592 CertificateAdapter(KeyStore keyStore, Context context, List<String> aliases)593 private CertificateAdapter(KeyStore keyStore, Context context, List<String> aliases) { 594 mAliases = aliases; 595 mSubjects.addAll(Collections.nCopies(aliases.size(), (String) null)); 596 mKeyStore = keyStore; 597 mContext = context; 598 } getCount()599 @Override public int getCount() { 600 return mAliases.size(); 601 } getItem(int adapterPosition)602 @Override public String getItem(int adapterPosition) { 603 return mAliases.get(adapterPosition); 604 } getItemId(int adapterPosition)605 @Override public long getItemId(int adapterPosition) { 606 return adapterPosition; 607 } getView(final int adapterPosition, View view, ViewGroup parent)608 @Override public View getView(final int adapterPosition, View view, ViewGroup parent) { 609 ViewHolder holder; 610 if (view == null) { 611 LayoutInflater inflater = LayoutInflater.from(mContext); 612 view = inflater.inflate(R.layout.cert_item, parent, false); 613 holder = new ViewHolder(); 614 holder.mAliasTextView = (TextView) view.findViewById(R.id.cert_item_alias); 615 holder.mSubjectTextView = (TextView) view.findViewById(R.id.cert_item_subject); 616 holder.mRadioButton = (RadioButton) view.findViewById(R.id.cert_item_selected); 617 view.setTag(holder); 618 } else { 619 holder = (ViewHolder) view.getTag(); 620 } 621 622 String alias = mAliases.get(adapterPosition); 623 624 holder.mAliasTextView.setText(alias); 625 626 String subject = mSubjects.get(adapterPosition); 627 if (subject == null) { 628 new CertLoader(adapterPosition, holder.mSubjectTextView).execute(); 629 } else { 630 holder.mSubjectTextView.setText(subject); 631 } 632 633 ListView lv = (ListView)parent; 634 int listViewCheckedItemPosition = lv.getCheckedItemPosition(); 635 int adapterCheckedItemPosition = listViewCheckedItemPosition-1; 636 holder.mRadioButton.setChecked(adapterPosition == adapterCheckedItemPosition); 637 return view; 638 } 639 640 /** 641 * Returns true if there are keys to choose from. 642 */ hasKeysToChoose()643 public boolean hasKeysToChoose() { 644 return !mAliases.isEmpty(); 645 } 646 647 private class CertLoader extends AsyncTask<Void, Void, String> { 648 private final int mAdapterPosition; 649 private final TextView mSubjectView; CertLoader(int adapterPosition, TextView subjectView)650 private CertLoader(int adapterPosition, TextView subjectView) { 651 mAdapterPosition = adapterPosition; 652 mSubjectView = subjectView; 653 } doInBackground(Void... params)654 @Override protected String doInBackground(Void... params) { 655 String alias = mAliases.get(mAdapterPosition); 656 X509Certificate cert = loadCertificate(mKeyStore, alias); 657 if (cert == null) { 658 return null; 659 } 660 // bouncycastle can handle the emailAddress OID of 1.2.840.113549.1.9.1 661 X500Principal subjectPrincipal = cert.getSubjectX500Principal(); 662 X509Name subjectName = X509Name.getInstance(subjectPrincipal.getEncoded()); 663 return subjectName.toString(true, X509Name.DefaultSymbols); 664 } onPostExecute(String subjectString)665 @Override protected void onPostExecute(String subjectString) { 666 mSubjects.set(mAdapterPosition, subjectString); 667 mSubjectView.setText(subjectString); 668 } 669 } 670 } 671 672 private static class ViewHolder { 673 TextView mAliasTextView; 674 TextView mSubjectTextView; 675 RadioButton mRadioButton; 676 } 677 finish(String alias)678 private void finish(String alias) { 679 finish(alias, false); 680 } 681 finishWithAliasFromPolicy(String alias)682 private void finishWithAliasFromPolicy(String alias) { 683 finish(alias, true); 684 } 685 finish(String alias, boolean isAliasFromPolicy)686 private void finish(String alias, boolean isAliasFromPolicy) { 687 if (alias == null || alias.equals(KeyChain.KEY_ALIAS_SELECTION_DENIED)) { 688 alias = null; 689 setResult(RESULT_CANCELED); 690 } else { 691 Intent result = new Intent(); 692 result.putExtra(Intent.EXTRA_TEXT, alias); 693 setResult(RESULT_OK, result); 694 } 695 IKeyChainAliasCallback keyChainAliasResponse 696 = IKeyChainAliasCallback.Stub.asInterface( 697 getIntent().getIBinderExtra(KeyChain.EXTRA_RESPONSE)); 698 if (keyChainAliasResponse != null) { 699 new ResponseSender(keyChainAliasResponse, alias, isAliasFromPolicy).execute(); 700 return; 701 } 702 finishActivity(); 703 } 704 705 private class ResponseSender extends AsyncTask<Void, Void, Void> { 706 private IKeyChainAliasCallback mKeyChainAliasResponse; 707 private String mAlias; 708 private boolean mFromPolicy; 709 ResponseSender(IKeyChainAliasCallback keyChainAliasResponse, String alias, boolean isFromPolicy)710 private ResponseSender(IKeyChainAliasCallback keyChainAliasResponse, String alias, 711 boolean isFromPolicy) { 712 mKeyChainAliasResponse = keyChainAliasResponse; 713 mAlias = alias; 714 mFromPolicy = isFromPolicy; 715 } doInBackground(Void... unused)716 @Override protected Void doInBackground(Void... unused) { 717 if (mAlias == null) { 718 respondWithAlias(null); 719 return null; 720 } 721 try (KeyChain.KeyChainConnection connection = KeyChain.bind(KeyChainActivity.this)) { 722 // This is a safety check to make sure an alias was not somehow chosen by 723 // the user but is not user-selectable. 724 // However, if the alias was selected by the Device Owner / Profile Owner 725 // (by implementing DeviceAdminReceiver), then there's no need to check 726 // this. 727 if (!mFromPolicy && (!connection.getService().isUserSelectable(mAlias))) { 728 Log.w(TAG, String.format("Alias %s not user-selectable.", mAlias)); 729 respondWithAlias(null); 730 return null; 731 } 732 connection.getService().setGrant(mSenderUid, mAlias, true); 733 respondWithAlias(mAlias); 734 } catch (InterruptedException ignored) { 735 Thread.currentThread().interrupt(); 736 Log.d(TAG, "interrupted while granting access", ignored); 737 respondWithAlias(null); 738 } catch (IllegalArgumentException ignored) { 739 Log.d(TAG, "attempt to set grant on a non-existent alias", ignored); 740 respondWithAlias(null); 741 } catch (Exception | AssertionError ignored) { 742 // Catchall so we always call mKeyChainAliasResponse. 743 // AssertionError is thrown in case of failure to connect to the service. 744 Log.e(TAG, "error while granting access", ignored); 745 respondWithAlias(null); 746 } 747 return null; 748 } 749 respondWithAlias(String alias)750 private void respondWithAlias(String alias) { 751 try { 752 mKeyChainAliasResponse.alias(alias); 753 } catch (Exception e) { 754 // don't just catch RemoteException, caller could 755 // throw back a RuntimeException across processes 756 // which we should protect against. 757 Log.e(TAG, "Error while returning alias", e); 758 } 759 } 760 onPostExecute(Void unused)761 @Override protected void onPostExecute(Void unused) { 762 finishActivity(); 763 } 764 } 765 finishActivity()766 private void finishActivity() { 767 long timeElapsedSinceFirstIntent = 768 System.currentTimeMillis() - mFirstIntentReceivedTimeMillis; 769 if (mFirstIntentReceivedTimeMillis == 0L 770 || timeElapsedSinceFirstIntent > SNACKBAR_MIN_TIME) { 771 finishSnackBar(); 772 finish(); 773 } else { 774 long remainingTimeToShowSnackBar = SNACKBAR_MIN_TIME - timeElapsedSinceFirstIntent; 775 handler.postDelayed(mFinishActivity, remainingTimeToShowSnackBar); 776 } 777 } 778 onBackPressed()779 @Override public void onBackPressed() { 780 finish(null); 781 } 782 loadCertificate(KeyStore keyStore, String alias)783 private static X509Certificate loadCertificate(KeyStore keyStore, String alias) { 784 final Certificate cert; 785 try { 786 if (keyStore.isCertificateEntry(alias)) { 787 return null; 788 } 789 cert = keyStore.getCertificate(alias); 790 } catch (KeyStoreException e) { 791 Log.e(TAG, String.format("Error trying to retrieve certificate for \"%s\".", alias), e); 792 return null; 793 } 794 if (cert != null) { 795 if (cert instanceof X509Certificate) { 796 return (X509Certificate) cert; 797 } else { 798 Log.w(TAG, String.format("Certificate associated with alias \"%s\" is not X509.", 799 alias)); 800 } 801 } 802 return null; 803 } 804 loadCertificateChain(KeyStore keyStore, String alias)805 private static List<X509Certificate> loadCertificateChain(KeyStore keyStore, 806 String alias) { 807 final Certificate[] certs; 808 final boolean isCertificateEntry; 809 try { 810 isCertificateEntry = keyStore.isCertificateEntry(alias); 811 certs = keyStore.getCertificateChain(alias); 812 } catch (KeyStoreException e) { 813 Log.e(TAG, String.format("Error trying to retrieve certificate chain for \"%s\".", 814 alias), e); 815 return Collections.emptyList(); 816 } 817 final List<X509Certificate> result = new ArrayList<>(); 818 // If this is a certificate entry we return the single certificate. Otherwise we trim the 819 // leaf and return only the rest of the chain. 820 for (int i = isCertificateEntry ? 0 : 1; i < certs.length; ++i) { 821 if (certs[i] instanceof X509Certificate) { 822 result.add((X509Certificate) certs[i]); 823 } else { 824 Log.w(TAG,"A certificate in the chain of alias \"" 825 + alias + "\" is not X509."); 826 return Collections.emptyList(); 827 } 828 } 829 return result; 830 } 831 } 832