blob: 33385232f03e1987c3bbdb1e359e009c97c1408c [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/webauthn/gpm_enclave_controller.h"
#include <algorithm>
#include <cstdint>
#include <iterator>
#include <memory>
#include <optional>
#include <string>
#include <tuple>
#include <utility>
#include <vector>
#include "ash/constants/ash_features.h"
#include "ash/public/cpp/auth/active_session_auth_controller.h"
#include "base/check.h"
#include "base/check_op.h"
#include "base/containers/span.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/i18n/time_formatting.h"
#include "base/location.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "base/no_destructor.h"
#include "base/notimplemented.h"
#include "base/notreached.h"
#include "base/ranges/algorithm.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/time/clock.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "build/buildflag.h"
#include "chrome/browser/net/system_network_context_manager.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/signin/identity_manager_factory.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_model.h"
#include "chrome/browser/webauthn/change_pin_controller_impl.h"
#include "chrome/browser/webauthn/enclave_manager.h"
#include "chrome/browser/webauthn/enclave_manager_factory.h"
#include "chrome/browser/webauthn/gpm_user_verification_policy.h"
#include "chrome/browser/webauthn/passkey_model_factory.h"
#include "chrome/browser/webauthn/proto/enclave_local_state.pb.h"
#include "chrome/browser/webauthn/webauthn_pref_names.h"
#include "components/device_event_log/device_event_log.h"
#include "components/prefs/pref_service.h"
#include "components/signin/public/base/consent_level.h"
#include "components/signin/public/identity_manager/account_info.h"
#include "components/signin/public/identity_manager/identity_manager.h"
#include "components/signin/public/identity_manager/primary_account_access_token_fetcher.h"
#include "components/trusted_vault/frontend_trusted_vault_connection.h"
#include "components/trusted_vault/securebox.h"
#include "components/trusted_vault/trusted_vault_connection.h"
#include "components/trusted_vault/trusted_vault_crypto.h"
#include "components/trusted_vault/trusted_vault_server_constants.h"
#include "components/webauthn/core/browser/passkey_model.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "device/fido/enclave/constants.h"
#include "device/fido/enclave/metrics.h"
#include "device/fido/enclave/types.h"
#include "device/fido/features.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_types.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "ash/public/cpp/webauthn_dialog_controller.h"
#endif
#if BUILDFLAG(IS_MAC)
#include "chrome/common/chrome_version.h"
#include "device/fido/enclave/icloud_recovery_key_mac.h"
#endif // BUILDFLAG(IS_MAC)
using Step = AuthenticatorRequestDialogModel::Step;
using ChangePinEvent = ChangePinControllerImpl::ChangePinEvent;
// These diagrams aren't exhaustive, but hopefully can help identify the control
// flow in this code, which is very callback-heavy. The "digraph" sections are
// the dot commands and the diagrams are generated from them with
// https://dot-to-ascii.ggerganov.com/
//
//
// create(), already enrolled
//
// digraph {
// OnGPMSelected -> kGPMCreatePasskey -> OnGPMCreatePasskey
// OnGPMCreatePasskey -> StartTransaction
// OnGPMCreatePasskey -> kGPMEnterPin -> OnGPMPinEntered ->
// StartTransaction
// OnGPMCreatePasskey -> kGPMTouchID -> OnTouchIDComplete ->
// StartTransaction
// }
//
// +--------------------+
// | OnGPMSelected |
// +--------------------+
// |
// |
// v
// +--------------------+
// | kGPMCreatePasskey |
// +--------------------+
// |
// |
// v
// +-------------------+ +--------------------+
// | kGPMTouchID | <-- | OnGPMCreatePasskey | -+
// +-------------------+ +--------------------+ |
// | | |
// | | |
// v v |
// +-------------------+ +--------------------+ |
// | OnTouchIDComplete | | kGPMEnterPin | |
// +-------------------+ +--------------------+ |
// | | |
// | | |
// | v |
// | +--------------------+ |
// | | OnGPMPinEntered | |
// | +--------------------+ |
// | | |
// | | |
// | v |
// | +--------------------+ |
// +---------------------> | StartTransaction | <+
// +--------------------+
// create(), empty security domain
//
// digraph {
// OnGPMSelected -> kGPMCreatePasskey -> kGPMCreatePin -> OnGPMPinEntered ->
// OnDeviceAdded
// OnDeviceAdded -> StartTransaction
// OnDeviceAdded -> kGPMTouchID -> OnTouchIDComplete -> StartTransaction
// }
//
// +-------------------------+
// | OnGPMSelected |
// +-------------------------+
// |
// |
// v
// +-------------------------+
// | kGPMCreatePasskey |
// +-------------------------+
// |
// |
// v
// +-------------------------+
// | kGPMCreatePin |
// +-------------------------+
// |
// |
// v
// +-------------------------+
// | OnGPMPinEntered |
// +-------------------------+
// |
// |
// v
// +-------------------------+
// | OnDeviceAdded | -+
// +-------------------------+ |
// | |
// | |
// v |
// +-------------------------+ |
// | kGPMTouchID | |
// +-------------------------+ |
// | |
// | |
// v |
// +-------------------------+ |
// | OnTouchIDComplete | |
// +-------------------------+ |
// | |
// | |
// v |
// +-------------------------+ |
// | StartTransaction | <+
// +-------------------------+
// get(), already enrolled
//
// digraph {
// OnGPMPasskeySelected -> StartTransaction
// OnGPMPasskeySelected -> kGPMEnterPin -> OnGPMPinEntered ->
// StartTransaction
// OnGPMPasskeySelected -> kGPMTouchID -> OnTouchIDComplete ->
// StartTransaction
// }
//
// +-------------------+ +----------------------+
// | kGPMTouchID | <-- | OnGPMPasskeySelected | -+
// +-------------------+ +----------------------+ |
// | | |
// | | |
// v v |
// +-------------------+ +----------------------+ |
// | OnTouchIDComplete | | kGPMEnterPin | |
// +-------------------+ +----------------------+ |
// | | |
// | | |
// | v |
// | +----------------------+ |
// | | OnGPMPinEntered | |
// | +----------------------+ |
// | | |
// | | |
// | v |
// | +----------------------+ |
// +---------------------> | StartTransaction | <+
// +----------------------+
// ICloudMember holds a copyable subset of trusted_vault::VaultMember that we
// need for recovery.
struct GPMEnclaveController::ICloudMember {
explicit ICloudMember(const trusted_vault::VaultMember& member)
: public_key(member.public_key->ExportToBytes()) {
for (const auto& member_key : member.member_keys) {
if (member_key.version > version) {
version = member_key.version;
wrapped_key = member_key.wrapped_key;
}
}
}
// The result of exporting the SecureBoxPublicKey.
std::vector<uint8_t> public_key;
// The newest wrapped key for the member.
std::vector<uint8_t> wrapped_key;
// The key epoch.
int version = 0;
};
// DownloadedAccountState holds the subset of information from
// `trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult` that is
// required for `GPMEnclaveController` to work. It exists because it's copyable,
// which the `trusted_vault` structure is not, and thus can be put in a cache.
struct GPMEnclaveController::DownloadedAccountState {
explicit DownloadedAccountState(
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult
result,
std::string gaia_id)
: state(result.state),
gpm_pin_metadata(std::move(result.gpm_pin_metadata)),
lskf_expiries(std::move(result.lskf_expiries)),
gaia_id(std::move(gaia_id)) {
std::ranges::transform(result.icloud_keys, std::back_inserter(icloud_keys),
[](const trusted_vault::VaultMember& member) {
return ICloudMember(member);
});
}
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult::State
state;
std::optional<trusted_vault::GpmPinMetadata> gpm_pin_metadata;
std::vector<ICloudMember> icloud_keys;
std::vector<base::Time> lskf_expiries;
std::string gaia_id;
};
// EnclaveUserVerificationMethod enumerates the possible ways that user
// verification will be performed for an enclave transaction.
enum class GPMEnclaveController::EnclaveUserVerificationMethod {
// No user verification will be performed.
kNone,
// The user will enter a GPM PIN.
kPIN,
// User verification is satisfied because the user performed account recovery.
kImplicit,
// The operating system will perform user verification and allow signing
// with the UV key.
kUVKeyWithSystemUI,
// The device is in a state waiting for an OS UV key to be created, which can
// be done when a UV request is required.
kDeferredUVKeyWithSystemUI,
// Chrome will show user verification UI for the operating system, which will
// then allow signing
// with the UV key.
kUVKeyWithChromeUI,
// The request cannot be satisfied.
kUnsatisfiable,
};
using EnclaveUserVerificationMethod =
GPMEnclaveController::EnclaveUserVerificationMethod;
namespace {
#if BUILDFLAG(IS_MAC)
constexpr char kICloudKeychainRecoveryKeyAccessGroup[] =
MAC_TEAM_IDENTIFIER_STRING ".com.google.common.folsom";
#endif // BUILDFLAG(IS_MAC)
// Pick an enclave user verification method for a specific request.
EnclaveUserVerificationMethod PickEnclaveUserVerificationMethod(
device::UserVerificationRequirement uv,
bool have_added_device,
bool has_pin,
EnclaveManager::UvKeyState uv_key_state,
bool platform_has_biometrics) {
if (have_added_device) {
return EnclaveUserVerificationMethod::kImplicit;
}
// If the platform has biometrics now, but didn't when we enrolled, we need to
// act as if they are missing because we've no UV key to use them with.
if (uv_key_state == EnclaveManager::UvKeyState::kNone) {
platform_has_biometrics = false;
}
if (!GpmWillDoUserVerification(uv, platform_has_biometrics)) {
return EnclaveUserVerificationMethod::kNone;
}
switch (uv_key_state) {
case EnclaveManager::UvKeyState::kNone:
if (has_pin) {
return EnclaveUserVerificationMethod::kPIN;
} else {
return EnclaveUserVerificationMethod::kUnsatisfiable;
}
case EnclaveManager::UvKeyState::kUsesSystemUI:
return EnclaveUserVerificationMethod::kUVKeyWithSystemUI;
case EnclaveManager::UvKeyState::kUsesSystemUIDeferredCreation:
return EnclaveUserVerificationMethod::kDeferredUVKeyWithSystemUI;
case EnclaveManager::UvKeyState::kUsesChromeUI:
return EnclaveUserVerificationMethod::kUVKeyWithChromeUI;
}
}
const char* ToString(
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult::State
state) {
using Result =
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult;
switch (state) {
case Result::State::kError:
return "Error";
case Result::State::kEmpty:
return "Empty";
case Result::State::kRecoverable:
return "Recoverable";
case Result::State::kIrrecoverable:
return "Irrecoverable";
}
}
// AccountStateCache caches the account state between requests to reduce the
// load on the security domain service.
class AccountStateCache {
public:
std::optional<GPMEnclaveController::DownloadedAccountState> Get(
base::Clock* clock) {
if (!cache_time_) {
return std::nullopt;
}
const base::Time now = clock->Now();
if (now < *cache_time_ || (now - *cache_time_) > base::Minutes(30)) {
cache_time_.reset();
value_.reset();
return std::nullopt;
}
return value_;
}
void Put(base::Clock* clock,
const GPMEnclaveController::DownloadedAccountState& state) {
if (base::FeatureList::IsEnabled(device::kWebAuthnCacheSecurityDomain)) {
cache_time_ = clock->Now();
value_ = state;
}
}
private:
std::optional<base::Time> cache_time_;
std::optional<GPMEnclaveController::DownloadedAccountState> value_;
};
AccountStateCache* GetAccountStateCache() {
static base::NoDestructor<AccountStateCache> cache;
return cache.get();
}
bool ExpiryTooSoon(base::Time expiry) {
const base::Time now = base::Time::Now();
// LSKFs must have at least 18 weeks of validity on them because we don't want
// users depending on an LSKF from a device that they've stopped using.
// Validities are generally six months, thus this implies that the device was
// used in the previous six weeks.
return expiry < now || (expiry - now) < base::Days(7 * 18);
}
void ResetDeclinedBootstrappingCount(
content::RenderFrameHost* render_frame_host) {
Profile::FromBrowserContext(render_frame_host->GetBrowserContext())
->GetPrefs()
->SetInteger(webauthn::pref_names::kEnclaveDeclinedGPMBootstrappingCount,
0);
}
void MaybeRecordUserActionForWinUv(bool is_create,
EnclaveUserVerificationMethod uv_method) {
#if BUILDFLAG(IS_WIN)
if (uv_method == EnclaveUserVerificationMethod::kUVKeyWithSystemUI ||
uv_method == EnclaveUserVerificationMethod::kDeferredUVKeyWithSystemUI) {
webauthn::user_actions::RecordGpmWinUvShown(is_create);
}
#endif // BUILDFLAG(IS_WIN)
}
} // namespace
GPMEnclaveController::GPMEnclaveController(
content::RenderFrameHost* render_frame_host,
AuthenticatorRequestDialogModel* model,
const std::string& rp_id,
device::FidoRequestType request_type,
device::UserVerificationRequirement user_verification_requirement,
base::Clock* clock,
std::unique_ptr<trusted_vault::TrustedVaultConnection> optional_connection)
: render_frame_host_id_(render_frame_host->GetGlobalId()),
rp_id_(rp_id),
request_type_(request_type),
user_verification_requirement_(user_verification_requirement),
enclave_manager_(EnclaveManagerFactory::GetAsEnclaveManagerForProfile(
Profile::FromBrowserContext(render_frame_host->GetBrowserContext()))),
model_(model),
vault_connection_override_(std::move(optional_connection)),
clock_(clock) {
enclave_manager_observer_.Observe(enclave_manager_);
model_observer_.Observe(model_);
Profile* const profile =
Profile::FromBrowserContext(render_frame_host->GetBrowserContext());
webauthn::PasskeyModel* passkey_model =
PasskeyModelFactory::GetInstance()->GetForProfile(profile);
creds_ = passkey_model->GetPasskeysForRelyingPartyId(rp_id_);
// The following code may do some asynchronous processing. However the control
// flow terminates, it must have:
// a) set `account_state_` to some value (unless it's happy with the default
// of `kNone`.)
// b) called `SetActive`.
if (creds_.empty() &&
request_type == device::FidoRequestType::kGetAssertion) {
// No possibility of using GPM for this request.
FIDO_LOG(EVENT) << "Enclave is not a candidate for this request";
SetActive(false);
} else if (enclave_manager_->is_loaded()) {
OnEnclaveLoaded();
} else {
FIDO_LOG(EVENT) << "Loading enclave state";
account_state_ = AccountState::kLoading;
enclave_manager_->Load(
base::BindOnce(&GPMEnclaveController::OnEnclaveLoaded,
weak_ptr_factory_.GetWeakPtr()));
}
}
GPMEnclaveController::~GPMEnclaveController() {
// Ensure that any secret is dropped from memory after a transaction.
enclave_manager_->TakeSecret();
}
bool GPMEnclaveController::is_active() const {
return *is_active_;
}
bool GPMEnclaveController::ready_for_ui() const {
return ready_for_ui_;
}
void GPMEnclaveController::ConfigureDiscoveries(
device::FidoDiscoveryFactory* discovery_factory) {
using EnclaveEventStream = device::FidoDiscoveryBase::EventStream<
std::unique_ptr<device::enclave::CredentialRequest>>;
std::unique_ptr<EnclaveEventStream> event_stream;
std::tie(enclave_request_callback_, event_stream) = EnclaveEventStream::New();
discovery_factory->set_enclave_ui_request_stream(std::move(event_stream));
}
const std::vector<sync_pb::WebauthnCredentialSpecifics>&
GPMEnclaveController::creds() const {
return creds_;
}
Profile* GPMEnclaveController::GetProfile() const {
return Profile::FromBrowserContext(
content::RenderFrameHost::FromID(render_frame_host_id_)
->GetBrowserContext())
->GetOriginalProfile();
}
GPMEnclaveController::AccountState
GPMEnclaveController::account_state_for_testing() const {
return account_state_;
}
void GPMEnclaveController::OnEnclaveLoaded() {
// Verify the state of the primary account sign-in info.
auto* const identity_manager =
IdentityManagerFactory::GetForProfile(GetProfile());
CoreAccountInfo account =
identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin);
GoogleServiceAuthError signin_error =
identity_manager->GetErrorStateOfRefreshTokenForAccount(
account.account_id);
if (signin_error.IsPersistentError()) {
FIDO_LOG(EVENT) << "Recoverable sign-in error: " << signin_error.ToString();
account_state_ = AccountState::kNone;
model_->EnclaveNeedsReauth();
SetActive(false);
return;
}
// For create() requests, we want to probe the security domain service to
// ensure that we never create a credential encrypted to an old security
// domain secret. For get() requests, we can generally skip the probe if
// already enrolled. However, if this request will require using the GPM PIN
// for UV then we want to probe to ensure that we catch any changes to the GPM
// PIN. While we cannot fully determine the UV method at this stage, because
// we don't know whether the platform has biometrics, we can know whether
// we'll use a GPM PIN for UV or not.
if (request_type_ == device::FidoRequestType::kGetAssertion) {
if (enclave_manager_->is_ready()) {
switch (PickEnclaveUserVerificationMethod(
user_verification_requirement_, /*have_added_device=*/false,
enclave_manager_->has_wrapped_pin(),
enclave_manager_->uv_key_state(/*platform_has_biometrics=*/false),
/*platform_has_biometrics=*/false)) {
case EnclaveUserVerificationMethod::kPIN:
FIDO_LOG(EVENT)
<< "Checking security domain service because a GPM PIN will be "
"used for user verification in this request.";
break;
default:
FIDO_LOG(EVENT) << "Enclave is ready and this request will not use a "
"GPM PIN for user verification";
SetAccountStateReady();
SetActive(true);
return;
}
}
if (device::kWebAuthnGpmPin.Get()) {
// For get() requests, progress the UI now because, with GPM PIN support,
// we can handle the account in any state and we'll block the UI if needed
// when the user selects a GPM credential.
SetActive(true);
}
}
FIDO_LOG(EVENT) << "Checking for UV key capability";
EnclaveManager::AreUserVerifyingKeysSupported(
base::BindOnce(&GPMEnclaveController::OnUVCapabilityKnown,
weak_ptr_factory_.GetWeakPtr()));
}
void GPMEnclaveController::OnUVCapabilityKnown(bool can_make_uv_keys) {
FIDO_LOG(EVENT) << "UV key capability: " << can_make_uv_keys;
can_make_uv_keys_ = can_make_uv_keys;
if (!can_make_uv_keys && !device::kWebAuthnGpmPin.Get()) {
// Without the ability to do user verification, we cannot enroll the current
// device.
account_state_ = AccountState::kNone;
SetActive(false);
return;
}
DownloadAccountState();
}
void GPMEnclaveController::DownloadAccountState() {
std::optional<DownloadedAccountState> maybe_cached;
// If the enclave_manager isn't ready then a cached account state can be used
// to reduce load on the security domain service. If it is ready then this
// must be a create() request, and we want to check that the security domain
// epoch hasn't changed and so don't use a cached state.
if (!enclave_manager_->is_ready()) {
// TODO(enclave): discard cache if gaia id no longer matches.
maybe_cached = GetAccountStateCache()->Get(clock_);
}
if (maybe_cached) {
FIDO_LOG(EVENT) << "Using cached account state";
OnHaveAccountState(std::move(*maybe_cached));
return;
}
FIDO_LOG(EVENT) << "Fetching account state";
account_state_ = AccountState::kChecking;
account_state_timeout_ = std::make_unique<base::OneShotTimer>();
account_state_timeout_->Start(
FROM_HERE, kDownloadAccountStateTimeout,
base::BindOnce(&GPMEnclaveController::OnAccountStateTimeOut,
weak_ptr_factory_.GetWeakPtr()));
auto* const identity_manager =
IdentityManagerFactory::GetForProfile(GetProfile());
scoped_refptr<network::SharedURLLoaderFactory> testing_url_loader =
EnclaveManagerFactory::url_loader_override();
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory =
testing_url_loader ? testing_url_loader
: SystemNetworkContextManager::GetInstance()
->GetSharedURLLoaderFactory();
std::unique_ptr<trusted_vault::TrustedVaultConnection> trusted_vault_conn =
vault_connection_override_
? std::move(vault_connection_override_)
: trusted_vault::NewFrontendTrustedVaultConnection(
trusted_vault::SecurityDomainId::kPasskeys, identity_manager,
url_loader_factory);
auto* conn = trusted_vault_conn.get();
CoreAccountInfo account =
identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin);
download_account_state_request_ =
conn->DownloadAuthenticationFactorsRegistrationState(
account,
base::BindOnce(&GPMEnclaveController::OnAccountStateDownloaded,
weak_ptr_factory_.GetWeakPtr(), account.gaia,
std::move(trusted_vault_conn)));
}
void GPMEnclaveController::OnAccountStateTimeOut() {
FIDO_LOG(ERROR) << "Fetching the account state timed out.";
download_account_state_request_.reset();
if (enclave_manager_->is_ready()) {
// If we were checking the security domain just to check whether the epoch
// has changed then we assume that it hasn't.
SetAccountStateReady();
SetActive(true);
} else {
model_->OnLoadingEnclaveTimeout();
account_state_ = AccountState::kNone;
SetActive(false);
}
}
void GPMEnclaveController::OnAccountStateDownloaded(
std::string gaia_id,
std::unique_ptr<trusted_vault::TrustedVaultConnection> unused,
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult
result) {
if (account_state_ != AccountState::kChecking) {
// This request timed out.
return;
}
download_account_state_request_.reset();
account_state_timeout_.reset();
FIDO_LOG(EVENT) << "Download account state result: " << ToString(result.state)
<< ", key_version: " << result.key_version.value_or(0)
<< ", has PIN: " << result.gpm_pin_metadata.has_value()
<< ", expiry: "
<< (result.gpm_pin_metadata.has_value()
? base::TimeFormatAsIso8601(
result.gpm_pin_metadata->expiry)
: "<none>")
<< ", iCloud Keychain keys: " << result.icloud_keys.size();
if (enclave_manager_->is_ready() &&
enclave_manager_->ConsiderSecurityDomainState(result,
base::DoNothing())) {
SetAccountStateReady();
SetActive(true);
return;
}
DownloadedAccountState downloaded(std::move(result), std::move(gaia_id));
GetAccountStateCache()->Put(clock_, downloaded);
OnHaveAccountState(DownloadedAccountState(std::move(downloaded)));
}
void GPMEnclaveController::OnHaveAccountState(DownloadedAccountState result) {
using Result =
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult;
FIDO_LOG(EVENT) << "Account state: " << ToString(result.state)
<< ", has PIN: " << result.gpm_pin_metadata.has_value()
<< ", iCloud Keychain keys: " << result.icloud_keys.size();
if (!device::kWebAuthnGpmPin.Get() &&
result.state == Result::State::kRecoverable &&
!result.lskf_expiries.empty() &&
base::ranges::all_of(result.lskf_expiries, ExpiryTooSoon)) {
std::vector<std::string> expiries;
base::ranges::transform(
result.lskf_expiries, std::back_inserter(expiries),
[](const auto& time) { return base::TimeFormatAsIso8601(time); });
FIDO_LOG(EVENT) << "Account considered irrecoverable because no LSKF has "
"acceptable expiry: "
<< base::JoinString(expiries, ", ");
result.state = Result::State::kIrrecoverable;
}
switch (result.state) {
case Result::State::kError:
account_state_ = AccountState::kNone;
break;
case Result::State::kEmpty:
account_state_ = AccountState::kEmpty;
break;
case Result::State::kRecoverable:
account_state_ = AccountState::kRecoverable;
break;
case Result::State::kIrrecoverable:
account_state_ = AccountState::kIrrecoverable;
break;
}
if (result.gpm_pin_metadata) {
pin_metadata_ = std::move(result.gpm_pin_metadata);
}
security_domain_icloud_recovery_keys_ = std::move(result.icloud_keys);
user_gaia_id_ = std::move(result.gaia_id);
if (device::kWebAuthnGpmPin.Get()) {
SetActive(account_state_ != AccountState::kNone);
} else {
SetActive(account_state_ == AccountState::kRecoverable);
}
}
void GPMEnclaveController::SetActive(bool active) {
is_active_ = active;
if (waiting_for_account_state_) {
std::move(waiting_for_account_state_).Run();
}
if (ready_for_ui_) {
return;
}
ready_for_ui_ = true;
if (active) {
model_->EnclaveEnabled();
}
model_->OnReadyForUI();
}
void GPMEnclaveController::OnKeysStored() {
if (recovered_with_icloud_keychain_) {
// iCloud keychain recovery.
device::enclave::RecordEvent(
device::enclave::Event::kICloudRecoverySuccessful);
recovered_with_icloud_keychain_ = false;
} else if (model_->step() == Step::kRecoverSecurityDomain) {
// MagicArch recovery.
webauthn::user_actions::RecordRecoverySucceeded();
device::enclave::RecordEvent(device::enclave::Event::kRecoverySuccessful);
} else {
// Keys were stored but we were not expecting it, e.g. because it happened
// during a request at a different step on another tab. Ignore it.
return;
}
CHECK(enclave_manager_->has_pending_keys());
CHECK(!enclave_manager_->is_ready());
if (pin_metadata_.has_value() || *can_make_uv_keys_) {
if (!enclave_manager_->AddDeviceToAccount(
std::move(pin_metadata_),
base::BindOnce(&GPMEnclaveController::OnDeviceAdded,
weak_ptr_factory_.GetWeakPtr()))) {
model_->SetStep(Step::kGPMError);
}
} else {
// Create a GPM PIN if the user doesn't have one and can't make
// a UV key locally.
model_->SetStep(Step::kGPMCreatePin);
}
}
void GPMEnclaveController::OnDeviceAdded(bool success) {
ResetDeclinedBootstrappingCount(
content::RenderFrameHost::FromID(render_frame_host_id_));
if (!success) {
model_->SetStep(Step::kGPMError);
return;
}
#if BUILDFLAG(IS_MAC)
if (base::FeatureList::IsEnabled(device::kWebAuthnICloudRecoveryKey)) {
MaybeAddICloudRecoveryKey();
return;
}
#endif
OnEnclaveAccountSetUpComplete();
}
void GPMEnclaveController::RecoverSecurityDomain() {
#if BUILDFLAG(IS_MAC)
if (base::FeatureList::IsEnabled(
device::kWebAuthnRecoverFromICloudRecoveryKey)) {
device::enclave::ICloudRecoveryKey::Retrieve(
base::BindOnce(&GPMEnclaveController::OnICloudKeysRetrievedForRecovery,
weak_ptr_factory_.GetWeakPtr()),
kICloudKeychainRecoveryKeyAccessGroup);
} else {
model_->SetStep(Step::kRecoverSecurityDomain);
}
#else
model_->SetStep(Step::kRecoverSecurityDomain);
#endif // BUILDFLAG(IS_MAC)
}
#if BUILDFLAG(IS_MAC)
void GPMEnclaveController::MaybeAddICloudRecoveryKey() {
device::enclave::ICloudRecoveryKey::Retrieve(
base::BindOnce(&GPMEnclaveController::OnICloudKeysRetrievedForEnrollment,
weak_ptr_factory_.GetWeakPtr()),
kICloudKeychainRecoveryKeyAccessGroup);
}
void GPMEnclaveController::OnICloudKeysRetrievedForEnrollment(
std::vector<std::unique_ptr<device::enclave::ICloudRecoveryKey>>
local_icloud_keys) {
for (const GPMEnclaveController::ICloudMember& recovery_icloud_key :
security_domain_icloud_recovery_keys_) {
const auto local_icloud_key_it = std::ranges::find_if(
local_icloud_keys, [&recovery_icloud_key](const auto& key) {
return key->id() == recovery_icloud_key.public_key;
});
if (local_icloud_key_it != local_icloud_keys.end()) {
// This device already has an iCloud keychain recovery factor configured
// for the passkey security domain. Nothing else to do here.
FIDO_LOG(EVENT) << "Device already has iCloud recovery key configured";
OnEnclaveAccountSetUpComplete();
return;
}
}
// The device has no iCloud recovery key for the passkeys security domain.
// Create a new key.
// TODO(nsatragno): it's possible we might want to share keys with other
// security domains. We would need to loop through all vault members across
// all security domains.
FIDO_LOG(EVENT) << "Creating new iCloud recovery key";
device::enclave::ICloudRecoveryKey::Create(
base::BindOnce(&GPMEnclaveController::EnrollICloudRecoveryKey,
weak_ptr_factory_.GetWeakPtr()),
kICloudKeychainRecoveryKeyAccessGroup);
}
void GPMEnclaveController::EnrollICloudRecoveryKey(
std::unique_ptr<device::enclave::ICloudRecoveryKey> key) {
if (!key) {
FIDO_LOG(ERROR) << "Could not create iCloud recovery key";
OnEnclaveAccountSetUpComplete();
return;
}
enclave_manager_->AddICloudRecoveryKey(
std::move(key), base::IgnoreArgs<bool>(base::BindOnce(
&GPMEnclaveController::OnEnclaveAccountSetUpComplete,
weak_ptr_factory_.GetWeakPtr())));
}
void GPMEnclaveController::OnICloudKeysRetrievedForRecovery(
std::vector<std::unique_ptr<device::enclave::ICloudRecoveryKey>>
local_icloud_keys) {
// Find the matching pair of local iCloud private key and the SDS recovery
// member.
auto local_icloud_key_it = local_icloud_keys.end();
auto recovery_icloud_key_it = std::ranges::find_if(
security_domain_icloud_recovery_keys_,
[&local_icloud_key_it,
&local_icloud_keys](const auto& recovery_icloud_key) {
local_icloud_key_it = std::ranges::find_if(
local_icloud_keys, [&recovery_icloud_key](const auto& key) {
return key->id() == recovery_icloud_key.public_key;
});
return local_icloud_key_it != local_icloud_keys.end();
});
if (local_icloud_key_it == local_icloud_keys.end()) {
FIDO_LOG(DEBUG) << "Could not find matching iCloud recovery key";
model_->SetStep(Step::kRecoverSecurityDomain);
return;
}
std::optional<std::vector<uint8_t>> security_domain_secret =
trusted_vault::DecryptTrustedVaultWrappedKey(
(*local_icloud_key_it)->key()->private_key(),
recovery_icloud_key_it->wrapped_key);
if (!security_domain_secret) {
FIDO_LOG(ERROR)
<< "Could not decrypt security domain secret with iCloud key";
model_->SetStep(Step::kRecoverSecurityDomain);
return;
}
FIDO_LOG(EVENT) << "Successful recovery from iCloud recovery key";
recovered_with_icloud_keychain_ = true;
enclave_manager_->StoreKeys(user_gaia_id_,
{std::move(*security_domain_secret)},
recovery_icloud_key_it->version);
}
#endif // BUILDFLAG(IS_MAC)
void GPMEnclaveController::OnEnclaveAccountSetUpComplete() {
have_added_device_ = true;
SetAccountStateReady();
SetFailedPINAttemptCount(0);
uv_method_ = PickEnclaveUserVerificationMethod(
user_verification_requirement_, have_added_device_,
enclave_manager_->has_wrapped_pin(),
enclave_manager_->uv_key_state(*model_->platform_has_biometrics),
*model_->platform_has_biometrics);
// `have_added_device_` is set, which satisfies UV, so we must have picked
// "implicit" UV.
CHECK_EQ(*uv_method_, EnclaveUserVerificationMethod::kImplicit);
model_->DisableUiOrShowLoadingDialog();
StartTransaction();
}
void GPMEnclaveController::SetAccountStateReady() {
account_state_ = AccountState::kReady;
pin_is_arbitrary_ = enclave_manager_->has_wrapped_pin() &&
enclave_manager_->wrapped_pin_is_arbitrary();
}
void GPMEnclaveController::OnGPMSelected() {
if (model_->is_off_the_record && !off_the_record_confirmed_) {
model_->SetStep(Step::kGPMConfirmOffTheRecordCreate);
return;
}
switch (account_state_) {
case AccountState::kEmpty:
model_->SetStep(Step::kGPMCreatePasskey);
break;
case AccountState::kReady:
uv_method_ = PickEnclaveUserVerificationMethod(
user_verification_requirement_, have_added_device_,
enclave_manager_->has_wrapped_pin(),
enclave_manager_->uv_key_state(*model_->platform_has_biometrics),
*model_->platform_has_biometrics);
switch (*uv_method_) {
case EnclaveUserVerificationMethod::kUVKeyWithSystemUI:
case EnclaveUserVerificationMethod::kDeferredUVKeyWithSystemUI:
case EnclaveUserVerificationMethod::kNone:
case EnclaveUserVerificationMethod::kImplicit:
case EnclaveUserVerificationMethod::kPIN:
model_->SetStep(Step::kGPMCreatePasskey);
break;
case EnclaveUserVerificationMethod::kUVKeyWithChromeUI:
model_->SetStep(Step::kGPMTouchID);
break;
case EnclaveUserVerificationMethod::kUnsatisfiable:
model_->SetStep(Step::kGPMError);
break;
}
break;
case AccountState::kRecoverable:
case AccountState::kIrrecoverable:
device::enclave::RecordEvent(device::enclave::Event::kOnboarding);
model_->SetStep(Step::kTrustThisComputerCreation);
break;
case AccountState::kLoading:
case AccountState::kChecking:
waiting_for_account_state_ = base::BindOnce(
&GPMEnclaveController::OnGPMSelected, weak_ptr_factory_.GetWeakPtr());
// TODO(rgod): If the model step is `kNotStarted`, no UI is visible yet.
// Display a loading dialog after a delay, so it doesn't flicker in case
// the account state is fetched quickly.
model_->DisableUiOrShowLoadingDialog();
break;
case AccountState::kNone:
model_->SetStep(Step::kGPMError);
break;
}
}
void GPMEnclaveController::OnGPMPasskeySelected(
std::vector<uint8_t> credential_id) {
selected_cred_id_ = std::move(credential_id);
switch (account_state_) {
case AccountState::kReady:
uv_method_ = PickEnclaveUserVerificationMethod(
user_verification_requirement_, have_added_device_,
enclave_manager_->has_wrapped_pin(),
enclave_manager_->uv_key_state(*model_->platform_has_biometrics),
*model_->platform_has_biometrics);
switch (*uv_method_) {
case EnclaveUserVerificationMethod::kUVKeyWithSystemUI:
case EnclaveUserVerificationMethod::kDeferredUVKeyWithSystemUI:
case EnclaveUserVerificationMethod::kNone:
case EnclaveUserVerificationMethod::kImplicit:
if (model_->step() != Step::kConditionalMediation) {
// The autofill UI shows its own loading indicator.
model_->DisableUiOrShowLoadingDialog();
}
StartTransaction();
break;
case EnclaveUserVerificationMethod::kPIN:
PromptForPin();
break;
case EnclaveUserVerificationMethod::kUVKeyWithChromeUI:
model_->SetStep(Step::kGPMTouchID);
break;
case EnclaveUserVerificationMethod::kUnsatisfiable:
model_->SetStep(Step::kGPMError);
break;
}
break;
case AccountState::kRecoverable:
case AccountState::kIrrecoverable:
if (model_->priority_phone_name.has_value()) {
device::enclave::RecordEvent(device::enclave::Event::kOnboarding);
model_->SetStep(Step::kTrustThisComputerAssertion);
} else {
RecoverSecurityDomain();
}
break;
case AccountState::kLoading:
case AccountState::kChecking:
if (model_->step() != Step::kConditionalMediation &&
model_->step() != Step::kNotStarted) {
// The autofill UI shows its own loading indicator.
model_->DisableUiOrShowLoadingDialog();
}
waiting_for_account_state_ =
base::BindOnce(&GPMEnclaveController::OnGPMPasskeySelected,
weak_ptr_factory_.GetWeakPtr(), *selected_cred_id_);
break;
case AccountState::kNone:
if (model_->priority_phone_name.has_value()) {
model_->ContactPriorityPhone();
} else {
// This can happen if a passkey is selected after the enclave times out.
model_->SetStep(Step::kGPMError);
}
break;
case AccountState::kEmpty:
if (model_->priority_phone_name.has_value()) {
model_->ContactPriorityPhone();
} else {
// The security domain is empty but there were
// sync entities. Most like the security domain was reset without
// clearing the entities, thus they are unusable.
model_->SetStep(Step::kGPMError);
}
break;
}
}
void GPMEnclaveController::PromptForPin() {
if (GetFailedPINAttemptCount() >= device::enclave::kMaxFailedPINAttempts) {
model_->SetStep(Step::kGPMLockedPin);
} else {
model_->SetStep(pin_is_arbitrary_ ? Step::kGPMEnterArbitraryPin
: Step::kGPMEnterPin);
}
}
void GPMEnclaveController::OnGpmPinChanged(bool success) {
changing_gpm_pin_ = false;
if (!success) {
model_->SetStep(Step::kGPMError);
ChangePinControllerImpl::RecordHistogram(ChangePinEvent::kFailed);
return;
}
SetFailedPINAttemptCount(0);
model_->gpm_pin_remaining_attempts_ = std::nullopt;
// Changing GPM Pin required reauth, hence we can just proceed with the
// get/create passkey transaction.
StartTransaction();
ChangePinControllerImpl::RecordHistogram(
ChangePinEvent::kCompletedSuccessfully);
}
void GPMEnclaveController::OnTrustThisComputer() {
CHECK(model_->step() == Step::kTrustThisComputerAssertion ||
model_->step() == Step::kTrustThisComputerCreation);
device::enclave::RecordEvent(device::enclave::Event::kOnboardingAccepted);
// Clicking through the bootstrapping dialog resets the count even if it
// doesn't end up being successful.
ResetDeclinedBootstrappingCount(
content::RenderFrameHost::FromID(render_frame_host_id_));
RecoverSecurityDomain();
}
void GPMEnclaveController::OnGPMPinOptionChanged(bool is_arbitrary) {
if (changing_gpm_pin_) {
CHECK(model_->step() == Step::kGPMChangePin ||
model_->step() == Step::kGPMChangeArbitraryPin);
model_->SetStep(is_arbitrary ? Step::kGPMChangeArbitraryPin
: Step::kGPMChangePin);
} else {
CHECK(model_->step() == Step::kGPMCreatePin ||
model_->step() == Step::kGPMCreateArbitraryPin);
model_->SetStep(is_arbitrary ? Step::kGPMCreateArbitraryPin
: Step::kGPMCreatePin);
}
}
void GPMEnclaveController::OnGPMCreatePasskey() {
CHECK_EQ(model_->step(), Step::kGPMCreatePasskey);
CHECK(account_state_ == AccountState::kEmpty ||
account_state_ == AccountState::kReady);
if (account_state_ == AccountState::kEmpty) {
model_->SetStep(Step::kGPMCreatePin);
} else {
switch (*uv_method_) {
case EnclaveUserVerificationMethod::kUVKeyWithSystemUI:
case EnclaveUserVerificationMethod::kDeferredUVKeyWithSystemUI:
case EnclaveUserVerificationMethod::kNone:
case EnclaveUserVerificationMethod::kImplicit:
model_->DisableUiOrShowLoadingDialog();
StartTransaction();
break;
case EnclaveUserVerificationMethod::kPIN:
PromptForPin();
break;
case EnclaveUserVerificationMethod::kUVKeyWithChromeUI:
model_->SetStep(Step::kGPMTouchID);
break;
case EnclaveUserVerificationMethod::kUnsatisfiable:
NOTREACHED();
}
}
}
void GPMEnclaveController::OnGPMConfirmOffTheRecordCreate() {
CHECK_EQ(model_->step(), Step::kGPMConfirmOffTheRecordCreate);
off_the_record_confirmed_ = true;
OnGPMSelected();
}
void GPMEnclaveController::OnGPMPinEntered(const std::u16string& pin) {
CHECK(model_->step() == Step::kGPMChangeArbitraryPin ||
model_->step() == Step::kGPMChangePin ||
model_->step() == Step::kGPMCreateArbitraryPin ||
model_->step() == Step::kGPMCreatePin ||
model_->step() == Step::kGPMEnterArbitraryPin ||
model_->step() == Step::kGPMEnterPin);
pin_ = base::UTF16ToUTF8(pin);
// Disable the pin entry view while waiting for the response from enclave.
model_->DisableUiOrShowLoadingDialog();
if (model_->step() == Step::kGPMChangeArbitraryPin ||
model_->step() == Step::kGPMChangePin ||
model_->step() == Step::kGPMCreateArbitraryPin ||
model_->step() == Step::kGPMCreatePin) {
gpm_pin_creation_confirmed_ = true;
}
if (account_state_ == AccountState::kRecoverable) {
CHECK(enclave_manager_->has_pending_keys());
// In this case, we were waiting for the user to create their GPM PIN.
enclave_manager_->AddDeviceAndPINToAccount(
*pin_, base::BindOnce(&GPMEnclaveController::OnDeviceAdded,
weak_ptr_factory_.GetWeakPtr()));
} else if (account_state_ == AccountState::kEmpty) {
// The user has set a PIN to create the account.
enclave_manager_->SetupWithPIN(
*pin_, base::BindOnce(&GPMEnclaveController::OnDeviceAdded,
weak_ptr_factory_.GetWeakPtr()));
} else if (changing_gpm_pin_) {
CHECK(model_->step() == Step::kGPMChangePin ||
model_->step() == Step::kGPMChangeArbitraryPin);
enclave_manager_->ChangePIN(
base::UTF16ToUTF8(pin), std::move(*rapt_),
base::BindOnce(&GPMEnclaveController::OnGpmPinChanged,
weak_ptr_factory_.GetWeakPtr()));
rapt_.reset();
ChangePinControllerImpl::RecordHistogram(ChangePinEvent::kNewPinEntered);
} else {
StartTransaction();
}
}
void GPMEnclaveController::OnTouchIDComplete(bool success) {
// On error no LAContext will be provided and macOS will show the system UI
// for user verification.
model_->DisableUiOrShowLoadingDialog();
StartTransaction();
}
void GPMEnclaveController::OnForgotGPMPinPressed() {
changing_gpm_pin_ = true;
model_->SetStep(Step::kGPMReauthForPinReset);
ChangePinControllerImpl::RecordHistogram(
ChangePinEvent::kFlowStartedFromPinDialog);
}
void GPMEnclaveController::OnReauthComplete(std::string rapt) {
CHECK_EQ(model_->step(), Step::kGPMReauthForPinReset);
rapt_ = std::move(rapt);
model_->SetStep(Step::kGPMChangePin);
ChangePinControllerImpl::RecordHistogram(ChangePinEvent::kReauthCompleted);
}
void GPMEnclaveController::StartTransaction() {
// Starting a transaction means the user has chosen to use GPM. Reset the
// decline count so GPM can again be the priority on creation.
content::RenderFrameHost* rfh =
content::RenderFrameHost::FromID(render_frame_host_id_);
Profile::FromBrowserContext(rfh->GetBrowserContext())
->GetPrefs()
->SetInteger(
webauthn::pref_names::kEnclaveDeclinedGPMCredentialCreationCount, 0);
access_token_fetcher_ = enclave_manager_->GetAccessToken(base::BindOnce(
&GPMEnclaveController::MaybeHashPinAndStartEnclaveTransaction,
weak_ptr_factory_.GetWeakPtr()));
}
void GPMEnclaveController::MaybeHashPinAndStartEnclaveTransaction(
std::optional<std::string> token) {
if (!pin_) {
StartEnclaveTransaction(std::move(token), nullptr);
return;
}
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::TaskPriority::USER_VISIBLE, base::MayBlock()},
base::BindOnce(
[](std::string pin,
std::unique_ptr<webauthn_pb::EnclaveLocalState_WrappedPIN>
wrapped_pin) -> std::unique_ptr<device::enclave::ClaimedPIN> {
return EnclaveManager::MakeClaimedPINSlowly(std::move(pin),
std::move(wrapped_pin));
},
*pin_, enclave_manager_->GetWrappedPIN()),
base::BindOnce(&GPMEnclaveController::StartEnclaveTransaction,
weak_ptr_factory_.GetWeakPtr(), std::move(token)));
}
void GPMEnclaveController::StartEnclaveTransaction(
std::optional<std::string> token,
std::unique_ptr<device::enclave::ClaimedPIN> claimed_pin) {
// The UI has advanced to the point where it wants to perform an enclave
// transaction. This code collects the needed values and triggers
// `enclave_request_callback_` which surfaces in
// `EnclaveDiscovery::OnUIRequest`.
if (!token || !enclave_manager_->is_ready()) {
model_->SetStep(Step::kGPMError);
return;
}
auto request = std::make_unique<device::enclave::CredentialRequest>();
request->access_token = std::move(*token);
// A request to the enclave can either provide a wrapped secret, which only
// the enclave can decrypt, or can provide the security domain secret
// directly. The latter is only possible immediately after registering a
// device because that's the only time that the actual security domain secret
// is in memory.
bool use_unwrapped_secret = false;
switch (*uv_method_) {
case EnclaveUserVerificationMethod::kNone:
request->signing_callback =
enclave_manager_->IdentityKeySigningCallback();
break;
case EnclaveUserVerificationMethod::kImplicit:
request->signing_callback =
enclave_manager_->IdentityKeySigningCallback();
use_unwrapped_secret = true;
request->user_verified = true;
break;
case EnclaveUserVerificationMethod::kPIN:
request->signing_callback =
enclave_manager_->IdentityKeySigningCallback();
CHECK(claimed_pin);
request->claimed_pin = std::move(claimed_pin);
request->pin_result_callback =
base::BindOnce(&GPMEnclaveController::HandlePINValidationResult,
weak_ptr_factory_.GetWeakPtr());
request->user_verified = true;
break;
case EnclaveUserVerificationMethod::kUVKeyWithChromeUI:
case EnclaveUserVerificationMethod::kUVKeyWithSystemUI: {
EnclaveManager::UVKeyOptions uv_options;
uv_options.rp_id = rp_id_;
uv_options.render_frame_host_id = render_frame_host_id_;
#if BUILDFLAG(IS_MAC)
uv_options.lacontext = std::move(model_->lacontext);
#endif // BUILDFLAG(IS_MAC)
#if BUILDFLAG(IS_CHROMEOS_ASH)
if (ash::features::IsWebAuthNAuthDialogMergeEnabled()) {
uv_options.dialog_controller = ash::ActiveSessionAuthController::Get();
} else {
uv_options.dialog_controller = ash::WebAuthNDialogController::Get();
}
#endif
request->signing_callback =
enclave_manager_->UserVerifyingKeySigningCallback(
std::move(uv_options));
request->user_verified = true;
MaybeRecordUserActionForWinUv(
request_type_ == device::FidoRequestType::kMakeCredential,
uv_method_.value());
break;
}
case EnclaveUserVerificationMethod::kDeferredUVKeyWithSystemUI:
// This submits a UV key, but is signed with the HW key. We still count
// it as being user verified because this will trigger UV creation and
// the system will verify the user for that operation.
request->signing_callback =
enclave_manager_->IdentityKeySigningCallback();
request->user_verified = true;
request->uv_key_creation_callback =
enclave_manager_->UserVerifyingKeyCreationCallback();
request->unregister_callback =
base::BindOnce(&EnclaveManager::Unenroll,
enclave_manager_->GetWeakPtr(), base::DoNothing());
MaybeRecordUserActionForWinUv(
request_type_ == device::FidoRequestType::kMakeCredential,
uv_method_.value());
break;
case EnclaveUserVerificationMethod::kUnsatisfiable:
NOTREACHED();
}
switch (request_type_) {
case device::FidoRequestType::kMakeCredential: {
if (use_unwrapped_secret) {
std::tie(request->key_version, request->secret) =
enclave_manager_->TakeSecret().value();
} else {
std::tie(request->key_version, request->wrapped_secret) =
enclave_manager_->GetCurrentWrappedSecret();
}
request->save_passkey_callback =
base::BindOnce(&GPMEnclaveController::OnPasskeyCreated,
weak_ptr_factory_.GetWeakPtr());
base::ranges::transform(
creds_, std::back_inserter(request->existing_cred_ids),
[](const auto& cred) {
const std::string& cred_id = cred.credential_id();
return std::vector<uint8_t>(cred_id.begin(), cred_id.end());
});
break;
}
case device::FidoRequestType::kGetAssertion: {
std::unique_ptr<sync_pb::WebauthnCredentialSpecifics> entity;
for (const auto& cred : creds_) {
if (base::ranges::equal(
base::as_bytes(base::make_span(cred.credential_id())),
base::make_span(*selected_cred_id_))) {
entity = std::make_unique<sync_pb::WebauthnCredentialSpecifics>(cred);
break;
}
}
CHECK(entity);
if (use_unwrapped_secret) {
std::tie(std::ignore, request->secret) =
enclave_manager_->TakeSecret().value();
} else {
if (entity->key_version()) {
std::optional<std::vector<uint8_t>> wrapped_secret =
enclave_manager_->GetWrappedSecret(entity->key_version());
if (wrapped_secret) {
request->wrapped_secret = std::move(*wrapped_secret);
} else {
FIDO_LOG(ERROR)
<< "Unexpectedly did not have a wrapped key for epoch "
<< entity->key_version();
}
}
if (!request->wrapped_secret.has_value()) {
request->wrapped_secret =
enclave_manager_->GetCurrentWrappedSecret().second;
}
}
request->entity = std::move(entity);
break;
}
}
CHECK(request->wrapped_secret.has_value() ^ request->secret.has_value());
enclave_request_callback_.Run(std::move(request));
}
void GPMEnclaveController::OnPasskeyCreated(
sync_pb::WebauthnCredentialSpecifics passkey) {
webauthn::PasskeyModel* passkey_model =
PasskeyModelFactory::GetInstance()->GetForProfile(GetProfile());
passkey_model->CreatePasskey(passkey);
if (device::kWebAuthnGpmPin.Get()) {
content::WebContents* web_contents =
content::WebContents::FromRenderFrameHost(
content::RenderFrameHost::FromID(render_frame_host_id_));
PasswordsClientUIDelegate* manage_passwords_ui_controller =
PasswordsClientUIDelegateFromWebContents(web_contents);
if (manage_passwords_ui_controller) {
bool gpm_pin_created_in_this_request =
gpm_pin_creation_confirmed_ && enclave_manager_->has_wrapped_pin();
manage_passwords_ui_controller->OnPasskeySaved(
gpm_pin_created_in_this_request);
}
}
}
int GPMEnclaveController::GetFailedPINAttemptCount() {
return GetProfile()->GetPrefs()->GetInteger(
webauthn::pref_names::kEnclaveFailedPINAttemptsCount);
}
void GPMEnclaveController::SetFailedPINAttemptCount(int count) {
GetProfile()->GetPrefs()->SetInteger(
webauthn::pref_names::kEnclaveFailedPINAttemptsCount, count);
}
void GPMEnclaveController::HandlePINValidationResult(
device::enclave::PINValidationResult result) {
switch (result) {
case device::enclave::PINValidationResult::kSuccess:
SetFailedPINAttemptCount(0);
model_->gpm_pin_remaining_attempts_ = std::nullopt;
break;
case device::enclave::PINValidationResult::kIncorrect: {
int count = GetFailedPINAttemptCount();
SetFailedPINAttemptCount(++count);
if (count >= device::enclave::kMaxFailedPINAttempts) {
model_->SetStep(Step::kGPMLockedPin);
} else {
model_->gpm_pin_remaining_attempts_ =
device::enclave::kMaxFailedPINAttempts - count;
PromptForPin();
}
break;
}
case device::enclave::PINValidationResult::kLocked:
model_->SetStep(Step::kGPMLockedPin);
break;
}
}
void GPMEnclaveController::OnGpmPasskeysReset(bool success) {
CHECK(model_->step() == Step::kRecoverSecurityDomain);
if (!success ||
model_->request_type != device::FidoRequestType::kMakeCredential ||
!device::kWebAuthnGpmPin.Get()) {
model_->CancelAuthenticatorRequest();
return;
}
// TODO(crbug.com/342554229): There might be a race between other members of
// the domain. Maybe re-download the account state.
account_state_ = AccountState::kEmpty;
model_->SetStep(AuthenticatorRequestDialogModel::Step::kGPMCreatePin);
}