blob: a721699a09b47fe1b07867d3c170cf804af5942e [file] [log] [blame]
// 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();
}
}