#include "device/fido/get_assertion_task.h"
#include <algorithm>
#include <utility>
#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "device/base/features.h"
#include "device/fido/authenticator_get_assertion_response.h"
#include "device/fido/ctap2_device_operation.h"
#include "device/fido/make_credential_task.h"
#include "device/fido/pin.h"
#include "device/fido/u2f_sign_operation.h"
namespace device {
namespace {
bool MayFallbackToU2fWithAppIdExtension(
const FidoDevice& device,
const CtapGetAssertionRequest& request) {
bool ctap2_device_supports_u2f =
device.device_info() &&
base::Contains(device.device_info()->versions, ProtocolVersion::kU2f);
return request.alternative_application_parameter &&
ctap2_device_supports_u2f && !request.allow_list.empty();
}
bool SetResponseCredential(
AuthenticatorGetAssertionResponse* response,
const std::vector<PublicKeyCredentialDescriptor>& allow_list) {
if (response->credential) {
if (!allow_list.empty() &&
!base::Contains(allow_list, response->credential->id,
&PublicKeyCredentialDescriptor::id)) {
return false;
}
return true;
}
if (allow_list.size() != 1) {
return false;
}
response->credential = allow_list[0];
return true;
}
bool HasCredentialSpecificPRFInputs(const CtapGetAssertionOptions& options) {
const size_t num = options.prf_inputs.size();
return num > 1 ||
(num == 1 && options.prf_inputs[0].credential_id.has_value());
}
const PRFInput* GetDefaultPRFInput(const CtapGetAssertionOptions& options) {
if (options.prf_inputs.empty() ||
options.prf_inputs[0].credential_id.has_value()) {
return nullptr;
}
return &options.prf_inputs[0];
}
const PRFInput* GetPRFInputForCredential(const CtapGetAssertionOptions& options,
const std::vector<uint8_t>& id) {
for (const auto& prf_input : options.prf_inputs) {
if (prf_input.credential_id == id) {
return &prf_input;
}
}
return GetDefaultPRFInput(options);
}
}
GetAssertionTask::GetAssertionTask(FidoDevice* device,
CtapGetAssertionRequest request,
CtapGetAssertionOptions options,
GetAssertionTaskCallback callback)
: FidoTask(device),
request_(std::move(request)),
options_(std::move(options)),
callback_(std::move(callback)) {
DCHECK(request_.user_presence_required);
DCHECK_NE(request_.user_verification,
UserVerificationRequirement::kPreferred);
}
GetAssertionTask::~GetAssertionTask() = default;
void GetAssertionTask::Cancel() {
canceled_ = true;
if (sign_operation_) {
sign_operation_->Cancel();
}
if (dummy_register_operation_) {
dummy_register_operation_->Cancel();
}
}
bool GetAssertionTask::StringFixupPredicate(
const std::vector<const cbor::Value*>& path) {
if (path.size() != 2 || !path[0]->is_unsigned() ||
path[0]->GetUnsigned() != 4 || !path[1]->is_string()) {
return false;
}
const std::string& user_key = path[1]->GetString();
return user_key == "name" || user_key == "displayName";
}
void GetAssertionTask::StartTask() {
if (device()->supported_protocol() == ProtocolVersion::kCtap2) {
GetAssertion();
} else {
DCHECK_EQ(device()->supported_protocol() == ProtocolVersion::kCtap2,
device()->device_info().has_value());
device()->set_supported_protocol(ProtocolVersion::kU2f);
U2fSign();
}
}
CtapGetAssertionRequest GetAssertionTask::NextSilentRequest() {
DCHECK(current_allow_list_batch_ < allow_list_batches_.size());
CtapGetAssertionRequest request = request_;
request.allow_list = allow_list_batches_.at(current_allow_list_batch_++);
request.user_presence_required = false;
request.user_verification = UserVerificationRequirement::kDiscouraged;
return request;
}
void GetAssertionTask::GetAssertion() {
if (request_.allow_list.empty()) {
MaybeSetPRFParameters(&request_, GetDefaultPRFInput(options_));
sign_operation_ = std::make_unique<Ctap2DeviceOperation<
CtapGetAssertionRequest, AuthenticatorGetAssertionResponse>>(
device(), request_,
base::BindOnce(&GetAssertionTask::HandleResponse,
weak_factory_.GetWeakPtr(), request_.allow_list),
base::BindOnce(&ReadCTAPGetAssertionResponse,
device()->DeviceTransport()),
StringFixupPredicate);
sign_operation_->Start();
return;
}
allow_list_batches_ =
FilterAndBatchCredentialDescriptors(request_.allow_list, *device());
DCHECK(!allow_list_batches_.empty());
if (allow_list_batches_.size() == 1 && allow_list_batches_[0].empty()) {
dummy_register_operation_ = std::make_unique<Ctap2DeviceOperation<
CtapMakeCredentialRequest, AuthenticatorMakeCredentialResponse>>(
device(), MakeCredentialTask::GetTouchRequest(device()),
base::BindOnce(&GetAssertionTask::HandleDummyMakeCredentialComplete,
weak_factory_.GetWeakPtr()),
base::BindOnce(&ReadCTAPMakeCredentialResponse,
device()->DeviceTransport()),
nullptr);
dummy_register_operation_->Start();
return;
}
if (allow_list_batches_.size() == 1 &&
!MayFallbackToU2fWithAppIdExtension(*device(), request_) &&
!HasCredentialSpecificPRFInputs(options_)) {
CtapGetAssertionRequest request = request_;
request.allow_list = allow_list_batches_.front();
MaybeSetPRFParameters(&request, GetDefaultPRFInput(options_));
sign_operation_ = std::make_unique<Ctap2DeviceOperation<
CtapGetAssertionRequest, AuthenticatorGetAssertionResponse>>(
device(), std::move(request),
base::BindOnce(&GetAssertionTask::HandleResponse,
weak_factory_.GetWeakPtr(), request.allow_list),
base::BindOnce(&ReadCTAPGetAssertionResponse,
device()->DeviceTransport()),
StringFixupPredicate);
sign_operation_->Start();
return;
}
sign_operation_ =
std::make_unique<Ctap2DeviceOperation<CtapGetAssertionRequest,
AuthenticatorGetAssertionResponse>>(
device(), NextSilentRequest(),
base::BindOnce(&GetAssertionTask::HandleResponseToSilentRequest,
weak_factory_.GetWeakPtr()),
base::BindOnce(&ReadCTAPGetAssertionResponse,
device()->DeviceTransport()),
nullptr);
sign_operation_->Start();
}
void GetAssertionTask::U2fSign() {
DCHECK_EQ(ProtocolVersion::kU2f, device()->supported_protocol());
sign_operation_ = std::make_unique<U2fSignOperation>(
device(), request_,
base::BindOnce(&GetAssertionTask::MaybeRevertU2fFallbackAndInvokeCallback,
weak_factory_.GetWeakPtr()));
sign_operation_->Start();
}
void GetAssertionTask::HandleResponse(
std::vector<PublicKeyCredentialDescriptor> allow_list,
CtapDeviceResponseCode response_code,
absl::optional<AuthenticatorGetAssertionResponse> response_data) {
if (canceled_) {
return;
}
if (response_code == CtapDeviceResponseCode::kCtap2ErrInvalidCredential) {
dummy_register_operation_ = std::make_unique<Ctap2DeviceOperation<
CtapMakeCredentialRequest, AuthenticatorMakeCredentialResponse>>(
device(), MakeCredentialTask::GetTouchRequest(device()),
base::BindOnce(&GetAssertionTask::HandleDummyMakeCredentialComplete,
weak_factory_.GetWeakPtr()),
base::BindOnce(&ReadCTAPMakeCredentialResponse,
device()->DeviceTransport()),
nullptr);
dummy_register_operation_->Start();
return;
}
if (response_code == CtapDeviceResponseCode::kSuccess) {
if (response_data->user_selected && !allow_list.empty()) {
return LogAndFail(
"Assertion response has userSelected for non-empty allowList");
}
if (!SetResponseCredential(&response_data.value(), allow_list)) {
return LogAndFail(
"Assertion response has invalid credential information");
}
const absl::optional<cbor::Value>& extensions_cbor =
response_data->authenticator_data.extensions();
if (extensions_cbor) {
const cbor::Value::MapValue& extensions = extensions_cbor->GetMap();
auto it = extensions.find(cbor::Value(kExtensionHmacSecret));
if (it != extensions.end()) {
if (!hmac_secret_request_ || !it->second.is_bytestring()) {
return LogAndFail("Unexpected or invalid hmac-secret extension");
}
if (response_data->hmac_secret.has_value()) {
return LogAndFail(
"Assertion response has both hmac-secret and prf extensions");
}
absl::optional<std::vector<uint8_t>> plaintext =
hmac_secret_request_->Decrypt(it->second.GetBytestring());
if (!plaintext) {
return LogAndFail("Failed to decrypt hmac-secret extension");
}
response_data->hmac_secret = std::move(plaintext.value());
}
}
}
std::move(callback_).Run(response_code, std::move(response_data));
}
void GetAssertionTask::HandleResponseToSilentRequest(
CtapDeviceResponseCode response_code,
absl::optional<AuthenticatorGetAssertionResponse> response_data) {
DCHECK(request_.allow_list.size() > 0);
DCHECK(allow_list_batches_.size() > 0);
DCHECK(0 < current_allow_list_batch_ &&
current_allow_list_batch_ <= allow_list_batches_.size());
if (canceled_) {
return;
}
if (response_code == CtapDeviceResponseCode::kSuccess &&
SetResponseCredential(
&response_data.value(),
allow_list_batches_.at(current_allow_list_batch_ - 1))) {
CtapGetAssertionRequest request = request_;
const PublicKeyCredentialDescriptor& matching_credential =
*response_data->credential;
request.allow_list = {matching_credential};
MaybeSetPRFParameters(
&request, GetPRFInputForCredential(options_, matching_credential.id));
sign_operation_ = std::make_unique<Ctap2DeviceOperation<
CtapGetAssertionRequest, AuthenticatorGetAssertionResponse>>(
device(), std::move(request),
base::BindOnce(&GetAssertionTask::HandleResponse,
weak_factory_.GetWeakPtr(), request.allow_list),
base::BindOnce(&ReadCTAPGetAssertionResponse,
device()->DeviceTransport()),
nullptr);
sign_operation_->Start();
return;
}
if (current_allow_list_batch_ < allow_list_batches_.size()) {
sign_operation_ = std::make_unique<Ctap2DeviceOperation<
CtapGetAssertionRequest, AuthenticatorGetAssertionResponse>>(
device(), NextSilentRequest(),
base::BindOnce(&GetAssertionTask::HandleResponseToSilentRequest,
weak_factory_.GetWeakPtr()),
base::BindOnce(&ReadCTAPGetAssertionResponse,
device()->DeviceTransport()),
nullptr);
sign_operation_->Start();
return;
}
if (MayFallbackToU2fWithAppIdExtension(*device(), request_)) {
device()->set_supported_protocol(ProtocolVersion::kU2f);
U2fSign();
return;
}
dummy_register_operation_ = std::make_unique<Ctap2DeviceOperation<
CtapMakeCredentialRequest, AuthenticatorMakeCredentialResponse>>(
device(), MakeCredentialTask::GetTouchRequest(device()),
base::BindOnce(&GetAssertionTask::HandleDummyMakeCredentialComplete,
weak_factory_.GetWeakPtr()),
base::BindOnce(&ReadCTAPMakeCredentialResponse,
device()->DeviceTransport()),
nullptr);
dummy_register_operation_->Start();
}
void GetAssertionTask::HandleDummyMakeCredentialComplete(
CtapDeviceResponseCode response_code,
absl::optional<AuthenticatorMakeCredentialResponse> response_data) {
std::move(callback_).Run(CtapDeviceResponseCode::kCtap2ErrNoCredentials,
absl::nullopt);
}
void GetAssertionTask::MaybeSetPRFParameters(CtapGetAssertionRequest* request,
const PRFInput* maybe_inputs) {
if (maybe_inputs == nullptr) {
return;
}
hmac_secret_request_ = std::make_unique<pin::HMACSecretRequest>(
*request->pin_protocol, *options_.pin_key_agreement, maybe_inputs->salt1,
maybe_inputs->salt2);
request->hmac_secret.emplace(hmac_secret_request_->public_key_x962,
hmac_secret_request_->encrypted_salts,
hmac_secret_request_->salts_auth);
}
void GetAssertionTask::MaybeRevertU2fFallbackAndInvokeCallback(
CtapDeviceResponseCode status,
absl::optional<AuthenticatorGetAssertionResponse> response) {
DCHECK_EQ(ProtocolVersion::kU2f, device()->supported_protocol());
if (device()->device_info()) {
device()->set_supported_protocol(ProtocolVersion::kCtap2);
}
std::move(callback_).Run(status, std::move(response));
}
void GetAssertionTask::LogAndFail(const char* error) {
FIDO_LOG(DEBUG) << error;
std::move(callback_).Run(CtapDeviceResponseCode::kCtap2ErrOther,
absl::nullopt);
}
}