blob: f4dbd3b3cab23ecd16922c1e5e435a20ad32c975 [file] [log] [blame]
// 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 "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:
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);
}
} // namespace
// TransportAvailabilityCallbackReadiness stores state that tracks whether
// |FidoRequestHandlerBase| is ready to call
// |OnTransportAvailabilityEnumerated|.
struct TransportAvailabilityCallbackReadiness {
// callback_made is true if the |OnTransportAvailabilityEnumerated| callback
// has been made.
bool callback_made = false;
// ble_information_pending is true if the |OnTransportAvailabilityEnumerated|
// callback is pending BLE status information.
bool ble_information_pending = false;
// num_platform_credential_checks_pending is true if the
// |OnTransportAvailabilityEnumerated| callback is pending
// |OnHasRecognizedPlatformCredentialFilled| being called after the platform
// authenticator has decided if it has credentials that are responsive to the
// request.
unsigned num_platform_credential_checks_pending = 0;
// win_is_uvpaa_check_pending is true if |OnTransportAvailabilityEnumerated|
// callback is pending |OnIsUvpaa| being called.
bool win_is_uvpaa_check_pending = false;
// platform_biometrics_check_pending is set if an asynchronous check for
// local biometric availability is pending.
bool platform_biometrics_check_pending = false;
// num_discoveries_pending is the number of discoveries that are still yet to
// signal that they have started.
unsigned num_discoveries_pending = 0;
// This separately counts for platform discoveries in order to track whether
// at least one discovery succeeded, for situations where there is more than
// one platform authenticator available.
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 --------------------------
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 -------------------------------------------
FidoRequestHandlerBase::Observer::~Observer() = default;
// FidoRequestHandlerBase::ScopedAlwaysAllowBLECalls --------------------------
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::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,
/*additional_discoveries=*/{},
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,
/*consider_enclave=*/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)
// Try to instantiate the discovery for proxying requests to the native
// Windows WebAuthn API; or fall back to using the regular device transport
// discoveries if the API is unavailable.
auto win_discovery =
fido_discovery_factory->MaybeCreateWinWebAuthnApiDiscovery();
if (win_discovery) {
// The Windows WebAuthn API is available. On this platform, communicating
// with authenticator devices directly is blocked by the OS, so we need to
// go through the native API instead. No device discoveries may be
// instantiated. The embedder will be responsible for dispatch of the
// authenticator and whether they display any UI in addition to the one
// provided by the OS.
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()));
// Allow caBLE as a potential additional transport if requested by
// the implementing class because it is not subject to the OS'
// device communication block (only GetAssertionRequestHandler uses
// caBLE). Otherwise, do not instantiate any other transports.
base::EraseIf(available_transports, [](auto transport) {
return transport != FidoTransportProtocol::kHybrid;
});
}
#endif // BUILDFLAG(IS_WIN)
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()) {
// This can occur in tests when a ScopedVirtualU2fDevice is in effect and
// HID transports are not configured or when caBLE discovery data isn't
// available.
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));
}
}
// `additional_discoveries` are injected by
// AuthenticatorRequestClientDelegate.
for (auto& discovery : additional_discoveries) {
// TODO: Make this work better for non-standard discoveries like Windows,
// which currently pretends to be `kInternal`.
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)
// On recent macOS a process must have listed Bluetooth metadata in its
// Info.plist in order to call Bluetooth APIs. Failure to do so results in
// the system killing with process with SIGABRT once Bluetooth calls are
// made.
//
// However, unless Chromium is started from the Finder, or with special
// posix_spawn flags, then the responsible process—the one that needs to have
// the right Info.plist—is one of the parent processes, often the terminal
// emulator. This can lead to Chromium getting killed when trying to do
// WebAuthn. This also affects layout tests.
//
// Thus, if the responsible process is not Chromium itself, then we do not
// make any Bluetooth API calls.
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
// Check if the platform supports BLE before trying to get a power manager.
// CaBLE might be in |available_transports| without actual BLE support under
// the virtual environment.
// TODO(nsatragno): Move the BLE power manager logic to CableDiscoveryFactory
// so we don't need this additional check.
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();
// Note that the pointer being erased is non-owning. The actual
// FidoAuthenticator instance is owned by its discovery (which in turn is
// owned by |discoveries_|.
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 /* set_power_on */);
}
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) {
// Device connection has been lost or device has already been removed.
// Thus, calling CancelTask() is not necessary. Also, below
// ongoing_tasks_.erase() will have no effect for the devices that have been
// already removed due to processing error or due to invocation of
// CancelOngoingTasks().
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);
}
// Allow GetAssertionRequestHandler to asynchronously check for known
// platform credentials and defer |OnTransportAvailabilityEnumerated| until
// that check is done.
// |authenticators| can be empty in tests.
if (is_platform_discovery && !authenticators.empty()) {
transport_availability_callback_readiness_->platform_discovery_succeeded =
true;
for (FidoAuthenticator* platform_authenticator : authenticators) {
if (IsGpmPasskeyAuthenticator(*platform_authenticator)) {
// GPM credential availability is checked in
// ChromeAuthenticatorRequestDelegate, so the authenticators don't
// implement GetPlatformCredentialStatus.
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();
}
// If |observer_| exists, dispatching request to |authenticator| is
// delegated to |observer_|. Else, dispatch request to |authenticator|
// immediately.
bool embedder_controls_dispatch = false;
if (observer_) {
embedder_controls_dispatch =
observer_->EmbedderControlsAuthenticatorDispatch(*authenticator);
observer_->FidoAuthenticatorAdded(*authenticator);
}
if (!embedder_controls_dispatch) {
// Post |InitializeAuthenticatorAndDispatchRequest| into its own task. This
// avoids hairpinning, even if the authenticator immediately invokes the
// request callback.
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 // BUILDFLAG(IS_WIN)
}
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) {
// iCloud Keychain is the second platform authenticator on the system and
// its status is reported via a different field.
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;
} // namespace device