| // 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 <memory> |
| #include <utility> |
| |
| #include "base/base64.h" |
| #include "base/command_line.h" |
| #include "base/containers/contains.h" |
| #include "base/feature_list.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/location.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/strings/string_piece.h" |
| #include "base/strings/string_split.h" |
| #include "base/sys_byteorder.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/ssl/security_state_tab_helper.h" |
| #include "chrome/browser/sync/device_info_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/page_action/page_action_icon_type.h" |
| #include "chrome/browser/webauthn/authenticator_request_dialog_model.h" |
| #include "chrome/browser/webauthn/cablev2_devices.h" |
| #include "chrome/browser/webauthn/webauthn_pref_names.h" |
| #include "chrome/browser/webauthn/webauthn_switches.h" |
| #include "chrome/common/chrome_switches.h" |
| #include "chrome/common/chrome_version.h" |
| #include "chrome/common/pref_names.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "components/device_event_log/device_event_log.h" |
| #include "components/network_session_configurator/common/network_switches.h" |
| #include "components/pref_registry/pref_registry_syncable.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/security_state/core/security_state.h" |
| #include "components/user_prefs/user_prefs.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/device_service.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/web_contents.h" |
| #include "crypto/random.h" |
| #include "device/fido/cable/v2_handshake.h" |
| #include "device/fido/features.h" |
| #include "device/fido/fido_authenticator.h" |
| #include "device/fido/fido_discovery_factory.h" |
| #include "device/fido/public_key_credential_descriptor.h" |
| #include "device/fido/public_key_credential_user_entity.h" |
| #include "extensions/common/constants.h" |
| #include "net/base/url_util.h" |
| #include "third_party/icu/source/common/unicode/locid.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/window_open_disposition.h" |
| |
| #if BUILDFLAG(IS_MAC) |
| #include "device/fido/mac/authenticator.h" |
| #include "device/fido/mac/credential_metadata.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 |
| |
| namespace { |
| |
| ChromeAuthenticatorRequestDelegate::TestObserver* g_observer = nullptr; |
| |
| // Returns true iff |relying_party_id| is listed in the |
| // SecurityKeyPermitAttestation policy. |
| bool IsWebAuthnRPIDListedInSecurityKeyPermitAttestationPolicy( |
| content::BrowserContext* browser_context, |
| const std::string& relying_party_id) { |
| const Profile* profile = Profile::FromBrowserContext(browser_context); |
| const PrefService* prefs = profile->GetPrefs(); |
| const base::Value::List& permit_attestation = |
| prefs->GetList(prefs::kSecurityKeyPermitAttestation); |
| const std::string& (base::Value::*get_string)() const = |
| &base::Value::GetString; |
| return base::Contains(permit_attestation, relying_party_id, get_string); |
| } |
| |
| bool IsOriginListedInEnterpriseAttestationSwitch( |
| const url::Origin& caller_origin) { |
| std::string cmdline_origins = |
| base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII( |
| webauthn::switches::kPermitEnterpriseAttestationOriginList); |
| std::vector<base::StringPiece> origin_strings = base::SplitStringPiece( |
| cmdline_origins, ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY); |
| return base::ranges::any_of( |
| origin_strings, [&caller_origin](base::StringPiece origin_string) { |
| return url::Origin::Create(GURL(origin_string)) == caller_origin; |
| }); |
| } |
| |
| #if BUILDFLAG(IS_MAC) |
| const char kWebAuthnTouchIdMetadataSecretPrefName[] = |
| "webauthn.touchid.metadata_secret"; |
| #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; |
| } |
| } |
| |
| void OnNewCablePairing(std::unique_ptr<device::cablev2::Pairing> pairing) { |
| if (!profile_) { |
| FIDO_LOG(DEBUG) << "Linking event was discarded because it was received " |
| "after the profile was destroyed."; |
| return; |
| } |
| |
| // Drop linking in Incognito sessions. While an argument could be made that |
| // it's OK to persist them, this seems like the safe option. |
| if (profile_->IsOffTheRecord()) { |
| FIDO_LOG(DEBUG) << "Linking event was discarded because the profile is " |
| "Off The Record."; |
| return; |
| } |
| |
| cablev2::AddPairing(profile_, std::move(pairing)); |
| } |
| |
| // ProfileObserver: |
| |
| void OnProfileWillBeDestroyed(Profile* profile) override { |
| DCHECK_EQ(profile, profile_); |
| profile_->RemoveObserver(this); |
| profile_ = nullptr; |
| } |
| |
| private: |
| raw_ptr<Profile> profile_; |
| }; |
| |
| } // namespace |
| |
| // --------------------------------------------------------------------- |
| // ChromeWebAuthenticationDelegate |
| // --------------------------------------------------------------------- |
| |
| ChromeWebAuthenticationDelegate::~ChromeWebAuthenticationDelegate() = default; |
| |
| #if !BUILDFLAG(IS_ANDROID) |
| |
| bool ChromeWebAuthenticationDelegate:: |
| OverrideCallerOriginAndRelyingPartyIdValidation( |
| content::BrowserContext* browser_context, |
| const url::Origin& caller_origin, |
| const std::string& relying_party_id) { |
| // Allow chrome-extensions:// origins to make WebAuthn requests. |
| // `MaybeGetRelyingPartyId` will override the RP ID to use when processing |
| // requests from extensions. |
| return caller_origin.scheme() == extensions::kExtensionScheme && |
| caller_origin.host() == relying_party_id; |
| } |
| |
| bool ChromeWebAuthenticationDelegate::OriginMayUseRemoteDesktopClientOverride( |
| content::BrowserContext* browser_context, |
| const url::Origin& caller_origin) { |
| // Allow the Google-internal version of Chrome Remote Desktop to use the |
| // RemoteDesktopClientOverride extension and make WebAuthn |
| // requests on behalf of other origins, if a corresponding enteprise policy is |
| // enabled. |
| // |
| // The policy explicitly does not cover external instances of CRD. It |
| // must not be extended to other origins or be made configurable without going |
| // through security review. |
| if (!base::FeatureList::IsEnabled( |
| device::kWebAuthnGoogleCorpRemoteDesktopClientPrivilege)) { |
| return false; |
| } |
| |
| const Profile* profile = Profile::FromBrowserContext(browser_context); |
| const PrefService* prefs = profile->GetPrefs(); |
| const bool google_corp_remote_proxied_request_allowed = |
| prefs->GetBoolean(webauthn::pref_names::kRemoteProxiedRequestsAllowed); |
| if (!google_corp_remote_proxied_request_allowed) { |
| return false; |
| } |
| |
| constexpr const char* const kGoogleCorpCrdOrigins[] = { |
| "https://remotedesktop.corp.google.com", |
| "https://remotedesktop-autopush.corp.google.com/", |
| "https://remotedesktop-daily-6.corp.google.com/", |
| }; |
| for (const char* corp_crd_origin : kGoogleCorpCrdOrigins) { |
| if (caller_origin == url::Origin::Create(GURL(corp_crd_origin))) { |
| return true; |
| } |
| } |
| |
| // An additional origin can be passed on the command line for testing. |
| if (!base::CommandLine::ForCurrentProcess()->HasSwitch( |
| webauthn::switches::kRemoteProxiedRequestsAllowedAdditionalOrigin)) { |
| return false; |
| } |
| // Note that `cmdline_allowed_origin` will be opaque if the flag is not a |
| // valid URL, which won't match `caller_origin`. |
| const url::Origin cmdline_allowed_origin = url::Origin::Create( |
| GURL(base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII( |
| webauthn::switches::kRemoteProxiedRequestsAllowedAdditionalOrigin))); |
| return caller_origin == cmdline_allowed_origin; |
| } |
| |
| bool ChromeWebAuthenticationDelegate::IsSecurityLevelAcceptableForWebAuthn( |
| content::RenderFrameHost* rfh, |
| const url::Origin& caller_origin) { |
| const Profile* profile = |
| Profile::FromBrowserContext(rfh->GetBrowserContext()); |
| if (profile->GetPrefs()->GetBoolean( |
| webauthn::pref_names::kAllowWithBrokenCerts)) { |
| return true; |
| } |
| if (caller_origin.scheme() == extensions::kExtensionScheme) { |
| return true; |
| } |
| if (net::IsLocalhost(caller_origin.GetURL())) { |
| return true; |
| } |
| content::WebContents* web_contents = |
| content::WebContents::FromRenderFrameHost(rfh); |
| SecurityStateTabHelper::CreateForWebContents(web_contents); |
| SecurityStateTabHelper* helper = |
| SecurityStateTabHelper::FromWebContents(web_contents); |
| security_state::SecurityLevel security_level = helper->GetSecurityLevel(); |
| return security_level == security_state::SecurityLevel::SECURE || |
| security_level == |
| security_state::SecurityLevel::SECURE_WITH_POLICY_INSTALLED_CERT || |
| base::CommandLine::ForCurrentProcess()->HasSwitch( |
| switches::kIgnoreCertificateErrors); |
| } |
| |
| absl::optional<std::string> |
| ChromeWebAuthenticationDelegate::MaybeGetRelyingPartyIdOverride( |
| const std::string& claimed_relying_party_id, |
| const url::Origin& caller_origin) { |
| // Otherwise, allow extensions to use WebAuthn and map their origins |
| // directly to RP IDs. |
| if (caller_origin.scheme() == extensions::kExtensionScheme) { |
| // `OverrideCallerOriginAndRelyingPartyIdValidation' ensures an extension |
| // must only use the extension identifier as the RP ID, no flexibility is |
| // permitted. When interacting with authenticators, however, we use the |
| // whole origin to avoid collisions with the RP ID space for HTTPS origins. |
| return caller_origin.Serialize(); |
| } |
| |
| return absl::nullopt; |
| } |
| |
| bool ChromeWebAuthenticationDelegate::ShouldPermitIndividualAttestation( |
| content::BrowserContext* browser_context, |
| const url::Origin& caller_origin, |
| const std::string& relying_party_id) { |
| return IsOriginListedInEnterpriseAttestationSwitch(caller_origin) || |
| IsWebAuthnRPIDListedInSecurityKeyPermitAttestationPolicy( |
| browser_context, relying_party_id); |
| } |
| |
| bool ChromeWebAuthenticationDelegate::SupportsResidentKeys( |
| content::RenderFrameHost* render_frame_host) { |
| return true; |
| } |
| |
| bool ChromeWebAuthenticationDelegate::IsFocused( |
| content::WebContents* web_contents) { |
| return web_contents->GetVisibility() == content::Visibility::VISIBLE; |
| } |
| |
| absl::optional<bool> ChromeWebAuthenticationDelegate:: |
| IsUserVerifyingPlatformAuthenticatorAvailableOverride( |
| content::RenderFrameHost* render_frame_host) { |
| // If the testing API is active, its override takes precedence. |
| absl::optional<bool> testing_api_override = |
| content::WebAuthenticationDelegate:: |
| IsUserVerifyingPlatformAuthenticatorAvailableOverride( |
| render_frame_host); |
| if (testing_api_override) { |
| return *testing_api_override; |
| } |
| |
| // Chrome disables platform authenticators is Guest sessions. They may be |
| // available (behind an additional interstitial) in Incognito mode. |
| Profile* profile = |
| Profile::FromBrowserContext(render_frame_host->GetBrowserContext()); |
| if (profile->IsGuestSession()) { |
| return false; |
| } |
| |
| return absl::nullopt; |
| } |
| |
| content::WebAuthenticationRequestProxy* |
| ChromeWebAuthenticationDelegate::MaybeGetRequestProxy( |
| content::BrowserContext* browser_context, |
| const url::Origin& caller_origin) { |
| auto* service = extensions::WebAuthenticationProxyService::GetIfProxyAttached( |
| Profile::FromBrowserContext(browser_context)); |
| return service && service->IsActive(caller_origin) ? service : nullptr; |
| } |
| |
| #endif // !IS_ANDROID |
| |
| #if BUILDFLAG(IS_MAC) |
| // static |
| ChromeWebAuthenticationDelegate::TouchIdAuthenticatorConfig |
| ChromeWebAuthenticationDelegate::TouchIdAuthenticatorConfigForProfile( |
| Profile* profile) { |
| constexpr char kKeychainAccessGroup[] = |
| MAC_TEAM_IDENTIFIER_STRING "." MAC_BUNDLE_IDENTIFIER_STRING ".webauthn"; |
| |
| std::string metadata_secret = |
| profile->GetPrefs()->GetString(kWebAuthnTouchIdMetadataSecretPrefName); |
| if (metadata_secret.empty() || |
| !base::Base64Decode(metadata_secret, &metadata_secret)) { |
| metadata_secret = device::fido::mac::GenerateCredentialMetadataSecret(); |
| profile->GetPrefs()->SetString( |
| kWebAuthnTouchIdMetadataSecretPrefName, |
| base::Base64Encode(base::as_bytes(base::make_span(metadata_secret)))); |
| } |
| |
| return TouchIdAuthenticatorConfig{ |
| .keychain_access_group = kKeychainAccessGroup, |
| .metadata_secret = std::move(metadata_secret)}; |
| } |
| |
| absl::optional<ChromeWebAuthenticationDelegate::TouchIdAuthenticatorConfig> |
| ChromeWebAuthenticationDelegate::GetTouchIdAuthenticatorConfig( |
| content::BrowserContext* browser_context) { |
| return TouchIdAuthenticatorConfigForProfile( |
| Profile::FromBrowserContext(browser_context)); |
| } |
| #endif // BUILDFLAG(IS_MAC) |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| content::WebAuthenticationDelegate::ChromeOSGenerateRequestIdCallback |
| ChromeWebAuthenticationDelegate::GetGenerateRequestIdCallback( |
| content::RenderFrameHost* render_frame_host) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| aura::Window* window = |
| render_frame_host->GetNativeView()->GetToplevelWindow(); |
| return chromeos::webauthn::WebAuthnRequestRegistrar::Get() |
| ->GetRegisterCallback(window); |
| } |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| |
| // --------------------------------------------------------------------- |
| // ChromeAuthenticatorRequestDelegate |
| // --------------------------------------------------------------------- |
| |
| // static |
| void ChromeAuthenticatorRequestDelegate::RegisterProfilePrefs( |
| user_prefs::PrefRegistrySyncable* registry) { |
| registry->RegisterListPref(prefs::kSecurityKeyPermitAttestation); |
| #if BUILDFLAG(IS_WIN) |
| LocalCredentialManagementWin::RegisterProfilePrefs(registry); |
| #endif |
| #if BUILDFLAG(IS_MAC) |
| registry->RegisterStringPref(kWebAuthnTouchIdMetadataSecretPrefName, |
| std::string()); |
| #endif |
| cablev2::RegisterProfilePrefs(registry); |
| } |
| |
| ChromeAuthenticatorRequestDelegate::ChromeAuthenticatorRequestDelegate( |
| content::RenderFrameHost* render_frame_host) |
| : render_frame_host_id_(render_frame_host->GetGlobalId()), |
| dialog_model_(std::make_unique<AuthenticatorRequestDialogModel>( |
| GetRenderFrameHost())) { |
| dialog_model_->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_->RemoveObserver(this); |
| } |
| |
| // static |
| void ChromeAuthenticatorRequestDelegate::SetGlobalObserverForTesting( |
| TestObserver* observer) { |
| CHECK(!observer || !g_observer); |
| g_observer = observer; |
| } |
| |
| base::WeakPtr<ChromeAuthenticatorRequestDelegate> |
| ChromeAuthenticatorRequestDelegate::AsWeakPtr() { |
| return weak_ptr_factory_.GetWeakPtr(); |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::SetRelyingPartyId( |
| const std::string& rp_id) { |
| dialog_model_->set_relying_party_id(rp_id); |
| } |
| |
| bool ChromeAuthenticatorRequestDelegate::DoesBlockRequestOnFailure( |
| InterestingFailureReason reason) { |
| if (!IsWebAuthnUIEnabled()) { |
| 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_model_->OnRequestTimeout(); |
| break; |
| case InterestingFailureReason::kKeyNotRegistered: |
| dialog_model_->OnActivatedKeyNotRegistered(); |
| break; |
| case InterestingFailureReason::kKeyAlreadyRegistered: |
| dialog_model_->OnActivatedKeyAlreadyRegistered(); |
| break; |
| case InterestingFailureReason::kSoftPINBlock: |
| dialog_model_->OnSoftPINBlock(); |
| break; |
| case InterestingFailureReason::kHardPINBlock: |
| dialog_model_->OnHardPINBlock(); |
| break; |
| case InterestingFailureReason::kAuthenticatorRemovedDuringPINEntry: |
| dialog_model_->OnAuthenticatorRemovedDuringPINEntry(); |
| break; |
| case InterestingFailureReason::kAuthenticatorMissingResidentKeys: |
| dialog_model_->OnAuthenticatorMissingResidentKeys(); |
| break; |
| case InterestingFailureReason::kAuthenticatorMissingUserVerification: |
| dialog_model_->OnAuthenticatorMissingUserVerification(); |
| break; |
| case InterestingFailureReason::kAuthenticatorMissingLargeBlob: |
| dialog_model_->OnAuthenticatorMissingLargeBlob(); |
| break; |
| case InterestingFailureReason::kNoCommonAlgorithms: |
| dialog_model_->OnNoCommonAlgorithms(); |
| break; |
| case InterestingFailureReason::kStorageFull: |
| dialog_model_->OnAuthenticatorStorageFull(); |
| break; |
| case InterestingFailureReason::kUserConsentDenied: |
| dialog_model_->OnUserConsentDenied(); |
| break; |
| case InterestingFailureReason::kWinUserCancelled: |
| return dialog_model_->OnWinUserCancelled(); |
| case InterestingFailureReason::kHybridTransportError: |
| return dialog_model_->OnHybridTransportError(); |
| } |
| return true; |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::RegisterActionCallbacks( |
| base::OnceClosure cancel_callback, |
| base::RepeatingClosure start_over_callback, |
| AccountPreselectedCallback account_preselected_callback, |
| device::FidoRequestHandlerBase::RequestCallback request_callback, |
| base::RepeatingClosure bluetooth_adapter_power_on_callback) { |
| request_callback_ = request_callback; |
| cancel_callback_ = std::move(cancel_callback); |
| start_over_callback_ = std::move(start_over_callback); |
| account_preselected_callback_ = std::move(account_preselected_callback); |
| |
| dialog_model_->SetRequestCallback(request_callback); |
| dialog_model_->SetAccountPreselectedCallback(account_preselected_callback_); |
| dialog_model_->SetBluetoothAdapterPowerOnCallback( |
| bluetooth_adapter_power_on_callback); |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::ShouldReturnAttestation( |
| const std::string& relying_party_id, |
| const device::FidoAuthenticator* authenticator, |
| bool is_enterprise_attestation, |
| base::OnceCallback<void(bool)> callback) { |
| if (disable_ui_ && IsVirtualEnvironmentEnabled()) { |
| std::move(callback).Run(true); |
| return; |
| } |
| if (IsWebAuthnRPIDListedInSecurityKeyPermitAttestationPolicy( |
| GetBrowserContext(), relying_party_id)) { |
| // Enterprise attestations should have been approved already and not reach |
| // this point. |
| DCHECK(!is_enterprise_attestation); |
| std::move(callback).Run(true); |
| return; |
| } |
| |
| // AuthenticatorCommon can't evaluate attestation decisions with the UI |
| // disabled. |
| if (disable_ui_) { |
| NOTREACHED(); |
| std::move(callback).Run(false); |
| return; |
| } |
| |
| #if BUILDFLAG(IS_WIN) |
| if (authenticator->GetType() == device::AuthenticatorType::kWinNative && |
| static_cast<const device::WinWebAuthnApiAuthenticator*>(authenticator) |
| ->ShowsPrivacyNotice()) { |
| // The OS' native API includes an attestation prompt. |
| std::move(callback).Run(true); |
| return; |
| } |
| #endif // BUILDFLAG(IS_WIN) |
| |
| dialog_model_->RequestAttestationPermission(is_enterprise_attestation, |
| std::move(callback)); |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::ConfigureCable( |
| const url::Origin& origin, |
| device::FidoRequestType request_type, |
| absl::optional<device::ResidentKeyRequirement> resident_key_requirement, |
| base::span<const device::CableDiscoveryData> pairings_from_extension, |
| device::FidoDiscoveryFactory* discovery_factory) { |
| DCHECK(request_type == device::FidoRequestType::kGetAssertion || |
| resident_key_requirement.has_value()); |
| |
| phone_names_.clear(); |
| phone_public_keys_.clear(); |
| |
| 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); |
| |
| std::vector<std::unique_ptr<device::cablev2::Pairing>> paired_phones; |
| std::vector<AuthenticatorRequestDialogModel::PairedPhone> |
| paired_phone_entries; |
| base::RepeatingCallback<void(size_t)> contact_phone_callback; |
| if (!cable_extension_provided || |
| base::FeatureList::IsEnabled(device::kWebAuthCableExtensionAnywhere)) { |
| DCHECK(phone_names_.empty()); |
| DCHECK(phone_public_keys_.empty()); |
| |
| std::unique_ptr<cablev2::KnownDevices> known_devices = |
| cablev2::KnownDevices::FromProfile( |
| Profile::FromBrowserContext(GetBrowserContext())); |
| if (g_observer) { |
| known_devices->synced_devices = |
| g_observer->GetCablePairingsFromSyncedDevices(); |
| } |
| paired_phones = cablev2::MergeDevices(std::move(known_devices), |
| &icu::Locale::getDefault()); |
| |
| // The debug log displays in reverse order, so the headline is emitted after |
| // the names. |
| for (const auto& pairing : paired_phones) { |
| FIDO_LOG(DEBUG) << "• " << pairing->name << " " << pairing->last_updated |
| << " priority:" << pairing->channel_priority; |
| } |
| FIDO_LOG(DEBUG) << "Found " << paired_phones.size() << " caBLEv2 devices"; |
| |
| if (!paired_phones.empty()) { |
| for (size_t i = 0; i < paired_phones.size(); i++) { |
| const auto& phone = paired_phones[i]; |
| paired_phone_entries.emplace_back(phone->name, i, |
| phone->peer_public_key_x962); |
| phone_names_.push_back(phone->name); |
| phone_public_keys_.push_back(phone->peer_public_key_x962); |
| } |
| |
| contact_phone_callback = discovery_factory->get_cable_contact_callback(); |
| } |
| } |
| |
| 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)); |
| |
| absl::optional<std::array<uint8_t, device::cablev2::kQRKeySize>> |
| qr_generator_key; |
| absl::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::FromBrowserContext(GetBrowserContext())); |
| discovery_factory->set_cable_pairing_callback( |
| base::BindRepeating(&CableLinkingEventHandler::OnNewCablePairing, |
| std::move(linking_handler))); |
| discovery_factory->set_cable_invalidated_pairing_callback( |
| base::BindRepeating( |
| &ChromeAuthenticatorRequestDelegate::OnInvalidatedCablePairing, |
| weak_ptr_factory_.GetWeakPtr())); |
| discovery_factory->set_cable_event_callback( |
| base::BindRepeating(&ChromeAuthenticatorRequestDelegate::OnCableEvent, |
| weak_ptr_factory_.GetWeakPtr())); |
| if (SystemNetworkContextManager::GetInstance()) { |
| discovery_factory->set_network_context( |
| SystemNetworkContextManager::GetInstance()->GetContext()); |
| } |
| } |
| |
| mojo::Remote<device::mojom::UsbDeviceManager> usb_device_manager; |
| if (!pass_empty_usb_device_manager_) { |
| content::GetDeviceService().BindUsbDeviceManager( |
| usb_device_manager.BindNewPipeAndPassReceiver()); |
| } |
| discovery_factory->set_android_accessory_params( |
| std::move(usb_device_manager), |
| l10n_util::GetStringUTF8(IDS_WEBAUTHN_CABLEV2_AOA_REQUEST_DESCRIPTION)); |
| |
| if (cable_extension_accepted || non_extension_cablev2_enabled) { |
| absl::optional<bool> extension_is_v2; |
| if (cable_extension_provided) { |
| extension_is_v2 = cablev2_extension_provided; |
| } |
| dialog_model_->set_cable_transport_info( |
| extension_is_v2, std::move(paired_phone_entries), |
| std::move(contact_phone_callback), qr_string); |
| discovery_factory->set_cable_data(request_type, std::move(pairings), |
| qr_generator_key, |
| std::move(paired_phones)); |
| } |
| |
| #if BUILDFLAG(IS_MAC) |
| content::WebContents* web_contents = |
| content::WebContents::FromRenderFrameHost(GetRenderFrameHost()); |
| BrowserWindow* browser_window = |
| BrowserWindow::FindBrowserWindowWithWebContents(web_contents); |
| if (browser_window) { |
| discovery_factory->set_nswindow(reinterpret_cast<uintptr_t>( |
| browser_window->GetNativeWindow().GetNativeNSWindow())); |
| } |
| #endif |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::SelectAccount( |
| std::vector<device::AuthenticatorGetAssertionResponse> responses, |
| base::OnceCallback<void(device::AuthenticatorGetAssertionResponse)> |
| callback) { |
| if (disable_ui_) { |
| // 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/991666): 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_model_->SelectAccount(std::move(responses), std::move(callback)); |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::DisableUI() { |
| disable_ui_ = true; |
| } |
| |
| bool ChromeAuthenticatorRequestDelegate::IsWebAuthnUIEnabled() { |
| // The UI is fully disabled for the entire request duration if either: |
| // 1) The UI was temporarily hidden, e.g. while showing the native Windows |
| // WebAuthn UI. But in those cases the UI is still enabled and can be shown |
| // e.g. for an attestation consent prompt. |
| // 2) A specialized UI is replacing the default WebAuthn UI, such as Secure |
| // Payment Confirmation or Autofill. |
| return !disable_ui_; |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::SetConditionalRequest( |
| bool is_conditional) { |
| is_conditional_ = is_conditional; |
| } |
| |
| 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()->set_user_entity(user_entity); |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::OnTransportAvailabilityEnumerated( |
| device::FidoRequestHandlerBase::TransportAvailabilityInfo data) { |
| if (is_conditional_ && !credential_filter_.empty()) { |
| std::vector<device::DiscoverableCredentialMetadata> filtered_list; |
| for (auto& platform_credential : |
| data.recognized_platform_authenticator_credentials) { |
| for (auto& filter_credential : credential_filter_) { |
| if (platform_credential.cred_id == filter_credential.id) { |
| filtered_list.push_back(platform_credential); |
| break; |
| } |
| } |
| } |
| data.recognized_platform_authenticator_credentials = |
| std::move(filtered_list); |
| } |
| |
| if (g_observer) { |
| g_observer->OnTransportAvailabilityEnumerated(this, &data); |
| } |
| |
| if (disable_ui_ || dialog_model_->current_step() != |
| AuthenticatorRequestDialogModel::Step::kNotStarted) { |
| return; |
| } |
| |
| dialog_model_->StartFlow(std::move(data), is_conditional_); |
| |
| if (g_observer) { |
| g_observer->UIShown(this); |
| } |
| } |
| |
| 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 (!IsWebAuthnUIEnabled()) { |
| // There is no UI to handle request dispatch. |
| return false; |
| } |
| if (is_conditional_ && |
| (dialog_model_->current_step() == |
| AuthenticatorRequestDialogModel::Step::kConditionalMediation || |
| dialog_model_->current_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 (!IsWebAuthnUIEnabled()) { |
| return; |
| } |
| |
| dialog_model_->AddAuthenticator(authenticator); |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::FidoAuthenticatorRemoved( |
| base::StringPiece authenticator_id) { |
| if (!IsWebAuthnUIEnabled()) { |
| return; |
| } |
| |
| dialog_model_->RemoveAuthenticator(authenticator_id); |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::BluetoothAdapterPowerChanged( |
| bool is_powered_on) { |
| dialog_model_->OnBluetoothPoweredStateChanged(is_powered_on); |
| } |
| |
| bool ChromeAuthenticatorRequestDelegate::SupportsPIN() const { |
| return true; |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::CollectPIN( |
| CollectPINOptions options, |
| base::OnceCallback<void(std::u16string)> provide_pin_cb) { |
| dialog_model_->CollectPIN(options.reason, options.error, |
| options.min_pin_length, options.attempts, |
| std::move(provide_pin_cb)); |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::StartBioEnrollment( |
| base::OnceClosure next_callback) { |
| dialog_model_->StartInlineBioEnrollment(std::move(next_callback)); |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::OnSampleCollected( |
| int bio_samples_remaining) { |
| dialog_model_->OnSampleCollected(bio_samples_remaining); |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::FinishCollectToken() { |
| dialog_model_->FinishCollectToken(); |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::OnRetryUserVerification(int attempts) { |
| dialog_model_->OnRetryUserVerification(attempts); |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::OnStartOver() { |
| DCHECK(start_over_callback_); |
| 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::OnManageDevicesClicked() { |
| content::WebContents* web_contents = |
| content::WebContents::FromRenderFrameHost(GetRenderFrameHost()); |
| Browser* browser = chrome::FindBrowserWithWebContents(web_contents); |
| if (browser) { |
| NavigateParams params(browser, |
| GURL("chrome://settings/securityKeys/phones"), |
| ui::PageTransition::PAGE_TRANSITION_AUTO_TOPLEVEL); |
| params.disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB; |
| Navigate(¶ms); |
| } |
| } |
| |
| AuthenticatorRequestDialogModel* |
| ChromeAuthenticatorRequestDelegate::GetDialogModelForTesting() { |
| return dialog_model_.get(); |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::SetPassEmptyUsbDeviceManagerForTesting( |
| bool value) { |
| pass_empty_usb_device_manager_ = value; |
| } |
| |
| 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(); |
| } |
| |
| 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::OnInvalidatedCablePairing( |
| size_t failed_contact_index) { |
| PrefService* const prefs = |
| Profile::FromBrowserContext(GetBrowserContext())->GetPrefs(); |
| |
| // A pairing was reported to be invalid. Delete it unless it came from Sync, |
| // in which case there's nothing to be done. |
| cablev2::DeletePairingByPublicKey( |
| prefs, phone_public_keys_.at(failed_contact_index)); |
| |
| // Contact the next phone with the same name, if any, given that no |
| // notification has been sent. |
| dialog_model_->OnPhoneContactFailed(phone_names_.at(failed_contact_index)); |
| } |
| |
| void ChromeAuthenticatorRequestDelegate::OnCableEvent( |
| device::cablev2::Event event) { |
| if (!base::FeatureList::IsEnabled(device::kWebAuthnNewHybridUI)) { |
| return; |
| } |
| |
| if (event == device::cablev2::Event::kReady) { |
| cable_device_ready_ = true; |
| } |
| |
| dialog_model_->OnCableEvent(event); |
| } |