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