#include "device/fido/fido_request_handler_base.h"
#include <string_view>
#include <utility>
#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/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/task/sequenced_task_runner.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "components/device_event_log/device_event_log.h"
#include "device/bluetooth/bluetooth_adapter_factory.h"
#include "device/fido/ble_adapter_manager.h"
#include "device/fido/discoverable_credential_metadata.h"
#include "device/fido/features.h"
#include "device/fido/fido_authenticator.h"
#include "device/fido/fido_constants.h"
#include "device/fido/fido_discovery_base.h"
#include "device/fido/fido_discovery_factory.h"
#if BUILDFLAG(IS_WIN)
#include "device/fido/win/authenticator.h"
#include "device/fido/win/util.h"
#include "device/fido/win/webauthn_api.h"
#endif
#if BUILDFLAG(IS_MAC)
#include "base/process/process_info.h"
#include "device/fido/mac/icloud_keychain.h"
#include "device/fido/mac/util.h"
#endif
namespace device {
namespace {
bool IsGpmPasskeyAuthenticator(const FidoAuthenticator& authenticator) {
switch (authenticator.GetType()) {
case AuthenticatorType::kWinNative:
case AuthenticatorType::kTouchID:
case AuthenticatorType::kChromeOS:
case AuthenticatorType::kPhone:
case AuthenticatorType::kICloudKeychain:
#if BUILDFLAG(ARKWEB_FIDO)
case AuthenticatorType::kOhosNative:
#endif
case AuthenticatorType::kOther:
return false;
case AuthenticatorType::kEnclave:
return true;
}
NOTREACHED();
}
void MaybeRecordPlatformCredentialStatus(AuthenticatorType type,
base::TimeDelta elapsed_time) {
std::string metric_name;
switch (type) {
case AuthenticatorType::kWinNative:
metric_name = "WebAuthentication.CredentialFetchDuration.WinHello";
break;
case AuthenticatorType::kTouchID:
metric_name = "WebAuthentication.CredentialFetchDuration.TouchId";
break;
case AuthenticatorType::kChromeOS:
metric_name = "WebAuthentication.CredentialFetchDuration.ChromeOS";
break;
case AuthenticatorType::kICloudKeychain:
metric_name = "WebAuthentication.CredentialFetchDuration.ICloudKeychain";
break;
default:
return;
}
base::UmaHistogramTimes(metric_name, elapsed_time);
}
}
struct TransportAvailabilityCallbackReadiness {
bool callback_made = false;
bool ble_information_pending = false;
unsigned num_platform_credential_checks_pending = 0;
bool win_is_uvpaa_check_pending = false;
bool platform_biometrics_check_pending = false;
unsigned num_discoveries_pending = 0;
unsigned num_platform_discoveries_pending = 0;
bool platform_discovery_succeeded = false;
bool CanMakeCallback() const {
return !callback_made && !ble_information_pending &&
num_platform_credential_checks_pending == 0 &&
!win_is_uvpaa_check_pending && !platform_biometrics_check_pending &&
num_discoveries_pending == 0;
}
};
FidoRequestHandlerBase::TransportAvailabilityInfo::TransportAvailabilityInfo() =
default;
FidoRequestHandlerBase::TransportAvailabilityInfo::TransportAvailabilityInfo(
const TransportAvailabilityInfo& data) = default;
FidoRequestHandlerBase::TransportAvailabilityInfo&
FidoRequestHandlerBase::TransportAvailabilityInfo::operator=(
const TransportAvailabilityInfo& other) = default;
FidoRequestHandlerBase::TransportAvailabilityInfo::
~TransportAvailabilityInfo() = default;
FidoRequestHandlerBase::Observer::~Observer() = default;
static bool g_always_allow_ble_calls = false;
FidoRequestHandlerBase::ScopedAlwaysAllowBLECalls::ScopedAlwaysAllowBLECalls() {
CHECK(!g_always_allow_ble_calls);
g_always_allow_ble_calls = true;
}
FidoRequestHandlerBase::ScopedAlwaysAllowBLECalls::
~ScopedAlwaysAllowBLECalls() {
CHECK(g_always_allow_ble_calls);
g_always_allow_ble_calls = false;
}
FidoRequestHandlerBase::FidoRequestHandlerBase()
: transport_availability_callback_readiness_(
new TransportAvailabilityCallbackReadiness) {}
FidoRequestHandlerBase::FidoRequestHandlerBase(
FidoDiscoveryFactory* fido_discovery_factory,
const base::flat_set<FidoTransportProtocol>& available_transports)
: device::FidoRequestHandlerBase(fido_discovery_factory,
{},
available_transports) {}
FidoRequestHandlerBase::FidoRequestHandlerBase(
FidoDiscoveryFactory* fido_discovery_factory,
std::vector<std::unique_ptr<FidoDiscoveryBase>> additional_discoveries,
const base::flat_set<FidoTransportProtocol>& available_transports)
: FidoRequestHandlerBase() {
InitDiscoveries(fido_discovery_factory, std::move(additional_discoveries),
available_transports,
true);
}
void FidoRequestHandlerBase::InitDiscoveries(
FidoDiscoveryFactory* fido_discovery_factory,
std::vector<std::unique_ptr<FidoDiscoveryBase>> additional_discoveries,
base::flat_set<FidoTransportProtocol> available_transports,
bool consider_enclave) {
FIDO_LOG(DEBUG) << "Initializing FIDO discoveries";
#if BUILDFLAG(IS_WIN)
auto win_discovery =
fido_discovery_factory->MaybeCreateWinWebAuthnApiDiscovery();
if (win_discovery) {
FIDO_LOG(DEBUG) << "Adding Windows Hello discovery";
win_discovery->set_observer(this);
discoveries_.push_back(std::move(win_discovery));
transport_availability_info_.has_win_native_api_authenticator = true;
transport_availability_callback_readiness_->win_is_uvpaa_check_pending =
true;
WinWebAuthnApiAuthenticator::IsUserVerifyingPlatformAuthenticatorAvailable(
device::WinWebAuthnApi::GetDefault(),
base::BindOnce(&FidoRequestHandlerBase::OnWinIsUvpaa,
weak_factory_.GetWeakPtr()));
base::EraseIf(available_transports, [](auto transport) {
return transport != FidoTransportProtocol::kHybrid;
});
}
#endif
#if BUILDFLAG(ARKWEB_FIDO)
auto ohos_discovery =
fido_discovery_factory->CreateOhosFidoDiscovery();
if (ohos_discovery) {
ohos_discovery->set_observer(this);
discoveries_.push_back(std::move(ohos_discovery));
base::EraseIf(available_transports, [](auto transport) {
return transport != FidoTransportProtocol::kHybrid;
});
}
#endif
transport_availability_info_.available_transports = available_transports;
for (const auto transport : available_transports) {
std::vector<std::unique_ptr<FidoDiscoveryBase>> discoveries =
fido_discovery_factory->Create(transport);
if (discoveries.empty()) {
transport_availability_info_.available_transports.erase(transport);
continue;
}
FIDO_LOG(DEBUG) << "Adding discovery for transport "
<< static_cast<int>(transport);
for (auto& discovery : discoveries) {
discovery->set_observer(this);
discoveries_.emplace_back(std::move(discovery));
}
}
for (auto& discovery : additional_discoveries) {
if (!base::Contains(available_transports, discovery->transport())) {
continue;
}
discovery->set_observer(this);
discoveries_.emplace_back(std::move(discovery));
}
if (consider_enclave) {
std::optional<std::unique_ptr<FidoDiscoveryBase>> enclave_discovery =
fido_discovery_factory->MaybeCreateEnclaveDiscovery();
if (enclave_discovery) {
FIDO_LOG(DEBUG) << "Adding discovery for enclave";
enclave_discovery.value()->set_observer(this);
discoveries_.emplace_back(std::move(*enclave_discovery));
}
}
for (auto& discovery : discoveries_) {
if (discovery->transport() == FidoTransportProtocol::kInternal) {
transport_availability_callback_readiness_
->num_platform_discoveries_pending++;
}
}
transport_availability_callback_readiness_->num_discoveries_pending =
discoveries_.size();
#if BUILDFLAG(IS_MAC)
const bool can_call_ble_apis =
g_always_allow_ble_calls ||
base::DoesResponsibleProcessHaveBluetoothMetadata();
if (!can_call_ble_apis) {
FIDO_LOG(ERROR) << "Cannot use Bluetooth because the responsible app for "
"the process does not have Bluetooth metadata in its "
"Info.plist. Launch from Finder to fix.";
}
#else
const bool can_call_ble_apis = true;
#endif
if (can_call_ble_apis &&
device::BluetoothAdapterFactory::Get()->IsLowEnergySupported() &&
base::Contains(transport_availability_info_.available_transports,
FidoTransportProtocol::kHybrid)) {
FIDO_LOG(DEBUG) << "Checking for bluetooth availability";
transport_availability_callback_readiness_->ble_information_pending = true;
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(&FidoRequestHandlerBase::ConstructBleAdapterPowerManager,
weak_factory_.GetWeakPtr()));
}
#if BUILDFLAG(IS_MAC)
transport_availability_info_.platform_has_biometrics =
device::fido::mac::DeviceHasBiometricsAvailable();
FIDO_LOG(DEBUG) << "MacOS biometrics availability check done";
MaybeSignalTransportsEnumerated();
#elif BUILDFLAG(IS_WIN)
transport_availability_callback_readiness_
->platform_biometrics_check_pending = true;
FIDO_LOG(DEBUG) << "Checking for Windows biometrics availability";
device::fido::win::DeviceHasBiometricsAvailable(base::BindOnce(
[](base::WeakPtr<FidoRequestHandlerBase> handler,
bool biometrics_available) {
if (!handler) {
return;
}
handler->transport_availability_info_.platform_has_biometrics =
biometrics_available;
handler->transport_availability_callback_readiness_
->platform_biometrics_check_pending = false;
FIDO_LOG(DEBUG) << "Windows biometric availability check done";
handler->MaybeSignalTransportsEnumerated();
},
GetWeakPtr()));
#else
FIDO_LOG(DEBUG) << "No need to check for biometrics on this platform";
MaybeSignalTransportsEnumerated();
#endif
}
FidoRequestHandlerBase::~FidoRequestHandlerBase() {
if (observer_) {
observer_->StopObserving(this);
}
CancelActiveAuthenticators();
}
void FidoRequestHandlerBase::StartAuthenticatorRequest(
const std::string& authenticator_id) {
InitializeAuthenticatorAndDispatchRequest(authenticator_id);
}
void FidoRequestHandlerBase::CancelActiveAuthenticators(
std::string_view exclude_device_id) {
for (auto task_it = active_authenticators_.begin();
task_it != active_authenticators_.end();) {
DCHECK(!task_it->first.empty());
if (task_it->first != exclude_device_id) {
DCHECK(task_it->second);
task_it->second->Cancel();
task_it = active_authenticators_.erase(task_it);
} else {
++task_it;
}
}
}
void FidoRequestHandlerBase::OnBluetoothAdapterEnumerated(
bool is_present,
BleStatus ble_status,
bool can_power_on,
bool is_peripheral_role_supported) {
if (!is_present) {
transport_availability_info_.available_transports.erase(
FidoTransportProtocol::kHybrid);
}
transport_availability_callback_readiness_->ble_information_pending = false;
transport_availability_info_.ble_status = ble_status;
transport_availability_info_.can_power_on_ble_adapter = can_power_on;
FIDO_LOG(DEBUG) << "Bluetooth status enumerated";
MaybeSignalTransportsEnumerated();
}
void FidoRequestHandlerBase::OnBluetoothAdapterStatusChanged(
BleStatus ble_status) {
transport_availability_info_.ble_status = ble_status;
if (observer_) {
observer_->BluetoothAdapterStatusChanged(ble_status);
}
}
void FidoRequestHandlerBase::PowerOnBluetoothAdapter() {
if (!bluetooth_adapter_manager_) {
return;
}
bluetooth_adapter_manager_->SetAdapterPower(true );
}
void FidoRequestHandlerBase::RequestBluetoothPermission(
BlePermissionCallback callback) {
return bluetooth_adapter_manager_->RequestBluetoothPermission(
std::move(callback));
}
base::WeakPtr<FidoRequestHandlerBase> FidoRequestHandlerBase::GetWeakPtr() {
return weak_factory_.GetWeakPtr();
}
void FidoRequestHandlerBase::SetObserver(
FidoRequestHandlerBase::Observer* observer) {
DCHECK(!observer_) << "Only one observer is supported.";
observer_ = observer;
FIDO_LOG(DEBUG) << "FidoRequestHandler observer set";
MaybeSignalTransportsEnumerated();
}
void FidoRequestHandlerBase::RemoveObserver(
FidoRequestHandlerBase::Observer* observer) {
observer_ = nullptr;
}
void FidoRequestHandlerBase::Start() {
for (const auto& discovery : discoveries_) {
discovery->Start();
}
}
void FidoRequestHandlerBase::AuthenticatorRemoved(
FidoDiscoveryBase* discovery,
FidoAuthenticator* authenticator) {
auto authenticator_it = active_authenticators_.find(authenticator->GetId());
if (authenticator_it == active_authenticators_.end()) {
return;
}
DCHECK_EQ(authenticator_it->second, authenticator);
active_authenticators_.erase(authenticator_it);
if (observer_) {
observer_->FidoAuthenticatorRemoved(authenticator->GetId());
}
}
void FidoRequestHandlerBase::DiscoveryStarted(
FidoDiscoveryBase* discovery,
bool success,
std::vector<FidoAuthenticator*> authenticators) {
transport_availability_callback_readiness_->num_discoveries_pending--;
bool is_platform_discovery =
discovery->transport() == FidoTransportProtocol::kInternal;
if (is_platform_discovery) {
CHECK(transport_availability_callback_readiness_
->num_platform_discoveries_pending > 0);
transport_availability_callback_readiness_
->num_platform_discoveries_pending--;
}
if (!success) {
if (!is_platform_discovery ||
(transport_availability_callback_readiness_
->num_platform_discoveries_pending == 0 &&
!transport_availability_callback_readiness_
->platform_discovery_succeeded)) {
transport_availability_info_.available_transports.erase(
discovery->transport());
}
} else {
for (auto* authenticator : authenticators) {
AuthenticatorAdded(discovery, authenticator);
}
if (is_platform_discovery && !authenticators.empty()) {
transport_availability_callback_readiness_->platform_discovery_succeeded =
true;
for (FidoAuthenticator* platform_authenticator : authenticators) {
if (IsGpmPasskeyAuthenticator(*platform_authenticator)) {
continue;
}
transport_availability_info_.has_icloud_keychain |=
platform_authenticator->GetType() ==
AuthenticatorType::kICloudKeychain;
transport_availability_callback_readiness_
->num_platform_credential_checks_pending++;
FIDO_LOG(DEBUG) << "Getting platform credential status";
GetPlatformCredentialStatus(platform_authenticator);
}
}
}
FIDO_LOG(DEBUG) << "Discovery started for transport "
<< static_cast<int>(discovery->transport());
MaybeSignalTransportsEnumerated();
}
void FidoRequestHandlerBase::AuthenticatorAdded(
FidoDiscoveryBase* discovery,
FidoAuthenticator* authenticator) {
DCHECK(!authenticator->GetId().empty());
bool was_inserted;
std::tie(std::ignore, was_inserted) =
active_authenticators_.insert({authenticator->GetId(), authenticator});
if (!was_inserted) {
NOTREACHED() << "Authenticator with duplicate ID "
<< authenticator->GetId();
}
bool embedder_controls_dispatch = false;
if (observer_) {
embedder_controls_dispatch =
observer_->EmbedderControlsAuthenticatorDispatch(*authenticator);
observer_->FidoAuthenticatorAdded(*authenticator);
}
if (!embedder_controls_dispatch) {
VLOG(2)
<< "Request handler dispatching request to authenticator immediately.";
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(
&FidoRequestHandlerBase::InitializeAuthenticatorAndDispatchRequest,
GetWeakPtr(), authenticator->GetId()));
} else {
VLOG(2) << "Embedder controls the dispatch.";
}
#if BUILDFLAG(IS_WIN)
if (authenticator->GetType() == AuthenticatorType::kWinNative) {
DCHECK(transport_availability_info_.has_win_native_api_authenticator);
transport_availability_info_
.win_native_ui_shows_resident_credential_notice =
static_cast<WinWebAuthnApiAuthenticator*>(authenticator)
->ShowsResidentCredentialNotice();
}
#endif
}
void FidoRequestHandlerBase::GetPlatformCredentialStatus(
FidoAuthenticator* platform_authenticator) {
transport_availability_callback_readiness_
->num_platform_credential_checks_pending--;
}
void FidoRequestHandlerBase::OnHavePlatformCredentialStatus(
AuthenticatorType authenticator_type,
std::optional<base::ElapsedTimer> timer,
std::vector<DiscoverableCredentialMetadata> creds,
RecognizedCredential has_credentials) {
if (creds.size() > 0 && timer.has_value()) {
MaybeRecordPlatformCredentialStatus(authenticator_type, timer->Elapsed());
}
if (authenticator_type == AuthenticatorType::kICloudKeychain) {
DCHECK_EQ(transport_availability_info_.has_icloud_keychain_credential,
RecognizedCredential::kNoRecognizedCredential);
transport_availability_info_.has_icloud_keychain_credential =
has_credentials;
} else {
DCHECK_EQ(
transport_availability_info_.has_platform_authenticator_credential,
RecognizedCredential::kNoRecognizedCredential);
transport_availability_info_.has_platform_authenticator_credential =
has_credentials;
if (has_credentials == RecognizedCredential::kNoRecognizedCredential) {
transport_availability_info_.available_transports.erase(
FidoTransportProtocol::kInternal);
}
}
auto& out_creds = transport_availability_info_.recognized_credentials;
if (out_creds.empty()) {
out_creds = std::move(creds);
} else if (!creds.empty()) {
out_creds.insert(out_creds.end(), creds.begin(), creds.end());
}
transport_availability_callback_readiness_
->num_platform_credential_checks_pending--;
FIDO_LOG(DEBUG) << "Obtained platform credential status";
MaybeSignalTransportsEnumerated();
}
bool FidoRequestHandlerBase::HasAuthenticator(
const std::string& authenticator_id) const {
return base::Contains(active_authenticators_, authenticator_id);
}
void FidoRequestHandlerBase::MaybeSignalTransportsEnumerated() {
if (!observer_ ||
!transport_availability_callback_readiness_->CanMakeCallback()) {
FIDO_LOG(DEBUG) << "Transport availability not yet ready";
return;
}
FIDO_LOG(DEBUG) << "Transport availability checks done";
transport_availability_callback_readiness_->callback_made = true;
observer_->OnTransportAvailabilityEnumerated(transport_availability_info_);
}
void FidoRequestHandlerBase::InitializeAuthenticatorAndDispatchRequest(
const std::string& authenticator_id) {
auto authenticator_it = active_authenticators_.find(authenticator_id);
if (authenticator_it == active_authenticators_.end()) {
return;
}
FidoAuthenticator* authenticator = authenticator_it->second;
authenticator->InitializeAuthenticator(
base::BindOnce(&FidoRequestHandlerBase::DispatchRequest,
weak_factory_.GetWeakPtr(), authenticator));
}
void FidoRequestHandlerBase::ConstructBleAdapterPowerManager() {
bluetooth_adapter_manager_ = std::make_unique<BleAdapterManager>(this);
}
void FidoRequestHandlerBase::OnWinIsUvpaa(bool is_uvpaa) {
transport_availability_info_.win_is_uvpaa = is_uvpaa;
transport_availability_callback_readiness_->win_is_uvpaa_check_pending =
false;
FIDO_LOG(DEBUG) << "Windows Hello IsUvpaa check done";
MaybeSignalTransportsEnumerated();
}
void FidoRequestHandlerBase::StopDiscoveries() {
for (const auto& discovery : discoveries_) {
discovery->Stop();
}
}
constexpr base::TimeDelta
FidoRequestHandlerBase::kMinExpectedAuthenticatorResponseTime;
}