| // Copyright 2018 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/webauthn/chrome_authenticator_request_delegate.h" |
| |
| #include <algorithm> |
| #include <array> |
| #include <cstdint> |
| #include <memory> |
| #include <optional> |
| #include <string> |
| #include <string_view> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/check.h" |
| #include "base/check_op.h" |
| #include "base/compiler_specific.h" |
| #include "base/containers/contains.h" |
| #include "base/containers/span.h" |
| #include "base/feature_list.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/i18n/time_formatting.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/strings/string_util.h" |
| #include "base/values.h" |
| #include "build/build_config.h" |
| #include "build/chromeos_buildflags.h" |
| #include "chrome/browser/extensions/api/web_authentication_proxy/web_authentication_proxy_service.h" |
| #include "chrome/browser/net/system_network_context_manager.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/profiles/profile_observer.h" |
| #include "chrome/browser/signin/identity_manager_factory.h" |
| #include "chrome/browser/sync/sync_service_factory.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_finder.h" |
| #include "chrome/browser/ui/browser_navigator.h" |
| #include "chrome/browser/ui/browser_navigator_params.h" |
| #include "chrome/browser/ui/browser_window.h" |
| #include "chrome/browser/ui/passwords/passwords_client_ui_delegate.h" |
| #include "chrome/browser/ui/webauthn/user_actions.h" |
| #include "chrome/browser/webauthn/authenticator_request_dialog_controller.h" |
| #include "chrome/browser/webauthn/authenticator_request_dialog_model.h" |
| #include "chrome/browser/webauthn/cablev2_devices.h" |
| #include "chrome/browser/webauthn/enclave_manager.h" |
| #include "chrome/browser/webauthn/gpm_enclave_controller.h" |
| #include "chrome/browser/webauthn/immediate_request_rate_limiter_factory.h" |
| #include "chrome/browser/webauthn/passkey_model_factory.h" |
| #include "chrome/browser/webauthn/webauthn_metrics_util.h" |
| #include "chrome/browser/webauthn/webauthn_pref_names.h" |
| #include "chrome/common/chrome_version.h" |
| #include "chrome/common/pref_names.h" |
| #include "components/device_event_log/device_event_log.h" |
| #include "components/password_manager/core/common/password_manager_pref_names.h" |
| #include "components/pref_registry/pref_registry_syncable.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/signin/public/base/consent_level.h" |
| #include "components/signin/public/identity_manager/identity_manager.h" |
| #include "components/sync/protocol/webauthn_credential_specifics.pb.h" |
| #include "components/sync/service/sync_service.h" |
| #include "components/trusted_vault/frontend_trusted_vault_connection.h" |
| #include "components/user_prefs/user_prefs.h" |
| #include "components/webauthn/core/browser/immediate_request_rate_limiter.h" |
| #include "components/webauthn/core/browser/passkey_model.h" |
| #include "content/public/browser/authenticator_request_client_delegate.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/web_authentication_request_proxy.h" |
| #include "content/public/browser/web_contents.h" |
| #include "crypto/random.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/cable/v2_handshake.h" |
| #include "device/fido/discoverable_credential_metadata.h" |
| #include "device/fido/features.h" |
| #include "device/fido/fido_authenticator.h" |
| #include "device/fido/fido_constants.h" |
| #include "device/fido/fido_discovery_base.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/public_key_credential_descriptor.h" |
| #include "device/fido/public_key_credential_user_entity.h" |
| #include "extensions/browser/extension_registry.h" |
| #include "extensions/common/permissions/permissions_data.h" |
| #include "extensions/common/url_pattern.h" |
| #include "net/base/registry_controlled_domains/registry_controlled_domain.h" |
| #include "net/base/url_util.h" |
| #include "third_party/blink/public/mojom/credentialmanagement/credential_type_flags.mojom.h" |
| #include "third_party/icu/source/common/unicode/locid.h" |
| #include "ui/base/page_transition_types.h" |
| #include "ui/base/window_open_disposition.h" |
| #include "ui/gfx/native_widget_types.h" |
| |
| #if BUILDFLAG(IS_MAC) |
| #include "chrome/browser/webauthn/chrome_authenticator_request_delegate_mac.h" |
| #include "device/fido/mac/credential_metadata.h" |
| #include "third_party/icu/source/i18n/unicode/timezone.h" |
| #include "ui/views/widget/widget.h" |
| #endif |
| |
| #if BUILDFLAG(IS_WIN) |
| #include "chrome/browser/webauthn/local_credential_management_win.h" |
| #include "device/fido/win/authenticator.h" |
| #endif |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| #include "chromeos/components/webauthn/webauthn_request_registrar.h" |
| #include "ui/aura/window.h" |
| #endif |
| |
| using PasswordCredentials = PasswordCredentialController::PasswordCredentials; |
| using UIPresentation = ChromeAuthenticatorRequestDelegate::UIPresentation; |
| using TransportAvailabilityInfo = |
| device::FidoRequestHandlerBase::TransportAvailabilityInfo; |
| |
| namespace { |
| |
| ChromeAuthenticatorRequestDelegate::TestObserver* g_observer = nullptr; |
| |
| static constexpr char kGoogleRpId[] = "google.com"; |
| |
| // Returns true iff the credential is reported as being present on the platform |
| // authenticator (i.e. it is not a phone or icloud credential). |
| bool IsCredentialFromPlatformAuthenticator( |
| device::DiscoverableCredentialMetadata cred) { |
| return cred.source != device::AuthenticatorType::kICloudKeychain && |
| cred.source != device::AuthenticatorType::kPhone; |
| } |
| |
| // Returns true iff |user_id| starts with the prefix reserved for passkeys used |
| // to authenticate to Google services. |
| bool UserIdHasGooglePasskeyAuthPrefix(const std::vector<uint8_t>& user_id) { |
| constexpr std::string_view kPrefix = "GOOGLE_ACCOUNT:"; |
| if (user_id.size() < kPrefix.size()) { |
| return false; |
| } |
| return UNSAFE_TODO(memcmp(user_id.data(), kPrefix.data(), kPrefix.size())) == |
| 0; |
| } |
| |
| // Filters |passkeys| to only contain credentials that are used to authenticate |
| // to Google services. |
| void FilterGoogleAuthPasskeys( |
| std::vector<device::DiscoverableCredentialMetadata>* passkeys) { |
| std::erase_if(*passkeys, [](const auto& passkey) { |
| return IsCredentialFromPlatformAuthenticator(passkey) && |
| !UserIdHasGooglePasskeyAuthPrefix(passkey.user.id); |
| }); |
| } |
| |
| #if BUILDFLAG(IS_MAC) |
| const char kWebAuthnTouchIdLastUsed[] = "webauthn.touchid.last_used"; |
| |
| // kMacOsRecentlyUsedMaxDays specifies how recently the macOS profile |
| // authenticator must have been used (for the current profile) to be considered |
| // "actively" used. Chrome may default to the profile authenticator in more |
| // cases if it is being actively used. |
| const int kMacOsRecentlyUsedMaxDays = 31; |
| #endif |
| |
| // CableLinkingEventHandler handles linking information sent by caBLEv2 |
| // authenticators. This linking information can come after the WebAuthn |
| // operation has resolved and thus after the |
| // `ChromeAuthenticatorRequestDelegate` has been destroyed. Thus this object is |
| // owned by the callback itself, and can save linking information until the |
| // point where the `Profile` itself is destroyed. |
| class CableLinkingEventHandler : public ProfileObserver { |
| public: |
| explicit CableLinkingEventHandler(Profile* profile) : profile_(profile) { |
| profile_->AddObserver(this); |
| } |
| |
| ~CableLinkingEventHandler() override { |
| if (profile_) { |
| profile_->RemoveObserver(this); |
| profile_ = nullptr; |
| } |
| } |
| |
| // ProfileObserver: |
| void OnProfileWillBeDestroyed(Profile* profile) override { |
| DCHECK_EQ(profile, profile_); |
| profile_->RemoveObserver(this); |
| profile_ = nullptr; |
| } |
| |
| private: |
| raw_ptr<Profile> profile_; |
| }; |
| |
| bool SkipGpmPasskeyCreationForOwnAccount( |
| device::FidoRequestType request_type, |
| const std::string& rp_id, |
| std::string_view user_name, |
| const CoreAccountInfo& primary_account_info) { |
| // Don't let GPM create a passkey for its own account within itself. |
| // |
| // The request username is either the full email address (GAIA users) or just |
| // the local part (google.com users). |
| // |
| // Note that if the string does not contain an '@', `substr(0, npos)` will |
| // return the whole string. |
| const std::string account_email_local_part = |
| primary_account_info.email.substr(0, |
| primary_account_info.email.find('@')); |
| return request_type == device::FidoRequestType::kMakeCredential && |
| rp_id == kGoogleRpId && |
| (user_name == primary_account_info.email || |
| user_name == account_email_local_part); |
| } |
| |
| bool PasswordsUsable(int credential_types, UIPresentation ui_presentation) { |
| if (!(credential_types & |
| static_cast<int>(blink::mojom::CredentialTypeFlags::kPassword))) { |
| return false; |
| } |
| |
| if (base::FeatureList::IsEnabled(device::kWebAuthnAmbientSignin) && |
| ui_presentation == UIPresentation::kAutofill) { |
| // 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. |
| return true; |
| } |
| |
| return ui_presentation == UIPresentation::kModalImmediate; |
| } |
| |
| } // namespace |
| |
| // static |
| void ChromeAuthenticatorRequestDelegate::RegisterProfilePrefs( |
| user_prefs::PrefRegistrySyncable* registry) { |
| registry->RegisterListPref(prefs::kSecurityKeyPermitAttestation); |
| registry->RegisterIntegerPref( |
| webauthn::pref_names::kEnclaveDeclinedGPMCredentialCreationCount, 0); |
| registry->RegisterIntegerPref( |
| webauthn::pref_names::kEnclaveDeclinedGPMBootstrappingCount, 0); |
| #if BUILDFLAG(IS_WIN) |
| LocalCredentialManagementWin::RegisterProfilePrefs(registry); |
| #endif |
| #if BUILDFLAG(IS_MAC) |
| registry->RegisterStringPref( |
| webauthn::pref_names::kWebAuthnTouchIdMetadataSecretPrefName, |
| std::string()); |
| registry->RegisterStringPref(kWebAuthnTouchIdLastUsed, std::string()); |
| // This boolean preference is used as a tristate. If unset, whether or not to |
| // default to iCloud is determined based on several factors. |
| // (See `ShouldCreateInICloudKeychain`.) If set, then this preference is |
| // controlling. |
| // |
| // The default value of this preference only determines whether the toggle |
| // in settings will show as set or not when the preference hasn't been |
| // explicitly set. Since the behaviour is actually more complex than can be |
| // expressed in a boolean, this is always an approximation. |
| registry->RegisterBooleanPref( |
| prefs::kCreatePasskeysInICloudKeychain, |
| ShouldCreateInICloudKeychain( |
| RequestSource::kWebAuthentication, |
| // Whether or not the user is actively using the profile authenticator |
| // is stored in preferences, which aren't available at this time while |
| // we're still registering them. Thus we assume that they are not. |
| /*is_active_profile_authenticator_user=*/false, |
| IsICloudDriveEnabled(), |
| /*request_is_for_google_com=*/false, /*preference=*/std::nullopt)); |
| #endif |
| } |
| |
| ChromeAuthenticatorRequestDelegate::ChromeAuthenticatorRequestDelegate( |
| content::RenderFrameHost* render_frame_host) |
| : render_frame_host_id_(render_frame_host->GetGlobalId()), |
| dialog_model_(base::MakeRefCounted<AuthenticatorRequestDialogModel>( |
| GetRenderFrameHost())), |
| dialog_controller_(std::make_unique<AuthenticatorRequestDialogController>( |
| dialog_model_.get(), |
| GetRenderFrameHost())) { |
| dialog_model_->observers.AddObserver(this); |
| if (g_observer) { |
| g_observer->Created(this); |
| } |
| } |
| |
| ChromeAuthenticatorRequestDelegate::~ChromeAuthenticatorRequestDelegate() { |
| // Currently, completion of the request is indicated by //content destroying |
| // this delegate. |
| dialog_model_->OnRequestComplete(); |
| dialog_model_->observers.RemoveObserver(this); |
| |
| if (g_observer) { |
| g_observer->OnDestroy(this); |
| } |
| } |
| |
| // static |
| void ChromeAuthenticatorRequestDelegate::SetGlobalObserverForTesting( |
| TestObserver* observer) { |
| CHECK(!observer || !g_observer); |
| g_observer = observer; |
| } |
| |
| base::WeakPtr<ChromeAuthenticatorRequestDelegate> |
| ChromeAuthenticatorRequestDelegate::AsWeakPtr() { |
| return weak_ptr_factory_.GetWeakPtr(); |
| } |
| |
| GPMEnclaveController* |
| ChromeAuthenticatorRequestDelegate::enclave_controller_for_testing() const { |
| return enclave_controller_.get(); |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::SetRelyingPartyId( |
| const std::string& rp_id) { |
| dialog_model_->relying_party_id = rp_id; |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::SetUIPresentation( |
| UIPresentation ui_presentation) { |
| dialog_controller_->SetUIPresentation(ui_presentation); |
| } |
| |
| bool ChromeAuthenticatorRequestDelegate::DoesBlockRequestOnFailure( |
| InterestingFailureReason reason) { |
| if (!webauthn_ui_enabled()) { |
| return false; |
| } |
| |
| // If the UI was already in the state where we asked the user to complete the |
| // transaction on the other device then any errors are immediately resolved. |
| // Very likely the user canceled on the phone and doesn't want to see another |
| // error UI on the desktop. |
| if (cable_device_ready_) { |
| return false; |
| } |
| |
| switch (reason) { |
| case InterestingFailureReason::kTimeout: |
| dialog_controller_->OnRequestTimeout(); |
| break; |
| case InterestingFailureReason::kKeyNotRegistered: |
| dialog_controller_->OnActivatedKeyNotRegistered(); |
| break; |
| case InterestingFailureReason::kKeyAlreadyRegistered: |
| dialog_controller_->OnActivatedKeyAlreadyRegistered(); |
| break; |
| case InterestingFailureReason::kSoftPINBlock: |
| dialog_controller_->OnSoftPINBlock(); |
| break; |
| case InterestingFailureReason::kHardPINBlock: |
| dialog_controller_->OnHardPINBlock(); |
| break; |
| case InterestingFailureReason::kAuthenticatorRemovedDuringPINEntry: |
| dialog_controller_->OnAuthenticatorRemovedDuringPINEntry(); |
| break; |
| case InterestingFailureReason::kAuthenticatorMissingResidentKeys: |
| dialog_controller_->OnAuthenticatorMissingResidentKeys(); |
| break; |
| case InterestingFailureReason::kAuthenticatorMissingUserVerification: |
| dialog_controller_->OnAuthenticatorMissingUserVerification(); |
| break; |
| case InterestingFailureReason::kAuthenticatorMissingLargeBlob: |
| dialog_controller_->OnAuthenticatorMissingLargeBlob(); |
| break; |
| case InterestingFailureReason::kNoCommonAlgorithms: |
| dialog_controller_->OnNoCommonAlgorithms(); |
| break; |
| case InterestingFailureReason::kStorageFull: |
| dialog_controller_->OnAuthenticatorStorageFull(); |
| break; |
| case InterestingFailureReason::kUserConsentDenied: |
| dialog_controller_->OnUserConsentDenied(); |
| break; |
| case InterestingFailureReason::kWinUserCancelled: |
| return dialog_controller_->OnWinUserCancelled(); |
| case InterestingFailureReason::kHybridTransportError: |
| return dialog_controller_->OnHybridTransportError(); |
| case InterestingFailureReason::kNoPasskeys: |
| return dialog_controller_->OnNoPasskeys(); |
| case InterestingFailureReason::kEnclaveError: |
| return dialog_controller_->OnEnclaveError(); |
| case InterestingFailureReason::kEnclaveCancel: |
| dialog_model_->CancelAuthenticatorRequest(); |
| break; |
| case InterestingFailureReason::kChallengeUrlFailure: |
| dialog_controller_->OnChallengeUrlFailure(); |
| } |
| return true; |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::OnTransactionSuccessful( |
| RequestSource request_source, |
| device::FidoRequestType request_type, |
| device::AuthenticatorType authenticator_type) { |
| if (request_source != RequestSource::kWebAuthentication) { |
| return; |
| } |
| #if BUILDFLAG(IS_MAC) |
| if (authenticator_type == device::AuthenticatorType::kTouchID) { |
| profile()->GetPrefs()->SetString( |
| kWebAuthnTouchIdLastUsed, |
| base::UnlocalizedTimeFormatWithPattern(base::Time::Now(), "yyyy-MM-dd", |
| icu::TimeZone::getGMT())); |
| webauthn::user_actions::RecordChromeProfileSuccess(); |
| } |
| if (authenticator_type == device::AuthenticatorType::kICloudKeychain) { |
| webauthn::user_actions::RecordICloudSuccess(); |
| } |
| |
| dialog_controller_->RecordMacOsSuccessHistogram(request_type, |
| authenticator_type); |
| #elif BUILDFLAG(IS_WIN) |
| if (authenticator_type == device::AuthenticatorType::kWinNative) { |
| webauthn::user_actions::RecordWindowsHelloSuccess(); |
| } |
| #endif // BUILDFLAG(IS_MAC) |
| if (authenticator_type == device::AuthenticatorType::kEnclave) { |
| if (dialog_model_->in_onboarding_flow) { |
| RecordOnboardingEvent(webauthn::metrics::OnboardingEvents::kSucceeded); |
| } |
| switch (request_type) { |
| case device::FidoRequestType::kGetAssertion: |
| RecordGPMGetAssertionEvent( |
| webauthn::metrics::GPMGetAssertionEvents::kSuccess); |
| break; |
| case device::FidoRequestType::kMakeCredential: |
| RecordGPMMakeCredentialEvent( |
| webauthn::metrics::GPMMakeCredentialEvents::kSuccess); |
| break; |
| } |
| webauthn::user_actions::RecordGpmSuccess(); |
| } |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::RegisterActionCallbacks( |
| base::OnceClosure cancel_callback, |
| base::OnceClosure immediate_not_found_callback, |
| base::RepeatingClosure start_over_callback, |
| AccountPreselectedCallback account_preselected_callback, |
| PasswordSelectedCallback password_selected_callback, |
| device::FidoRequestHandlerBase::RequestCallback request_callback, |
| base::OnceClosure cancel_ui_timeout_callback, |
| base::RepeatingClosure bluetooth_adapter_power_on_callback, |
| base::RepeatingCallback< |
| void(device::FidoRequestHandlerBase::BlePermissionCallback)> |
| request_ble_permission_callback) { |
| cancel_callback_ = std::move(cancel_callback); |
| immediate_not_found_callback_ = std::move(immediate_not_found_callback); |
| start_over_callback_ = std::move(start_over_callback); |
| account_preselected_callback_ = std::move(account_preselected_callback); |
| password_selected_callback_ = std::move(password_selected_callback); |
| request_callback_ = request_callback; |
| cancel_ui_timeout_callback_ = std::move(cancel_ui_timeout_callback); |
| |
| dialog_controller_->SetRequestCallback(request_callback); |
| dialog_controller_->SetAccountPreselectedCallback( |
| account_preselected_callback_); |
| dialog_controller_->SetBluetoothAdapterPowerOnCallback( |
| bluetooth_adapter_power_on_callback); |
| dialog_controller_->SetRequestBlePermissionCallback( |
| request_ble_permission_callback); |
| if (password_controller_) { |
| password_controller_->SetPasswordSelectedCallback( |
| password_selected_callback_); |
| } |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::ConfigureDiscoveries( |
| const url::Origin& origin, |
| const std::string& rp_id, |
| RequestSource request_source, |
| device::FidoRequestType request_type, |
| std::optional<device::ResidentKeyRequirement> resident_key_requirement, |
| device::UserVerificationRequirement user_verification_requirement, |
| std::optional<std::string_view> user_name, |
| base::span<const device::CableDiscoveryData> pairings_from_extension, |
| bool browser_provided_passkeys_available, |
| device::FidoDiscoveryFactory* discovery_factory) { |
| DCHECK(request_type == device::FidoRequestType::kGetAssertion || |
| resident_key_requirement.has_value()); |
| |
| // Without the UI enabled, discoveries like caBLE, Android AOA, iCloud |
| // keychain, and the enclave, don't make sense. |
| if (!webauthn_ui_enabled()) { |
| return; |
| } |
| |
| // Configure the enclave authenticator. |
| if (browser_provided_passkeys_available && !IsVirtualEnvironmentEnabled() && |
| request_source == RequestSource::kWebAuthentication) { |
| // Creating credentials in GPM can be disabled by policy, but get() is |
| // always allowed. |
| const bool enclave_create_enabled = |
| profile()->GetPrefs()->GetBoolean( |
| password_manager::prefs::kCredentialsEnableService) && |
| profile()->GetPrefs()->GetBoolean( |
| password_manager::prefs::kCredentialsEnablePasskeys); |
| if (dialog_controller_->ui_presentation() == |
| UIPresentation::kPasskeyUpgrade && |
| enclave_create_enabled) { |
| // PasskeyUpgradeRequestController will handle enclave transactions in |
| // place of the "regular" GPMEnclaveController. |
| CHECK(!enclave_controller_); |
| dialog_controller_->InitializeEnclaveRequestCallback(discovery_factory); |
| discovery_factory->set_network_context_factory(base::BindRepeating([]() { |
| return SystemNetworkContextManager::GetInstance()->GetContext(); |
| })); |
| } else if (request_type == device::FidoRequestType::kGetAssertion || |
| enclave_create_enabled) { |
| // Set up the "regular" enclave controller. |
| auto* const identity_manager = IdentityManagerFactory::GetForProfile( |
| profile()->GetOriginalProfile()); |
| const auto consent = signin::ConsentLevel::kSignin; |
| if (identity_manager->HasPrimaryAccount(consent)) { |
| CoreAccountInfo account_info = |
| identity_manager->GetPrimaryAccountInfo(consent); |
| if (SkipGpmPasskeyCreationForOwnAccount( |
| request_type, rp_id, user_name.value_or(""), account_info)) { |
| FIDO_LOG(EVENT) |
| << "Creation in GPM not offered (same primary account)"; |
| } else { |
| enclave_controller_ = std::make_unique<GPMEnclaveController>( |
| GetRenderFrameHost(), dialog_model_.get(), rp_id, request_type, |
| user_verification_requirement); |
| } |
| } |
| } else { |
| FIDO_LOG(EVENT) |
| << "Enclave unavailable for creating passkeys due to policy."; |
| } |
| } |
| |
| const bool cable_extension_permitted = ShouldPermitCableExtension(origin); |
| const bool cable_extension_provided = |
| cable_extension_permitted && !pairings_from_extension.empty(); |
| |
| if (g_observer) { |
| for (const auto& pairing : pairings_from_extension) { |
| if (pairing.version == device::CableDiscoveryData::Version::V2) { |
| g_observer->CableV2ExtensionSeen(pairing.v2->server_link_data); |
| } |
| } |
| |
| g_observer->ConfiguringCable(request_type); |
| } |
| |
| #if BUILDFLAG(IS_LINUX) |
| // No caBLEv1 on Linux. It tends to crash bluez. |
| if (base::Contains(pairings_from_extension, |
| device::CableDiscoveryData::Version::V1, |
| &device::CableDiscoveryData::version)) { |
| pairings_from_extension = base::span<const device::CableDiscoveryData>(); |
| } |
| #endif |
| |
| std::vector<device::CableDiscoveryData> pairings; |
| if (cable_extension_permitted) { |
| pairings.insert(pairings.end(), pairings_from_extension.begin(), |
| pairings_from_extension.end()); |
| } |
| const bool cable_extension_accepted = !pairings.empty(); |
| const bool cablev2_extension_provided = |
| base::Contains(pairings, device::CableDiscoveryData::Version::V2, |
| &device::CableDiscoveryData::version); |
| |
| const bool non_extension_cablev2_enabled = |
| (!cable_extension_permitted || |
| (!cable_extension_provided && |
| request_type == device::FidoRequestType::kGetAssertion) || |
| (request_type == device::FidoRequestType::kMakeCredential && |
| resident_key_requirement.has_value() && |
| resident_key_requirement.value() != |
| device::ResidentKeyRequirement::kDiscouraged) || |
| base::FeatureList::IsEnabled(device::kWebAuthCableExtensionAnywhere)); |
| |
| std::optional<std::array<uint8_t, device::cablev2::kQRKeySize>> |
| qr_generator_key; |
| std::optional<std::string> qr_string; |
| if (non_extension_cablev2_enabled || cablev2_extension_provided) { |
| // A QR key is generated for all caBLEv2 cases but whether the QR code is |
| // displayed is up to the UI. |
| qr_generator_key.emplace(); |
| crypto::RandBytes(*qr_generator_key); |
| qr_string = device::cablev2::qr::Encode(*qr_generator_key, request_type); |
| |
| auto linking_handler = |
| std::make_unique<CableLinkingEventHandler>(profile()); |
| discovery_factory->set_cable_event_callback( |
| base::BindRepeating(&ChromeAuthenticatorRequestDelegate::OnCableEvent, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| if (SystemNetworkContextManager::GetInstance()) { |
| // caBLE and the enclave depend on the network context factory. |
| // TODO(nsatragno): this should probably use a storage partition network |
| // context instead. See the SystemNetworkContextManager class comments. |
| discovery_factory->set_network_context_factory(base::BindRepeating([]() { |
| return SystemNetworkContextManager::GetInstance()->GetContext(); |
| })); |
| } |
| |
| if (cable_extension_accepted || non_extension_cablev2_enabled) { |
| std::optional<bool> extension_is_v2; |
| if (cable_extension_provided) { |
| extension_is_v2 = cablev2_extension_provided; |
| } |
| dialog_controller_->set_cable_transport_info(extension_is_v2, qr_string); |
| discovery_factory->set_cable_data(request_type, std::move(pairings), |
| qr_generator_key); |
| } |
| |
| #if BUILDFLAG(IS_MAC) |
| ConfigureNSWindow(discovery_factory); |
| #endif |
| |
| if (enclave_controller_) { |
| enclave_controller_->ConfigureDiscoveries(discovery_factory); |
| } |
| |
| dialog_controller_->set_is_non_webauthn_request( |
| request_source != RequestSource::kWebAuthentication); |
| |
| #if BUILDFLAG(IS_MAC) |
| ConfigureICloudKeychain(request_source, rp_id); |
| #endif |
| |
| if (PasswordsUsable(credential_types_, |
| dialog_controller_->ui_presentation())) { |
| // Only valid for the main frame. |
| if (!password_controller_ && GetRenderFrameHost()->IsInPrimaryMainFrame()) { |
| password_controller_ = std::make_unique<PasswordCredentialController>( |
| render_frame_host_id_, dialog_model_.get()); |
| } |
| if (!password_controller_) { |
| return; |
| } |
| password_controller_->FetchPasswords( |
| origin.GetURL(), |
| base::BindOnce( |
| &ChromeAuthenticatorRequestDelegate::OnPasswordCredentialsReceived, |
| AsWeakPtr())); |
| } |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::SetHints( |
| const AuthenticatorRequestClientDelegate::Hints& hints) { |
| if (g_observer) { |
| g_observer->HintsSet(hints); |
| } |
| dialog_controller_->SetHints(hints); |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::SelectAccount( |
| std::vector<device::AuthenticatorGetAssertionResponse> responses, |
| base::OnceCallback<void(device::AuthenticatorGetAssertionResponse)> |
| callback) { |
| if (!webauthn_ui_enabled()) { |
| // Requests with UI disabled should never reach account selection. |
| DCHECK(IsVirtualEnvironmentEnabled()); |
| |
| // The browser is being automated. Select the first credential to support |
| // automation of discoverable credentials. |
| // TODO(crbug.com/40639383): Provide a way to determine which account gets |
| // picked. |
| std::move(callback).Run(std::move(responses.at(0))); |
| return; |
| } |
| |
| if (g_observer) { |
| g_observer->AccountSelectorShown(responses); |
| std::move(callback).Run(std::move(responses.at(0))); |
| return; |
| } |
| |
| dialog_controller_->SelectAccount(std::move(responses), std::move(callback)); |
| } |
| |
| bool ChromeAuthenticatorRequestDelegate::webauthn_ui_enabled() const { |
| return dialog_controller_->ui_presentation() != UIPresentation::kDisabled; |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::SetCredentialTypes( |
| int credential_type_flags) { |
| credential_types_ = credential_type_flags; |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::SetCredentialIdFilter( |
| std::vector<device::PublicKeyCredentialDescriptor> credential_list) { |
| credential_filter_ = std::move(credential_list); |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::SetUserEntityForMakeCredentialRequest( |
| const device::PublicKeyCredentialUserEntity& user_entity) { |
| dialog_model_->user_entity = user_entity; |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::ProvideChallengeUrl( |
| const GURL& url, |
| base::OnceCallback<void(std::optional<base::span<const uint8_t>>)> |
| callback) { |
| dialog_controller_->ProvideChallengeUrl(url, std::move(callback)); |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::OnTransportAvailabilityEnumerated( |
| TransportAvailabilityInfo data) { |
| if (g_observer) { |
| g_observer->OnPreTransportAvailabilityEnumerated(this); |
| } |
| |
| if (!webauthn_ui_enabled()) { |
| return; |
| } |
| |
| pending_transport_availability_info_ = std::make_unique< |
| device::FidoRequestHandlerBase::TransportAvailabilityInfo>( |
| std::move(data)); |
| TryToShowUI(); |
| } |
| |
| bool ChromeAuthenticatorRequestDelegate::EmbedderControlsAuthenticatorDispatch( |
| const device::FidoAuthenticator& authenticator) { |
| // Decide whether the //device/fido code should dispatch the current |
| // request to an authenticator immediately after it has been |
| // discovered, or whether the embedder/UI takes charge of that by |
| // invoking its RequestCallback. |
| if (!webauthn_ui_enabled()) { |
| // There is no UI to handle request dispatch. |
| return false; |
| } |
| if (authenticator.GetType() == device::AuthenticatorType::kEnclave) { |
| return false; |
| } |
| |
| if (dialog_controller_->ui_presentation() == UIPresentation::kAutofill && |
| (dialog_model_->step() == |
| AuthenticatorRequestDialogModel::Step::kPasskeyAutofill || |
| dialog_model_->step() == |
| AuthenticatorRequestDialogModel::Step::kNotStarted)) { |
| // There is an active conditional request that is not showing any UI. The UI |
| // will dispatch to any plugged in authenticators after the user selects an |
| // option. |
| return true; |
| } |
| auto transport = authenticator.AuthenticatorTransport(); |
| return !transport || // Windows |
| *transport == device::FidoTransportProtocol::kInternal; |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::FidoAuthenticatorAdded( |
| const device::FidoAuthenticator& authenticator) { |
| if (!webauthn_ui_enabled()) { |
| return; |
| } |
| |
| dialog_controller_->AddAuthenticator(authenticator); |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::FidoAuthenticatorRemoved( |
| std::string_view authenticator_id) { |
| if (!webauthn_ui_enabled()) { |
| return; |
| } |
| |
| dialog_controller_->RemoveAuthenticator(authenticator_id); |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::BluetoothAdapterStatusChanged( |
| device::FidoRequestHandlerBase::BleStatus ble_status) { |
| dialog_controller_->BluetoothAdapterStatusChanged(ble_status); |
| } |
| |
| bool ChromeAuthenticatorRequestDelegate::SupportsPIN() const { |
| return true; |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::CollectPIN( |
| CollectPINOptions options, |
| base::OnceCallback<void(std::u16string)> provide_pin_cb) { |
| dialog_controller_->CollectPIN(options.reason, options.error, |
| options.min_pin_length, options.attempts, |
| std::move(provide_pin_cb)); |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::StartBioEnrollment( |
| base::OnceClosure next_callback) { |
| dialog_controller_->StartInlineBioEnrollment(std::move(next_callback)); |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::OnSampleCollected( |
| int bio_samples_remaining) { |
| dialog_controller_->OnSampleCollected(bio_samples_remaining); |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::FinishCollectToken() { |
| dialog_controller_->FinishCollectToken(); |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::OnRetryUserVerification(int attempts) { |
| dialog_controller_->OnRetryUserVerification(attempts); |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::OnStartOver() { |
| DCHECK(start_over_callback_); |
| dialog_model_->generation++; |
| if (g_observer) { |
| g_observer->PreStartOver(); |
| } |
| start_over_callback_.Run(); |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::OnModelDestroyed( |
| AuthenticatorRequestDialogModel* model) { |
| DCHECK_EQ(model, dialog_model_.get()); |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::OnCancelRequest() { |
| // |cancel_callback_| must be invoked at most once as invocation of |
| // |cancel_callback_| will destroy |this|. |
| DCHECK(cancel_callback_); |
| std::move(cancel_callback_).Run(); |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::SetPasswordControllerForTesting( |
| std::unique_ptr<PasswordCredentialController> controller) { |
| password_controller_ = std::move(controller); |
| } |
| |
| content::RenderFrameHost* |
| ChromeAuthenticatorRequestDelegate::GetRenderFrameHost() const { |
| content::RenderFrameHost* ret = |
| content::RenderFrameHost::FromID(render_frame_host_id_); |
| DCHECK(ret); |
| return ret; |
| } |
| |
| content::BrowserContext* ChromeAuthenticatorRequestDelegate::GetBrowserContext() |
| const { |
| return GetRenderFrameHost()->GetBrowserContext(); |
| } |
| |
| Profile* ChromeAuthenticatorRequestDelegate::profile() const { |
| return Profile::FromBrowserContext(GetRenderFrameHost()->GetBrowserContext()); |
| } |
| |
| bool ChromeAuthenticatorRequestDelegate::MaybeHandleImmediateMediation( |
| const TransportAvailabilityInfo& data, |
| const PasswordCredentials& passwords) { |
| if (data.request_type != device::FidoRequestType::kGetAssertion || |
| dialog_controller_->ui_presentation() != |
| UIPresentation::kModalImmediate) { |
| return false; |
| } |
| |
| // Always return not allowed immediate in incognito. |
| if (profile()->IsOffTheRecord()) { |
| base::UmaHistogramEnumeration( |
| "WebAuthentication.GetAssertion.Immediate.RejectionReason", |
| content::ImmediateMediationRejectionReason::kIncognito); |
| return true; |
| } |
| |
| if (auto* rate_limiter = |
| ImmediateRequestRateLimiterFactory::GetForProfile(profile())) { |
| const url::Origin origin = GetRenderFrameHost()->GetLastCommittedOrigin(); |
| if (!rate_limiter->IsRequestAllowed(origin)) { |
| FIDO_LOG(ERROR) |
| << "Immediate request rate limit exceeded for the origin."; |
| base::UmaHistogramEnumeration( |
| "WebAuthentication.GetAssertion.Immediate.RejectionReason", |
| content::ImmediateMediationRejectionReason::kRateLimited); |
| return true; |
| } |
| } |
| |
| if (data.recognized_credentials.size() + passwords.size() == 0) { |
| base::UmaHistogramEnumeration( |
| "WebAuthentication.GetAssertion.Immediate.RejectionReason", |
| content::ImmediateMediationRejectionReason::kNoCredentials); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::TryToShowUI() { |
| if (!pending_transport_availability_info_) { |
| return; |
| } |
| if (enclave_controller_ && !enclave_controller_->ready_for_ui()) { |
| // Delay showing UI until GPM state is loaded. It's only after this |
| // point that we know whether GPM will be active for this request or not. |
| return; |
| } |
| if (PasswordsUsable(credential_types_, |
| dialog_controller_->ui_presentation()) && |
| !pending_password_credentials_) { |
| return; |
| } |
| auto tai = std::move(pending_transport_availability_info_); |
| auto passwords = pending_password_credentials_ |
| ? std::move(pending_password_credentials_) |
| : std::make_unique<PasswordCredentials>(); |
| MaybeShowUI(std::move(*tai), std::move(*passwords)); |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::MaybeShowUI( |
| TransportAvailabilityInfo tai, |
| PasswordCredentials passwords) { |
| if (can_use_synced_phone_passkeys_ || |
| (enclave_controller_ && enclave_controller_->is_active())) { |
| GetPhoneContactableGpmPasskeysForRpId( |
| std::move(tai), |
| base::BindOnce(&ChromeAuthenticatorRequestDelegate::FinishMaybeShowUI, |
| weak_ptr_factory_.GetWeakPtr(), std::move(passwords))); |
| return; |
| } |
| |
| FinishMaybeShowUI(std::move(passwords), std::move(tai)); |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::FinishMaybeShowUI( |
| PasswordCredentials passwords, |
| TransportAvailabilityInfo tai) { |
| FilterRecognizedCredentials(&tai); |
| |
| if (MaybeHandleImmediateMediation(tai, passwords)) { |
| std::move(immediate_not_found_callback_).Run(); |
| return; |
| } |
| |
| if (!cancel_ui_timeout_callback_.is_null()) { |
| std::move(cancel_ui_timeout_callback_).Run(); |
| } |
| |
| if (g_observer) { |
| g_observer->OnTransportAvailabilityEnumerated(this, &tai); |
| } |
| |
| if (dialog_model_->step() != |
| AuthenticatorRequestDialogModel::Step::kNotStarted) { |
| return; |
| } |
| |
| dialog_controller_->SetCredentialTypes(credential_types_); |
| UpdateModelForTransportAvailability(tai); |
| |
| // Precalculate the UV method for immediate mode requests. |
| dialog_model_->gpm_uv_method.reset(); |
| if (enclave_controller_) { |
| dialog_model_->gpm_uv_method = |
| enclave_controller_->GetEnclaveUserVerificationMethod(); |
| } |
| |
| dialog_controller_->StartFlow(std::move(tai), std::move(passwords)); |
| |
| if (g_observer) { |
| g_observer->UIShown(this); |
| } |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::OnReadyForUI() { |
| TryToShowUI(); |
| } |
| |
| bool ChromeAuthenticatorRequestDelegate::ShouldPermitCableExtension( |
| const url::Origin& origin) { |
| if (base::FeatureList::IsEnabled(device::kWebAuthCableExtensionAnywhere)) { |
| return true; |
| } |
| |
| // Because the future of the caBLE extension might be that we transition |
| // everything to QR-code or sync-based pairing, we don't want use of the |
| // extension to spread without consideration. Therefore it's limited to |
| // origins that are already depending on it and test sites. |
| if (origin.DomainIs("google.com")) { |
| return true; |
| } |
| |
| const GURL test_site("https://webauthndemo.appspot.com"); |
| DCHECK(test_site.is_valid()); |
| return origin.IsSameOriginWith(test_site); |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::OnCableEvent( |
| device::cablev2::Event event) { |
| if (event == device::cablev2::Event::kReady) { |
| cable_device_ready_ = true; |
| } |
| |
| dialog_controller_->OnCableEvent(event); |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::GetPhoneContactableGpmPasskeysForRpId( |
| TransportAvailabilityInfo tai, |
| base::OnceCallback<void(TransportAvailabilityInfo)> callback) { |
| // For immediate `get()` requests, the enclave might need to do an async check |
| // to see if the GPM PIN is still valid before it can be offered for user |
| // verification. In this case, the enclave account state will be `kLoading` or |
| // `kChecking`. This function waits for that check to complete before adding |
| // GPM passkeys to the request. For other request types, this runs |
| // synchronously. Note that if the account state check takes longer than the |
| // immediate mode timeout, enclave passkeys won't be offered. |
| if (dialog_controller_->ui_presentation() == |
| UIPresentation::kModalImmediate && |
| enclave_controller_) { |
| switch (enclave_controller_->account_ready_state()) { |
| case GPMEnclaveController::AccountReadyState::kLoading: |
| enclave_controller_->RunWhenAccountReady( |
| base::BindOnce(&ChromeAuthenticatorRequestDelegate:: |
| DoGetPhoneContactableGpmPasskeysForRpId, |
| weak_ptr_factory_.GetWeakPtr(), std::move(tai), |
| std::move(callback))); |
| return; |
| case GPMEnclaveController::AccountReadyState::kReady: |
| case GPMEnclaveController::AccountReadyState::kNotReady: |
| // Fall through to run synchronously. |
| break; |
| } |
| } |
| |
| DoGetPhoneContactableGpmPasskeysForRpId(std::move(tai), std::move(callback)); |
| } |
| |
| void ChromeAuthenticatorRequestDelegate:: |
| DoGetPhoneContactableGpmPasskeysForRpId( |
| TransportAvailabilityInfo tai, |
| base::OnceCallback<void(TransportAvailabilityInfo)> callback) { |
| if (!enclave_controller_ || !enclave_controller_->is_active() || |
| enclave_controller_->creds().empty()) { |
| std::move(callback).Run(std::move(tai)); |
| return; |
| } |
| if (dialog_controller_->ui_presentation() == |
| UIPresentation::kModalImmediate) { |
| bool enclave_ready = enclave_controller_->account_ready_state() == |
| GPMEnclaveController::AccountReadyState::kReady; |
| base::UmaHistogramBoolean( |
| "WebAuthentication.GetAssertion.Immediate.EnclaveReady", enclave_ready); |
| if (!enclave_ready) { |
| std::move(callback).Run(std::move(tai)); |
| return; |
| } |
| } |
| for (const sync_pb::WebauthnCredentialSpecifics& passkey : |
| enclave_controller_->creds()) { |
| const base::Time last_used_time = base::Time::FromDeltaSinceWindowsEpoch( |
| base::Microseconds(passkey.last_used_time_windows_epoch_micros())); |
| const base::Time creation_time = |
| base::Time::FromMillisecondsSinceUnixEpoch(passkey.creation_time()); |
| tai.recognized_credentials.emplace_back( |
| device::AuthenticatorType::kEnclave, passkey.rp_id(), |
| std::vector<uint8_t>(passkey.credential_id().begin(), |
| passkey.credential_id().end()), |
| device::PublicKeyCredentialUserEntity( |
| std::vector<uint8_t>(passkey.user_id().begin(), |
| passkey.user_id().end()), |
| passkey.user_name(), passkey.user_display_name()), |
| /*provider_name=*/std::nullopt, |
| last_used_time > creation_time ? last_used_time : creation_time); |
| } |
| std::move(callback).Run(std::move(tai)); |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::FilterRecognizedCredentials( |
| TransportAvailabilityInfo* tai) { |
| if (dialog_model()->relying_party_id == kGoogleRpId && |
| tai->has_empty_allow_list && |
| std::ranges::any_of(tai->recognized_credentials, |
| IsCredentialFromPlatformAuthenticator)) { |
| // Regrettably, Chrome will create webauthn credentials for things other |
| // than authentication (e.g. credit card autofill auth) under the rp id |
| // "google.com". To differentiate those credentials from actual passkeys you |
| // can use to sign in, Google adds a prefix to the user id. |
| // This code filter passkeys that do not match that prefix. |
| FilterGoogleAuthPasskeys(&tai->recognized_credentials); |
| if (tai->has_platform_authenticator_credential == |
| device::FidoRequestHandlerBase::RecognizedCredential:: |
| kHasRecognizedCredential && |
| std::ranges::none_of(tai->recognized_credentials, |
| IsCredentialFromPlatformAuthenticator)) { |
| tai->has_platform_authenticator_credential = device:: |
| FidoRequestHandlerBase::RecognizedCredential::kNoRecognizedCredential; |
| } |
| } |
| |
| if (!credential_filter_.empty()) { |
| std::vector<device::DiscoverableCredentialMetadata> filtered_list; |
| for (auto& platform_credential : tai->recognized_credentials) { |
| for (auto& filter_credential : credential_filter_) { |
| if (platform_credential.cred_id == filter_credential.id) { |
| filtered_list.push_back(platform_credential); |
| break; |
| } |
| } |
| } |
| tai->recognized_credentials = std::move(filtered_list); |
| } |
| |
| const auto kImmediateTypes = |
| std::unordered_set{device::AuthenticatorType::kEnclave, |
| device::AuthenticatorType::kICloudKeychain, |
| device::AuthenticatorType::kWinNative, |
| device::AuthenticatorType::kChromeOS, |
| device::AuthenticatorType::kTouchID}; |
| if (dialog_controller_->ui_presentation() == |
| UIPresentation::kModalImmediate) { |
| std::erase_if(tai->recognized_credentials, |
| [&kImmediateTypes](const auto& passkey) { |
| return !kImmediateTypes.contains(passkey.source); |
| }); |
| } |
| } |
| |
| #if BUILDFLAG(IS_MAC) |
| // static |
| std::optional<int> ChromeAuthenticatorRequestDelegate::DaysSinceDate( |
| const std::string& formatted_date, |
| const base::Time now) { |
| int year, month, day_of_month; |
| // sscanf will ignore trailing garbage, but we don't need to be strict here. |
| if (UNSAFE_TODO(sscanf(formatted_date.c_str(), "%u-%u-%u", &year, &month, |
| &day_of_month)) != 3) { |
| return std::nullopt; |
| } |
| |
| const base::Time::Exploded exploded = { |
| .year = year, .month = month, .day_of_month = day_of_month}; |
| |
| base::Time t; |
| if (!base::Time::FromUTCExploded(exploded, &t) || now < t) { |
| return std::nullopt; |
| } |
| |
| const base::TimeDelta difference = now - t; |
| return difference.InDays(); |
| } |
| |
| // static |
| std::optional<bool> ChromeAuthenticatorRequestDelegate::GetICloudKeychainPref( |
| const PrefService* prefs) { |
| const PrefService::Preference* pref = |
| prefs->FindPreference(prefs::kCreatePasskeysInICloudKeychain); |
| if (pref->IsDefaultValue()) { |
| return std::nullopt; |
| } |
| return pref->GetValue()->GetBool(); |
| } |
| |
| // static |
| bool ChromeAuthenticatorRequestDelegate::IsActiveProfileAuthenticatorUser( |
| const PrefService* prefs) { |
| const std::string& last_used = prefs->GetString(kWebAuthnTouchIdLastUsed); |
| if (last_used.empty()) { |
| return false; |
| } |
| const std::optional<int> days = DaysSinceDate(last_used, base::Time::Now()); |
| return days.has_value() && days.value() <= kMacOsRecentlyUsedMaxDays; |
| } |
| |
| // static |
| bool ChromeAuthenticatorRequestDelegate::ShouldCreateInICloudKeychain( |
| RequestSource request_source, |
| bool is_active_profile_authenticator_user, |
| bool has_icloud_drive_enabled, |
| bool request_is_for_google_com, |
| std::optional<bool> preference) { |
| // Secure Payment Confirmation and credit-card autofill continue to use |
| // the profile authenticator. |
| if (request_source != RequestSource::kWebAuthentication) { |
| return false; |
| } |
| if (preference.has_value()) { |
| return *preference; |
| } |
| const base::Feature* feature; |
| if (request_is_for_google_com) { |
| feature = &device::kWebAuthnICloudKeychainForGoogle; |
| } else { |
| if (is_active_profile_authenticator_user) { |
| if (has_icloud_drive_enabled) { |
| feature = &device::kWebAuthnICloudKeychainForActiveWithDrive; |
| } else { |
| feature = &device::kWebAuthnICloudKeychainForActiveWithoutDrive; |
| } |
| } else { |
| if (has_icloud_drive_enabled) { |
| feature = &device::kWebAuthnICloudKeychainForInactiveWithDrive; |
| } else { |
| feature = &device::kWebAuthnICloudKeychainForInactiveWithoutDrive; |
| } |
| } |
| } |
| |
| return base::FeatureList::IsEnabled(*feature); |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::ConfigureNSWindow( |
| device::FidoDiscoveryFactory* discovery_factory) { |
| content::WebContents* web_contents = |
| content::WebContents::FromRenderFrameHost(GetRenderFrameHost()); |
| Browser* browser = chrome::FindBrowserWithTab(web_contents); |
| if (browser && browser->is_type_app()) { |
| // PWAs render the UI in an out-of-process window, thus there is no valid |
| // NSWindow* available in the browser process. |
| // TODO: crbug.com/364926914 - potentially do iCloud Keychain operations out |
| // of process so that they can work in PWAs. |
| return; |
| } |
| |
| // Not all contexts in which this code runs have a BrowserWindow. |
| // Notably the dialog containing a WebContents that is used for signing |
| // into a new profile does not. Thus the NSWindow is fetched more directly. |
| const views::Widget* widget = views::Widget::GetTopLevelWidgetForNativeView( |
| web_contents->GetNativeView()); |
| if (widget) { |
| const gfx::NativeWindow window = widget->GetNativeWindow(); |
| if (window) { |
| discovery_factory->set_nswindow(window); |
| } |
| } |
| } |
| void ChromeAuthenticatorRequestDelegate::ConfigureICloudKeychain( |
| RequestSource request_source, |
| const std::string& rp_id) { |
| const PrefService* prefs = profile()->GetPrefs(); |
| const bool is_icloud_drive_enabled = IsICloudDriveEnabled(); |
| const bool is_active_profile_authenticator_user = |
| IsActiveProfileAuthenticatorUser(prefs); |
| dialog_controller_->set_allow_icloud_keychain( |
| request_source == RequestSource::kWebAuthentication); |
| dialog_controller_->set_has_icloud_drive_enabled(is_icloud_drive_enabled); |
| dialog_controller_->set_is_active_profile_authenticator_user( |
| is_active_profile_authenticator_user); |
| dialog_controller_->set_should_create_in_icloud_keychain( |
| ShouldCreateInICloudKeychain( |
| request_source, is_active_profile_authenticator_user, |
| is_icloud_drive_enabled, rp_id == "google.com", |
| GetICloudKeychainPref(prefs))); |
| } |
| |
| #endif |
| |
| void ChromeAuthenticatorRequestDelegate::OnPasswordCredentialsReceived( |
| PasswordCredentials credentials) { |
| pending_password_credentials_ = |
| std::make_unique<PasswordCredentials>(std::move(credentials)); |
| TryToShowUI(); |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::UpdateModelForTransportAvailability( |
| const TransportAvailabilityInfo& tai) { |
| dialog_model_->request_type = tai.request_type; |
| dialog_model_->resident_key_requirement = tai.resident_key_requirement; |
| dialog_model_->attestation_conveyance_preference = |
| tai.attestation_conveyance_preference; |
| dialog_model_->ble_adapter_is_powered = |
| tai.ble_status == device::FidoRequestHandlerBase::BleStatus::kOn; |
| dialog_model_->show_security_key_on_qr_sheet = |
| base::Contains(tai.available_transports, |
| device::FidoTransportProtocol::kUsbHumanInterfaceDevice); |
| dialog_model_->is_off_the_record = tai.is_off_the_record_context; |
| dialog_model_->platform_has_biometrics = tai.platform_has_biometrics; |
| } |