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