| // Copyright 2024 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_controller.h" |
| |
| #include <algorithm> |
| #include <array> |
| #include <cstddef> |
| #include <cstdint> |
| #include <iterator> |
| #include <memory> |
| #include <optional> |
| #include <string> |
| #include <string_view> |
| #include <tuple> |
| #include <utility> |
| #include <variant> |
| #include <vector> |
| |
| #include "base/check.h" |
| #include "base/check_op.h" |
| #include "base/containers/contains.h" |
| #include "base/feature_list.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/i18n/string_compare.h" |
| #include "base/location.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/notreached.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/time/time.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/profiles/profile.h" |
| #include "chrome/browser/signin/signin_ui_util.h" |
| #include "chrome/browser/ui/passwords/ui_utils.h" |
| #include "chrome/browser/ui/webauthn/ambient/ambient_signin_controller.h" |
| #include "chrome/browser/ui/webauthn/passkey_upgrade_request_controller.h" |
| #include "chrome/browser/ui/webauthn/user_actions.h" |
| #include "chrome/browser/webauthn/authenticator_reference.h" |
| #include "chrome/browser/webauthn/authenticator_request_dialog_model.h" |
| #include "chrome/browser/webauthn/authenticator_transport.h" |
| #include "chrome/browser/webauthn/challenge_url_fetcher.h" |
| #include "chrome/browser/webauthn/change_pin_controller_impl.h" |
| #include "chrome/browser/webauthn/gpm_enclave_transaction.h" |
| #include "chrome/browser/webauthn/gpm_user_verification_policy.h" |
| #include "chrome/browser/webauthn/mechanism_sorter.h" |
| #include "chrome/browser/webauthn/passkey_model_factory.h" |
| #include "chrome/browser/webauthn/password_credential_fetcher.h" |
| #include "chrome/browser/webauthn/webauthn_metrics_util.h" |
| #include "chrome/browser/webauthn/webauthn_pref_names.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "components/device_event_log/device_event_log.h" |
| #include "components/password_manager/core/browser/credential_manager_utils.h" |
| #include "components/password_manager/core/browser/passkey_credential.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/strings/grit/components_strings.h" |
| #include "components/vector_icons/vector_icons.h" |
| #include "components/webauthn/core/browser/passkey_model.h" |
| #include "components/webauthn/core/browser/passkey_model_change.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/storage_partition.h" |
| #include "content/public/browser/web_contents.h" |
| #include "device/fido/authenticator_get_assertion_response.h" |
| #include "device/fido/cable/cable_discovery_data.h" |
| #include "device/fido/cable/v2_constants.h" |
| #include "device/fido/discoverable_credential_metadata.h" |
| #include "device/fido/enclave/constants.h" |
| #include "device/fido/enclave/metrics.h" |
| #include "device/fido/features.h" |
| #include "device/fido/fido_authenticator.h" |
| #include "device/fido/fido_constants.h" |
| #include "device/fido/fido_discovery_factory.h" |
| #include "device/fido/fido_request_handler_base.h" |
| #include "device/fido/fido_transport_protocol.h" |
| #include "device/fido/fido_types.h" |
| #include "device/fido/pin.h" |
| #include "third_party/abseil-cpp/absl/functional/overload.h" |
| #include "third_party/icu/source/common/unicode/locid.h" |
| #include "third_party/icu/source/common/unicode/utypes.h" |
| #include "third_party/icu/source/i18n/unicode/coll.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/gfx/paint_vector_icon.h" |
| #include "ui/gfx/text_elider.h" |
| #include "url/scheme_host_port.h" |
| |
| #if BUILDFLAG(IS_WIN) |
| #include "device/fido/win/webauthn_api.h" |
| #endif |
| |
| #if BUILDFLAG(IS_MAC) |
| #include "base/mac/mac_util.h" |
| #include "device/fido/mac/util.h" |
| #endif |
| |
| namespace { |
| |
| constexpr int kMaxPriorityGPMCredentialCreations = 2; |
| |
| using BleStatus = device::FidoRequestHandlerBase::BleStatus; |
| using ChangePinEvent = ChangePinControllerImpl::ChangePinEvent; |
| using Mechanism = AuthenticatorRequestDialogModel::Mechanism; |
| using Step = AuthenticatorRequestDialogModel::Step; |
| using TransportAvailabilityInfo = |
| device::FidoRequestHandlerBase::TransportAvailabilityInfo; |
| using UIPresentation = |
| content::AuthenticatorRequestClientDelegate::UIPresentation; |
| using device::AuthenticatorType; |
| using device::FidoRequestType; |
| using PasswordCredentials = PasswordCredentialFetcher::PasswordCredentials; |
| |
| constexpr int GetMessageIdForTransportDescription( |
| AuthenticatorTransport transport) { |
| switch (transport) { |
| case AuthenticatorTransport::kUsbHumanInterfaceDevice: |
| return IDS_WEBAUTHN_TRANSPORT_USB; |
| case AuthenticatorTransport::kInternal: |
| #if BUILDFLAG(IS_MAC) |
| return IDS_WEBAUTHN_YOUR_CHROME_PROFILE; |
| #else |
| return IDS_WEBAUTHN_TRANSPORT_INTERNAL; |
| #endif |
| case AuthenticatorTransport::kHybrid: |
| return IDS_WEBAUTHN_TRANSPORT_CABLE; |
| case AuthenticatorTransport::kDeprecatedAoa: |
| case AuthenticatorTransport::kBluetoothLowEnergy: |
| case AuthenticatorTransport::kNearFieldCommunication: |
| NOTREACHED(); |
| } |
| } |
| |
| std::u16string GetTransportDescription(AuthenticatorTransport transport) { |
| const int msg_id = GetMessageIdForTransportDescription(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 kUsbSecurityKeyIcon; |
| case AuthenticatorTransport::kInternal: |
| return kLaptopIcon; |
| case AuthenticatorTransport::kHybrid: |
| return kSmartphoneIcon; |
| case AuthenticatorTransport::kDeprecatedAoa: |
| case AuthenticatorTransport::kBluetoothLowEnergy: |
| case AuthenticatorTransport::kNearFieldCommunication: |
| NOTREACHED(); |
| } |
| } |
| |
| // 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( |
| AuthenticatorType type) { |
| switch (type) { |
| case AuthenticatorType::kWinNative: |
| return password_manager::PasskeyCredential::Source::kWindowsHello; |
| case AuthenticatorType::kTouchID: |
| return password_manager::PasskeyCredential::Source::kTouchId; |
| case AuthenticatorType::kPhone: |
| return password_manager::PasskeyCredential::Source::kAndroidPhone; |
| case AuthenticatorType::kICloudKeychain: |
| return password_manager::PasskeyCredential::Source::kICloudKeychain; |
| case AuthenticatorType::kEnclave: |
| return password_manager::PasskeyCredential::Source:: |
| kGooglePasswordManager; |
| case AuthenticatorType::kChromeOS: |
| case AuthenticatorType::kOther: |
| return password_manager::PasskeyCredential::Source::kOther; |
| } |
| } |
| |
| bool WebAuthnApiSupportsHybrid() { |
| #if BUILDFLAG(IS_WIN) |
| device::WinWebAuthnApi* const webauthn_api = |
| device::WinWebAuthnApi::GetDefault(); |
| return webauthn_api && webauthn_api->SupportsHybrid(); |
| #else |
| return false; |
| #endif |
| } |
| |
| const gfx::VectorIcon& GetCredentialIcon(AuthenticatorType type) { |
| if (type == AuthenticatorType::kPhone) { |
| return kSmartphoneIcon; |
| } |
| return vector_icons::kPasskeyIcon; |
| } |
| |
| int GetHybridButtonLabel(bool has_security_key) { |
| return has_security_key |
| ? IDS_WEBAUTHN_PASSKEY_PHONE_TABLET_OR_SECURITY_KEY_LABEL |
| : IDS_WEBAUTHN_PASSKEY_PHONE_OR_TABLET_LABEL; |
| } |
| |
| // SourcePriority determines which credential will be used when doing a modal |
| // get and multiple platform authenticators have credentials, all with the same |
| // user ID. |
| int SourcePriority(AuthenticatorType source) { |
| switch (source) { |
| case AuthenticatorType::kEnclave: |
| return 4; |
| case AuthenticatorType::kICloudKeychain: |
| return 3; |
| case AuthenticatorType::kTouchID: |
| return 2; |
| case AuthenticatorType::kWinNative: |
| return 1; |
| default: |
| return 0; |
| } |
| } |
| |
| // Returns the ID of a string and authenticator transport to label a button that |
| // triggers the Windows native WebAuthn API, or std::nullopt if the button |
| // should not be shown. The transport represents the option Windows will prefer |
| // when tapping the button and is used to pick an icon and position on the list. |
| std::optional<std::pair<int, AuthenticatorTransport>> GetWindowsAPIButtonLabel( |
| const device::FidoRequestHandlerBase::TransportAvailabilityInfo& |
| transport_availability, |
| UIPresentation ui_presentation) { |
| if (ui_presentation == UIPresentation::kModalImmediate) { |
| return std::nullopt; |
| } |
| if (!transport_availability.has_win_native_api_authenticator) { |
| return std::nullopt; |
| } |
| bool win_handles_internal; |
| bool win_handles_hybrid; |
| bool win_handles_security_key; |
| if (transport_availability.request_type == FidoRequestType::kGetAssertion) { |
| win_handles_internal = |
| (transport_availability.transport_list_did_include_internal || |
| transport_availability.has_empty_allow_list) && |
| transport_availability.has_platform_authenticator_credential == |
| device::FidoRequestHandlerBase::RecognizedCredential::kUnknown; |
| win_handles_hybrid = |
| (transport_availability.transport_list_did_include_hybrid || |
| transport_availability.has_empty_allow_list) && |
| WebAuthnApiSupportsHybrid(); |
| win_handles_security_key = |
| transport_availability.transport_list_did_include_security_key || |
| transport_availability.has_empty_allow_list; |
| } else { |
| win_handles_internal = transport_availability.make_credential_attachment == |
| device::AuthenticatorAttachment::kPlatform || |
| transport_availability.make_credential_attachment == |
| device::AuthenticatorAttachment::kAny; |
| win_handles_security_key = |
| transport_availability.make_credential_attachment == |
| device::AuthenticatorAttachment::kCrossPlatform || |
| transport_availability.make_credential_attachment == |
| device::AuthenticatorAttachment::kAny; |
| win_handles_hybrid = |
| WebAuthnApiSupportsHybrid() && win_handles_security_key; |
| } |
| if (win_handles_internal) { |
| if (win_handles_security_key) { |
| return std::make_pair( |
| IDS_WEBAUTHN_TRANSPORT_WINDOWS_HELLO_OR_SECURITY_KEY, |
| AuthenticatorTransport::kInternal); |
| } else { |
| return std::make_pair(IDS_WEBAUTHN_TRANSPORT_WINDOWS_HELLO, |
| AuthenticatorTransport::kInternal); |
| } |
| } |
| if (win_handles_hybrid) { |
| return std::make_pair(GetHybridButtonLabel(win_handles_security_key), |
| AuthenticatorTransport::kHybrid); |
| } |
| if (win_handles_security_key) { |
| return std::make_pair(IDS_WEBAUTHN_TRANSPORT_EXTERNAL_SECURITY_KEY, |
| AuthenticatorTransport::kUsbHumanInterfaceDevice); |
| } |
| return std::nullopt; |
| } |
| |
| // Returns whether the given authenticator type is implemented within Chrome |
| // itself for the purposes of `StartPlatformAuthenticatorFlow`. |
| bool IsChromeImplemented(AuthenticatorType type) { |
| // Note: it must never be possible for any machine to observe two different |
| // sources of "Chrome implemented" credentials. I.e. a given platform only |
| // ever has one type of Chrome-implemented platform authenticator. |
| // This is CHECKed in `StartFlow`. |
| switch (type) { |
| case AuthenticatorType::kWinNative: |
| case AuthenticatorType::kPhone: |
| case AuthenticatorType::kEnclave: |
| case AuthenticatorType::kICloudKeychain: |
| return false; |
| case AuthenticatorType::kTouchID: |
| case AuthenticatorType::kChromeOS: |
| return true; |
| case AuthenticatorType::kOther: |
| // For testing purposes. |
| return true; |
| } |
| } |
| |
| bool ProfileAuthenticatorWillDoUserVerification( |
| device::UserVerificationRequirement requirement, |
| bool platform_has_biometrics) { |
| #if BUILDFLAG(IS_MAC) |
| return device::fido::mac::ProfileAuthenticatorWillDoUserVerification( |
| requirement, platform_has_biometrics); |
| #else |
| return false; |
| #endif |
| } |
| |
| inline bool IsModalRequest(UIPresentation ui_presentation) { |
| return ui_presentation == UIPresentation::kModal || |
| ui_presentation == UIPresentation::kModalImmediate; |
| } |
| |
| // Returns the vector icon associated with the given mechanism type. |
| // For Mechanism::WindowsAPI, the effective transport must be provided. |
| const gfx::VectorIcon& GetMechanismIcon( |
| const Mechanism::Type& type, |
| content::AuthenticatorRequestClientDelegate::UIPresentation ui_presentation, |
| std::optional<AuthenticatorTransport> effective_transport = std::nullopt) { |
| return std::visit( |
| absl::Overload{ |
| [ui_presentation](const Mechanism::Credential& credential) |
| -> const gfx::VectorIcon& { |
| if (ui_presentation == UIPresentation::kModalImmediate) { |
| switch (credential.value().source) { |
| case AuthenticatorType::kICloudKeychain: |
| return kIcloudKeychainColorIcon; |
| case AuthenticatorType::kEnclave: |
| return GooglePasswordManagerVectorIcon(); |
| case AuthenticatorType::kWinNative: |
| return kWindowsHelloColorIcon; |
| case AuthenticatorType::kTouchID: |
| return vector_icons::kProductRefreshIcon; |
| default: |
| break; |
| } |
| } |
| // Default icon for non-immediate mode or other credential sources. |
| return GetCredentialIcon(credential.value().source); |
| }, |
| [](const Mechanism::Password&) -> const gfx::VectorIcon& { |
| return GooglePasswordManagerVectorIcon(); |
| }, |
| [](const Mechanism::Transport& transport) -> const gfx::VectorIcon& { |
| return GetTransportIcon(transport.value()); |
| }, |
| [&effective_transport]( |
| const Mechanism::WindowsAPI&) -> const gfx::VectorIcon& { |
| CHECK(effective_transport.has_value()); |
| return GetTransportIcon(*effective_transport); |
| }, |
| [](const Mechanism::ICloudKeychain&) -> const gfx::VectorIcon& { |
| // Always use the standard iCloud Keychain icon here. |
| return kIcloudKeychainIcon; |
| }, |
| [](const Mechanism::AddPhone&) -> const gfx::VectorIcon& { |
| return kQrcodeGeneratorIcon; |
| }, |
| [](const Mechanism::Enclave&) -> const gfx::VectorIcon& { |
| // Always use the standard password manager icon here. |
| return vector_icons::kPasswordManagerIcon; |
| }, |
| [](const Mechanism::SignInAgain&) -> const gfx::VectorIcon& { |
| return vector_icons::kSyncIcon; |
| }}, |
| type); |
| } |
| |
| // Returns `true` if `mech` satisfies the given `hint`, `false` otherwise. |
| bool MechanismMatchesHint(const Mechanism::Type& mech, |
| AuthenticatorTransport hint) { |
| return std::visit( |
| absl::Overload{ |
| [hint](const Mechanism::Transport& transport) { |
| return transport.value() == hint; |
| }, |
| [hint](const Mechanism::AddPhone&) { |
| return hint == AuthenticatorTransport::kHybrid; |
| }, |
| [hint](const Mechanism::Enclave&) { |
| return hint == AuthenticatorTransport::kInternal; |
| }, |
| [hint](const Mechanism::ICloudKeychain&) { |
| return hint == AuthenticatorTransport::kInternal; |
| }, |
| [hint](const Mechanism::WindowsAPI&) { |
| return hint == AuthenticatorTransport::kInternal || |
| hint == AuthenticatorTransport::kUsbHumanInterfaceDevice || |
| (hint == AuthenticatorTransport::kHybrid && |
| WebAuthnApiSupportsHybrid()); |
| }, |
| [](const Mechanism::Credential&) { |
| // Credentials are always given priority over hints. |
| return false; |
| }, |
| [](const Mechanism::Password&) { return false; }, |
| [](const Mechanism::SignInAgain&) { return false; }, |
| }, |
| mech); |
| } |
| |
| // Returns the index of the first mechanism that matches `type`, or std::nullopt |
| // if none is found. |
| std::optional<int> FindIndexOfFirstMechanismOfType( |
| base::span<const Mechanism> mechanisms, |
| const Mechanism::Type& type) { |
| for (size_t i = 0; i < mechanisms.size(); i++) { |
| if (type == mechanisms[i].type) { |
| return i; |
| } |
| } |
| return std::nullopt; |
| } |
| |
| } // namespace |
| |
| AuthenticatorRequestDialogController::EphemeralState::EphemeralState() = |
| default; |
| AuthenticatorRequestDialogController::EphemeralState::EphemeralState( |
| EphemeralState&&) = default; |
| AuthenticatorRequestDialogController::EphemeralState& |
| AuthenticatorRequestDialogController::EphemeralState::operator=( |
| EphemeralState&&) = default; |
| AuthenticatorRequestDialogController::EphemeralState::~EphemeralState() = |
| default; |
| |
| void AuthenticatorRequestDialogController::ResetEphemeralState() { |
| ephemeral_state_ = {}; |
| model_->creds.clear(); |
| model_->priority_mechanism_index.reset(); |
| } |
| |
| AuthenticatorRequestDialogController::AuthenticatorRequestDialogController( |
| AuthenticatorRequestDialogModel* model, |
| content::RenderFrameHost* render_frame_host) |
| : model_(model), frame_host_id_(render_frame_host->GetGlobalId()) { |
| model_->observers.AddObserver(this); |
| webauthn::PasskeyModel* passkey_model = |
| PasskeyModelFactory::GetInstance()->GetForProfile( |
| Profile::FromBrowserContext(render_frame_host->GetBrowserContext())); |
| if (passkey_model) { |
| passkey_model_observation_.Observe(passkey_model); |
| } |
| } |
| |
| AuthenticatorRequestDialogController::~AuthenticatorRequestDialogController() { |
| if (model_) { |
| model_->observers.RemoveObserver(this); |
| } |
| } |
| |
| AuthenticatorRequestDialogModel* AuthenticatorRequestDialogController::model() |
| const { |
| return model_; |
| } |
| |
| void AuthenticatorRequestDialogController::OnModelDestroyed( |
| AuthenticatorRequestDialogModel* model) { |
| // This stops the destructor of this object from trying to remove itself from |
| // the list of observers. But this is not a valid state for this object to be |
| // in: many functions will crash. So this is just to make destroying the two |
| // objects together not depend on the order of destruction. |
| CHECK_EQ(model, model_); |
| model_ = nullptr; |
| } |
| |
| void AuthenticatorRequestDialogController::StartOver() { |
| PrefService* pref_service = |
| Profile::FromBrowserContext(GetRenderFrameHost()->GetBrowserContext()) |
| ->GetOriginalProfile() |
| ->GetPrefs(); |
| if (model_->step() == Step::kTrustThisComputerCreation || |
| model_->step() == Step::kTrustThisComputerAssertion || |
| model_->step() == Step::kRecoverSecurityDomain) { |
| device::enclave::RecordEvent(device::enclave::Event::kOnboardingRejected); |
| int current_gpm_decline_count = pref_service->GetInteger( |
| webauthn::pref_names::kEnclaveDeclinedGPMBootstrappingCount); |
| pref_service->SetInteger( |
| webauthn::pref_names::kEnclaveDeclinedGPMBootstrappingCount, |
| std::min(current_gpm_decline_count + 1, |
| device::enclave::kMaxGPMBootstrapPrompts)); |
| } else if (enclave_was_priority_mechanism_) { |
| int current_gpm_decline_count = pref_service->GetInteger( |
| webauthn::pref_names::kEnclaveDeclinedGPMCredentialCreationCount); |
| pref_service->SetInteger( |
| webauthn::pref_names::kEnclaveDeclinedGPMCredentialCreationCount, |
| std::min(current_gpm_decline_count + 1, |
| kMaxPriorityGPMCredentialCreations)); |
| device::enclave::RecordEvent( |
| device::enclave::Event::kMakeCredentialPriorityDeclined); |
| enclave_was_priority_mechanism_ = false; |
| } |
| ResetEphemeralState(); |
| |
| for (auto& observer : model_->observers) { |
| observer.OnStartOver(); |
| } |
| SetCurrentStep(Step::kMechanismSelection); |
| } |
| |
| void AuthenticatorRequestDialogController::OnCreatePasskeyAccepted() { |
| HideDialogAndDispatchToPlatformAuthenticator(); |
| } |
| |
| void AuthenticatorRequestDialogController::OnRecoverSecurityDomainClosed() { |
| if (model_->step() == Step::kGPMReauthForPinReset) { |
| ChangePinControllerImpl::RecordHistogram(ChangePinEvent::kReauthCancelled); |
| } |
| // For modal get requests, fallback to the credential selector if the user |
| // dismissed the recovery window. This will ensure the users to have a backup |
| // such as hybrid. |
| if (transport_availability_.request_type == FidoRequestType::kGetAssertion && |
| IsModalRequest(ui_presentation()) && |
| model_->step() == Step::kRecoverSecurityDomain) { |
| model_->StartOver(); |
| return; |
| } |
| CancelAuthenticatorRequest(); |
| } |
| |
| void AuthenticatorRequestDialogController:: |
| ContinueWithFlowAfterBleAdapterPowered() { |
| DCHECK(model_->step() == Step::kBlePowerOnManual || |
| model_->step() == Step::kBlePowerOnAutomatic); |
| DCHECK(model_->ble_adapter_is_powered); |
| |
| std::move(after_ble_adapter_powered_).Run(); |
| } |
| |
| void AuthenticatorRequestDialogController::PowerOnBleAdapter() { |
| DCHECK_EQ(model_->step(), Step::kBlePowerOnAutomatic); |
| if (!bluetooth_adapter_power_on_callback_) { |
| return; |
| } |
| |
| bluetooth_adapter_power_on_callback_.Run(); |
| } |
| |
| void AuthenticatorRequestDialogController::OpenBlePreferences() { |
| #if BUILDFLAG(IS_MAC) |
| DCHECK_EQ(model_->step(), Step::kBlePermissionMac); |
| base::mac::OpenSystemSettingsPane( |
| base::mac::SystemSettingsPane::kPrivacySecurity_Bluetooth); |
| #endif // IS_MAC |
| } |
| |
| void AuthenticatorRequestDialogController:: |
| OnOffTheRecordInterstitialAccepted() { |
| std::move(after_off_the_record_interstitial_).Run(); |
| } |
| |
| void AuthenticatorRequestDialogController::CancelAuthenticatorRequest() { |
| if (model_->step() == Step::kGPMChangeArbitraryPin || |
| model_->step() == Step::kGPMChangePin) { |
| ChangePinControllerImpl::RecordHistogram(ChangePinEvent::kNewPinCancelled); |
| } |
| if (ui_presentation() == UIPresentation::kAutofill) { |
| // Conditional UI requests are never cancelled, they restart silently. |
| ResetEphemeralState(); |
| for (auto& observer : model_->observers) { |
| observer.OnStartOver(); |
| } |
| StartAutofillRequest(); |
| return; |
| } |
| |
| if (is_request_complete()) { |
| SetCurrentStep(Step::kClosed); |
| } |
| |
| for (auto& observer : model_->observers) { |
| observer.OnCancelRequest(); |
| } |
| } |
| |
| void AuthenticatorRequestDialogController::OnRequestComplete() { |
| if (ui_presentation() == UIPresentation::kAutofill) { |
| auto* render_frame_host = GetRenderFrameHost(); |
| auto* web_contents = |
| content::WebContents::FromRenderFrameHost(render_frame_host); |
| if (web_contents && render_frame_host) { |
| ChromeWebAuthnCredentialsDelegate* delegate = |
| ChromeWebAuthnCredentialsDelegateFactory::GetFactory(web_contents) |
| ->GetDelegateForFrame(render_frame_host); |
| if (delegate) { |
| delegate->NotifyWebAuthnRequestAborted(); |
| } |
| } |
| } |
| SetCurrentStep(Step::kClosed); |
| } |
| |
| void AuthenticatorRequestDialogController::OnResidentCredentialConfirmed() { |
| DCHECK_EQ(model_->step(), Step::kResidentCredentialConfirmation); |
| HideDialogAndDispatchToPlatformAuthenticator(AuthenticatorType::kWinNative); |
| } |
| |
| void AuthenticatorRequestDialogController::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 AuthenticatorRequestDialogController::EnclaveEnabledStatusChanged( |
| EnclaveEnabledStatus status) { |
| enclave_enabled_status_ = status; |
| } |
| |
| void AuthenticatorRequestDialogController::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)); |
| model_->creds.clear(); |
| ephemeral_state_.responses_.clear(); |
| std::move(selection_callback_).Run(std::move(response)); |
| } |
| |
| void AuthenticatorRequestDialogController::OnAccountPreselectedIndex( |
| size_t index) { |
| OnAccountPreselected(model_->creds.at(index).cred_id); |
| } |
| |
| void AuthenticatorRequestDialogController::OnBioEnrollmentDone() { |
| std::move(bio_enrollment_callback_).Run(); |
| } |
| |
| void AuthenticatorRequestDialogController::OnUserConfirmedPriorityMechanism() { |
| model_->mechanisms[*model_->priority_mechanism_index].callback.Run(); |
| } |
| |
| void AuthenticatorRequestDialogController::OnPasskeysChanged( |
| const std::vector<webauthn::PasskeyModelChange>& changes) {} |
| |
| void AuthenticatorRequestDialogController::OnPasskeyModelShuttingDown() { |
| passkey_model_observation_.Reset(); |
| } |
| |
| void AuthenticatorRequestDialogController::OnPasskeyModelIsReady( |
| bool is_ready) {} |
| |
| void AuthenticatorRequestDialogController::PasskeyUpgradeSucceeded() { |
| // Nothing to do. The WebAuthn request will be resolved automatically via the |
| // request handler success callback. The PasskeyUpgradeRequestController shows |
| // its own UI. |
| CHECK_EQ(model_->step(), Step::kPasskeyUpgrade); |
| } |
| |
| void AuthenticatorRequestDialogController::PasskeyUpgradeFailed() { |
| CHECK_EQ(model_->step(), Step::kPasskeyUpgrade); |
| CancelAuthenticatorRequest(); |
| } |
| |
| void AuthenticatorRequestDialogController::HideDialog() { |
| SetCurrentStep(Step::kNotStarted); |
| } |
| |
| bool AuthenticatorRequestDialogController::is_request_complete() const { |
| return model_->step() == Step::kTimedOut || |
| model_->step() == Step::kKeyNotRegistered || |
| model_->step() == Step::kKeyAlreadyRegistered || |
| model_->step() == Step::kMissingCapability || |
| model_->step() == Step::kErrorWindowsHelloNotEnabled || |
| model_->step() == Step::kErrorFetchingChallenge || |
| model_->step() == Step::kClosed; |
| } |
| |
| void AuthenticatorRequestDialogController::StartFlow( |
| TransportAvailabilityInfo transport_availability, |
| PasswordCredentials passwords) { |
| DCHECK(!started_); |
| DCHECK_EQ(model_->step(), Step::kNotStarted); |
| DCHECK_EQ( |
| transport_availability.attestation_conveyance_preference.has_value(), |
| transport_availability.request_type == FidoRequestType::kMakeCredential); |
| |
| started_ = true; |
| transport_availability_ = std::move(transport_availability); |
| passwords_ = std::move(passwords); |
| // All recognised credentials that are "Chrome implemented" are from the |
| // same source, i.e. a platform never has two Chrome implemented platform |
| // authenticators. |
| std::optional<AuthenticatorType> chrome_implemented_type; |
| for (const auto& cred : transport_availability_.recognized_credentials) { |
| if (IsChromeImplemented(cred.source)) { |
| if (chrome_implemented_type.has_value()) { |
| CHECK_EQ(*chrome_implemented_type, cred.source); |
| } else { |
| chrome_implemented_type = cred.source; |
| } |
| } |
| } |
| |
| SortRecognizedCredentials(); |
| |
| #if BUILDFLAG(IS_MAC) |
| RecordMacOsStartedHistogram(); |
| #endif |
| |
| PopulateMechanisms(); |
| model_->priority_mechanism_index = IndexOfPriorityMechanism(); |
| |
| switch (ui_presentation()) { |
| case UIPresentation::kModal: |
| case UIPresentation::kModalImmediate: |
| StartGuidedFlowForMostLikelyTransportOrShowMechanismSelection(); |
| break; |
| case UIPresentation::kAutofill: |
| StartAutofillRequest(); |
| break; |
| case UIPresentation::kPasskeyUpgrade: |
| StartPasskeyUpgradeRequest(); |
| break; |
| case UIPresentation::kDisabled: |
| NOTREACHED(); |
| } |
| } |
| |
| void AuthenticatorRequestDialogController::TransitionToModalWebAuthnRequest() { |
| DCHECK_EQ(model_->step(), Step::kPasskeyAutofill); |
| |
| // 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 AuthenticatorRequestDialogController:: |
| StartGuidedFlowForMostLikelyTransportOrShowMechanismSelection() { |
| const bool enclave_will_do_uv = GpmWillDoUserVerification( |
| transport_availability_.user_verification_requirement, |
| transport_availability_.platform_has_biometrics); |
| constexpr bool kIsMac = BUILDFLAG(IS_MAC); |
| |
| MaybeStartChallengeFetch(); |
| |
| if (pending_step_) { |
| SetCurrentStep(*pending_step_); |
| pending_step_.reset(); |
| } else if (model_->mechanisms.empty()) { |
| if (transport_availability_.transport_list_did_include_internal) { |
| SetCurrentStep(Step::kErrorNoPasskeys); |
| } else { |
| SetCurrentStep(Step::kErrorNoAvailableTransports); |
| } |
| } else if (model_->priority_mechanism_index) { |
| Mechanism& mechanism = |
| model_->mechanisms[*model_->priority_mechanism_index]; |
| const Mechanism::Credential* cred = |
| std::get_if<Mechanism::Credential>(&mechanism.type); |
| |
| // If the authenticator will show its own confirmation then we don't want to |
| // duplicate it. |
| const bool authenticator_shows_own_confirmation = |
| cred && (cred->value().source == AuthenticatorType::kICloudKeychain || |
| // The enclave Touch ID prompts shows the credential details. |
| (cred->value().source == AuthenticatorType::kEnclave && |
| enclave_will_do_uv && kIsMac && |
| transport_availability_.platform_has_biometrics)); |
| |
| if (cred != nullptr && |
| // Credentials on phones should never be triggered automatically. |
| (cred->value().source == AuthenticatorType::kPhone || |
| // In the case of an empty allow list, the user should be able to see |
| // the account that they're signing in with. So either |
| // `kSelectPriorityMechanism` is used or else the authenticator shows |
| // their own UI. |
| (transport_availability_.has_empty_allow_list && |
| !authenticator_shows_own_confirmation) || |
| // Never auto-trigger macOS profile credentials without either a local |
| // biometric or a UV requirement because, otherwise, there'll not be |
| // *any* UI. |
| (cred->value().source == AuthenticatorType::kTouchID && |
| !ProfileAuthenticatorWillDoUserVerification( |
| transport_availability_.user_verification_requirement, |
| transport_availability_.platform_has_biometrics)) || |
| // Never auto-trigger the enclave unless UV will be performed because, |
| // otherwise, there'll not be any UI. |
| (cred->value().source == AuthenticatorType::kEnclave && |
| !enclave_will_do_uv))) { |
| SetCurrentStep(Step::kSelectPriorityMechanism); |
| } else if (std::holds_alternative<Mechanism::Password>(mechanism.type)) { |
| SetCurrentStep(Step::kSelectPriorityMechanism); |
| } else { |
| if (std::holds_alternative<Mechanism::Enclave>(mechanism.type)) { |
| device::enclave::RecordEvent( |
| device::enclave::Event::kMakeCredentialPriorityShown); |
| enclave_was_priority_mechanism_ = true; |
| } else { |
| enclave_was_priority_mechanism_ = false; |
| } |
| mechanism.callback.Run(); |
| } |
| } else { |
| // If an allowlist was included and there are matches on a local |
| // authenticator, jump to it. There are no mechanisms for these |
| // authenticators so `priority_mechanism_index` cannot handle this. |
| if (!transport_availability_.has_empty_allow_list) { |
| if (transport_availability_.has_icloud_keychain_credential == |
| device::FidoRequestHandlerBase::RecognizedCredential:: |
| kHasRecognizedCredential && |
| allow_icloud_keychain_) { |
| ephemeral_state_.did_invoke_platform_despite_no_priority_mechanism_ = |
| true; |
| StartICloudKeychain(); |
| return; |
| } |
| if (transport_availability_.has_platform_authenticator_credential == |
| device::FidoRequestHandlerBase::RecognizedCredential:: |
| kHasRecognizedCredential) { |
| ephemeral_state_.did_invoke_platform_despite_no_priority_mechanism_ = |
| true; |
| if (transport_availability_.has_win_native_api_authenticator) { |
| StartWinNativeApi(); |
| } else { |
| StartPlatformAuthenticatorFlow(); |
| } |
| return; |
| } |
| |
| // Don't jump to an enclave credential if we need to do reauth because the |
| // OAuth token won't work. Also, don't jump to a phone credential either |
| // because reauthenticating is probably a better option for the user. |
| if (enclave_enabled_status_ != |
| EnclaveEnabledStatus::kEnabledAndReauthNeeded) { |
| // If not doing UV, but the allowlist matches an enclave credential, |
| // show UI to serve as user presence. |
| if (!enclave_will_do_uv && transport_availability_.request_type == |
| FidoRequestType::kGetAssertion) { |
| for (size_t i = 0; i < model_->mechanisms.size(); ++i) { |
| const auto& type = model_->mechanisms[i].type; |
| if (std::holds_alternative<Mechanism::Credential>(type) && |
| std::get<Mechanism::Credential>(type)->source == |
| AuthenticatorType::kEnclave) { |
| model_->priority_mechanism_index = i; |
| SetCurrentStep(Step::kSelectPriorityMechanism); |
| return; |
| } |
| } |
| } |
| if (transport_availability_.has_platform_authenticator_credential == |
| device::FidoRequestHandlerBase::RecognizedCredential:: |
| kNoRecognizedCredential) { |
| // If there are no local matches but there are phone or enclave |
| // passkeys, jump to the first one of them. |
| for (auto& mechanism : model_->mechanisms) { |
| const auto& type = mechanism.type; |
| if (std::holds_alternative<Mechanism::Credential>(type)) { |
| if (std::get<Mechanism::Credential>(type)->source == |
| AuthenticatorType::kEnclave) { |
| CHECK(enclave_will_do_uv); |
| mechanism.callback.Run(); |
| return; |
| } |
| } |
| } |
| } |
| } |
| } |
| // If a request only includes mechanisms that can be serviced by the Windows |
| // API and local credentials, there is no point showing Chrome UI as an |
| // extra step. Jump to Windows instead. |
| if (transport_availability_.has_win_native_api_authenticator && |
| std::ranges::all_of(model_->mechanisms, [](const auto& mech) { |
| return std::holds_alternative<Mechanism::WindowsAPI>(mech.type) || |
| (std::holds_alternative<Mechanism::Credential>(mech.type) && |
| std::get<Mechanism::Credential>(mech.type).value().source == |
| AuthenticatorType::kWinNative); |
| })) { |
| ephemeral_state_.did_invoke_platform_despite_no_priority_mechanism_ = |
| true; |
| StartWinNativeApi(); |
| return; |
| } |
| if (!hints_.transport.has_value() || |
| transport_availability_.request_type != |
| FidoRequestType::kGetAssertion || |
| // If there were any matches, ignore a hint and show the user the list. |
| std::ranges::any_of(model_->mechanisms, |
| [](const auto& mech) { |
| return std::get_if<Mechanism::Credential>( |
| &mech.type); |
| }) || |
| !StartGuidedFlowForHint(*hints_.transport)) { |
| SetCurrentStep(Step::kMechanismSelection); |
| } |
| } |
| } |
| |
| bool AuthenticatorRequestDialogController::StartGuidedFlowForHint( |
| AuthenticatorTransport transport) { |
| // The RP has given a hint about the expected transport for a create() or |
| // get() call. |
| // See https://w3c.github.io/webauthn/#enum-hints |
| if (transport == AuthenticatorTransport::kInternal && |
| enclave_enabled_status_ == |
| EnclaveEnabledStatus::kEnabledAndReauthNeeded) { |
| // Go to the mechanism selection screen to give the user a chance to use |
| // GPM. |
| return false; |
| } |
| Profile* const profile = |
| Profile::FromBrowserContext(GetRenderFrameHost()->GetBrowserContext()) |
| ->GetOriginalProfile(); |
| bool can_default_to_enclave = CanDefaultToEnclave(profile); |
| |
| const auto mech_it = std::ranges::find_if( |
| model_->mechanisms, |
| [transport, can_default_to_enclave](const auto& mech) { |
| if (std::holds_alternative<Mechanism::Enclave>(mech.type) && |
| !can_default_to_enclave) { |
| return false; |
| } |
| return MechanismMatchesHint(mech.type, transport); |
| }); |
| |
| if (mech_it != model_->mechanisms.end()) { |
| if (transport == AuthenticatorTransport::kInternal) { |
| ephemeral_state_.did_invoke_platform_despite_no_priority_mechanism_ = |
| true; |
| } |
| mech_it->callback.Run(); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| void AuthenticatorRequestDialogController:: |
| HideDialogAndDispatchToPlatformAuthenticator( |
| std::optional<AuthenticatorType> type) { |
| HideDialog(); |
| |
| std::vector<AuthenticatorReference>& authenticators = |
| ephemeral_state_.saved_authenticators_.authenticator_list(); |
| #if BUILDFLAG(IS_WIN) |
| // The Windows-native UI already handles retrying so we do not offer a second |
| // level of retry in that case. |
| if (type && *type != AuthenticatorType::kEnclave) { |
| model_->offer_try_again_in_ui = false; |
| } |
| #elif BUILDFLAG(IS_MAC) |
| // If there are multiple platform authenticators, one of them is the default. |
| if (!type.has_value()) { |
| if (std::ranges::any_of( |
| authenticators, [](const AuthenticatorReference& ref) { |
| return ref.type == AuthenticatorType::kOther && |
| ref.transport == device::FidoTransportProtocol::kInternal; |
| })) { |
| type = AuthenticatorType::kOther; |
| } |
| } |
| |
| if (!type.has_value()) { |
| type = AuthenticatorType::kTouchID; |
| } |
| #endif |
| |
| auto platform_authenticator_it = std::ranges::find_if( |
| authenticators, [type](const AuthenticatorReference& ref) -> bool { |
| if (type && *type == AuthenticatorType::kEnclave) { |
| return ref.type == *type; |
| } |
| return ref.transport == device::FidoTransportProtocol::kInternal && |
| (!type || ref.type == *type); |
| }); |
| |
| if (platform_authenticator_it == authenticators.end()) { |
| return; |
| } |
| |
| ephemeral_state_.dispatched_platform_authenticator_type_ = |
| platform_authenticator_it->type; |
| if (platform_authenticator_it->type == AuthenticatorType::kICloudKeychain) { |
| webauthn::user_actions::RecordICloudShown( |
| transport_availability_.request_type); |
| } else if (platform_authenticator_it->type == AuthenticatorType::kTouchID) { |
| webauthn::user_actions::RecordChromeProfileAuthenticatorShown( |
| transport_availability_.request_type); |
| } else if (platform_authenticator_it->type == AuthenticatorType::kWinNative) { |
| webauthn::user_actions::RecordWindowsHelloShown( |
| transport_availability_.request_type); |
| } |
| |
| DispatchRequestAsync(&*platform_authenticator_it); |
| } |
| |
| void AuthenticatorRequestDialogController::OnCableEvent( |
| device::cablev2::Event event) { |
| switch (event) { |
| case device::cablev2::Event::kPhoneConnected: |
| case device::cablev2::Event::kBLEAdvertReceived: |
| if (model_->step() != Step::kCableV2Connecting) { |
| SetCurrentStep(Step::kCableV2Connecting); |
| cable_connecting_sheet_timer_.Start( |
| FROM_HERE, base::Milliseconds(1250), |
| base::BindOnce(&AuthenticatorRequestDialogController:: |
| OnCableConnectingTimerComplete, |
| weak_factory_.GetWeakPtr())); |
| } |
| break; |
| case device::cablev2::Event::kReady: |
| if (cable_connecting_sheet_timer_.IsRunning()) { |
| cable_connecting_ready_to_advance_ = true; |
| } else { |
| SetCurrentStep(Step::kCableV2Connected); |
| } |
| break; |
| } |
| } |
| |
| void AuthenticatorRequestDialogController::OnCableConnectingTimerComplete() { |
| if (cable_connecting_ready_to_advance_ && |
| model_->step() == Step::kCableV2Connecting) { |
| SetCurrentStep(Step::kCableV2Connected); |
| } |
| } |
| |
| void AuthenticatorRequestDialogController::EnsureBleAdapterIsPoweredAndContinue( |
| base::OnceClosure action) { |
| after_ble_adapter_powered_ = std::move(action); |
| if (transport_availability_.ble_status == |
| BleStatus::kPendingPermissionRequest) { |
| // Trigger requesting Bluetooth permissions on macOS. |
| model_->ui_disabled_ = true; |
| model_->OnSheetModelChanged(); |
| request_ble_permission_callback_.Run( |
| base::BindOnce(&AuthenticatorRequestDialogController::OnBleStatusKnown, |
| weak_factory_.GetWeakPtr())); |
| return; |
| } |
| OnBleStatusKnown(transport_availability_.ble_status); |
| } |
| |
| void AuthenticatorRequestDialogController::OnBleStatusKnown( |
| BleStatus ble_status) { |
| if (!after_ble_adapter_powered_) { |
| // Drop the callback if there is no action pending after the adapter is |
| // powered. This could happen e.g. if the |
| // EnsureBleAdapterIsPoweredAndContinue was called twice before |
| // OnBleStatusKnown had a chance to resolve. |
| FIDO_LOG(ERROR) << "Ignoring BLE status: no action pending."; |
| return; |
| } |
| model_->ui_disabled_ = false; |
| transport_availability_.ble_status = ble_status; |
| model_->ble_adapter_is_powered = |
| transport_availability_.ble_status == |
| device::FidoRequestHandlerBase::BleStatus::kOn; |
| switch (transport_availability_.ble_status) { |
| case BleStatus::kOn: |
| std::move(after_ble_adapter_powered_).Run(); |
| return; |
| case BleStatus::kOff: |
| if (transport_availability_.can_power_on_ble_adapter) { |
| SetCurrentStep(Step::kBlePowerOnAutomatic); |
| } else { |
| SetCurrentStep(Step::kBlePowerOnManual); |
| } |
| return; |
| case BleStatus::kPermissionDenied: |
| // |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; |
| case BleStatus::kPendingPermissionRequest: |
| // This should have been handled by EnsureBleAdapterIsPoweredAndContinue. |
| NOTREACHED(); |
| } |
| } |
| |
| void AuthenticatorRequestDialogController::TryUsbDevice() { |
| DCHECK_EQ(model_->step(), Step::kUsbInsertAndActivate); |
| } |
| |
| void AuthenticatorRequestDialogController::StartPlatformAuthenticatorFlow() { |
| if (transport_availability_.request_type == FidoRequestType::kGetAssertion) { |
| switch (transport_availability_.has_platform_authenticator_credential) { |
| case device::FidoRequestHandlerBase::RecognizedCredential::kUnknown: |
| NOTREACHED(); |
| case device::FidoRequestHandlerBase::RecognizedCredential:: |
| kNoRecognizedCredential: |
| // Never try the platform authenticator if the request is known in |
| // advance to fail. Proceed to a special error screen instead. |
| SetCurrentStep(Step::kErrorInternalUnrecognized); |
| return; |
| case device::FidoRequestHandlerBase::RecognizedCredential:: |
| kHasRecognizedCredential: |
| break; |
| } |
| |
| // If the platform authenticator reports known credentials, show them in the |
| // UI. It is possible for the platform authenticator to report |
| // `kHasRecognizedCredential` without reporting any metadata (e.g. Chrome |
| // OS) but `recognized_credentials` could include other credentials. So we |
| // need to filter to check that metadata for a Chrome-implemented platform |
| // authenticator is really present. |
| std::vector<device::DiscoverableCredentialMetadata> platform_credentials; |
| std::ranges::copy_if( |
| transport_availability_.recognized_credentials, |
| std::back_inserter(platform_credentials), |
| [](const auto& cred) { return IsChromeImplemented(cred.source); }); |
| |
| if (!platform_credentials.empty()) { |
| if (transport_availability_.has_empty_allow_list) { |
| // For discoverable credential requests, show an account picker. |
| model_->creds = std::move(platform_credentials); |
| SetCurrentStep(Step::kPreSelectAccount); |
| } else { |
| // For requests with an allow list, pre-select a random credential. |
| model_->creds = {platform_credentials.front()}; |
| if (ProfileAuthenticatorWillDoUserVerification( |
| transport_availability_.user_verification_requirement, |
| transport_availability_.platform_has_biometrics)) { |
| // If it's not preferable to complete the request by clicking |
| // "Continue" then don't show the account selection sheet. |
| HideDialogAndDispatchToPlatformAuthenticator(); |
| } else { |
| // Otherwise show the chosen credential to the user. For platform |
| // authenticators with optional UV (e.g. Touch ID), this step |
| // essentially acts as the user presence check. |
| SetCurrentStep(Step::kPreSelectAccount); |
| } |
| } |
| return; |
| } |
| } |
| |
| if (transport_availability_.request_type == |
| 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(&AuthenticatorRequestDialogController:: |
| HideDialogAndDispatchToPlatformAuthenticator, |
| weak_factory_.GetWeakPtr(), std::nullopt); |
| SetCurrentStep(Step::kOffTheRecordInterstitial); |
| return; |
| } |
| } |
| |
| HideDialogAndDispatchToPlatformAuthenticator(); |
| } |
| |
| void AuthenticatorRequestDialogController::OnRequestTimeout() { |
| // The request may time out while the UI shows a different error. |
| if (!is_request_complete()) { |
| SetCurrentStep(Step::kTimedOut); |
| } |
| } |
| |
| void AuthenticatorRequestDialogController::OnActivatedKeyNotRegistered() { |
| DCHECK(!is_request_complete()); |
| SetCurrentStep(Step::kKeyNotRegistered); |
| } |
| |
| void AuthenticatorRequestDialogController::OnActivatedKeyAlreadyRegistered() { |
| DCHECK(!is_request_complete()); |
| SetCurrentStep(Step::kKeyAlreadyRegistered); |
| } |
| |
| void AuthenticatorRequestDialogController::OnSoftPINBlock() { |
| SetCurrentStep(Step::kClientPinErrorSoftBlock); |
| } |
| |
| void AuthenticatorRequestDialogController::OnHardPINBlock() { |
| SetCurrentStep(Step::kClientPinErrorHardBlock); |
| } |
| |
| void AuthenticatorRequestDialogController:: |
| OnAuthenticatorRemovedDuringPINEntry() { |
| SetCurrentStep(Step::kClientPinErrorAuthenticatorRemoved); |
| } |
| |
| void AuthenticatorRequestDialogController:: |
| OnAuthenticatorMissingResidentKeys() { |
| SetCurrentStep(Step::kMissingCapability); |
| } |
| |
| void AuthenticatorRequestDialogController:: |
| OnAuthenticatorMissingUserVerification() { |
| SetCurrentStep(Step::kMissingCapability); |
| } |
| |
| void AuthenticatorRequestDialogController::OnAuthenticatorMissingLargeBlob() { |
| // TODO(nsatragno): on Windows we should have a more accurate message if large |
| // blob is missing. |
| SetCurrentStep(Step::kMissingCapability); |
| } |
| |
| void AuthenticatorRequestDialogController::OnNoCommonAlgorithms() { |
| SetCurrentStep(Step::kMissingCapability); |
| } |
| |
| void AuthenticatorRequestDialogController::OnAuthenticatorStorageFull() { |
| SetCurrentStep(Step::kStorageFull); |
| } |
| |
| void AuthenticatorRequestDialogController::OnUserConsentDenied() { |
| if (ui_presentation() == UIPresentation::kAutofill) { |
| // 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. |
| CancelAuthenticatorRequest(); |
| return; |
| } |
| |
| if (ephemeral_state_.dispatched_platform_authenticator_type_ == |
| AuthenticatorType::kICloudKeychain) { |
| webauthn::user_actions::RecordICloudCancelled(); |
| // If we dispatched automatically to iCloud Keychain and the |
| // user clicked cancel, give them the option to try something else. |
| bool did_trigger_automatically = |
| ephemeral_state_.did_invoke_platform_despite_no_priority_mechanism_; |
| if (!did_trigger_automatically && |
| model_->priority_mechanism_index.has_value()) { |
| const auto& priority_type = |
| model_->mechanisms[*model_->priority_mechanism_index].type; |
| if (std::holds_alternative<Mechanism::Credential>(priority_type)) { |
| const Mechanism::CredentialInfo* cred_info = |
| &std::get<Mechanism::Credential>(priority_type).value(); |
| if (cred_info->source == AuthenticatorType::kICloudKeychain) { |
| did_trigger_automatically = true; |
| } |
| } else if (std::holds_alternative<Mechanism::ICloudKeychain>( |
| priority_type)) { |
| did_trigger_automatically = true; |
| } |
| } |
| |
| if (did_trigger_automatically && model_->mechanisms.size() > 1) { |
| StartOver(); |
| } else { |
| // Otherwise, respect the "Cancel" button in macOS UI as if it were our |
| // own. |
| CancelAuthenticatorRequest(); |
| } |
| return; |
| } |
| if (ephemeral_state_.dispatched_platform_authenticator_type_ == |
| AuthenticatorType::kTouchID) { |
| webauthn::user_actions::RecordChromeProfileCancelled(); |
| if (ui_presentation() == UIPresentation::kModalImmediate) { |
| // On immediate mode there's no need to show the error sheet where the |
| // user can retry. Instead fail early and let the relying party handle the |
| // error. |
| CancelAuthenticatorRequest(); |
| return; |
| } |
| } |
| SetCurrentStep(Step::kErrorInternalUnrecognized); |
| } |
| |
| bool AuthenticatorRequestDialogController::OnWinUserCancelled() { |
| #if BUILDFLAG(IS_WIN) |
| if (ui_presentation() == UIPresentation::kAutofill) { |
| // 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. |
| CancelAuthenticatorRequest(); |
| return true; |
| } |
| |
| if (ephemeral_state_.dispatched_platform_authenticator_type_ == |
| AuthenticatorType::kWinNative) { |
| webauthn::user_actions::RecordWindowsHelloCancelled(); |
| } |
| // 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. |
| bool enclave_is_option = |
| std::ranges::any_of(model_->mechanisms, [](const Mechanism& m) { |
| return std::holds_alternative<Mechanism::Enclave>(m.type); |
| }); |
| bool phone_is_option = |
| !WebAuthnApiSupportsHybrid() && |
| std::ranges::any_of(model_->mechanisms, [](const Mechanism& m) -> bool { |
| return std::holds_alternative<Mechanism::AddPhone>(m.type); |
| }); |
| bool have_other_option = enclave_is_option || phone_is_option; |
| bool windows_was_priority = |
| ephemeral_state_.did_invoke_platform_despite_no_priority_mechanism_ || |
| (model_->priority_mechanism_index && |
| std::holds_alternative<Mechanism::WindowsAPI>( |
| model_->mechanisms[*model_->priority_mechanism_index].type)); |
| if (have_other_option && windows_was_priority) { |
| StartOver(); |
| return true; |
| } |
| #endif |
| |
| return false; |
| } |
| |
| bool AuthenticatorRequestDialogController::OnHybridTransportError() { |
| SetCurrentStep(Step::kCableV2Error); |
| return true; |
| } |
| |
| bool AuthenticatorRequestDialogController::OnEnclaveError() { |
| SetCurrentStep(Step::kGPMError); |
| return true; |
| } |
| |
| bool AuthenticatorRequestDialogController::OnNoPasskeys() { |
| SetCurrentStep(Step::kErrorNoPasskeys); |
| return true; |
| } |
| |
| void AuthenticatorRequestDialogController::OnChallengeUrlFailure() { |
| if (!is_request_complete()) { |
| SetCurrentStep(Step::kErrorFetchingChallenge); |
| } |
| } |
| |
| void AuthenticatorRequestDialogController::BluetoothAdapterStatusChanged( |
| BleStatus ble_status) { |
| transport_availability_.ble_status = ble_status; |
| model_->ble_adapter_is_powered = ble_status == BleStatus::kOn; |
| model_->OnBluetoothPoweredStateChanged(); |
| |
| // For the manual flow, the user has to click the "next" button explicitly. |
| if (model_->step() == Step::kBlePowerOnAutomatic) { |
| ContinueWithFlowAfterBleAdapterPowered(); |
| } |
| } |
| |
| void AuthenticatorRequestDialogController::SetRequestCallback( |
| RequestCallback request_callback) { |
| request_callback_ = request_callback; |
| } |
| |
| void AuthenticatorRequestDialogController::SetAccountPreselectedCallback( |
| content::AuthenticatorRequestClientDelegate::AccountPreselectedCallback |
| callback) { |
| account_preselected_callback_ = callback; |
| } |
| |
| void AuthenticatorRequestDialogController::SetBluetoothAdapterPowerOnCallback( |
| base::RepeatingClosure bluetooth_adapter_power_on_callback) { |
| bluetooth_adapter_power_on_callback_ = bluetooth_adapter_power_on_callback; |
| } |
| |
| void AuthenticatorRequestDialogController::SetRequestBlePermissionCallback( |
| BlePermissionCallback callback) { |
| request_ble_permission_callback_ = std::move(callback); |
| } |
| |
| void AuthenticatorRequestDialogController::OnRetryUserVerification( |
| int attempts) { |
| model_->uv_attempts = attempts; |
| SetCurrentStep(Step::kRetryInternalUserVerification); |
| } |
| |
| void AuthenticatorRequestDialogController::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() == 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 AuthenticatorRequestDialogController::RemoveAuthenticator( |
| std::string_view authenticator_id) { |
| ephemeral_state_.saved_authenticators_.RemoveAuthenticator(authenticator_id); |
| } |
| |
| // SelectAccount is called to trigger an account selection dialog. |
| void AuthenticatorRequestDialogController::SelectAccount( |
| std::vector<device::AuthenticatorGetAssertionResponse> responses, |
| base::OnceCallback<void(device::AuthenticatorGetAssertionResponse)> |
| callback) { |
| ephemeral_state_.responses_ = std::move(responses); |
| model_->creds = {}; |
| for (const auto& response : ephemeral_state_.responses_) { |
| model_->creds.emplace_back(AuthenticatorType::kOther, |
| model_->relying_party_id, |
| response.credential->id, *response.user_entity, |
| /*provider_name=*/std::nullopt); |
| } |
| selection_callback_ = std::move(callback); |
| SetCurrentStep(Step::kSelectAccount); |
| } |
| |
| AuthenticatorType AuthenticatorRequestDialogController::OnAccountPreselected( |
| const std::vector<uint8_t> credential_id) { |
| // 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 auto cred = |
| std::ranges::find_if(transport_availability_.recognized_credentials, |
| [&credential_id](const auto& cred) { |
| return cred.cred_id == credential_id; |
| }); |
| CHECK(cred != transport_availability_.recognized_credentials.end()) |
| << "OnAccountPreselected() called with unknown credential_id " |
| << base::HexEncode(credential_id); |
| const AuthenticatorType source = cred->source; |
| DCHECK(account_preselected_callback_); |
| account_preselected_callback_.Run(*cred); |
| model_->preselected_cred = *cred; |
| |
| MaybeStartChallengeFetch(); |
| |
| // `source` should not be `kPhone` here. |
| if (source != AuthenticatorType::kEnclave) { |
| HideDialogAndDispatchToPlatformAuthenticator(source); |
| return source; |
| } |
| |
| model_->OnGPMPasskeySelected(credential_id); |
| return source; |
| } |
| |
| void AuthenticatorRequestDialogController::SetSelectedAuthenticatorForTesting( |
| AuthenticatorReference test_authenticator) { |
| ephemeral_state_.saved_authenticators_.AddAuthenticator( |
| std::move(test_authenticator)); |
| } |
| |
| void AuthenticatorRequestDialogController::StartTransportFlowForTesting( |
| AuthenticatorTransport transport) { |
| StartGuidedFlowForTransport(transport); |
| } |
| |
| void AuthenticatorRequestDialogController::SetCurrentStepForTesting(Step step) { |
| SetCurrentStep(step); |
| } |
| |
| void AuthenticatorRequestDialogController::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); |
| model_->min_pin_length = min_pin_length; |
| model_->pin_error = error; |
| switch (reason) { |
| case device::pin::PINEntryReason::kChallenge: |
| model_->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 AuthenticatorRequestDialogController::FinishCollectToken() { |
| SetCurrentStep(Step::kClientPinTapAgain); |
| } |
| |
| void AuthenticatorRequestDialogController::StartInlineBioEnrollment( |
| base::OnceClosure next_callback) { |
| model_->max_bio_samples = std::nullopt; |
| model_->bio_samples_remaining = std::nullopt; |
| bio_enrollment_callback_ = std::move(next_callback); |
| SetCurrentStep(Step::kInlineBioEnrollment); |
| } |
| |
| void AuthenticatorRequestDialogController::OnSampleCollected( |
| int bio_samples_remaining) { |
| DCHECK(model_->step() == Step::kInlineBioEnrollment); |
| |
| model_->bio_samples_remaining = bio_samples_remaining; |
| if (!model_->max_bio_samples) { |
| model_->max_bio_samples = bio_samples_remaining + 1; |
| } |
| model_->OnSheetModelChanged(); |
| } |
| |
| void AuthenticatorRequestDialogController::set_cable_transport_info( |
| std::optional<bool> extension_is_v2, |
| const std::optional<std::string>& cable_qr_string) { |
| if (extension_is_v2.has_value()) { |
| if (*extension_is_v2) { |
| model_->cable_ui_type = |
| AuthenticatorRequestDialogModel::CableUIType::CABLE_V2_SERVER_LINK; |
| } else { |
| model_->cable_ui_type = |
| AuthenticatorRequestDialogModel::CableUIType::CABLE_V1; |
| } |
| } else { |
| model_->cable_ui_type = |
| AuthenticatorRequestDialogModel::CableUIType::CABLE_V2_2ND_FACTOR; |
| } |
| |
| model_->cable_qr_string = cable_qr_string; |
| } |
| |
| void AuthenticatorRequestDialogController::set_allow_icloud_keychain( |
| bool is_allowed) { |
| allow_icloud_keychain_ = is_allowed; |
| } |
| |
| void AuthenticatorRequestDialogController::set_should_create_in_icloud_keychain( |
| bool is_enabled) { |
| should_create_in_icloud_keychain_ = is_enabled; |
| } |
| |
| #if BUILDFLAG(IS_MAC) |
| |
| // This enum is used in a histogram. Never change assigned values and only add |
| // new entries at the end. |
| enum class MacOsHistogramValues { |
| kStartedCreateForProfileAuthenticatorICloudDriveEnabled = 0, |
| kStartedCreateForProfileAuthenticatorICloudDriveDisabled = 1, |
| kStartedCreateForICloudKeychainICloudDriveEnabled = 2, |
| kStartedCreateForICloudKeychainICloudDriveDisabled = 3, |
| |
| kSuccessfulCreateForProfileAuthenticatorICloudDriveEnabled = 4, |
| kSuccessfulCreateForProfileAuthenticatorICloudDriveDisabled = 5, |
| kSuccessfulCreateForICloudKeychainICloudDriveEnabled = 6, |
| kSuccessfulCreateForICloudKeychainICloudDriveDisabled = 7, |
| |
| kStartedGetOnlyProfileAuthenticatorRecognised = 8, |
| kStartedGetOnlyICloudKeychainRecognised = 9, |
| kStartedGetBothRecognised = 10, |
| |
| kSuccessfulGetFromProfileAuthenticator = 11, |
| kSuccessfulGetFromICloudKeychain = 12, |
| kMaxValue = kSuccessfulGetFromICloudKeychain, |
| }; |
| |
| void AuthenticatorRequestDialogController::RecordMacOsStartedHistogram() { |
| if (is_non_webauthn_request_ || model_->relying_party_id == "google.com") { |
| return; |
| } |
| |
| std::optional<MacOsHistogramValues> v; |
| if (transport_availability_.request_type == |
| FidoRequestType::kMakeCredential && |
| transport_availability_.make_credential_attachment.has_value() && |
| *transport_availability_.make_credential_attachment != |
| device::AuthenticatorAttachment::kCrossPlatform) { |
| if (should_create_in_icloud_keychain_) { |
| v = has_icloud_drive_enabled_ |
| ? MacOsHistogramValues:: |
| kStartedCreateForICloudKeychainICloudDriveEnabled |
| : MacOsHistogramValues:: |
| kStartedCreateForICloudKeychainICloudDriveDisabled; |
| } else { |
| v = has_icloud_drive_enabled_ |
| ? MacOsHistogramValues:: |
| kStartedCreateForProfileAuthenticatorICloudDriveEnabled |
| : MacOsHistogramValues:: |
| kStartedCreateForProfileAuthenticatorICloudDriveDisabled; |
| } |
| } else if (transport_availability_.request_type == |
| device::FidoRequestType::kGetAssertion && |
| IsModalRequest(ui_presentation())) { |
| const bool profile = |
| transport_availability_.has_platform_authenticator_credential == |
| device::FidoRequestHandlerBase::RecognizedCredential:: |
| kHasRecognizedCredential; |
| const bool icloud = |
| transport_availability_.has_icloud_keychain_credential == |
| device::FidoRequestHandlerBase::RecognizedCredential:: |
| kHasRecognizedCredential; |
| if (profile && !icloud) { |
| v = MacOsHistogramValues::kStartedGetOnlyProfileAuthenticatorRecognised; |
| } else if (icloud && !profile) { |
| v = MacOsHistogramValues::kStartedGetOnlyICloudKeychainRecognised; |
| } else if (icloud && profile) { |
| v = MacOsHistogramValues::kStartedGetBothRecognised; |
| } |
| } |
| |
| if (v) { |
| base::UmaHistogramEnumeration( |
| "WebAuthentication.MacOS.PlatformAuthenticatorAction", *v); |
| did_record_macos_start_histogram_ = true; |
| } |
| } |
| |
| void AuthenticatorRequestDialogController::RecordMacOsSuccessHistogram( |
| FidoRequestType request_type, |
| AuthenticatorType authenticator_type) { |
| if (!did_record_macos_start_histogram_) { |
| return; |
| } |
| |
| std::optional<MacOsHistogramValues> v; |
| |
| if (transport_availability_.request_type == |
| FidoRequestType::kMakeCredential) { |
| if (authenticator_type == AuthenticatorType::kTouchID) { |
| v = has_icloud_drive_enabled_ |
| ? MacOsHistogramValues:: |
| kSuccessfulCreateForProfileAuthenticatorICloudDriveEnabled |
| : MacOsHistogramValues:: |
| kSuccessfulCreateForProfileAuthenticatorICloudDriveDisabled; |
| } else if (authenticator_type == AuthenticatorType::kICloudKeychain) { |
| v = has_icloud_drive_enabled_ |
| ? MacOsHistogramValues:: |
| kSuccessfulCreateForICloudKeychainICloudDriveEnabled |
| : MacOsHistogramValues:: |
| kSuccessfulCreateForICloudKeychainICloudDriveDisabled; |
| } |
| } else { |
| if (authenticator_type == AuthenticatorType::kTouchID) { |
| v = MacOsHistogramValues::kSuccessfulGetFromProfileAuthenticator; |
| } else if (authenticator_type == AuthenticatorType::kICloudKeychain) { |
| v = MacOsHistogramValues::kSuccessfulGetFromICloudKeychain; |
| } |
| } |
| |
| if (v) { |
| base::UmaHistogramEnumeration( |
| "WebAuthentication.MacOS.PlatformAuthenticatorAction", *v); |
| } |
| } |
| |
| void AuthenticatorRequestDialogController::set_has_icloud_drive_enabled( |
| bool is_enabled) { |
| has_icloud_drive_enabled_ = is_enabled; |
| } |
| |
| #endif |
| |
| void AuthenticatorRequestDialogController::SetCredentialTypes(int types) { |
| credential_types_ = types; |
| } |
| |
| content::AuthenticatorRequestClientDelegate::UIPresentation |
| AuthenticatorRequestDialogController::ui_presentation() const { |
| return model_->ui_presentation; |
| } |
| |
| void AuthenticatorRequestDialogController::SetUIPresentation( |
| UIPresentation modality) { |
| model_->set_ui_presentation(modality); |
| } |
| |
| void AuthenticatorRequestDialogController::ProvideChallengeUrl( |
| const GURL& url, |
| base::OnceCallback<void(std::optional<base::span<const uint8_t>>)> |
| callback) { |
| CHECK(url.is_valid()); |
| challenge_url_ = url; |
| challenge_callback_ = std::move(callback); |
| |
| // Conditional requests don't initiate a challenge fetch unless and until the |
| // user triggers it, but modal requests always perform the fetch so it can |
| // be started immediately. |
| if (IsModalRequest(ui_presentation())) { |
| MaybeStartChallengeFetch(); |
| } |
| } |
| |
| void AuthenticatorRequestDialogController::InitializeEnclaveRequestCallback( |
| device::FidoDiscoveryFactory* discovery_factory) { |
| CHECK(!enclave_request_callback_); |
| |
| using EnclaveEventStream = device::FidoDiscoveryBase::EventStream< |
| std::unique_ptr<device::enclave::CredentialRequest>>; |
| std::unique_ptr<EnclaveEventStream> event_stream; |
| std::tie(enclave_request_callback_, event_stream) = EnclaveEventStream::New(); |
| discovery_factory->set_enclave_ui_request_stream(std::move(event_stream)); |
| } |
| |
| void AuthenticatorRequestDialogController::MaybeStartChallengeFetch() { |
| if (!challenge_callback_) { |
| return; |
| } |
| |
| auto challenge_or_error = GetChallengeUrlFetcher()->GetChallenge(); |
| if (!challenge_or_error.has_value() && |
| challenge_or_error.error() == |
| ChallengeUrlFetcher::ChallengeNotAvailableReason::kNotRequested) { |
| GetChallengeUrlFetcher()->FetchUrl( |
| challenge_url_, |
| base::BindOnce( |
| &AuthenticatorRequestDialogController::OnChallengeFetched, |
| weak_factory_.GetWeakPtr())); |
| } |
| } |
| |
| void AuthenticatorRequestDialogController::OnChallengeFetched() { |
| auto challenge_or_error = GetChallengeUrlFetcher()->GetChallenge(); |
| |
| if (challenge_or_error.has_value()) { |
| std::move(challenge_callback_).Run(challenge_or_error.value()); |
| return; |
| } |
| |
| CHECK_EQ(challenge_or_error.error(), |
| ChallengeUrlFetcher::ChallengeNotAvailableReason:: |
| kErrorFetchingChallenge); |
| |
| std::move(challenge_callback_).Run(std::nullopt); |
| } |
| |
| ChallengeUrlFetcher* |
| AuthenticatorRequestDialogController::GetChallengeUrlFetcher() { |
| if (!challenge_url_fetcher_) { |
| challenge_url_fetcher_ = std::make_unique<ChallengeUrlFetcher>( |
| Profile::FromBrowserContext(GetRenderFrameHost()->GetBrowserContext()) |
| ->GetDefaultStoragePartition() |
| ->GetURLLoaderFactoryForBrowserProcess()); |
| } |
| return challenge_url_fetcher_.get(); |
| } |
| |
| base::WeakPtr<AuthenticatorRequestDialogController> |
| AuthenticatorRequestDialogController::GetWeakPtr() { |
| return weak_factory_.GetWeakPtr(); |
| } |
| |
| void AuthenticatorRequestDialogController::SetCurrentStep(Step step) { |
| if (!started_) { |
| // Dialog isn't showing yet. Remember to show this step when it appears. |
| pending_step_ = step; |
| return; |
| } |
| |
| // Reset state related to automatically advancing the state. |
| cable_connecting_sheet_timer_.Stop(); |
| cable_connecting_ready_to_advance_ = false; |
| |
| model_->SetStep(step); |
| } |
| |
| void AuthenticatorRequestDialogController::StartGuidedFlowForTransport( |
| AuthenticatorTransport transport) { |
| DCHECK(model_->step() == Step::kMechanismSelection || |
| model_->step() == Step::kUsbInsertAndActivate || |
| model_->step() == Step::kCableActivate || |
| model_->step() == Step::kPasskeyAutofill || |
| model_->step() == Step::kCreatePasskey || |
| model_->step() == Step::kPreSelectAccount || |
| model_->step() == Step::kSelectPriorityMechanism || |
| model_->step() == Step::kSelectAccount || |
| model_->step() == Step::kNotStarted); |
| switch (transport) { |
| case AuthenticatorTransport::kUsbHumanInterfaceDevice: |
| SetCurrentStep(Step::kUsbInsertAndActivate); |
| break; |
| case AuthenticatorTransport::kInternal: |
| StartPlatformAuthenticatorFlow(); |
| break; |
| case AuthenticatorTransport::kHybrid: |
| EnsureBleAdapterIsPoweredAndContinue( |
| base::BindOnce(&AuthenticatorRequestDialogController::SetCurrentStep, |
| weak_factory_.GetWeakPtr(), Step::kCableActivate)); |
| break; |
| default: |
| break; |
| } |
| } |
| |
| void AuthenticatorRequestDialogController::StartGuidedFlowForAddPhone() { |
| EnsureBleAdapterIsPoweredAndContinue( |
| base::BindOnce(&AuthenticatorRequestDialogController::SetCurrentStep, |
| weak_factory_.GetWeakPtr(), Step::kCableV2QRCode)); |
| } |
| |
| void AuthenticatorRequestDialogController::StartWinNativeApi() { |
| DCHECK(transport_availability_.has_win_native_api_authenticator); |
| if (transport_availability_.request_is_internal_only && |
| !transport_availability_.win_is_uvpaa) { |
| model_->offer_try_again_in_ui = false; |
| SetCurrentStep(Step::kErrorWindowsHelloNotEnabled); |
| return; |
| } |
| |
| if (model_->resident_key_requirement != |
| device::ResidentKeyRequirement::kDiscouraged && |
| !transport_availability_.win_native_ui_shows_resident_credential_notice) { |
| SetCurrentStep(Step::kResidentCredentialConfirmation); |
| } else { |
| HideDialogAndDispatchToPlatformAuthenticator(AuthenticatorType::kWinNative); |
| } |
| } |
| |
| void AuthenticatorRequestDialogController::StartICloudKeychain() { |
| DCHECK(transport_availability_.has_icloud_keychain); |
| if (transport_availability_.has_icloud_keychain_credential == |
| device::FidoRequestHandlerBase::RecognizedCredential:: |
| kHasRecognizedCredential && |
| !transport_availability_.has_empty_allow_list) { |
| // For requests with an allow list, pre-select a random credential. |
| const device::DiscoverableCredentialMetadata* selected = nullptr; |
| for (const auto& cred : transport_availability_.recognized_credentials) { |
| if (cred.source == AuthenticatorType::kICloudKeychain) { |
| selected = &cred; |
| break; |
| } |
| } |
| account_preselected_callback_.Run(*selected); |
| } |
| |
| HideDialogAndDispatchToPlatformAuthenticator( |
| AuthenticatorType::kICloudKeychain); |
| } |
| |
| void AuthenticatorRequestDialogController::StartEnclave() { |
| model_->OnGPMSelected(); |
| } |
| |
| void AuthenticatorRequestDialogController::ReauthForSyncRestore() { |
| signin_ui_util::ShowReauthForPrimaryAccountWithAuthError( |
| Profile::FromBrowserContext(GetRenderFrameHost()->GetBrowserContext()) |
| ->GetOriginalProfile(), |
| signin_metrics::AccessPoint::kWebauthnModalDialog); |
| CancelAuthenticatorRequest(); |
| } |
| |
| void AuthenticatorRequestDialogController::StartAutofillRequest() { |
| model_->creds = transport_availability_.recognized_credentials; |
| |
| auto* render_frame_host = GetRenderFrameHost(); |
| auto* web_contents = |
| content::WebContents::FromRenderFrameHost(render_frame_host); |
| std::vector<password_manager::PasskeyCredential> credentials; |
| std::optional<std::u16string> priority_phone_name; |
| for (const auto& credential : model_->creds) { |
| if (credential.source == AuthenticatorType::kEnclave && |
| enclave_enabled_status_ != EnclaveEnabledStatus::kEnabled) { |
| continue; |
| } |
| password_manager::PasskeyCredential& passkey = credentials.emplace_back( |
| 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(""))); |
| if (credential.provider_name) { |
| passkey.SetAuthenticatorLabel(l10n_util::GetStringFUTF16( |
| IDS_PASSWORD_MANAGER_PASSKEY_FROM_PROVIDER, |
| base::UTF8ToUTF16(*credential.provider_name))); |
| } else if (credential.source == AuthenticatorType::kPhone) { |
| passkey.SetAuthenticatorLabel(l10n_util::GetStringFUTF16( |
| IDS_PASSWORD_MANAGER_PASSKEY_FROM_PHONE, *priority_phone_name)); |
| } |
| } |
| ReportConditionalUiPasskeyCount(credentials.size()); |
| |
| // TODO(https://crbug.com/358119268): This will probably get its own mediation |
| // type, but for prototyping we assume any conditional request with passwords |
| // uses ambient. |
| bool has_ambient_credentials = !credentials.empty() || !passwords_.empty(); |
| if (base::FeatureList::IsEnabled(device::kWebAuthnAmbientSignin) && |
| has_ambient_credentials && |
| (credential_types_ & |
| static_cast<int>(blink::mojom::CredentialTypeFlags::kPassword))) { |
| auto* controller = |
| ambient_signin::AmbientSigninController::GetOrCreateForCurrentDocument( |
| render_frame_host); |
| // TODO(https://crbug.com/358119268): `AmbientSigninController` needs to be |
| // refactored, since this is now the single source of all credentials it |
| // shows. |
| controller->AddAndShowWebAuthnMethods( |
| model(), credentials, credential_types_, |
| base::BindOnce( |
| IgnoreResult( |
| &AuthenticatorRequestDialogController::OnAccountPreselected), |
| weak_factory_.GetWeakPtr())); |
| controller->AddAndShowPasswordMethods( |
| std::move(passwords_), credential_types_, |
| base::BindRepeating( |
| &AuthenticatorRequestDialogModel::OnPasswordCredentialSelected, |
| base::Unretained(model_))); |
| } |
| |
| ChromeWebAuthnCredentialsDelegate* webauthn_credentials_delegate = |
| ChromeWebAuthnCredentialsDelegateFactory::GetFactory(web_contents) |
| ->GetDelegateForFrame(render_frame_host); |
| if (webauthn_credentials_delegate) { |
| // May be null on tests. |
| webauthn_credentials_delegate->OnCredentialsReceived( |
| std::move(credentials), |
| ChromeWebAuthnCredentialsDelegate::SecurityKeyOrHybridFlowAvailable( |
| true)); |
| } |
| SetCurrentStep(Step::kPasskeyAutofill); |
| } |
| |
| void AuthenticatorRequestDialogController::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 AuthenticatorRequestDialogController::SortRecognizedCredentials() { |
| struct { |
| bool operator()(const device::DiscoverableCredentialMetadata& a, |
| const device::DiscoverableCredentialMetadata& b) { |
| return std::tie(a.user.id, a.cred_id) < std::tie(b.user.id, b.cred_id); |
| } |
| } id_comparator; |
| std::ranges::sort(transport_availability_.recognized_credentials, |
| std::ref(id_comparator)); |
| |
| struct UsernameComparator { |
| explicit UsernameComparator(const icu::Locale* locale) { |
| UErrorCode error = U_ZERO_ERROR; |
| collator_.reset(icu::Collator::createInstance(*locale, error)); |
| } |
| |
| bool operator()(const device::DiscoverableCredentialMetadata& a, |
| const device::DiscoverableCredentialMetadata& b) { |
| return base::i18n::CompareString16WithCollator( |
| *collator_, base::UTF8ToUTF16(a.user.name.value_or("")), |
| base::UTF8ToUTF16(b.user.name.value_or(""))) == UCOL_LESS; |
| } |
| |
| std::unique_ptr<icu::Collator> collator_; |
| }; |
| UsernameComparator user_name_comparator(&icu::Locale::getDefault()); |
| |
| std::ranges::stable_sort(transport_availability_.recognized_credentials, |
| std::ref(user_name_comparator)); |
| } |
| |
| void AuthenticatorRequestDialogController::PopulateMechanisms() { |
| const bool is_get_assertion = |
| transport_availability_.request_type == FidoRequestType::kGetAssertion; |
| bool specific_local_passkeys_listed = false; |
| if (is_get_assertion && IsModalRequest(ui_presentation())) { |
| // List passkeys instead of mechanisms for platform & GPM authenticators. |
| for (const auto& cred : transport_availability_.recognized_credentials) { |
| if (cred.source == AuthenticatorType::kICloudKeychain && |
| !allow_icloud_keychain_) { |
| continue; |
| } |
| if (cred.source == AuthenticatorType::kEnclave) { |
| if (enclave_enabled_status_ != EnclaveEnabledStatus::kEnabled) { |
| // Do not list passkeys from the enclave if it needs reauth before |
| // proceeding. Instead, we'll show a button to trigger reauth. |
| continue; |
| } |
| } |
| specific_local_passkeys_listed = true; |
| std::u16string name = base::UTF8ToUTF16(cred.user.name.value_or("")); |
| Mechanism::Type mechanism_type = Mechanism::Credential( |
| {cred.source, cred.user.id, cred.last_used_time}); |
| auto& mechanism = model_->mechanisms.emplace_back( |
| mechanism_type, name, |
| GetMechanismIcon(mechanism_type, ui_presentation()), |
| base::BindRepeating( |
| base::IgnoreResult( |
| &AuthenticatorRequestDialogController::OnAccountPreselected), |
| base::Unretained(this), cred.cred_id), |
| base::UTF8ToUTF16(cred.user.display_name.value_or(""))); |
| mechanism.description = |
| AuthenticatorRequestDialogModel::GetMechanismDescription( |
| cred, ui_presentation()); |
| } |
| if (!passwords_.empty()) { |
| PopulatePasswords(); |
| } |
| } |
| |
| std::vector<AuthenticatorTransport> transports_to_list_if_active; |
| // Do not list the internal transport if we can offer users to select a |
| // platform credential directly. This is true for both conditional requests |
| // and the new passkey selector UI. |
| bool did_enumerate_local_passkeys = false; |
| if (ui_presentation() == UIPresentation::kAutofill) { |
| did_enumerate_local_passkeys = true; |
| } else if (is_get_assertion) { |
| switch (transport_availability_.has_platform_authenticator_credential) { |
| case device::FidoRequestHandlerBase::RecognizedCredential:: |
| kNoRecognizedCredential: |
| did_enumerate_local_passkeys = true; |
| break; |
| case device::FidoRequestHandlerBase::RecognizedCredential:: |
| kHasRecognizedCredential: |
| // Some platform authenticators (like ChromeOS) will report passkey |
| // availability but will not enumerate them. |
| did_enumerate_local_passkeys = specific_local_passkeys_listed; |
| break; |
| case device::FidoRequestHandlerBase::RecognizedCredential::kUnknown: |
| did_enumerate_local_passkeys = false; |
| break; |
| } |
| } |
| if (!did_enumerate_local_passkeys && |
| base::Contains(transport_availability_.available_transports, |
| AuthenticatorTransport::kInternal)) { |
| transports_to_list_if_active.push_back(AuthenticatorTransport::kInternal); |
| } |
| |
| const auto kCable = AuthenticatorTransport::kHybrid; |
| const bool windows_handles_hybrid = WebAuthnApiSupportsHybrid(); |
| bool include_add_phone_option = false; |
| |
| if (model_->cable_ui_type) { |
| switch (*model_->cable_ui_type) { |
| case AuthenticatorRequestDialogModel::CableUIType::CABLE_V2_2ND_FACTOR: |
| if (base::Contains(transport_availability_.available_transports, |
| kCable)) { |
| include_add_phone_option = !windows_handles_hybrid; |
| } |
| break; |
| |
| case AuthenticatorRequestDialogModel::CableUIType::CABLE_V2_SERVER_LINK: |
| 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. |
| model_->offer_try_again_in_ui = false; |
| } |
| break; |
| } |
| } |
| } |
| |
| if (!is_get_assertion && |
| enclave_enabled_status_ == EnclaveEnabledStatus::kEnabled && |
| *transport_availability_.make_credential_attachment != |
| device::AuthenticatorAttachment::kCrossPlatform) { |
| const std::u16string name = |
| l10n_util::GetStringUTF16(IDS_WEBAUTHN_SOURCE_GOOGLE_PASSWORD_MANAGER); |
| Mechanism::Type mechanism_type = Mechanism::Enclave(); |
| Mechanism mechanism( |
| mechanism_type, name, |
| GetMechanismIcon(mechanism_type, ui_presentation()), |
| base::BindRepeating(&AuthenticatorRequestDialogController::StartEnclave, |
| base::Unretained(this))); |
| mechanism.description = base::UTF8ToUTF16(model_->GetGpmAccountEmail()); |
| model_->mechanisms.emplace_back(std::move(mechanism)); |
| } |
| if (enclave_enabled_status_ == |
| EnclaveEnabledStatus::kEnabledAndReauthNeeded && |
| UIPresentation::kModal == ui_presentation() && |
| model_->relying_party_id != "google.com") { |
| // Show a button that lets the user sign in again to restore sync. This |
| // cancels the request, so we can't do it for conditional UI requests. |
| // TODO(crbug.com/345413738): add support for conditional UI. |
| const std::u16string name = |
| l10n_util::GetStringUTF16(IDS_WEBAUTHN_SIGN_IN_AGAIN_TITLE); |
| Mechanism::Type mechanism_type = Mechanism::SignInAgain(); |
| Mechanism enclave( |
| mechanism_type, name, |
| GetMechanismIcon(mechanism_type, ui_presentation()), |
| base::BindRepeating( |
| &AuthenticatorRequestDialogController::ReauthForSyncRestore, |
| base::Unretained(this))); |
| enclave.description = |
| l10n_util::GetStringUTF16(IDS_WEBAUTHN_SIGN_IN_AGAIN_DESCRIPTION); |
| model_->mechanisms.emplace_back(std::move(enclave)); |
| } |
| |
| if (transport_availability_.has_icloud_keychain && allow_icloud_keychain_ && |
| // The mechanism for iCloud Keychain only appears for create(), or if |
| // Chrome doesn't have permission to enumerate credentials and thus the |
| // user needs a generic mechanism to trigger it. |
| (!is_get_assertion || |
| (transport_availability_.has_icloud_keychain_credential == |
| device::FidoRequestHandlerBase::RecognizedCredential::kUnknown && |
| ui_presentation() != UIPresentation::kModalImmediate))) { |
| const std::u16string name = |
| l10n_util::GetStringUTF16(IDS_WEBAUTHN_TRANSPORT_ICLOUD_KEYCHAIN); |
| Mechanism::Type mechanism_type = Mechanism::ICloudKeychain(); |
| model_->mechanisms.emplace_back( |
| mechanism_type, name, |
| GetMechanismIcon(mechanism_type, ui_presentation()), |
| base::BindRepeating( |
| &AuthenticatorRequestDialogController::StartICloudKeychain, |
| base::Unretained(this))); |
| } |
| |
| std::optional<std::pair<int, AuthenticatorTransport>> windows_button_label; |
| windows_button_label = |
| GetWindowsAPIButtonLabel(transport_availability_, ui_presentation()); |
| if (windows_button_label && |
| windows_button_label->second == AuthenticatorTransport::kInternal) { |
| // Add the Windows button before phones if it can trigger Windows Hello. |
| AddWindowsButton(windows_button_label->first, windows_button_label->second); |
| } |
| |
| // If the new UI is enabled, only show USB as an option if the QR code is |
| // not available, if tapping it would trigger a prompt to enable BLE, or if |
| // hints suggest that hybrid and USB should be separate options. |
| const bool include_usb_option = |
| base::Contains(transport_availability_.available_transports, |
| AuthenticatorTransport::kUsbHumanInterfaceDevice) && |
| (!include_add_phone_option || |
| transport_availability_.ble_status != BleStatus::kOn || |
| hints_.transport == AuthenticatorTransport::kUsbHumanInterfaceDevice || |
| hints_.transport == AuthenticatorTransport::kHybrid); |
| |
| if (include_add_phone_option) { |
| model_->show_security_key_on_qr_sheet = |
| base::Contains(transport_availability_.available_transports, |
| AuthenticatorTransport::kUsbHumanInterfaceDevice) && |
| !include_usb_option; |
| std::u16string label = l10n_util::GetStringUTF16( |
| GetHybridButtonLabel(model_->show_security_key_on_qr_sheet)); |
| Mechanism::Type mechanism_type = Mechanism::AddPhone(); |
| model_->mechanisms.emplace_back( |
| mechanism_type, label, |
| GetMechanismIcon(mechanism_type, ui_presentation()), |
| base::BindRepeating( |
| &AuthenticatorRequestDialogController::StartGuidedFlowForAddPhone, |
| base::Unretained(this))); |
| } |
| if (include_usb_option) { |
| transports_to_list_if_active.push_back( |
| AuthenticatorTransport::kUsbHumanInterfaceDevice); |
| } |
| |
| for (const auto transport : transports_to_list_if_active) { |
| if (!base::Contains(transport_availability_.available_transports, |
| transport)) { |
| continue; |
| } |
| |
| Mechanism::Type mechanism_type = Mechanism::Transport(transport); |
| model_->mechanisms.emplace_back( |
| mechanism_type, GetTransportDescription(transport), |
| GetMechanismIcon(mechanism_type, ui_presentation()), |
| base::BindRepeating( |
| &AuthenticatorRequestDialogController::StartGuidedFlowForTransport, |
| base::Unretained(this), transport)); |
| } |
| // Add the Windows native API button last if it does not do Windows Hello. |
| if (windows_button_label && |
| windows_button_label->second != AuthenticatorTransport::kInternal) { |
| AddWindowsButton(windows_button_label->first, windows_button_label->second); |
| } |
| |
| model_->mechanisms = MechanismSorter::ProcessMechanisms( |
| std::move(model_->mechanisms), ui_presentation()); |
| } |
| |
| void AuthenticatorRequestDialogController::AddWindowsButton( |
| int label, |
| AuthenticatorTransport transport) { |
| const std::u16string desc = l10n_util::GetStringUTF16(label); |
| Mechanism::Type mechanism_type = Mechanism::WindowsAPI(); |
| model_->mechanisms.emplace_back( |
| mechanism_type, desc, |
| GetMechanismIcon(mechanism_type, ui_presentation(), transport), |
| base::BindRepeating( |
| &AuthenticatorRequestDialogController::StartWinNativeApi, |
| base::Unretained(this))); |
| } |
| |
| std::optional<size_t> |
| AuthenticatorRequestDialogController::IndexOfPriorityMechanism() { |
| // Never pick a priority mechanism if we are showing the enclave reauth |
| // button. |
| if (enclave_enabled_status_ == |
| EnclaveEnabledStatus::kEnabledAndReauthNeeded && |
| IsModalRequest(ui_presentation())) { |
| return std::nullopt; |
| } |
| |
| switch (transport_availability_.request_type) { |
| case FidoRequestType::kGetAssertion: |
| return ui_presentation() == UIPresentation::kModalImmediate |
| ? IndexOfImmediateGetPriorityMechanism() |
| : IndexOfGetAssertionPriorityMechanism(); |
| case FidoRequestType::kMakeCredential: |
| return IndexOfMakeCredentialPriorityMechanism(); |
| } |
| } |
| |
| std::optional<size_t> |
| AuthenticatorRequestDialogController::IndexOfGetAssertionPriorityMechanism() { |
| CHECK_EQ(transport_availability_.request_type, |
| FidoRequestType::kGetAssertion); |
| |
| // If there is a single mechanism, go to that. |
| if (model_->mechanisms.size() == 1) { |
| return 0; |
| } |
| |
| if (transport_availability_.has_empty_allow_list) { |
| // The index and info of the credential that the UI should default to. |
| std::optional<std::pair<size_t, const Mechanism::CredentialInfo*>> |
| best_cred; |
| bool multiple_distinct_creds = false; |
| bool has_password = false; |
| |
| for (size_t i = 0; i < model_->mechanisms.size(); ++i) { |
| const auto& type = model_->mechanisms[i].type; |
| if (std::holds_alternative<Mechanism::Credential>(type)) { |
| const Mechanism::CredentialInfo* cred_info = |
| &std::get<Mechanism::Credential>(type).value(); |
| |
| if (!best_cred.has_value()) { |
| best_cred = std::make_pair(i, cred_info); |
| } else if (best_cred->second->user_id == cred_info->user_id) { |
| if (SourcePriority(cred_info->source) > |
| SourcePriority(best_cred->second->source)) { |
| best_cred = std::make_pair(i, cred_info); |
| } |
| } else { |
| multiple_distinct_creds = true; |
| } |
| } else if (std::holds_alternative<Mechanism::Password>(type)) { |
| has_password = true; |
| } |
| } |
| // If one of the passkeys is a valid default, go to that. |
| if (!has_password && !multiple_distinct_creds && best_cred.has_value() && |
| (best_cred->second->source != AuthenticatorType::kEnclave || |
| CanDefaultToEnclave(Profile::FromBrowserContext( |
| GetRenderFrameHost()->GetBrowserContext()) |
| ->GetOriginalProfile()))) { |
| return best_cred->first; |
| } |
| } |
| |
| // If it's caBLEv1, or server-linked caBLEv2, jump to that. |
| if (model_->cable_ui_type) { |
| switch (*model_->cable_ui_type) { |
| case AuthenticatorRequestDialogModel::CableUIType::CABLE_V2_SERVER_LINK: |
| case AuthenticatorRequestDialogModel::CableUIType::CABLE_V1: |
| for (size_t i = 0; i < model_->mechanisms.size(); ++i) { |
| if (model_->mechanisms[i].type == |
| Mechanism::Type( |
| Mechanism::Transport(AuthenticatorTransport::kHybrid))) { |
| return i; |
| } |
| } |
| break; |
| case AuthenticatorRequestDialogModel::CableUIType::CABLE_V2_2ND_FACTOR: |
| break; |
| } |
| } |
| |
| // For all other cases, go to the multi source passkey picker. |
| return std::nullopt; |
| } |
| |
| std::optional<size_t> |
| AuthenticatorRequestDialogController::IndexOfImmediateGetPriorityMechanism() { |
| CHECK_EQ(transport_availability_.request_type, |
| FidoRequestType::kGetAssertion); |
| CHECK_EQ(ui_presentation(), UIPresentation::kModalImmediate); |
| |
| if (model_->mechanisms.size() != 1) { |
| return std::nullopt; |
| } |
| |
| const auto& mechanism = model_->mechanisms[0]; |
| |
| const bool is_enclave = |
| std::holds_alternative<Mechanism::Credential>(mechanism.type) && |
| (std::get<Mechanism::Credential>(mechanism.type).value().source == |
| AuthenticatorType::kEnclave); |
| const bool chrome_does_uv_for_gpm = |
| model_->gpm_uv_method.value_or( |
| EnclaveUserVerificationMethod::kUnsatisfiable) == |
| EnclaveUserVerificationMethod::kUVKeyWithChromeUI; |
| |
| if (transport_availability_.autoselect_in_immediate_mediation) { |
| bool is_password = |
| std::holds_alternative<Mechanism::Password>(mechanism.type); |
| bool is_chrome_profile = |
| std::holds_alternative<Mechanism::Credential>(mechanism.type) && |
| std::get<Mechanism::Credential>(mechanism.type).value().source == |
| AuthenticatorType::kTouchID; |
| if (is_password || is_chrome_profile || |
| (is_enclave && !chrome_does_uv_for_gpm)) { |
| // Password and Chrome Profile UV does not display account details. |
| // Similarly non-Chrome user verification UI for enclave passkeys does not |
| // display the selected account details. Show the Chrome UI first. |
| return std::nullopt; |
| } |
| return 0; |
| } |
| |
| if (is_enclave && chrome_does_uv_for_gpm) { |
| return 0; |
| } |
| return std::nullopt; |
| } |
| |
| std::optional<size_t> |
| AuthenticatorRequestDialogController::IndexOfMakeCredentialPriorityMechanism() { |
| CHECK_EQ(transport_availability_.request_type, |
| FidoRequestType::kMakeCredential); |
| |
| if (model_->mechanisms.size() == 1) { |
| return 0; |
| } else if (model_->mechanisms.empty()) { |
| return std::nullopt; |
| } |
| |
| bool windows_handles_hybrid = WebAuthnApiSupportsHybrid(); |
| std::vector<Mechanism::Type> priority_list; |
| |
| // For non-cross-platform requests, we attempt to jump to the platform |
| // authenticator and avoid showing the mechanism selection sheet. |
| if (transport_availability_.make_credential_attachment != |
| device::AuthenticatorAttachment::kCrossPlatform) { |
| Profile* profile = |
| Profile::FromBrowserContext(GetRenderFrameHost()->GetBrowserContext()) |
| ->GetOriginalProfile(); |
| if (CanDefaultToEnclave(profile) && |
| enclave_enabled_status_ == EnclaveEnabledStatus::kEnabled) { |
| priority_list.emplace_back(Mechanism::Enclave()); |
| } |
| |
| // If Windows Hello is enabled, jump to it if it's a candidate. |
| if (transport_availability_.win_is_uvpaa) { |
| priority_list.emplace_back(Mechanism::WindowsAPI()); |
| } |
| |
| #if BUILDFLAG(IS_MAC) |
| // For non-cross-platform try to trigger either platform authenticator. |
| if (should_create_in_icloud_keychain_) { |
| priority_list.emplace_back(Mechanism::ICloudKeychain()); |
| } else { |
| priority_list.emplace_back( |
| Mechanism::Transport(AuthenticatorTransport::kInternal)); |
| } |
| #endif |
| } |
| |
| // If Windows handles hybrid, then jump to it because all remaining options |
| // are handled by Windows. |
| if (windows_handles_hybrid) { |
| priority_list.emplace_back(Mechanism::WindowsAPI()); |
| } |
| |
| // If there are no platform authenticators, then show the QR code for |
| // passkey requests unless the user might be able to select a paired phone. |
| const bool is_passkey_request = model_->resident_key_requirement != |
| device::ResidentKeyRequirement::kDiscouraged; |
| if (is_passkey_request) { |
| priority_list.emplace_back(Mechanism::AddPhone()); |
| } else { |
| priority_list.emplace_back(Mechanism::WindowsAPI()); |
| } |
| |
| if (hints_.transport) { |
| // Hints were specified, make sure to consider USB and hybrid. |
| priority_list.emplace_back(Mechanism::Transport(*hints_.transport)); |
| |
| // Find the highest priority mechanism that matches the hint. |
| for (const auto& priority_mechanism : priority_list) { |
| if (!MechanismMatchesHint(priority_mechanism, *hints_.transport)) { |
| continue; |
| } |
| std::optional<int> index = FindIndexOfFirstMechanismOfType( |
| model_->mechanisms, priority_mechanism); |
| if (index.has_value()) { |
| return *index; |
| } |
| } |
| // No mechanism matching `hints_` was found. Continue to return the highest |
| // priority mechanism ignoring `hints_` instead. |
| } |
| |
| for (const auto& priority_mechanism : priority_list) { |
| std::optional<int> index = |
| FindIndexOfFirstMechanismOfType(model_->mechanisms, priority_mechanism); |
| if (index.has_value()) { |
| return *index; |
| } |
| } |
| |
| return std::nullopt; |
| } |
| |
| bool AuthenticatorRequestDialogController::CanDefaultToEnclave( |
| Profile* profile) { |
| const bool enclave_decline_limit_reached = |
| profile->GetPrefs()->GetInteger( |
| webauthn::pref_names::kEnclaveDeclinedGPMCredentialCreationCount) >= |
| kMaxPriorityGPMCredentialCreations; |
| // If a user has declined bootstrapping too many times then GPM will still |
| // be available in the mechanism selection screen for credential creation, |
| // but it can no longer be a priority mechanism. |
| const bool enclave_bootstrap_limit_reached = |
| profile->GetPrefs()->GetInteger( |
| webauthn::pref_names::kEnclaveDeclinedGPMBootstrappingCount) >= |
| device::enclave::kMaxGPMBootstrapPrompts; |
| return !enclave_decline_limit_reached && !enclave_bootstrap_limit_reached; |
| } |
| |
| content::RenderFrameHost* |
| AuthenticatorRequestDialogController::GetRenderFrameHost() const { |
| return content::RenderFrameHost::FromID(frame_host_id_); |
| } |
| |
| void AuthenticatorRequestDialogController::StartPasskeyUpgradeRequest() { |
| SetCurrentStep(Step::kPasskeyUpgrade); |
| |
| if (!enclave_request_callback_) { |
| RecordPasskeyUpgradeResultHistogram(PasskeyUpgradeResult::kGpmDisabled); |
| FIDO_LOG(ERROR) |
| << "Passkey upgrade request failed because GPM is disabled by policy."; |
| PasskeyUpgradeFailed(); |
| return; |
| } |
| |
| passkey_upgrade_request_controller_ = |
| std::make_unique<PasskeyUpgradeRequestController>( |
| GetRenderFrameHost(), std::move(enclave_request_callback_)); |
| passkey_upgrade_request_controller_->TryUpgradePasswordToPasskey( |
| model_->relying_party_id, model_->user_entity.name.value_or(""), |
| /*delegate=*/this); |
| } |
| |
| void AuthenticatorRequestDialogController::PopulatePasswords() { |
| for (const auto& password : passwords_) { |
| Mechanism::Type mechanism_type = Mechanism::Password( |
| AuthenticatorRequestDialogModel::Mechanism::PasswordInfo( |
| password->date_last_used)); |
| Mechanism mechanism( |
| mechanism_type, password->username_value, |
| GetMechanismIcon(mechanism_type, ui_presentation()), |
| base::BindRepeating( |
| &AuthenticatorRequestDialogModel::OnPasswordCredentialSelected, |
| base::Unretained(model_), |
| std::make_pair(password->username_value, |
| password->password_value))); |
| mechanism.description = l10n_util::GetStringUTF16( |
| IDS_PASSWORD_MANAGER_PASSWORD_FROM_GOOGLE_PASSWORD_MANAGER); |
| model_->mechanisms.emplace_back(std::move(mechanism)); |
| } |
| } |