| // 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/ui/webauthn/passkey_upgrade_request_controller.h" |
| |
| #include <memory> |
| #include <optional> |
| #include <string> |
| #include <tuple> |
| #include <utility> |
| #include <variant> |
| |
| #include "base/check.h" |
| #include "base/check_op.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/notimplemented.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/time/time.h" |
| #include "chrome/browser/password_manager/account_password_store_factory.h" |
| #include "chrome/browser/password_manager/profile_password_store_factory.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/sync/sync_service_factory.h" |
| #include "chrome/browser/ui/passwords/passwords_client_ui_delegate.h" |
| #include "chrome/browser/webauthn/enclave_manager_factory.h" |
| #include "chrome/browser/webauthn/gpm_enclave_controller.h" |
| #include "chrome/browser/webauthn/gpm_enclave_transaction.h" |
| #include "chrome/browser/webauthn/passkey_model_factory.h" |
| #include "components/device_event_log/device_event_log.h" |
| #include "components/keyed_service/core/service_access_type.h" |
| #include "components/password_manager/core/browser/features/password_manager_features_util.h" |
| #include "components/password_manager/core/browser/form_parsing/form_data_parser.h" |
| #include "components/password_manager/core/browser/password_form_digest.h" |
| #include "components/password_manager/core/browser/password_store/password_store.h" |
| #include "components/password_manager/core/browser/password_store/password_store_backend_error.h" |
| #include "components/password_manager/core/browser/password_store/password_store_consumer.h" |
| #include "components/password_manager/core/browser/password_store/password_store_interface.h" |
| #include "components/password_manager/core/browser/password_store/password_store_util.h" |
| #include "components/password_manager/core/browser/password_sync_util.h" |
| #include "components/password_manager/core/common/password_manager_pref_names.h" |
| #include "components/sync/service/sync_service.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/web_contents.h" |
| #include "device/fido/fido_discovery_base.h" |
| #include "device/fido/fido_discovery_factory.h" |
| |
| using RenderFrameHost = content::RenderFrameHost; |
| |
| enum class PasskeyUpgradeRequestController::EnclaveState { |
| kUnknown, |
| kReady, |
| kError, |
| }; |
| |
| void RecordPasskeyUpgradeResultHistogram(PasskeyUpgradeResult result) { |
| base::UmaHistogramEnumeration( |
| "WebAuthentication.AutomaticPasskeyUpgrade.Result", result); |
| } |
| |
| PasskeyUpgradeRequestController::PasskeyUpgradeRequestController( |
| RenderFrameHost* rfh, |
| EnclaveRequestCallback enclave_request_callback) |
| : frame_host_id_(rfh->GetGlobalId()), |
| enclave_manager_( |
| EnclaveManagerFactory::GetAsEnclaveManagerForProfile(profile())), |
| enclave_request_callback_(enclave_request_callback) { |
| if (enclave_manager_->is_loaded()) { |
| OnEnclaveLoaded(); |
| return; |
| } |
| enclave_manager_->Load( |
| base::BindOnce(&PasskeyUpgradeRequestController::OnEnclaveLoaded, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| PasskeyUpgradeRequestController::~PasskeyUpgradeRequestController() = default; |
| |
| void PasskeyUpgradeRequestController::TryUpgradePasswordToPasskey( |
| std::string rp_id, |
| const std::string& username, |
| Delegate* delegate) { |
| FIDO_LOG(EVENT) << "Passkey upgrade request started"; |
| CHECK(enclave_request_callback_) |
| << "InitializeEnclaveRequestCallback() must be called first"; |
| CHECK(!pending_request_); |
| CHECK(delegate); |
| CHECK(!delegate_); |
| |
| pending_request_ = true; |
| delegate_ = delegate; |
| rp_id_ = std::move(rp_id); |
| username_ = base::UTF8ToUTF16(username); |
| |
| if (!profile()->GetPrefs()->GetBoolean( |
| password_manager::prefs::kAutomaticPasskeyUpgrades)) { |
| FinishRequest(PasskeyUpgradeResult::kOptOut); |
| return; |
| } |
| |
| switch (enclave_state_) { |
| case EnclaveState::kUnknown: |
| // EnclaveLoaded() will invoke ContinuePendingUpgradeRequest(). |
| break; |
| case EnclaveState::kError: |
| FinishRequest(PasskeyUpgradeResult::kEnclaveNotInitialized); |
| break; |
| case EnclaveState::kReady: |
| ContinuePendingUpgradeRequest(); |
| break; |
| } |
| } |
| |
| void PasskeyUpgradeRequestController::ContinuePendingUpgradeRequest() { |
| CHECK(pending_request_); |
| |
| // When looking for passwords that might be eligible to be upgraded, only |
| // consider passwords stored in GPM. |
| syncer::SyncService* sync_service = |
| SyncServiceFactory::GetForProfile(profile()); |
| password_manager::PasswordStoreInterface* password_store = nullptr; |
| if (password_manager::features_util::IsAccountStorageEnabled(sync_service)) { |
| password_store = AccountPasswordStoreFactory::GetForProfile( |
| profile(), ServiceAccessType::EXPLICIT_ACCESS) |
| .get(); |
| } else if (password_manager::sync_util:: |
| IsSyncFeatureEnabledIncludingPasswords(sync_service)) { |
| // TODO(crbug.com/40066949): Remove this codepath once |
| // `IsSyncFeatureEnabled()` is fully deprecated. |
| password_store = ProfilePasswordStoreFactory::GetForProfile( |
| profile(), ServiceAccessType::EXPLICIT_ACCESS) |
| .get(); |
| } |
| |
| if (!password_store) { |
| FinishRequest(PasskeyUpgradeResult::kPasswordStoreError); |
| return; |
| } |
| |
| GURL url = render_frame_host().GetLastCommittedOrigin().GetURL(); |
| password_manager::PasswordFormDigest form_digest( |
| password_manager::PasswordForm::Scheme::kHtml, |
| password_manager::GetSignonRealm(url), url); |
| password_store->GetLogins(form_digest, weak_factory_.GetWeakPtr()); |
| } |
| |
| void PasskeyUpgradeRequestController::OnGetPasswordStoreResultsOrErrorFrom( |
| password_manager::PasswordStoreInterface* store, |
| password_manager::LoginsResultOrError results_or_error) { |
| if (std::holds_alternative<password_manager::PasswordStoreBackendError>( |
| results_or_error)) { |
| FinishRequest(PasskeyUpgradeResult::kPasswordStoreError); |
| return; |
| } |
| password_manager::LoginsResult result = |
| password_manager::GetLoginsOrEmptyListOnFailure(results_or_error); |
| bool upgrade_eligible = false; |
| bool match_not_recent = false; |
| // A password with a matching username must have been used within the last 5 |
| // minutes in order for the automatic passkey upgrade to succeed. |
| base::TimeDelta kLastUsedThreshold = base::Minutes(5); |
| const auto min_last_used = base::Time::Now() - kLastUsedThreshold; |
| for (const password_manager::PasswordForm& password_form : result) { |
| if (password_form.username_value != username_) { |
| continue; |
| } |
| // Consider multiple last use attributes for robustness. N.B. |
| // `date_last_used` is updated after successful form submission on |
| // Desktop, while `date_last_filled` is updated during form filling. |
| if (std::max({password_form.date_created, password_form.date_last_filled, |
| password_form.date_last_used}) < min_last_used) { |
| match_not_recent = true; |
| continue; |
| } |
| upgrade_eligible = true; |
| break; |
| } |
| |
| if (!upgrade_eligible) { |
| FinishRequest(match_not_recent |
| ? PasskeyUpgradeResult::kNoRecentlyUsedPassword |
| : PasskeyUpgradeResult::kNoMatchingPassword); |
| return; |
| } |
| |
| CHECK(enclave_request_callback_); |
| enclave_transaction_ = std::make_unique<GPMEnclaveTransaction>( |
| /*delegate=*/this, PasskeyModelFactory::GetForProfile(profile()), |
| device::FidoRequestType::kMakeCredential, rp_id_, |
| EnclaveManagerFactory::GetAsEnclaveManagerForProfile(profile()), |
| /*pin=*/std::nullopt, /*selected_credential_id=*/std::nullopt, |
| enclave_request_callback_); |
| enclave_transaction_->Start(); |
| } |
| |
| void PasskeyUpgradeRequestController::HandleEnclaveTransactionError() { |
| FinishRequest(PasskeyUpgradeResult::kEnclaveError); |
| } |
| |
| void PasskeyUpgradeRequestController::BuildUVKeyOptions( |
| EnclaveManager::UVKeyOptions&) { |
| // Upgrade requests don't perform user verification. |
| NOTIMPLEMENTED(); |
| } |
| |
| void PasskeyUpgradeRequestController::HandlePINValidationResult( |
| device::enclave::PINValidationResult) { |
| // Upgrade requests don't perform user verification. |
| NOTIMPLEMENTED(); |
| } |
| |
| void PasskeyUpgradeRequestController::OnPasskeyCreated( |
| const sync_pb::WebauthnCredentialSpecifics& passkey) { |
| FinishRequest(PasskeyUpgradeResult::kSuccess); |
| |
| // Show the confirmation bubble. |
| PasswordsClientUIDelegate* manage_passwords_ui_controller = |
| PasswordsClientUIDelegateFromWebContents( |
| content::WebContents::FromRenderFrameHost(&render_frame_host())); |
| if (manage_passwords_ui_controller) { |
| manage_passwords_ui_controller->OnPasskeyUpgrade(rp_id_); |
| } |
| } |
| |
| EnclaveUserVerificationMethod PasskeyUpgradeRequestController::GetUvMethod() { |
| return EnclaveUserVerificationMethod::kNoUserVerificationAndNoUserPresence; |
| } |
| |
| content::RenderFrameHost& PasskeyUpgradeRequestController::render_frame_host() |
| const { |
| auto* rfh = content::RenderFrameHost::FromID(frame_host_id_); |
| CHECK(rfh); |
| return *rfh; |
| } |
| |
| Profile* PasskeyUpgradeRequestController::profile() const { |
| return Profile::FromBrowserContext(render_frame_host().GetBrowserContext()); |
| } |
| |
| void PasskeyUpgradeRequestController::OnEnclaveLoaded() { |
| CHECK(enclave_manager_->is_loaded()); |
| enclave_state_ = enclave_manager_->is_ready() ? EnclaveState::kReady |
| : EnclaveState::kError; |
| if (!pending_request_) { |
| return; |
| } |
| if (enclave_state_ == EnclaveState::kReady) { |
| ContinuePendingUpgradeRequest(); |
| } else { |
| FinishRequest(PasskeyUpgradeResult::kEnclaveNotInitialized); |
| } |
| } |
| |
| void PasskeyUpgradeRequestController::FinishRequest( |
| PasskeyUpgradeResult result) { |
| FIDO_LOG(ERROR) << "Passkey upgrade request complete: " |
| << static_cast<int>(result); |
| |
| RecordPasskeyUpgradeResultHistogram(result); |
| |
| if (result == PasskeyUpgradeResult::kSuccess) { |
| delegate_->PasskeyUpgradeSucceeded(); |
| } else { |
| delegate_->PasskeyUpgradeFailed(); |
| } |
| } |