| // Copyright 2018 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "device/fido/get_assertion_request_handler.h" |
| |
| #include <set> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/bind.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/ranges/algorithm.h" |
| #include "base/stl_util.h" |
| #include "base/timer/elapsed_timer.h" |
| #include "build/build_config.h" |
| #include "build/chromeos_buildflags.h" |
| #include "components/cbor/diagnostic_writer.h" |
| #include "components/device_event_log/device_event_log.h" |
| #include "device/fido/cable/fido_cable_discovery.h" |
| #include "device/fido/features.h" |
| #include "device/fido/fido_authenticator.h" |
| #include "device/fido/fido_discovery_factory.h" |
| #include "device/fido/fido_parsing_utils.h" |
| #include "device/fido/filter.h" |
| #include "device/fido/get_assertion_task.h" |
| #include "device/fido/large_blob.h" |
| #include "device/fido/pin.h" |
| |
| #if defined(OS_MAC) |
| #include "device/fido/mac/authenticator.h" |
| #endif // defined(OS_MAC) |
| |
| #if defined(OS_WIN) |
| #include "device/fido/win/authenticator.h" |
| #include "device/fido/win/type_conversions.h" |
| #endif |
| |
| #if BUILDFLAG(IS_CHROMEOS_ASH) |
| #include "device/fido/cros/authenticator.h" |
| #endif |
| |
| namespace device { |
| |
| namespace { |
| |
| using PINUVDisposition = FidoAuthenticator::PINUVDisposition; |
| |
| const std::set<pin::Permissions> GetPinTokenPermissionsFor( |
| const FidoAuthenticator& authenticator, |
| const CtapGetAssertionRequest& request) { |
| std::set<pin::Permissions> permissions = {pin::Permissions::kGetAssertion}; |
| if (request.large_blob_write && authenticator.Options() && |
| authenticator.Options()->supports_large_blobs) { |
| permissions.emplace(pin::Permissions::kLargeBlobWrite); |
| } |
| return permissions; |
| } |
| |
| base::Optional<GetAssertionStatus> ConvertDeviceResponseCode( |
| CtapDeviceResponseCode device_response_code) { |
| switch (device_response_code) { |
| case CtapDeviceResponseCode::kSuccess: |
| return GetAssertionStatus::kSuccess; |
| |
| // Only returned after the user interacted with the |
| // authenticator. |
| case CtapDeviceResponseCode::kCtap2ErrNoCredentials: |
| return GetAssertionStatus::kUserConsentButCredentialNotRecognized; |
| |
| // The user explicitly denied the operation. Touch ID returns this error |
| // when the user cancels the macOS prompt. External authenticators may |
| // return it e.g. after the user fails fingerprint verification. |
| case CtapDeviceResponseCode::kCtap2ErrOperationDenied: |
| return GetAssertionStatus::kUserConsentDenied; |
| |
| // External authenticators may return this error if internal user |
| // verification fails or if the pin token is not valid. |
| case CtapDeviceResponseCode::kCtap2ErrPinAuthInvalid: |
| return GetAssertionStatus::kUserConsentDenied; |
| |
| // This error is returned by some authenticators (e.g. the "Yubico FIDO |
| // 2" CTAP2 USB keys) during GetAssertion **before the user interacted |
| // with the device**. The authenticator does this to avoid blinking (and |
| // possibly asking the user for their PIN) for requests it knows |
| // beforehand it cannot handle. |
| // |
| // Ignore this error to avoid canceling the request without user |
| // interaction. |
| case CtapDeviceResponseCode::kCtap2ErrInvalidCredential: |
| return base::nullopt; |
| |
| // For all other errors, the authenticator will be dropped, and other |
| // authenticators may continue. |
| default: |
| return base::nullopt; |
| } |
| } |
| |
| // ValidateResponseExtensions returns true iff |extensions| is valid as a |
| // response to |request| and |options|. |
| bool ValidateResponseExtensions(const CtapGetAssertionRequest& request, |
| const CtapGetAssertionOptions& options, |
| const cbor::Value& extensions) { |
| if (!extensions.is_map()) { |
| return false; |
| } |
| |
| for (const auto& it : extensions.GetMap()) { |
| if (!it.first.is_string()) { |
| return false; |
| } |
| const std::string& ext_name = it.first.GetString(); |
| |
| if (ext_name == kExtensionLargeBlobKey && !request.large_blob_key) { |
| return false; |
| } |
| |
| if (ext_name == kExtensionHmacSecret) { |
| // This extension is checked by |GetAssertionTask| because it needs to be |
| // decrypted there. |
| continue; |
| } else { |
| // Authenticators may not return unknown extensions. |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| // ResponseValid returns whether |response| is permissible for the given |
| // |authenticator| and |request|. |
| bool ResponseValid(const FidoAuthenticator& authenticator, |
| const CtapGetAssertionRequest& request, |
| const CtapGetAssertionOptions& options, |
| const AuthenticatorGetAssertionResponse& response) { |
| // The underlying code must take care of filling in the credential from the |
| // allow list as needed. |
| CHECK(response.credential()); |
| |
| if (response.GetRpIdHash() != |
| fido_parsing_utils::CreateSHA256Hash(request.rp_id) && |
| (!request.app_id || |
| response.GetRpIdHash() != request.alternative_application_parameter)) { |
| return false; |
| } |
| |
| // PublicKeyUserEntity field in GetAssertion response is optional with the |
| // following constraints: |
| // - If assertion has been made without user verification, user identifiable |
| // information must not be included. |
| // - For resident key credentials, user id of the user entity is mandatory. |
| // - When multiple accounts exist for specified RP ID, user entity is |
| // mandatory. |
| // TODO(hongjunchoi) : Add link to section of the CTAP spec once it is |
| // published. |
| const auto& user_entity = response.user_entity(); |
| const bool has_user_identifying_info = |
| user_entity && |
| (user_entity->display_name || user_entity->name || user_entity->icon_url); |
| if (!response.auth_data().obtained_user_verification() && |
| has_user_identifying_info) { |
| return false; |
| } |
| |
| if (request.allow_list.empty() && !user_entity) { |
| return false; |
| } |
| |
| if (response.num_credentials().value_or(0u) > 1 && !user_entity) { |
| return false; |
| } |
| |
| // The authenticatorData on an GetAssertionResponse must not have |
| // attestedCredentialData set. |
| if (response.auth_data().attested_data().has_value()) { |
| return false; |
| } |
| |
| const base::Optional<cbor::Value>& extensions = |
| response.auth_data().extensions(); |
| if (extensions && |
| !ValidateResponseExtensions(request, options, *extensions)) { |
| FIDO_LOG(ERROR) << "assertion response invalid due to extensions block: " |
| << cbor::DiagnosticWriter::Write(*extensions); |
| return false; |
| } |
| |
| if (response.android_client_data_ext() && |
| (!request.android_client_data_ext || !authenticator.Options() || |
| !authenticator.Options()->supports_android_client_data_ext || |
| !IsValidAndroidClientDataJSON( |
| *request.android_client_data_ext, |
| base::StringPiece(reinterpret_cast<const char*>( |
| response.android_client_data_ext()->data()), |
| response.android_client_data_ext()->size())))) { |
| FIDO_LOG(ERROR) << "Invalid androidClientData extension"; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| base::flat_set<FidoTransportProtocol> GetTransportsAllowedByRP( |
| const CtapGetAssertionRequest& request) { |
| const base::flat_set<FidoTransportProtocol> kAllTransports = { |
| FidoTransportProtocol::kInternal, |
| FidoTransportProtocol::kNearFieldCommunication, |
| FidoTransportProtocol::kUsbHumanInterfaceDevice, |
| FidoTransportProtocol::kBluetoothLowEnergy, |
| FidoTransportProtocol::kCloudAssistedBluetoothLowEnergy, |
| FidoTransportProtocol::kAndroidAccessory, |
| }; |
| |
| const auto& allowed_list = request.allow_list; |
| if (allowed_list.empty()) { |
| return kAllTransports; |
| } |
| |
| base::flat_set<FidoTransportProtocol> transports; |
| for (const auto& credential : allowed_list) { |
| if (credential.transports().empty()) { |
| return kAllTransports; |
| } |
| transports.insert(credential.transports().begin(), |
| credential.transports().end()); |
| } |
| |
| if (base::FeatureList::IsEnabled(device::kWebAuthPhoneSupport)) { |
| transports.insert(device::FidoTransportProtocol::kAndroidAccessory); |
| } |
| |
| return transports; |
| } |
| |
| void ReportGetAssertionRequestTransport(FidoAuthenticator* authenticator) { |
| if (authenticator->AuthenticatorTransport()) { |
| base::UmaHistogramEnumeration( |
| "WebAuthentication.GetAssertionRequestTransport", |
| *authenticator->AuthenticatorTransport()); |
| } |
| } |
| |
| void ReportGetAssertionResponseTransport(FidoAuthenticator* authenticator) { |
| if (authenticator->AuthenticatorTransport()) { |
| base::UmaHistogramEnumeration( |
| "WebAuthentication.GetAssertionResponseTransport", |
| *authenticator->AuthenticatorTransport()); |
| } |
| } |
| |
| CtapGetAssertionRequest SpecializeRequestForAuthenticator( |
| const CtapGetAssertionRequest& request, |
| const FidoAuthenticator& authenticator) { |
| CtapGetAssertionRequest specialized_request(request); |
| if (!authenticator.Options() || |
| !authenticator.Options()->supports_android_client_data_ext) { |
| // Only send the googleAndroidClientData extension to authenticators that |
| // support it. |
| specialized_request.android_client_data_ext.reset(); |
| } |
| |
| if (!authenticator.Options() || |
| !authenticator.Options()->supports_large_blobs) { |
| // Do not attempt large blob operations on devices not supporting it. |
| specialized_request.large_blob_key = false; |
| specialized_request.large_blob_read = false; |
| specialized_request.large_blob_write.reset(); |
| } |
| if (!request.is_u2f_only && authenticator.Options() && |
| authenticator.Options()->always_uv) { |
| specialized_request.user_verification = |
| UserVerificationRequirement::kRequired; |
| } |
| return specialized_request; |
| } |
| |
| } // namespace |
| |
| GetAssertionRequestHandler::GetAssertionRequestHandler( |
| FidoDiscoveryFactory* fido_discovery_factory, |
| const base::flat_set<FidoTransportProtocol>& supported_transports, |
| CtapGetAssertionRequest request, |
| CtapGetAssertionOptions options, |
| bool allow_skipping_pin_touch, |
| CompletionCallback completion_callback) |
| : FidoRequestHandlerBase( |
| fido_discovery_factory, |
| base::STLSetIntersection<base::flat_set<FidoTransportProtocol>>( |
| supported_transports, |
| GetTransportsAllowedByRP(request))), |
| completion_callback_(std::move(completion_callback)), |
| request_(std::move(request)), |
| options_(std::move(options)), |
| allow_skipping_pin_touch_(allow_skipping_pin_touch) { |
| transport_availability_info().request_type = |
| FidoRequestHandlerBase::RequestType::kGetAssertion; |
| transport_availability_info().has_empty_allow_list = |
| request_.allow_list.empty(); |
| transport_availability_info().is_off_the_record_context = |
| request_.is_off_the_record_context; |
| |
| if (request_.allow_list.empty()) { |
| // Resident credential requests always involve user verification. |
| request_.user_verification = UserVerificationRequirement::kRequired; |
| } |
| |
| FIDO_LOG(EVENT) << "Starting GetAssertion flow"; |
| Start(); |
| } |
| |
| GetAssertionRequestHandler::~GetAssertionRequestHandler() = default; |
| |
| void GetAssertionRequestHandler::OnBluetoothAdapterEnumerated( |
| bool is_present, |
| bool is_powered_on, |
| bool can_power_on, |
| bool is_peripheral_role_supported) { |
| if (!is_peripheral_role_supported && request_.cable_extension) { |
| // caBLEv1 relies on the client being able to broadcast Bluetooth |
| // advertisements. |is_peripheral_role_supported| supposedly indicates |
| // whether the adapter supports advertising, but there appear to be false |
| // negatives (crbug/1074692). So we can't really do anything about it |
| // besides log it to aid diagnostics. |
| FIDO_LOG(ERROR) |
| << "caBLEv1 request, but BLE adapter does not support peripheral role"; |
| } |
| FidoRequestHandlerBase::OnBluetoothAdapterEnumerated( |
| is_present, is_powered_on, can_power_on, is_peripheral_role_supported); |
| } |
| |
| void GetAssertionRequestHandler::DispatchRequest( |
| FidoAuthenticator* authenticator) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(my_sequence_checker_); |
| |
| if (state_ != State::kWaitingForTouch) { |
| FIDO_LOG(DEBUG) << "Not dispatching request to " |
| << authenticator->GetDisplayName() |
| << " because no longer waiting for touch"; |
| return; |
| } |
| |
| const std::string authenticator_name = authenticator->GetDisplayName(); |
| |
| if (fido_filter::Evaluate(fido_filter::Operation::GET_ASSERTION, |
| request_.rp_id, authenticator_name, |
| base::nullopt) == fido_filter::Action::BLOCK) { |
| FIDO_LOG(DEBUG) << "Filtered request to device " << authenticator_name; |
| return; |
| } |
| |
| for (const auto& cred : request_.allow_list) { |
| if (fido_filter::Evaluate( |
| fido_filter::Operation::GET_ASSERTION, request_.rp_id, |
| authenticator_name, |
| std::pair<fido_filter::IDType, base::span<const uint8_t>>( |
| fido_filter::IDType::CREDENTIAL_ID, cred.id())) == |
| fido_filter::Action::BLOCK) { |
| FIDO_LOG(DEBUG) << "Filtered request to device " << authenticator_name |
| << " for credential ID " << base::HexEncode(cred.id()); |
| return; |
| } |
| } |
| |
| CtapGetAssertionRequest request = |
| SpecializeRequestForAuthenticator(request_, *authenticator); |
| PINUVDisposition uv_disposition = |
| authenticator->PINUVDispositionForGetAssertion(request, observer()); |
| switch (uv_disposition) { |
| case PINUVDisposition::kNoUV: |
| case PINUVDisposition::kNoTokenInternalUV: |
| case PINUVDisposition::kNoTokenInternalUVPINFallback: |
| // Proceed without a token. |
| break; |
| case PINUVDisposition::kGetToken: |
| ObtainPINUVAuthToken( |
| authenticator, GetPinTokenPermissionsFor(*authenticator, request), |
| active_authenticators().size() == 1 && allow_skipping_pin_touch_, |
| /*internal_uv_locked=*/false); |
| return; |
| case PINUVDisposition::kUnsatisfiable: |
| FIDO_LOG(DEBUG) << authenticator->GetDisplayName() |
| << " cannot satisfy assertion request. Requesting " |
| "touch in order to handle error case."; |
| authenticator->GetTouch(base::BindOnce( |
| &GetAssertionRequestHandler::TerminateUnsatisfiableRequestPostTouch, |
| weak_factory_.GetWeakPtr(), authenticator)); |
| return; |
| } |
| |
| ReportGetAssertionRequestTransport(authenticator); |
| |
| CtapGetAssertionRequest request_copy(request); |
| authenticator->GetAssertion( |
| std::move(request_copy), options_, |
| base::BindOnce(&GetAssertionRequestHandler::HandleResponse, |
| weak_factory_.GetWeakPtr(), authenticator, |
| std::move(request), base::ElapsedTimer())); |
| } |
| |
| void GetAssertionRequestHandler::AuthenticatorAdded( |
| FidoDiscoveryBase* discovery, |
| FidoAuthenticator* authenticator) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(my_sequence_checker_); |
| |
| #if defined(OS_MAC) |
| // Indicate to the UI whether a GetAssertion call to Touch ID would succeed |
| // or not. This needs to happen before the base AuthenticatorAdded() |
| // implementation runs |notify_observer_callback_| for this callback. |
| if (authenticator->IsTouchIdAuthenticator()) { |
| DCHECK(!transport_availability_info() |
| .has_recognized_platform_authenticator_credential.has_value()); |
| transport_availability_info() |
| .has_recognized_platform_authenticator_credential = |
| static_cast<fido::mac::TouchIdAuthenticator*>(authenticator) |
| ->HasCredentialForGetAssertionRequest(request_); |
| } |
| #endif // defined(OS_MAC) |
| |
| #if BUILDFLAG(IS_CHROMEOS_ASH) |
| if (authenticator->IsChromeOSAuthenticator()) { |
| DCHECK(!transport_availability_info() |
| .has_recognized_platform_authenticator_credential.has_value()); |
| transport_availability_info() |
| .has_recognized_platform_authenticator_credential = |
| static_cast<ChromeOSAuthenticator*>(authenticator) |
| ->HasCredentialForGetAssertionRequest(request_); |
| } |
| #endif // BUILDFLAG(IS_CHROMEOS_ASH) |
| |
| FidoRequestHandlerBase::AuthenticatorAdded(discovery, authenticator); |
| } |
| |
| void GetAssertionRequestHandler::AuthenticatorRemoved( |
| FidoDiscoveryBase* discovery, |
| FidoAuthenticator* authenticator) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(my_sequence_checker_); |
| |
| auth_token_requester_map_.erase(authenticator); |
| |
| FidoRequestHandlerBase::AuthenticatorRemoved(discovery, authenticator); |
| |
| if (authenticator == selected_authenticator_for_pin_uv_auth_token_) { |
| selected_authenticator_for_pin_uv_auth_token_ = nullptr; |
| // Authenticator could have been removed during PIN entry or PIN fallback |
| // after failed internal UV. Bail and show an error. |
| if (state_ != State::kFinished) { |
| state_ = State::kFinished; |
| std::move(completion_callback_) |
| .Run(GetAssertionStatus::kAuthenticatorRemovedDuringPINEntry, |
| base::nullopt, nullptr); |
| } |
| } |
| } |
| |
| void GetAssertionRequestHandler::AuthenticatorSelectedForPINUVAuthToken( |
| FidoAuthenticator* authenticator) { |
| DCHECK_EQ(state_, State::kWaitingForTouch); |
| state_ = State::kWaitingForToken; |
| selected_authenticator_for_pin_uv_auth_token_ = authenticator; |
| |
| base::EraseIf(auth_token_requester_map_, [authenticator](auto& entry) { |
| return entry.first != authenticator; |
| }); |
| CancelActiveAuthenticators(authenticator->GetId()); |
| } |
| |
| void GetAssertionRequestHandler::CollectPIN(pin::PINEntryReason reason, |
| pin::PINEntryError error, |
| uint32_t min_pin_length, |
| int attempts, |
| ProvidePINCallback provide_pin_cb) { |
| DCHECK_EQ(state_, State::kWaitingForToken); |
| observer()->CollectPIN({.reason = reason, |
| .error = error, |
| .min_pin_length = min_pin_length, |
| .attempts = attempts}, |
| std::move(provide_pin_cb)); |
| } |
| |
| void GetAssertionRequestHandler::PromptForInternalUVRetry(int attempts) { |
| DCHECK(state_ == State::kWaitingForTouch || |
| state_ == State::kWaitingForToken); |
| observer()->OnRetryUserVerification(attempts); |
| } |
| |
| void GetAssertionRequestHandler::HavePINUVAuthTokenResultForAuthenticator( |
| FidoAuthenticator* authenticator, |
| AuthTokenRequester::Result result, |
| base::Optional<pin::TokenResponse> token_response) { |
| base::Optional<GetAssertionStatus> error; |
| switch (result) { |
| case AuthTokenRequester::Result::kPreTouchUnsatisfiableRequest: |
| case AuthTokenRequester::Result::kPreTouchAuthenticatorResponseInvalid: |
| FIDO_LOG(ERROR) << "Ignoring AuthTokenRequester::Result=" |
| << static_cast<int>(result) << " from " |
| << authenticator->GetId(); |
| return; |
| case AuthTokenRequester::Result::kPostTouchAuthenticatorInternalUVLock: |
| error = GetAssertionStatus::kAuthenticatorMissingUserVerification; |
| break; |
| case AuthTokenRequester::Result::kPostTouchAuthenticatorResponseInvalid: |
| error = GetAssertionStatus::kAuthenticatorResponseInvalid; |
| break; |
| case AuthTokenRequester::Result::kPostTouchAuthenticatorOperationDenied: |
| error = GetAssertionStatus::kUserConsentDenied; |
| break; |
| case AuthTokenRequester::Result::kPostTouchAuthenticatorPINSoftLock: |
| error = GetAssertionStatus::kSoftPINBlock; |
| break; |
| case AuthTokenRequester::Result::kPostTouchAuthenticatorPINHardLock: |
| error = GetAssertionStatus::kHardPINBlock; |
| break; |
| case AuthTokenRequester::Result::kSuccess: |
| break; |
| } |
| |
| // Pre touch events should be handled above. |
| DCHECK_EQ(state_, State::kWaitingForToken); |
| DCHECK_EQ(selected_authenticator_for_pin_uv_auth_token_, authenticator); |
| if (error) { |
| state_ = State::kFinished; |
| std::move(completion_callback_).Run(*error, base::nullopt, authenticator); |
| return; |
| } |
| |
| DCHECK_EQ(result, AuthTokenRequester::Result::kSuccess); |
| |
| auto request = std::make_unique<CtapGetAssertionRequest>(request_); |
| SpecializeRequestForAuthenticator(*request, *authenticator); |
| DispatchRequestWithToken(std::move(*token_response)); |
| } |
| |
| void GetAssertionRequestHandler::ObtainPINUVAuthToken( |
| FidoAuthenticator* authenticator, |
| std::set<pin::Permissions> permissions, |
| bool skip_pin_touch, |
| bool internal_uv_locked) { |
| AuthTokenRequester::Options options; |
| options.token_permissions = std::move(permissions); |
| options.rp_id = request_.rp_id; |
| options.skip_pin_touch = skip_pin_touch; |
| options.internal_uv_locked = internal_uv_locked; |
| |
| auth_token_requester_map_.insert( |
| {authenticator, std::make_unique<AuthTokenRequester>( |
| this, authenticator, std::move(options))}); |
| auth_token_requester_map_.at(authenticator)->ObtainPINUVAuthToken(); |
| } |
| |
| void GetAssertionRequestHandler::HandleResponse( |
| FidoAuthenticator* authenticator, |
| CtapGetAssertionRequest request, |
| base::ElapsedTimer request_timer, |
| CtapDeviceResponseCode status, |
| base::Optional<AuthenticatorGetAssertionResponse> response) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(my_sequence_checker_); |
| |
| if (state_ != State::kWaitingForTouch && |
| state_ != State::kWaitingForResponseWithToken) { |
| FIDO_LOG(DEBUG) << "Ignoring response from " |
| << authenticator->GetDisplayName() |
| << " because no longer waiting for touch"; |
| return; |
| } |
| |
| #if defined(OS_WIN) |
| if (authenticator->IsWinNativeApiAuthenticator()) { |
| state_ = State::kFinished; |
| CancelActiveAuthenticators(authenticator->GetId()); |
| if (status != CtapDeviceResponseCode::kSuccess) { |
| std::move(completion_callback_) |
| .Run(WinCtapDeviceResponseCodeToGetAssertionStatus(status), |
| base::nullopt, authenticator); |
| return; |
| } |
| if (!ResponseValid(*authenticator, request, options_, *response)) { |
| FIDO_LOG(ERROR) << "Failing assertion request due to bad response from " |
| << authenticator->GetDisplayName(); |
| std::move(completion_callback_) |
| .Run(GetAssertionStatus::kWinNotAllowedError, base::nullopt, |
| authenticator); |
| return; |
| } |
| |
| DCHECK(responses_.empty()); |
| responses_.emplace_back(std::move(*response)); |
| std::move(completion_callback_) |
| .Run(WinCtapDeviceResponseCodeToGetAssertionStatus(status), |
| std::move(responses_), authenticator); |
| return; |
| } |
| #endif |
| |
| // If we requested UV from an authentiator without uvToken support, UV failed, |
| // and the authenticator supports PIN, fall back to that. |
| if (request.user_verification != UserVerificationRequirement::kDiscouraged && |
| !request.pin_auth && |
| (status == CtapDeviceResponseCode::kCtap2ErrPinAuthInvalid || |
| status == CtapDeviceResponseCode::kCtap2ErrPinRequired || |
| status == CtapDeviceResponseCode::kCtap2ErrOperationDenied) && |
| authenticator->PINUVDispositionForGetAssertion(request, observer()) == |
| PINUVDisposition::kNoTokenInternalUVPINFallback) { |
| // Authenticators without uvToken support will return this error immediately |
| // without user interaction when internal UV is locked. |
| const base::TimeDelta response_time = request_timer.Elapsed(); |
| if (response_time < kMinExpectedAuthenticatorResponseTime) { |
| FIDO_LOG(DEBUG) << "Authenticator is probably locked, response_time=" |
| << response_time; |
| ObtainPINUVAuthToken( |
| authenticator, GetPinTokenPermissionsFor(*authenticator, request), |
| /*skip_pin_touch=*/false, /*internal_uv_locked=*/true); |
| return; |
| } |
| ObtainPINUVAuthToken(authenticator, |
| GetPinTokenPermissionsFor(*authenticator, request), |
| /*skip_pin_touch=*/true, /*internal_uv_locked=*/true); |
| return; |
| } |
| |
| const base::Optional<GetAssertionStatus> maybe_result = |
| ConvertDeviceResponseCode(status); |
| if (!maybe_result) { |
| if (state_ == State::kWaitingForResponseWithToken) { |
| std::move(completion_callback_) |
| .Run(GetAssertionStatus::kAuthenticatorResponseInvalid, base::nullopt, |
| authenticator); |
| } else { |
| FIDO_LOG(ERROR) << "Ignoring status " << static_cast<int>(status) |
| << " from " << authenticator->GetDisplayName(); |
| } |
| return; |
| } |
| |
| state_ = State::kFinished; |
| CancelActiveAuthenticators(authenticator->GetId()); |
| |
| if (status != CtapDeviceResponseCode::kSuccess) { |
| FIDO_LOG(ERROR) << "Failing assertion request due to status " |
| << static_cast<int>(status) << " from " |
| << authenticator->GetDisplayName(); |
| std::move(completion_callback_) |
| .Run(*maybe_result, base::nullopt, authenticator); |
| return; |
| } |
| |
| if (!response || |
| !ResponseValid(*authenticator, request, options_, *response)) { |
| FIDO_LOG(ERROR) << "Failing assertion request due to bad response from " |
| << authenticator->GetDisplayName(); |
| std::move(completion_callback_) |
| .Run(GetAssertionStatus::kAuthenticatorResponseInvalid, base::nullopt, |
| authenticator); |
| return; |
| } |
| |
| const size_t num_responses = response->num_credentials().value_or(1); |
| if (num_responses == 0 || |
| (num_responses > 1 && !request.allow_list.empty())) { |
| std::move(completion_callback_) |
| .Run(GetAssertionStatus::kAuthenticatorResponseInvalid, base::nullopt, |
| authenticator); |
| return; |
| } |
| |
| DCHECK(responses_.empty()); |
| responses_.emplace_back(std::move(*response)); |
| if (num_responses > 1) { |
| // Multiple responses. Need to read them all. |
| state_ = State::kReadingMultipleResponses; |
| remaining_responses_ = num_responses - 1; |
| authenticator->GetNextAssertion(base::BindOnce( |
| &GetAssertionRequestHandler::HandleNextResponse, |
| weak_factory_.GetWeakPtr(), authenticator, std::move(request))); |
| return; |
| } |
| |
| ReportGetAssertionResponseTransport(authenticator); |
| OnGetAssertionSuccess(authenticator, std::move(request)); |
| } |
| |
| void GetAssertionRequestHandler::HandleNextResponse( |
| FidoAuthenticator* authenticator, |
| CtapGetAssertionRequest request, |
| CtapDeviceResponseCode status, |
| base::Optional<AuthenticatorGetAssertionResponse> response) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(my_sequence_checker_); |
| DCHECK_EQ(State::kReadingMultipleResponses, state_); |
| DCHECK_LT(0u, remaining_responses_); |
| |
| state_ = State::kFinished; |
| if (status != CtapDeviceResponseCode::kSuccess) { |
| FIDO_LOG(ERROR) << "Failing assertion request due to status " |
| << static_cast<int>(status) << " from " |
| << authenticator->GetDisplayName(); |
| std::move(completion_callback_) |
| .Run(GetAssertionStatus::kAuthenticatorResponseInvalid, base::nullopt, |
| authenticator); |
| return; |
| } |
| |
| if (!ResponseValid(*authenticator, request, options_, *response)) { |
| FIDO_LOG(ERROR) << "Failing assertion request due to bad response from " |
| << authenticator->GetDisplayName(); |
| std::move(completion_callback_) |
| .Run(GetAssertionStatus::kAuthenticatorResponseInvalid, base::nullopt, |
| authenticator); |
| return; |
| } |
| |
| DCHECK(!responses_.empty()); |
| responses_.emplace_back(std::move(*response)); |
| remaining_responses_--; |
| if (remaining_responses_ > 0) { |
| state_ = State::kReadingMultipleResponses; |
| authenticator->GetNextAssertion(base::BindOnce( |
| &GetAssertionRequestHandler::HandleNextResponse, |
| weak_factory_.GetWeakPtr(), authenticator, std::move(request))); |
| return; |
| } |
| |
| ReportGetAssertionResponseTransport(authenticator); |
| |
| OnGetAssertionSuccess(authenticator, std::move(request)); |
| } |
| |
| void GetAssertionRequestHandler::TerminateUnsatisfiableRequestPostTouch( |
| FidoAuthenticator* authenticator) { |
| // User touched an authenticator that cannot handle this request or internal |
| // user verification has failed but the authenticator does not support PIN. |
| // The latter should not happen, show an error to the user as well. |
| state_ = State::kFinished; |
| CancelActiveAuthenticators(authenticator->GetId()); |
| std::move(completion_callback_) |
| .Run(GetAssertionStatus::kAuthenticatorMissingUserVerification, |
| base::nullopt, nullptr); |
| } |
| |
| void GetAssertionRequestHandler::DispatchRequestWithToken( |
| pin::TokenResponse token) { |
| DCHECK(selected_authenticator_for_pin_uv_auth_token_); |
| |
| observer()->FinishCollectToken(); |
| pin_token_ = std::move(token); |
| state_ = State::kWaitingForResponseWithToken; |
| CtapGetAssertionRequest request = SpecializeRequestForAuthenticator( |
| request_, *selected_authenticator_for_pin_uv_auth_token_); |
| std::tie(request.pin_protocol, request.pin_auth) = |
| pin_token_->PinAuth(request.client_data_hash); |
| |
| ReportGetAssertionRequestTransport( |
| selected_authenticator_for_pin_uv_auth_token_); |
| |
| auto request_copy(request); |
| selected_authenticator_for_pin_uv_auth_token_->GetAssertion( |
| std::move(request_copy), options_, |
| base::BindOnce(&GetAssertionRequestHandler::HandleResponse, |
| weak_factory_.GetWeakPtr(), |
| selected_authenticator_for_pin_uv_auth_token_, |
| std::move(request), base::ElapsedTimer())); |
| } |
| |
| void GetAssertionRequestHandler::OnGetAssertionSuccess( |
| FidoAuthenticator* authenticator, |
| CtapGetAssertionRequest request) { |
| if (request.large_blob_read || request.large_blob_write) { |
| DCHECK(authenticator->Options()->supports_large_blobs); |
| std::vector<LargeBlobKey> keys; |
| for (const auto& response : responses_) { |
| if (response.large_blob_key()) { |
| keys.emplace_back(*response.large_blob_key()); |
| } |
| } |
| if (!keys.empty()) { |
| if (request.large_blob_read) { |
| authenticator->ReadLargeBlob( |
| keys, pin_token_, |
| base::BindOnce(&GetAssertionRequestHandler::OnReadLargeBlobs, |
| weak_factory_.GetWeakPtr(), authenticator)); |
| return; |
| } |
| DCHECK(request.large_blob_write); |
| DCHECK_EQ(1u, keys.size()); |
| authenticator->WriteLargeBlob( |
| *request.large_blob_write, keys.at(0), pin_token_, |
| base::BindOnce(&GetAssertionRequestHandler::OnWriteLargeBlob, |
| weak_factory_.GetWeakPtr(), authenticator)); |
| return; |
| } |
| } |
| |
| std::move(completion_callback_) |
| .Run(GetAssertionStatus::kSuccess, std::move(responses_), authenticator); |
| } |
| |
| void GetAssertionRequestHandler::OnReadLargeBlobs( |
| FidoAuthenticator* authenticator, |
| CtapDeviceResponseCode status, |
| base::Optional<std::vector<std::pair<LargeBlobKey, std::vector<uint8_t>>>> |
| blobs) { |
| if (status == CtapDeviceResponseCode::kSuccess) { |
| for (auto& response : responses_) { |
| const auto blob = |
| base::ranges::find_if(*blobs, [&response](const auto& pair) { |
| return pair.first == response.large_blob_key(); |
| }); |
| if (blob != blobs->end()) { |
| response.set_large_blob(std::move(blob->second)); |
| } |
| } |
| } else { |
| FIDO_LOG(ERROR) << "Reading large blob failed with code " |
| << static_cast<int>(status); |
| } |
| std::move(completion_callback_) |
| .Run(GetAssertionStatus::kSuccess, std::move(responses_), authenticator); |
| } |
| |
| void GetAssertionRequestHandler::OnWriteLargeBlob( |
| FidoAuthenticator* authenticator, |
| CtapDeviceResponseCode status) { |
| if (status != CtapDeviceResponseCode::kSuccess) { |
| FIDO_LOG(ERROR) << "Writing large blob failed with code " |
| << static_cast<int>(status); |
| } |
| responses_.at(0).set_large_blob_written(status == |
| CtapDeviceResponseCode::kSuccess); |
| std::move(completion_callback_) |
| .Run(GetAssertionStatus::kSuccess, std::move(responses_), authenticator); |
| } |
| |
| } // namespace device |