blob: 5c1d6c37d7778e2c85db8a6205936e1711732b8b [file] [log] [blame]
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/webauthn/authenticator_request_dialog_model.h"
#include <iterator>
#include <utility>
#include "base/base64.h"
#include "base/containers/contains.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_functions.h"
#include "base/observer_list.h"
#include "base/ranges/algorithm.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "build/build_config.h"
#include "build/buildflag.h"
#include "chrome/app/vector_icons/vector_icons.h"
#include "chrome/browser/password_manager/chrome_webauthn_credentials_delegate.h"
#include "chrome/browser/password_manager/chrome_webauthn_credentials_delegate_factory.h"
#include "chrome/browser/ui/webauthn/authenticator_request_dialog.h"
#include "chrome/browser/webauthn/webauthn_metrics_util.h"
#include "chrome/grit/generated_resources.h"
#include "components/password_manager/core/browser/passkey_credential.h"
#include "components/strings/grit/components_strings.h"
#include "components/vector_icons/vector_icons.h"
#include "content/public/browser/global_routing_id.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "device/fido/discoverable_credential_metadata.h"
#include "device/fido/features.h"
#include "device/fido/fido_authenticator.h"
#include "device/fido/fido_transport_protocol.h"
#include "device/fido/fido_types.h"
#include "device/fido/pin.h"
#include "device/fido/public_key_credential_user_entity.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/gfx/text_elider.h"
#if BUILDFLAG(IS_MAC)
#include "base/mac/mac_util.h"
#endif
namespace {
// BleEvent enumerates user-visible BLE events.
enum class BleEvent {
kAlreadyPowered = 0, // BLE was already powered.
kNeedsPowerAuto = 1, // BLE was not powered and so we asked the user.
kNeedsPowerManual = 2, // BLE was not powered and so we asked the user, but
// they have to do it manually.
kNewlyPowered = 3, // BLE wasn't powered, but the user turned it on.
kMaxValue = kNewlyPowered,
};
constexpr int GetMessageIdForTransportDescription(
AuthenticatorTransport transport) {
switch (transport) {
case AuthenticatorTransport::kUsbHumanInterfaceDevice:
return IDS_WEBAUTHN_TRANSPORT_USB;
case AuthenticatorTransport::kInternal:
return IDS_WEBAUTHN_TRANSPORT_INTERNAL;
case AuthenticatorTransport::kHybrid:
return IDS_WEBAUTHN_TRANSPORT_CABLE;
case AuthenticatorTransport::kAndroidAccessory:
return IDS_WEBAUTHN_TRANSPORT_AOA;
case AuthenticatorTransport::kBluetoothLowEnergy:
case AuthenticatorTransport::kNearFieldCommunication:
NOTREACHED();
return 0;
}
}
std::u16string GetTransportDescription(AuthenticatorTransport transport) {
const int msg_id = GetMessageIdForTransportDescription(transport);
if (!msg_id) {
return std::u16string();
}
return l10n_util::GetStringUTF16(msg_id);
}
constexpr int GetMessageIdForTransportShortDescription(
AuthenticatorTransport transport) {
switch (transport) {
case AuthenticatorTransport::kUsbHumanInterfaceDevice:
return IDS_WEBAUTHN_TRANSPORT_POPUP_USB;
case AuthenticatorTransport::kInternal:
return IDS_WEBAUTHN_TRANSPORT_POPUP_INTERNAL;
case AuthenticatorTransport::kHybrid:
return IDS_WEBAUTHN_TRANSPORT_POPUP_CABLE;
case AuthenticatorTransport::kAndroidAccessory:
return IDS_WEBAUTHN_TRANSPORT_POPUP_AOA;
case AuthenticatorTransport::kBluetoothLowEnergy:
case AuthenticatorTransport::kNearFieldCommunication:
NOTREACHED();
return 0;
}
}
std::u16string GetTransportShortDescription(AuthenticatorTransport transport) {
const int msg_id = GetMessageIdForTransportShortDescription(transport);
if (!msg_id) {
return std::u16string();
}
return l10n_util::GetStringUTF16(msg_id);
}
constexpr const gfx::VectorIcon& GetTransportIcon(
AuthenticatorTransport transport) {
switch (transport) {
case AuthenticatorTransport::kUsbHumanInterfaceDevice:
return vector_icons::kUsbIcon;
case AuthenticatorTransport::kInternal:
return kLaptopIcon;
case AuthenticatorTransport::kHybrid:
return kSmartphoneIcon;
case AuthenticatorTransport::kAndroidAccessory:
return kUsbCableIcon;
case AuthenticatorTransport::kBluetoothLowEnergy:
case AuthenticatorTransport::kNearFieldCommunication:
NOTREACHED();
return gfx::kNoneIcon;
}
}
// Whether to show Step::kCreatePasskey, which prompts the user before platform
// authenticator dispatch during MakeCredential. This is currently only shown on
// MacOS, because that is the only desktop platform authenticator without a
// "native" WebAuthn UI.
constexpr bool kShowCreatePlatformPasskeyStep = BUILDFLAG(IS_MAC);
password_manager::PasskeyCredential::Source ToPasswordManagerSource(
device::AuthenticatorType type) {
switch (type) {
case device::AuthenticatorType::kWinNative:
return password_manager::PasskeyCredential::Source::kWindowsHello;
case device::AuthenticatorType::kTouchID:
return password_manager::PasskeyCredential::Source::kTouchId;
case device::AuthenticatorType::kPhone:
return password_manager::PasskeyCredential::Source::kAndroidPhone;
case device::AuthenticatorType::kChromeOS:
case device::AuthenticatorType::kICloudKeychain:
case device::AuthenticatorType::kOther:
return password_manager::PasskeyCredential::Source::kOther;
}
}
} // namespace
AuthenticatorRequestDialogModel::EphemeralState::EphemeralState() = default;
AuthenticatorRequestDialogModel::EphemeralState::EphemeralState(
EphemeralState&&) = default;
AuthenticatorRequestDialogModel::EphemeralState&
AuthenticatorRequestDialogModel::EphemeralState::operator=(EphemeralState&&) =
default;
AuthenticatorRequestDialogModel::EphemeralState::~EphemeralState() = default;
AuthenticatorRequestDialogModel::Mechanism::Mechanism(
AuthenticatorRequestDialogModel::Mechanism::Type in_type,
std::u16string in_name,
std::u16string in_short_name,
const gfx::VectorIcon& in_icon,
base::RepeatingClosure in_callback)
: type(std::move(in_type)),
name(std::move(in_name)),
short_name(std::move(in_short_name)),
icon(in_icon),
callback(std::move(in_callback)) {}
AuthenticatorRequestDialogModel::Mechanism::~Mechanism() = default;
AuthenticatorRequestDialogModel::Mechanism::Mechanism(Mechanism&&) = default;
AuthenticatorRequestDialogModel::PairedPhone::PairedPhone(const PairedPhone&) =
default;
AuthenticatorRequestDialogModel::PairedPhone::PairedPhone(
const std::string& name,
size_t contact_id,
const std::array<uint8_t, device::kP256X962Length> public_key_x962) {
this->name = name;
this->contact_id = contact_id;
this->public_key_x962 = public_key_x962;
}
AuthenticatorRequestDialogModel::PairedPhone::~PairedPhone() = default;
AuthenticatorRequestDialogModel::PairedPhone&
AuthenticatorRequestDialogModel::PairedPhone::operator=(const PairedPhone&) =
default;
void AuthenticatorRequestDialogModel::ResetEphemeralState() {
ephemeral_state_ = {};
}
AuthenticatorRequestDialogModel::AuthenticatorRequestDialogModel(
content::RenderFrameHost* frame_host) {
if (frame_host) {
frame_host_id_ = frame_host->GetGlobalId();
}
}
AuthenticatorRequestDialogModel::~AuthenticatorRequestDialogModel() {
for (auto& observer : observers_) {
observer.OnModelDestroyed(this);
}
}
void AuthenticatorRequestDialogModel::HideDialog() {
SetCurrentStep(Step::kNotStarted);
}
void AuthenticatorRequestDialogModel::StartFlow(
TransportAvailabilityInfo transport_availability,
bool use_conditional_mediation) {
DCHECK(!started_);
DCHECK_EQ(current_step(), Step::kNotStarted);
started_ = true;
transport_availability_ = std::move(transport_availability);
use_conditional_mediation_ = use_conditional_mediation;
PopulateMechanisms();
priority_mechanism_index_ = IndexOfPriorityMechanism();
if (use_conditional_mediation_) {
// This is a conditional mediation request.
StartConditionalMediationRequest();
} else {
StartGuidedFlowForMostLikelyTransportOrShowMechanismSelection();
}
}
void AuthenticatorRequestDialogModel::StartOver() {
ResetEphemeralState();
for (auto& observer : observers_) {
observer.OnStartOver();
}
current_mechanism_.reset();
current_step_ = Step::kNotStarted;
SetCurrentStep(Step::kMechanismSelection);
}
void AuthenticatorRequestDialogModel::TransitionToModalWebAuthnRequest() {
DCHECK_EQ(current_step(), Step::kConditionalMediation);
// Dispatch requests to any plugged in authenticators.
for (auto& authenticator :
ephemeral_state_.saved_authenticators_.authenticator_list()) {
if (authenticator.transport != device::FidoTransportProtocol::kInternal) {
DispatchRequestAsync(&authenticator);
}
}
StartGuidedFlowForMostLikelyTransportOrShowMechanismSelection();
}
void AuthenticatorRequestDialogModel::
StartGuidedFlowForMostLikelyTransportOrShowMechanismSelection() {
if (pending_step_) {
SetCurrentStep(*pending_step_);
pending_step_.reset();
} else if (mechanisms_.empty()) {
if (transport_availability_.transport_list_did_include_internal) {
SetCurrentStep(Step::kErrorNoPasskeys);
} else {
SetCurrentStep(Step::kErrorNoAvailableTransports);
}
} else if (priority_mechanism_index_) {
mechanisms_[*priority_mechanism_index_].callback.Run();
} else {
SetCurrentStep(Step::kMechanismSelection);
}
}
void AuthenticatorRequestDialogModel::OnPhoneContactFailed(
const std::string& name) {
ContactNextPhoneByName(name);
}
void AuthenticatorRequestDialogModel::StartPhonePairing() {
DCHECK(cable_qr_string_);
SetCurrentStep(Step::kCableV2QRCode);
}
void AuthenticatorRequestDialogModel::
EnsureBleAdapterIsPoweredAndContinueWithStep(Step step) {
DCHECK(current_step() == Step::kMechanismSelection ||
current_step() == Step::kUsbInsertAndActivate ||
current_step() == Step::kCableActivate ||
current_step() == Step::kAndroidAccessory ||
current_step() == Step::kOffTheRecordInterstitial ||
current_step() == Step::kPreSelectAccount ||
current_step() == Step::kSelectAccount ||
current_step() == Step::kMechanismSelection ||
current_step() == Step::kConditionalMediation ||
current_step() == Step::kNotStarted)
<< "Invalid step " << static_cast<int>(current_step());
#if BUILDFLAG(IS_MAC)
if (transport_availability()->ble_access_denied) {
// |step| is not saved because macOS asks the user to restart Chrome
// after permission has been granted. So the user will end up retrying
// the whole WebAuthn request in the new process.
SetCurrentStep(Step::kBlePermissionMac);
return;
}
#endif
if (ble_adapter_is_powered()) {
base::UmaHistogramEnumeration("WebAuthentication.BLEUserEvents",
BleEvent::kAlreadyPowered);
SetCurrentStep(step);
return;
}
after_ble_adapter_powered_ =
base::BindOnce(&AuthenticatorRequestDialogModel::SetCurrentStep,
weak_factory_.GetWeakPtr(), step);
BleEvent event;
if (transport_availability()->can_power_on_ble_adapter) {
event = BleEvent::kNeedsPowerAuto;
SetCurrentStep(Step::kBlePowerOnAutomatic);
} else {
event = BleEvent::kNeedsPowerManual;
SetCurrentStep(Step::kBlePowerOnManual);
}
base::UmaHistogramEnumeration("WebAuthentication.BLEUserEvents", event);
}
void AuthenticatorRequestDialogModel::ContinueWithFlowAfterBleAdapterPowered() {
DCHECK(current_step() == Step::kBlePowerOnManual ||
current_step() == Step::kBlePowerOnAutomatic);
DCHECK(ble_adapter_is_powered());
base::UmaHistogramEnumeration("WebAuthentication.BLEUserEvents",
BleEvent::kNewlyPowered);
std::move(after_ble_adapter_powered_).Run();
}
void AuthenticatorRequestDialogModel::PowerOnBleAdapter() {
DCHECK_EQ(current_step(), Step::kBlePowerOnAutomatic);
if (!bluetooth_adapter_power_on_callback_) {
return;
}
bluetooth_adapter_power_on_callback_.Run();
}
#if BUILDFLAG(IS_MAC)
void AuthenticatorRequestDialogModel::OpenBlePreferences() {
DCHECK_EQ(current_step(), Step::kBlePermissionMac);
base::mac::OpenSystemSettingsPane(
base::mac::SystemSettingsPane::kPrivacySecurity_Bluetooth);
}
#endif // IS_MAC
void AuthenticatorRequestDialogModel::TryUsbDevice() {
DCHECK_EQ(current_step(), Step::kUsbInsertAndActivate);
}
void AuthenticatorRequestDialogModel::StartPlatformAuthenticatorFlow() {
// Never try the platform authenticator if the request is known in advance to
// fail. Proceed to a special error screen instead.
if (transport_availability_.request_type ==
device::FidoRequestType::kGetAssertion) {
DCHECK_NE(transport_availability_.has_platform_authenticator_credential,
device::FidoRequestHandlerBase::RecognizedCredential::kUnknown);
if (transport_availability_.has_platform_authenticator_credential ==
device::FidoRequestHandlerBase::RecognizedCredential::
kNoRecognizedCredential) {
SetCurrentStep(Step::kErrorInternalUnrecognized);
return;
}
// If the platform authenticator reports known credentials, show them in the
// UI.
if (!transport_availability_.recognized_platform_authenticator_credentials
.empty()) {
if (transport_availability_.has_empty_allow_list) {
// For discoverable credential requests, show an account picker.
ephemeral_state_.creds_ =
transport_availability_
.recognized_platform_authenticator_credentials;
SetCurrentStep(ephemeral_state_.creds_.size() == 1
? Step::kPreSelectSingleAccount
: Step::kPreSelectAccount);
} else {
// For requests with an allow list, pre-select a random credential and
// show that one to the user. For platform authenticators with optional
// UV (e.g. Touch ID), this step essentially acts as the user presence
// check.
ephemeral_state_.creds_ = {
transport_availability_
.recognized_platform_authenticator_credentials.front()};
SetCurrentStep(Step::kPreSelectSingleAccount);
}
return;
}
}
if (transport_availability_.request_type ==
device::FidoRequestType::kMakeCredential) {
if (kShowCreatePlatformPasskeyStep) {
SetCurrentStep(Step::kCreatePasskey);
return;
}
if (transport_availability_.is_off_the_record_context) {
// Step::kCreatePasskey incorporates an incognito warning if
// applicable, so the OTR interstitial step only needs to show in the
// "old" UI.
after_off_the_record_interstitial_ =
base::BindOnce(&AuthenticatorRequestDialogModel::
HideDialogAndDispatchToPlatformAuthenticator,
weak_factory_.GetWeakPtr(), absl::nullopt);
SetCurrentStep(Step::kOffTheRecordInterstitial);
return;
}
}
HideDialogAndDispatchToPlatformAuthenticator();
}
void AuthenticatorRequestDialogModel::OnOffTheRecordInterstitialAccepted() {
std::move(after_off_the_record_interstitial_).Run();
}
void AuthenticatorRequestDialogModel::ShowCableUsbFallback() {
DCHECK_EQ(current_step(), Step::kCableActivate);
SetCurrentStep(Step::kAndroidAccessory);
}
void AuthenticatorRequestDialogModel::ShowCable() {
DCHECK_EQ(current_step(), Step::kAndroidAccessory);
SetCurrentStep(Step::kCableActivate);
}
void AuthenticatorRequestDialogModel::Cancel() {
if (use_conditional_mediation_) {
// Conditional UI requests are never cancelled, they restart silently.
ResetEphemeralState();
for (auto& observer : observers_) {
observer.OnStartOver();
}
StartConditionalMediationRequest();
return;
}
if (is_request_complete()) {
SetCurrentStep(Step::kClosed);
}
for (auto& observer : observers_) {
observer.OnCancelRequest();
}
}
void AuthenticatorRequestDialogModel::ManageDevices() {
for (auto& observer : observers_) {
observer.OnManageDevicesClicked();
}
}
void AuthenticatorRequestDialogModel::OnSheetModelDidChange() {
for (auto& observer : observers_) {
observer.OnSheetModelChanged();
}
}
void AuthenticatorRequestDialogModel::AddObserver(Observer* observer) {
observers_.AddObserver(observer);
}
void AuthenticatorRequestDialogModel::RemoveObserver(Observer* observer) {
observers_.RemoveObserver(observer);
}
void AuthenticatorRequestDialogModel::OnRequestComplete() {
if (use_conditional_mediation_) {
auto* render_frame_host = content::RenderFrameHost::FromID(frame_host_id_);
auto* web_contents = GetWebContents();
if (web_contents && render_frame_host) {
ChromeWebAuthnCredentialsDelegateFactory::GetFactory(web_contents)
->GetDelegateForFrame(render_frame_host)
->NotifyWebAuthnRequestAborted();
}
}
SetCurrentStep(Step::kClosed);
}
void AuthenticatorRequestDialogModel::OnRequestTimeout() {
// The request may time out while the UI shows a different error.
if (!is_request_complete()) {
SetCurrentStep(Step::kTimedOut);
}
}
void AuthenticatorRequestDialogModel::OnActivatedKeyNotRegistered() {
DCHECK(!is_request_complete());
SetCurrentStep(Step::kKeyNotRegistered);
}
void AuthenticatorRequestDialogModel::OnActivatedKeyAlreadyRegistered() {
DCHECK(!is_request_complete());
SetCurrentStep(Step::kKeyAlreadyRegistered);
}
void AuthenticatorRequestDialogModel::OnSoftPINBlock() {
SetCurrentStep(Step::kClientPinErrorSoftBlock);
}
void AuthenticatorRequestDialogModel::OnHardPINBlock() {
SetCurrentStep(Step::kClientPinErrorHardBlock);
}
void AuthenticatorRequestDialogModel::OnAuthenticatorRemovedDuringPINEntry() {
SetCurrentStep(Step::kClientPinErrorAuthenticatorRemoved);
}
void AuthenticatorRequestDialogModel::OnAuthenticatorMissingResidentKeys() {
SetCurrentStep(Step::kMissingCapability);
}
void AuthenticatorRequestDialogModel::OnAuthenticatorMissingUserVerification() {
SetCurrentStep(Step::kMissingCapability);
}
void AuthenticatorRequestDialogModel::OnAuthenticatorMissingLargeBlob() {
// TODO(nsatragno): on Windows we should have a more accurate message if large
// blob is missing.
SetCurrentStep(Step::kMissingCapability);
}
void AuthenticatorRequestDialogModel::OnNoCommonAlgorithms() {
SetCurrentStep(Step::kMissingCapability);
}
void AuthenticatorRequestDialogModel::OnAuthenticatorStorageFull() {
SetCurrentStep(Step::kStorageFull);
}
void AuthenticatorRequestDialogModel::OnUserConsentDenied() {
if (use_conditional_mediation_) {
// Do not show a page-modal retry error sheet if the user cancelled out of
// their platform authenticator during a conditional UI request.
// Instead, retry silently.
Cancel();
return;
}
SetCurrentStep(Step::kErrorInternalUnrecognized);
}
bool AuthenticatorRequestDialogModel::OnWinUserCancelled() {
#if BUILDFLAG(IS_WIN)
if (use_conditional_mediation_) {
// Do not show a page-modal retry error sheet if the user cancelled out of
// their platform authenticator during a conditional UI request.
// Instead, retry silently.
Cancel();
return true;
}
// If the native Windows API was triggered immediately (i.e. before any Chrome
// dialog) then start the request over (once) if the user cancels the Windows
// UI and there are other options in Chrome's UI.
if (!have_restarted_due_to_windows_cancel_) {
bool have_other_option =
base::ranges::any_of(mechanisms_, [](const Mechanism& m) -> bool {
return absl::holds_alternative<Mechanism::Phone>(m.type) ||
absl::holds_alternative<Mechanism::AddPhone>(m.type);
});
bool windows_was_priority =
priority_mechanism_index_ &&
absl::holds_alternative<Mechanism::WindowsAPI>(
mechanisms_[*priority_mechanism_index_].type);
if (have_other_option && windows_was_priority) {
have_restarted_due_to_windows_cancel_ = true;
StartOver();
return true;
}
}
#endif
return false;
}
void AuthenticatorRequestDialogModel::OnBluetoothPoweredStateChanged(
bool powered) {
transport_availability_.is_ble_powered = powered;
for (auto& observer : observers_) {
observer.OnBluetoothPoweredStateChanged();
}
// For the manual flow, the user has to click the "next" button explicitly.
if (current_step() == Step::kBlePowerOnAutomatic) {
ContinueWithFlowAfterBleAdapterPowered();
}
}
void AuthenticatorRequestDialogModel::SetRequestCallback(
RequestCallback request_callback) {
request_callback_ = request_callback;
}
void AuthenticatorRequestDialogModel::SetAccountPreselectedCallback(
content::AuthenticatorRequestClientDelegate::AccountPreselectedCallback
callback) {
account_preselected_callback_ = callback;
}
void AuthenticatorRequestDialogModel::SetBluetoothAdapterPowerOnCallback(
base::RepeatingClosure bluetooth_adapter_power_on_callback) {
bluetooth_adapter_power_on_callback_ = bluetooth_adapter_power_on_callback;
}
void AuthenticatorRequestDialogModel::OnHavePIN(std::u16string pin) {
if (!pin_callback_) {
// Protect against the view submitting a PIN more than once without
// receiving a matching response first. |CollectPIN| is called again if
// the user needs to be prompted for a retry.
return;
}
std::move(pin_callback_).Run(pin);
}
void AuthenticatorRequestDialogModel::OnRetryUserVerification(int attempts) {
uv_attempts_ = attempts;
SetCurrentStep(Step::kRetryInternalUserVerification);
}
void AuthenticatorRequestDialogModel::OnResidentCredentialConfirmed() {
DCHECK_EQ(current_step(), Step::kResidentCredentialConfirmation);
HideDialogAndDispatchToPlatformAuthenticator();
}
void AuthenticatorRequestDialogModel::OnAttestationPermissionResponse(
bool attestation_permission_granted) {
if (!attestation_callback_) {
return;
}
std::move(attestation_callback_).Run(attestation_permission_granted);
}
void AuthenticatorRequestDialogModel::AddAuthenticator(
const device::FidoAuthenticator& authenticator) {
// Only the webauthn.dll authenticator omits a transport completely. This
// makes sense given how it works, but here it is treated as a platform
// authenticator and so given a `kInternal` transport.
DCHECK(authenticator.AuthenticatorTransport() ||
authenticator.GetType() == device::AuthenticatorType::kWinNative);
const AuthenticatorTransport transport =
authenticator.AuthenticatorTransport().value_or(
AuthenticatorTransport::kInternal);
AuthenticatorReference authenticator_reference(
authenticator.GetId(), transport, authenticator.GetType());
ephemeral_state_.saved_authenticators_.AddAuthenticator(
std::move(authenticator_reference));
}
void AuthenticatorRequestDialogModel::RemoveAuthenticator(
base::StringPiece authenticator_id) {
ephemeral_state_.saved_authenticators_.RemoveAuthenticator(authenticator_id);
}
// SelectAccount is called to trigger an account selection dialog.
void AuthenticatorRequestDialogModel::SelectAccount(
std::vector<device::AuthenticatorGetAssertionResponse> responses,
base::OnceCallback<void(device::AuthenticatorGetAssertionResponse)>
callback) {
ephemeral_state_.responses_ = std::move(responses);
ephemeral_state_.creds_ = {};
for (const auto& response : ephemeral_state_.responses_) {
ephemeral_state_.creds_.emplace_back(
device::AuthenticatorType::kOther, relying_party_id_,
response.credential->id, *response.user_entity);
}
selection_callback_ = std::move(callback);
SetCurrentStep(ephemeral_state_.creds_.size() == 1
? Step::kSelectSingleAccount
: Step::kSelectAccount);
}
void AuthenticatorRequestDialogModel::OnAccountSelected(size_t index) {
if (!selection_callback_) {
// It's possible that the user could activate the dialog more than once
// before the Webauthn request is completed and its torn down.
return;
}
device::AuthenticatorGetAssertionResponse response =
std::move(ephemeral_state_.responses_.at(index));
ephemeral_state_.creds_.clear();
ephemeral_state_.responses_.clear();
std::move(selection_callback_).Run(std::move(response));
}
void AuthenticatorRequestDialogModel::OnAccountPreselected(
const std::vector<uint8_t>& credential_id) {
for (size_t i = 0; i < creds().size(); ++i) {
if (creds().at(i).cred_id == credential_id) {
OnAccountPreselectedIndex(i);
return;
}
}
NOTREACHED() << "OnAccountPreselected() called with unknown credential_id "
<< base::HexEncode(credential_id);
}
void AuthenticatorRequestDialogModel::OnAccountPreselectedIndex(size_t index) {
// User selected one of the platform authenticator credentials enumerated in
// Conditional or regular modal UI prior to collecting user verification.
// Run `account_preselected_callback_` to narrow the request to the selected
// credential and dispatch to the platform authenticator.
const device::DiscoverableCredentialMetadata& cred = creds().at(index);
const device::AuthenticatorType source = cred.source;
DCHECK(account_preselected_callback_);
account_preselected_callback_.Run(cred.cred_id);
ephemeral_state_.creds_.clear();
HideDialogAndDispatchToPlatformAuthenticator(source);
}
void AuthenticatorRequestDialogModel::SetSelectedAuthenticatorForTesting(
AuthenticatorReference test_authenticator) {
ephemeral_state_.selected_authenticator_id_ =
test_authenticator.authenticator_id;
ephemeral_state_.saved_authenticators_.AddAuthenticator(
std::move(test_authenticator));
}
base::span<const AuthenticatorRequestDialogModel::Mechanism>
AuthenticatorRequestDialogModel::mechanisms() const {
return mechanisms_;
}
absl::optional<size_t> AuthenticatorRequestDialogModel::current_mechanism()
const {
return current_mechanism_;
}
void AuthenticatorRequestDialogModel::ContactPriorityPhone() {
for (auto& mechanism : mechanisms_) {
if (absl::holds_alternative<Mechanism::Phone>(mechanism.type)) {
mechanism.callback.Run();
return;
}
}
NOTREACHED();
}
void AuthenticatorRequestDialogModel::ContactPhoneForTesting(
const std::string& name) {
// Ensure BLE is powered so that `ContactPhone()` shows the "Check your phone"
// screen right away.
transport_availability_.is_ble_powered = true;
ContactPhone(name, /*mechanism_index=*/0);
}
void AuthenticatorRequestDialogModel::StartTransportFlowForTesting(
AuthenticatorTransport transport) {
StartGuidedFlowForTransport(transport, /*mechanism_index=*/0);
}
void AuthenticatorRequestDialogModel::SetCurrentStepForTesting(Step step) {
SetCurrentStep(step);
}
bool AuthenticatorRequestDialogModel::cable_should_suggest_usb() const {
// Offer AoA only for linked caBLEv2 authenticators, not caBLEv1.
return cable_ui_type_ != CableUIType::CABLE_V1 &&
base::Contains(transport_availability_.available_transports,
AuthenticatorTransport::kAndroidAccessory);
}
void AuthenticatorRequestDialogModel::CollectPIN(
device::pin::PINEntryReason reason,
device::pin::PINEntryError error,
uint32_t min_pin_length,
int attempts,
base::OnceCallback<void(std::u16string)> provide_pin_cb) {
pin_callback_ = std::move(provide_pin_cb);
min_pin_length_ = min_pin_length;
pin_error_ = error;
switch (reason) {
case device::pin::PINEntryReason::kChallenge:
pin_attempts_ = attempts;
SetCurrentStep(Step::kClientPinEntry);
return;
case device::pin::PINEntryReason::kChange:
SetCurrentStep(Step::kClientPinChange);
return;
case device::pin::PINEntryReason::kSet:
SetCurrentStep(Step::kClientPinSetup);
return;
}
}
void AuthenticatorRequestDialogModel::FinishCollectToken() {
SetCurrentStep(Step::kClientPinTapAgain);
}
void AuthenticatorRequestDialogModel::StartInlineBioEnrollment(
base::OnceClosure next_callback) {
max_bio_samples_ = absl::nullopt;
bio_samples_remaining_ = absl::nullopt;
bio_enrollment_callback_ = std::move(next_callback);
SetCurrentStep(Step::kInlineBioEnrollment);
}
void AuthenticatorRequestDialogModel::OnSampleCollected(
int bio_samples_remaining) {
DCHECK(current_step_ == Step::kInlineBioEnrollment);
bio_samples_remaining_ = bio_samples_remaining;
if (!max_bio_samples_) {
max_bio_samples_ = bio_samples_remaining + 1;
}
OnSheetModelDidChange();
}
void AuthenticatorRequestDialogModel::OnBioEnrollmentDone() {
std::move(bio_enrollment_callback_).Run();
}
void AuthenticatorRequestDialogModel::RequestAttestationPermission(
bool is_enterprise_attestation,
base::OnceCallback<void(bool)> callback) {
DCHECK(current_step_ != Step::kClosed);
attestation_callback_ = std::move(callback);
SetCurrentStep(is_enterprise_attestation
? Step::kEnterpriseAttestationPermissionRequest
: Step::kAttestationPermissionRequest);
}
void AuthenticatorRequestDialogModel::set_cable_transport_info(
absl::optional<bool> extension_is_v2,
std::vector<PairedPhone> paired_phones,
base::RepeatingCallback<void(size_t)> contact_phone_callback,
const absl::optional<std::string>& cable_qr_string) {
DCHECK(paired_phones.empty() || contact_phone_callback);
if (extension_is_v2.has_value()) {
cable_extension_provided_ = true;
if (*extension_is_v2) {
cable_ui_type_ = CableUIType::CABLE_V2_SERVER_LINK;
} else {
cable_ui_type_ = CableUIType::CABLE_V1;
}
} else {
cable_ui_type_ = CableUIType::CABLE_V2_2ND_FACTOR;
}
paired_phones_ = std::move(paired_phones);
contact_phone_callback_ = std::move(contact_phone_callback);
cable_qr_string_ = cable_qr_string;
paired_phones_contacted_.assign(paired_phones_.size(), false);
}
std::vector<std::string> AuthenticatorRequestDialogModel::paired_phone_names()
const {
std::vector<std::string> names;
base::ranges::transform(paired_phones_, std::back_inserter(names),
&PairedPhone::name);
names.erase(std::unique(names.begin(), names.end()), names.end());
return names;
}
void AuthenticatorRequestDialogModel::ReplaceCredListForTesting(
std::vector<device::DiscoverableCredentialMetadata> creds) {
ephemeral_state_.creds_ = std::move(creds);
}
base::WeakPtr<AuthenticatorRequestDialogModel>
AuthenticatorRequestDialogModel::GetWeakPtr() {
return weak_factory_.GetWeakPtr();
}
content::WebContents* AuthenticatorRequestDialogModel::GetWebContents() {
return content::WebContents::FromRenderFrameHost(
content::RenderFrameHost::FromID(frame_host_id_));
}
void AuthenticatorRequestDialogModel::SetCurrentStep(Step step) {
if (!started_) {
// Dialog isn't showing yet. Remember to show this step when it appears.
pending_step_ = step;
return;
}
current_step_ = step;
if (should_dialog_be_closed()) {
// The dialog will close itself.
showing_dialog_ = false;
} else {
auto* web_contents = GetWebContents();
if (!showing_dialog_ && web_contents) {
ShowAuthenticatorRequestDialog(web_contents, this);
showing_dialog_ = true;
}
}
for (auto& observer : observers_) {
observer.OnStepTransition();
}
}
void AuthenticatorRequestDialogModel::StartGuidedFlowForTransport(
AuthenticatorTransport transport,
size_t mechanism_index) {
current_mechanism_ = mechanism_index;
DCHECK(current_step() == Step::kMechanismSelection ||
current_step() == Step::kUsbInsertAndActivate ||
current_step() == Step::kCableActivate ||
current_step() == Step::kAndroidAccessory ||
current_step() == Step::kConditionalMediation ||
current_step() == Step::kCreatePasskey ||
current_step() == Step::kPreSelectAccount ||
current_step() == Step::kSelectAccount ||
current_step() == Step::kNotStarted);
switch (transport) {
case AuthenticatorTransport::kUsbHumanInterfaceDevice:
SetCurrentStep(Step::kUsbInsertAndActivate);
break;
case AuthenticatorTransport::kInternal:
StartPlatformAuthenticatorFlow();
break;
case AuthenticatorTransport::kHybrid:
EnsureBleAdapterIsPoweredAndContinueWithStep(Step::kCableActivate);
break;
case AuthenticatorTransport::kAndroidAccessory:
SetCurrentStep(Step::kAndroidAccessory);
break;
default:
break;
}
}
void AuthenticatorRequestDialogModel::StartGuidedFlowForAddPhone(
size_t mechanism_index) {
current_mechanism_ = mechanism_index;
EnsureBleAdapterIsPoweredAndContinueWithStep(Step::kCableV2QRCode);
}
void AuthenticatorRequestDialogModel::StartWinNativeApi(
size_t mechanism_index) {
DCHECK(transport_availability_.has_win_native_api_authenticator);
current_mechanism_ = mechanism_index;
if (transport_availability_.request_is_internal_only &&
!transport_availability_.win_is_uvpaa) {
offer_try_again_in_ui_ = false;
SetCurrentStep(Step::kErrorWindowsHelloNotEnabled);
return;
}
if (resident_key_requirement() !=
device::ResidentKeyRequirement::kDiscouraged &&
!transport_availability_.win_native_ui_shows_resident_credential_notice) {
SetCurrentStep(Step::kResidentCredentialConfirmation);
} else {
HideDialogAndDispatchToPlatformAuthenticator();
}
}
void AuthenticatorRequestDialogModel::StartICloudKeychain(
size_t mechanism_index) {
DCHECK(transport_availability_.has_icloud_keychain);
current_mechanism_ = mechanism_index;
HideDialogAndDispatchToPlatformAuthenticator(
device::AuthenticatorType::kICloudKeychain);
}
void AuthenticatorRequestDialogModel::ContactPhone(const std::string& name,
size_t mechanism_index) {
current_mechanism_ = mechanism_index;
#if BUILDFLAG(IS_MAC)
if (transport_availability()->ble_access_denied) {
// |step| is not saved because macOS asks the user to restart Chrome
// after permission has been granted. So the user will end up retrying
// the whole WebAuthn request in the new process.
SetCurrentStep(Step::kBlePermissionMac);
return;
}
#endif
if (transport_availability_.request_type ==
device::FidoRequestType::kMakeCredential &&
transport_availability_.is_off_the_record_context) {
after_off_the_record_interstitial_ =
base::BindOnce(&AuthenticatorRequestDialogModel::
ContactPhoneAfterOffTheRecordInterstitial,
weak_factory_.GetWeakPtr(), name);
SetCurrentStep(Step::kOffTheRecordInterstitial);
return;
}
ContactPhoneAfterOffTheRecordInterstitial(name);
}
void AuthenticatorRequestDialogModel::ContactPhoneAfterOffTheRecordInterstitial(
std::string name) {
if (!ble_adapter_is_powered()) {
after_ble_adapter_powered_ = base::BindOnce(
&AuthenticatorRequestDialogModel::ContactPhoneAfterBleIsPowered,
weak_factory_.GetWeakPtr(), std::move(name));
BleEvent event;
if (transport_availability()->can_power_on_ble_adapter) {
event = BleEvent::kNeedsPowerAuto;
SetCurrentStep(Step::kBlePowerOnAutomatic);
} else {
event = BleEvent::kNeedsPowerManual;
SetCurrentStep(Step::kBlePowerOnManual);
}
base::UmaHistogramEnumeration("WebAuthentication.BLEUserEvents", event);
return;
}
base::UmaHistogramEnumeration("WebAuthentication.BLEUserEvents",
BleEvent::kAlreadyPowered);
ContactPhoneAfterBleIsPowered(std::move(name));
}
void AuthenticatorRequestDialogModel::ContactPhoneAfterBleIsPowered(
std::string name) {
ContactNextPhoneByName(name);
SetCurrentStep(Step::kCableActivate);
}
void AuthenticatorRequestDialogModel::StartConditionalMediationRequest() {
ephemeral_state_.creds_ =
transport_availability_.recognized_platform_authenticator_credentials;
auto* render_frame_host = content::RenderFrameHost::FromID(frame_host_id_);
auto* web_contents = GetWebContents();
if (web_contents && render_frame_host) {
std::vector<password_manager::PasskeyCredential> credentials;
base::ranges::transform(
ephemeral_state_.creds_, std::back_inserter(credentials),
[](const auto& credential) {
return password_manager::PasskeyCredential(
ToPasswordManagerSource(credential.source),
password_manager::PasskeyCredential::RpId(credential.rp_id),
password_manager::PasskeyCredential::CredentialId(
credential.cred_id),
password_manager::PasskeyCredential::UserId(credential.user.id),
password_manager::PasskeyCredential::Username(
credential.user.name.value_or("")),
password_manager::PasskeyCredential::DisplayName(
credential.user.display_name.value_or("")));
});
ReportConditionalUiPasskeyCount(credentials.size());
ChromeWebAuthnCredentialsDelegateFactory::GetFactory(web_contents)
->GetDelegateForFrame(render_frame_host)
->OnCredentialsReceived(std::move(credentials));
}
SetCurrentStep(Step::kConditionalMediation);
}
void AuthenticatorRequestDialogModel::DispatchRequestAsync(
AuthenticatorReference* authenticator) {
// Dispatching to the same authenticator twice may result in unexpected
// behavior.
if (authenticator->dispatched || !request_callback_) {
return;
}
authenticator->dispatched = true;
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(request_callback_, authenticator->authenticator_id));
}
void AuthenticatorRequestDialogModel::ContactNextPhoneByName(
const std::string& name) {
bool found_name = false;
ephemeral_state_.selected_phone_name_.reset();
for (size_t i = 0; i != paired_phones_.size(); i++) {
const PairedPhone& phone = paired_phones_[i];
if (phone.name == name) {
found_name = true;
ephemeral_state_.selected_phone_name_ = name;
if (!paired_phones_contacted_[i]) {
paired_phones_contacted_[i] = true;
contact_phone_callback_.Run(phone.contact_id);
break;
}
} else if (found_name) {
// |paired_phones_| is sorted by name so as soon as we see a mismatch
// after a match, we're done.
break;
}
}
DCHECK(found_name);
}
void AuthenticatorRequestDialogModel::PopulateMechanisms() {
const bool is_get_assertion = transport_availability_.request_type ==
device::FidoRequestType::kGetAssertion;
std::vector<AuthenticatorTransport> transports_to_list_if_active;
if (!use_conditional_mediation_ &&
base::Contains(transport_availability_.available_transports,
AuthenticatorTransport::kInternal)) {
// Conditional requests offer platform credentials through the autofill UI.
transports_to_list_if_active.push_back(AuthenticatorTransport::kInternal);
}
transports_to_list_if_active.push_back(
AuthenticatorTransport::kUsbHumanInterfaceDevice);
const auto kCable = AuthenticatorTransport::kHybrid;
bool include_add_phone_option = false;
if (cable_ui_type_) {
switch (*cable_ui_type_) {
case AuthenticatorRequestDialogModel::CableUIType::CABLE_V2_2ND_FACTOR:
if (base::Contains(transport_availability_.available_transports,
kCable)) {
include_add_phone_option = true;
}
break;
case AuthenticatorRequestDialogModel::CableUIType::CABLE_V2_SERVER_LINK:
transports_to_list_if_active.push_back(
AuthenticatorTransport::kAndroidAccessory);
[[fallthrough]];
case AuthenticatorRequestDialogModel::CableUIType::CABLE_V1: {
if (base::Contains(transport_availability_.available_transports,
kCable)) {
transports_to_list_if_active.push_back(kCable);
// If this is a caBLEv1 or server-link request then offering to "Try
// Again" is unfortunate because the server won't send another ping
// to the phone. It is valid if trying to use USB devices but the
// confusion of the caBLE case overrides that.
offer_try_again_in_ui_ = false;
}
break;
}
}
}
if (transport_availability_.has_icloud_keychain) {
const std::u16string name = u"iCloud Keychain (UNTRANSLATED)";
mechanisms_.emplace_back(
Mechanism::ICloudKeychain(), name, name, kSmartphoneIcon,
base::BindRepeating(
&AuthenticatorRequestDialogModel::StartICloudKeychain,
base::Unretained(this), mechanisms_.size()));
}
// The Windows API option comes first so that it gets focus and people can
// select it by simply hitting enter.
if (win_native_api_enabled()) {
const std::u16string desc = l10n_util::GetStringUTF16(
IDS_WEBAUTHN_TRANSPORT_POPUP_DIFFERENT_AUTHENTICATOR_WIN);
// Prefer going straight to Windows native UI for requests that are not
// clearly passkeys related, or where a platform credential may satisfy the
// request, except for:
// - conditional UI
// - "legacy" caBLE (caBLEv1 and server-link caBLEv2 on a.g.c)
mechanisms_.emplace_back(
Mechanism::WindowsAPI(), desc, desc,
GetTransportIcon(AuthenticatorTransport::kInternal),
base::BindRepeating(&AuthenticatorRequestDialogModel::StartWinNativeApi,
base::Unretained(this), mechanisms_.size()));
}
bool specific_phones_listed = false;
if (base::Contains(transport_availability_.available_transports, kCable)) {
for (const auto& phone_name : paired_phone_names()) {
const std::u16string name16 = base::UTF8ToUTF16(phone_name);
static constexpr size_t kMaxLongNameChars = 50;
static constexpr size_t kMaxShortNameChars = 30;
std::u16string long_name, short_name;
gfx::ElideString(name16, kMaxLongNameChars, &long_name);
gfx::ElideString(name16, kMaxShortNameChars, &short_name);
mechanisms_.emplace_back(
Mechanism::Phone(phone_name), std::move(long_name),
std::move(short_name), kSmartphoneIcon,
base::BindRepeating(&AuthenticatorRequestDialogModel::ContactPhone,
base::Unretained(this), phone_name,
mechanisms_.size()));
specific_phones_listed = true;
}
bool skip_to_phone_confirmation =
is_get_assertion &&
transport_availability_.has_platform_authenticator_credential ==
device::FidoRequestHandlerBase::RecognizedCredential::
kNoRecognizedCredential &&
paired_phones_.size() == 1 && !use_conditional_mediation_ &&
transport_availability_.is_only_hybrid_or_internal;
if (skip_to_phone_confirmation) {
pending_step_ = Step::kPhoneConfirmationSheet;
}
}
if (include_add_phone_option) {
const std::u16string label = l10n_util::GetStringUTF16(
specific_phones_listed
? IDS_WEBAUTHN_PASSKEY_DIFFERENT_PHONE_OR_TABLET_LABEL
: IDS_WEBAUTHN_PASSKEY_PHONE_OR_TABLET_LABEL);
mechanisms_.emplace_back(
Mechanism::AddPhone(), label, label, kQrcodeGeneratorIcon,
base::BindRepeating(
&AuthenticatorRequestDialogModel::StartGuidedFlowForAddPhone,
base::Unretained(this), mechanisms_.size()));
}
for (const auto transport : transports_to_list_if_active) {
if (!base::Contains(transport_availability_.available_transports,
transport)) {
continue;
}
mechanisms_.emplace_back(
Mechanism::Transport(transport), GetTransportDescription(transport),
GetTransportShortDescription(transport), GetTransportIcon(transport),
base::BindRepeating(
&AuthenticatorRequestDialogModel::StartGuidedFlowForTransport,
base::Unretained(this), transport, mechanisms_.size()));
}
}
absl::optional<size_t>
AuthenticatorRequestDialogModel::IndexOfPriorityMechanism() {
if (mechanisms_.size() == 1) {
return 0;
} else if (mechanisms_.empty()) {
return absl::nullopt;
}
std::vector<Mechanism::Type> priority_list;
if (transport_availability_.request_type ==
device::FidoRequestType::kGetAssertion) {
const bool is_passkey_request =
transport_availability_.has_empty_allow_list ||
transport_availability_.is_only_hybrid_or_internal;
if (!use_conditional_mediation_) {
// If there's a match on the platform authenticator, jump to that.
if (transport_availability_.has_icloud_keychain_credential ==
device::FidoRequestHandlerBase::RecognizedCredential::
kHasRecognizedCredential) {
priority_list.emplace_back(Mechanism::ICloudKeychain());
}
if (transport_availability_.has_platform_authenticator_credential ==
device::FidoRequestHandlerBase::RecognizedCredential::
kHasRecognizedCredential) {
priority_list.emplace_back(
Mechanism::Transport(AuthenticatorTransport::kInternal));
}
// If it's caBLEv1, or server-linked caBLEv2, jump to that.
if (cable_ui_type_) {
switch (*cable_ui_type_) {
case AuthenticatorRequestDialogModel::CableUIType::
CABLE_V2_SERVER_LINK:
case AuthenticatorRequestDialogModel::CableUIType::CABLE_V1:
priority_list.emplace_back(
Mechanism::Transport(AuthenticatorTransport::kHybrid));
break;
case AuthenticatorRequestDialogModel::CableUIType::
CABLE_V2_2ND_FACTOR:
break;
}
}
// This seems like it might be an error (crbug.com/1426243): kInternal has
// priority over caBLE extensions if there's a recognised platform
// credential, but Windows doesn't.
if (transport_availability_.has_platform_authenticator_credential ==
device::FidoRequestHandlerBase::RecognizedCredential::
kHasRecognizedCredential) {
priority_list.emplace_back(Mechanism::WindowsAPI());
}
// Prefer going straight to Windows native UI for requests that are not
// clearly passkeys related,
if (!is_passkey_request) {
priority_list.emplace_back(Mechanism::WindowsAPI());
}
}
if (is_passkey_request && paired_phone_names().empty() &&
// On Windows WebAuthn API < 4, we cannot tell in advance if the
// platform authenticator can fulfill a get assertion request. In that
// case, don't jump to the QR code.
(use_conditional_mediation_ ||
transport_availability_.has_platform_authenticator_credential ==
device::FidoRequestHandlerBase::RecognizedCredential::
kNoRecognizedCredential)) {
priority_list.emplace_back(Mechanism::AddPhone());
}
} else {
CHECK_EQ(transport_availability_.request_type,
device::FidoRequestType::kMakeCredential);
const bool is_passkey_request =
resident_key_requirement() !=
device::ResidentKeyRequirement::kDiscouraged;
if (is_passkey_request) {
// If attachment=any, then don't jump to suggesting a phone.
// TODO(crbug.com/1426628): makeCredential requests should always have
// `make_credential_attachment` set. Stop being hesitant.
if ((!transport_availability_.make_credential_attachment ||
*transport_availability_.make_credential_attachment !=
device::AuthenticatorAttachment::kAny) &&
paired_phone_names().empty()) {
priority_list.emplace_back(Mechanism::AddPhone());
}
} else {
// This seems like it might be an error (crbug.com/1426244) as we might
// still want to jump to platform authenticators for passkey requests if
// we don't jump to a phone.
if (kShowCreatePlatformPasskeyStep) {
priority_list.emplace_back(
Mechanism::Transport(AuthenticatorTransport::kInternal));
}
priority_list.emplace_back(Mechanism::WindowsAPI());
}
}
for (const auto& priority_mechanism : priority_list) {
// A phone should never be triggered immediately.
CHECK(!absl::holds_alternative<Mechanism::Phone>(priority_mechanism));
for (size_t i = 0; i < mechanisms_.size(); i++) {
if (priority_mechanism == mechanisms_[i].type) {
return i;
}
}
}
return absl::nullopt;
}
void AuthenticatorRequestDialogModel::
HideDialogAndDispatchToPlatformAuthenticator(
absl::optional<device::AuthenticatorType> type) {
HideDialog();
#if BUILDFLAG(IS_WIN)
// The Windows-native UI already handles retrying so we do not offer a second
// level of retry in that case.
offer_try_again_in_ui_ = false;
#endif
auto& authenticators =
ephemeral_state_.saved_authenticators_.authenticator_list();
auto platform_authenticator_it = base::ranges::find_if(
authenticators, [type](const AuthenticatorReference& ref) -> bool {
return ref.transport == device::FidoTransportProtocol::kInternal &&
(!type || ref.type == *type ||
!base::FeatureList::IsEnabled(device::kWebAuthnICloudKeychain));
});
if (platform_authenticator_it == authenticators.end()) {
return;
}
DispatchRequestAsync(&*platform_authenticator_it);
}