// 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_device_authenticator.h"

#include <algorithm>
#include <numeric>
#include <utility>

#include "base/containers/contains.h"
#include "base/containers/cxx20_erase_vector.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/ranges/algorithm.h"
#include "base/task/sequenced_task_runner.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "components/cbor/values.h"
#include "device/fido/appid_exclude_probe_task.h"
#include "device/fido/authenticator_get_assertion_response.h"
#include "device/fido/authenticator_supported_options.h"
#include "device/fido/credential_management.h"
#include "device/fido/ctap2_device_operation.h"
#include "device/fido/ctap_authenticator_selection_request.h"
#include "device/fido/ctap_get_assertion_request.h"
#include "device/fido/ctap_make_credential_request.h"
#include "device/fido/fido_constants.h"
#include "device/fido/fido_device.h"
#include "device/fido/fido_parsing_utils.h"
#include "device/fido/fido_types.h"
#include "device/fido/get_assertion_task.h"
#include "device/fido/large_blob.h"
#include "device/fido/make_credential_task.h"
#include "device/fido/pin.h"
#include "device/fido/u2f_command_constructor.h"

namespace device {

using ClientPinAvailability =
    AuthenticatorSupportedOptions::ClientPinAvailability;
using UserVerificationAvailability =
    AuthenticatorSupportedOptions::UserVerificationAvailability;

namespace {

// Helper method for determining correct bio enrollment version.
BioEnrollmentRequest::Version GetBioEnrollmentRequestVersion(
    const AuthenticatorSupportedOptions& options) {
  DCHECK(options.bio_enrollment_availability_preview !=
             AuthenticatorSupportedOptions::BioEnrollmentAvailability::
                 kNotSupported ||
         options.bio_enrollment_availability !=
             AuthenticatorSupportedOptions::BioEnrollmentAvailability::
                 kNotSupported);
  return options.bio_enrollment_availability !=
                 AuthenticatorSupportedOptions::BioEnrollmentAvailability::
                     kNotSupported
             ? BioEnrollmentRequest::kDefault
             : BioEnrollmentRequest::kPreview;
}

CredentialManagementRequest::Version GetCredentialManagementRequestVersion(
    const AuthenticatorSupportedOptions& options) {
  DCHECK(options.supports_credential_management_preview ||
         options.supports_credential_management);
  return options.supports_credential_management
             ? CredentialManagementRequest::kDefault
             : CredentialManagementRequest::kPreview;
}

}  // namespace

FidoDeviceAuthenticator::FidoDeviceAuthenticator(
    std::unique_ptr<FidoDevice> device)
    : device_(std::move(device)) {}
FidoDeviceAuthenticator::~FidoDeviceAuthenticator() = default;

void FidoDeviceAuthenticator::InitializeAuthenticator(
    base::OnceClosure callback) {
  base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE,
      base::BindOnce(
          &FidoDevice::DiscoverSupportedProtocolAndDeviceInfo,
          device()->GetWeakPtr(),
          base::BindOnce(&FidoDeviceAuthenticator::InitializeAuthenticatorDone,
                         weak_factory_.GetWeakPtr(), std::move(callback))));
}

void FidoDeviceAuthenticator::InitializeAuthenticatorDone(
    base::OnceClosure callback) {
  DCHECK(!initialized_);
  initialized_ = true;
  switch (device_->supported_protocol()) {
    case ProtocolVersion::kU2f:
      // U2F devices always "support" enterprise attestation because it turns
      // into a bit in the makeCredential command that is ignored if not
      // supported.
      options_.enterprise_attestation = true;
      break;
    case ProtocolVersion::kCtap2:
      DCHECK(device_->device_info()) << "uninitialized device";
      options_ = device_->device_info()->options;
      if (device_->device_info()->pin_protocols) {
        DCHECK(!device_->device_info()->pin_protocols->empty());
        // Choose the highest supported version.
        chosen_pin_uv_auth_protocol_ =
            *(device_->device_info()->pin_protocols->end() - 1);
      }
      // The hmac-secret extension involves encrypting the values passed back
      // and forth, thus there must be a valid PIN protocol.
      options_.supports_hmac_secret &= chosen_pin_uv_auth_protocol_.has_value();
      break;
    case ProtocolVersion::kUnknown:
      NOTREACHED() << "uninitialized device";
  }
  std::move(callback).Run();
}

void FidoDeviceAuthenticator::ExcludeAppIdCredentialsBeforeMakeCredential(
    CtapMakeCredentialRequest request,
    MakeCredentialOptions options,
    base::OnceCallback<void(CtapDeviceResponseCode, absl::optional<bool>)>
        callback) {
  // If the device (or request) is U2F-only then |MakeCredential| will handle
  // the AppID-excluded credentials, if any. There's no interaction with PUATs
  // to worry about because U2F doesn't have them.
  //
  // (If the device is AlwaysUV then it should still support up=false requests
  // without a PUAT, so they aren't excluded here.)
  if (!MakeCredentialTask::WillUseCTAP2(device_.get(), request, options) ||
      device_->NoSilentRequests()) {
    std::move(callback).Run(CtapDeviceResponseCode::kSuccess, absl::nullopt);
    return;
  }

  // This is a CTAP2 device. In CTAP 2.1, a PUAT is invalidated if a request is
  // made with a different RP ID, even if the PUAT isn't used on that request.
  // Therefore appidExclude probing has to happen before the PUAT is obtained.
  // For CTAP 2.0 devices we follow the same pattern, even though a PIN token
  // doesn't have that issue.
  RunTask<AppIdExcludeProbeTask, bool, CtapMakeCredentialRequest,
          MakeCredentialOptions>(std::move(request), std::move(options),
                                 std::move(callback));
}

void FidoDeviceAuthenticator::MakeCredential(
    CtapMakeCredentialRequest request,
    MakeCredentialOptions request_options,
    MakeCredentialCallback callback) {
  // If the authenticator has UV configured then UV will be required in
  // order to create a credential (as specified by CTAP 2.0), even if
  // user-verification is "discouraged".
  if (!request.pin_auth &&
      options_.user_verification_availability ==
          UserVerificationAvailability::kSupportedAndConfigured &&
      !options_.make_cred_uv_not_required) {
    request.user_verification = UserVerificationRequirement::kRequired;
  } else {
    request.user_verification = UserVerificationRequirement::kDiscouraged;
  }

  RunTask<MakeCredentialTask, AuthenticatorMakeCredentialResponse,
          CtapMakeCredentialRequest, MakeCredentialOptions>(
      std::move(request), std::move(request_options), std::move(callback));
}

void FidoDeviceAuthenticator::GetAssertion(CtapGetAssertionRequest request,
                                           CtapGetAssertionOptions options,
                                           GetAssertionCallback callback) {
  if (options.pin_uv_auth_token) {
    std::tie(request.pin_protocol, request.pin_auth) =
        options.pin_uv_auth_token->PinAuth(request.client_data_hash);
  }

  large_blob_.reset();
  large_blob_read_ = false;
  if (options_.large_blob_type == LargeBlobSupportType::kExtension) {
    if (options.large_blob_read) {
      request.large_blob_extension_read = true;
    }
  } else if (options.large_blob_read || options.large_blob_write) {
    DCHECK(options_.large_blob_type == LargeBlobSupportType::kKey);
    request.large_blob_key = true;
    large_blob_read_ = options.large_blob_read;
  }

  if (options.large_blob_write) {
    // This copy is done because `options` is also moved in this call. While
    // moving the object would hopefully not reallocate member buffers, that's
    // not guaranteed and this is simpler than worrying about it.
    const std::vector<uint8_t> large_blob_data = *options.large_blob_write;
    data_decoder_.Deflate(
        large_blob_data,
        base::BindOnce(
            &FidoDeviceAuthenticator::OnHaveCompressedLargeBlobForGetAssertion,
            weak_factory_.GetWeakPtr(), std::move(request), std::move(options),
            std::move(callback), large_blob_data.size()));
  } else {
    MaybeGetEphemeralKeyForGetAssertion(std::move(request), std::move(options),
                                        std::move(callback));
  }
}

void FidoDeviceAuthenticator::OnHaveCompressedLargeBlobForGetAssertion(
    CtapGetAssertionRequest request,
    CtapGetAssertionOptions options,
    GetAssertionCallback callback,
    size_t original_size,
    base::expected<mojo_base::BigBuffer, std::string> result) {
  if (!result.has_value()) {
    LogLargeBlobResult(LargeBlobKeyWriteResult::kCompressionError);
    FIDO_LOG(ERROR) << "Failed to compress large blob: " << result.error();
  } else {
    // If the authenticator supports the largeBlob extension then the blob is
    // sent directly in the request. Otherwise it's saved in `large_blob_` to
    // be written after the request, using the result of the `largeBlobKey`
    // extension.
    absl::optional<LargeBlob>* destination;
    if (options_.large_blob_type == LargeBlobSupportType::kExtension) {
      destination = &request.large_blob_extension_write;
    } else {
      DCHECK(request.large_blob_key);
      destination = &large_blob_;
    }
    destination->emplace(fido_parsing_utils::Materialize(result.value()),
                         original_size);
  }

  MaybeGetEphemeralKeyForGetAssertion(std::move(request), std::move(options),
                                      std::move(callback));
}

void FidoDeviceAuthenticator::MaybeGetEphemeralKeyForGetAssertion(
    CtapGetAssertionRequest request,
    CtapGetAssertionOptions options,
    GetAssertionCallback callback) {
  if (!options.prf_inputs.empty()) {
    GetEphemeralKey(base::BindOnce(
        &FidoDeviceAuthenticator::OnHaveEphemeralKeyForGetAssertion,
        weak_factory_.GetWeakPtr(), std::move(request), std::move(options),
        std::move(callback)));
    return;
  }

  DoGetAssertion(std::move(request), std::move(options), std::move(callback));
}

void FidoDeviceAuthenticator::OnHaveEphemeralKeyForGetAssertion(
    CtapGetAssertionRequest request,
    CtapGetAssertionOptions options,
    GetAssertionCallback callback,
    CtapDeviceResponseCode status,
    absl::optional<pin::KeyAgreementResponse> key) {
  if (status != CtapDeviceResponseCode::kSuccess) {
    std::move(callback).Run(status, {});
    return;
  }
  options.pin_key_agreement = std::move(*key);
  if (!request.pin_protocol) {
    // If `chosen_pin_uv_auth_protocol_` is `nullopt` then hmac_secret support
    // isn't advertised and the caller should never have requested it.
    DCHECK(chosen_pin_uv_auth_protocol_);
    request.pin_protocol = chosen_pin_uv_auth_protocol_;
  }
  DoGetAssertion(std::move(request), std::move(options), std::move(callback));
}

void FidoDeviceAuthenticator::DoGetAssertion(CtapGetAssertionRequest request,
                                             CtapGetAssertionOptions options,
                                             GetAssertionCallback callback) {
  if (!request.pin_auth &&
      options_.user_verification_availability ==
          UserVerificationAvailability::kSupportedAndConfigured &&
      request.user_verification != UserVerificationRequirement::kDiscouraged) {
    request.user_verification = UserVerificationRequirement::kRequired;
  } else {
    request.user_verification = UserVerificationRequirement::kDiscouraged;
  }

  CtapGetAssertionRequest request_copy(request);
  CtapGetAssertionOptions options_copy(options);
  RunTask<GetAssertionTask, AuthenticatorGetAssertionResponse,
          CtapGetAssertionRequest, CtapGetAssertionOptions>(
      std::move(request), std::move(options),
      base::BindOnce(&FidoDeviceAuthenticator::OnHaveNextAssertion,
                     weak_factory_.GetWeakPtr(), std::move(request_copy),
                     std::move(options_copy),
                     std::vector<AuthenticatorGetAssertionResponse>{},
                     std::move(callback)));
}

void FidoDeviceAuthenticator::OnHaveNextAssertion(
    CtapGetAssertionRequest request,
    CtapGetAssertionOptions options,
    std::vector<AuthenticatorGetAssertionResponse> responses,
    GetAssertionCallback callback,
    CtapDeviceResponseCode status,
    absl::optional<AuthenticatorGetAssertionResponse> response) {
  if (status != CtapDeviceResponseCode::kSuccess) {
    std::move(callback).Run(status, {});
    return;
  }
  responses.emplace_back(std::move(*response));
  uint8_t num_responses = responses.at(0).num_credentials.value_or(1u);
  if (num_responses == 0 ||
      (num_responses > 1 && !request.allow_list.empty())) {
    std::move(callback).Run(CtapDeviceResponseCode::kCtap2ErrInvalidCBOR, {});
    return;
  }
  if (responses.size() >= num_responses) {
    PerformGetAssertionLargeBlobOperation(
        std::move(request), std::move(options), std::move(responses),
        std::move(callback));
    return;
  }
  // Read the next response.
  RunOperation<CtapGetNextAssertionRequest, AuthenticatorGetAssertionResponse>(
      CtapGetNextAssertionRequest(),
      base::BindOnce(&FidoDeviceAuthenticator::OnHaveNextAssertion,
                     weak_factory_.GetWeakPtr(), std::move(request),
                     std::move(options), std::move(responses),
                     std::move(callback)),
      base::BindOnce(&ReadCTAPGetAssertionResponse, device_->DeviceTransport()),
      GetAssertionTask::StringFixupPredicate);
}

void FidoDeviceAuthenticator::PerformGetAssertionLargeBlobOperation(
    CtapGetAssertionRequest request,
    CtapGetAssertionOptions options,
    std::vector<AuthenticatorGetAssertionResponse> responses,
    GetAssertionCallback callback) {
  // Only a single response is supported when using the largeBlob extension
  // because we assume that only large authenticators will implement largeBlobs
  // that way and they do internal account selection.
  if (responses.size() == 1 && responses[0].large_blob_extension) {
    if (!request.large_blob_extension_read) {
      std::move(callback).Run(CtapDeviceResponseCode::kCtap2ErrInvalidCBOR, {});
      return;
    }
    LargeBlob large_blob(std::move(*responses[0].large_blob_extension));
    data_decoder_.Inflate(
        std::move(large_blob.compressed_data), large_blob.original_size,
        base::BindOnce(
            &FidoDeviceAuthenticator::OnLargeBlobExtensionUncompressed,
            weak_factory_.GetWeakPtr(), std::move(responses),
            std::move(callback)));
    return;
  }
  if (responses.size() == 1 && responses[0].large_blob_written &&
      !request.large_blob_extension_write) {
    std::move(callback).Run(CtapDeviceResponseCode::kCtap2ErrInvalidCBOR, {});
    return;
  }
  if (large_blob_) {
    DCHECK(options_.large_blob_type == LargeBlobSupportType::kKey);
    DCHECK_EQ(responses.size(), 1u);
    if (!responses.at(0).large_blob_key) {
      LogLargeBlobResult(LargeBlobKeyWriteResult::kCredentialHasNoLargeBlobKey);
      std::move(callback).Run(CtapDeviceResponseCode::kSuccess,
                              std::move(responses));
      return;
    }
    LargeBlobKey large_blob_key = *responses.at(0).large_blob_key;
    DCHECK(large_blob_);
    FetchLargeBlobArray(
        LargeBlobArrayReader(),
        base::BindOnce(
            &FidoDeviceAuthenticator::OnHaveLargeBlobArrayForWrite,
            weak_factory_.GetWeakPtr(), large_blob_key,
            options.pin_uv_auth_token,
            base::BindOnce(
                &FidoDeviceAuthenticator::OnWroteLargeBlobForGetAssertion,
                weak_factory_.GetWeakPtr(), std::move(responses),
                std::move(callback))));
    return;
  }
  if (large_blob_read_) {
    DCHECK(options_.large_blob_type == LargeBlobSupportType::kKey);
    std::vector<LargeBlobKey> keys;
    for (const auto& assertion : responses) {
      if (assertion.large_blob_key) {
        keys.emplace_back(*assertion.large_blob_key);
      }
    }
    if (keys.empty()) {
      std::move(callback).Run(CtapDeviceResponseCode::kSuccess,
                              std::move(responses));
      return;
    }
    ReadLargeBlob(
        keys,
        base::BindOnce(&FidoDeviceAuthenticator::OnReadLargeBlobForGetAssertion,
                       weak_factory_.GetWeakPtr(), std::move(responses),
                       std::move(callback)));
    return;
  }
  std::move(callback).Run(CtapDeviceResponseCode::kSuccess,
                          std::move(responses));
}

void FidoDeviceAuthenticator::GetTouch(base::OnceClosure callback) {
  if (device()->device_info() &&
      device()->device_info()->SupportsAtLeast(Ctap2Version::kCtap2_1)) {
    RunOperation<CtapAuthenticatorSelectionRequest, pin::EmptyResponse>(
        CtapAuthenticatorSelectionRequest(),
        base::BindOnce(
            [](std::string authenticator_id, base::OnceClosure callback,
               CtapDeviceResponseCode status,
               absl::optional<pin::EmptyResponse> _) {
              if (status == CtapDeviceResponseCode::kSuccess) {
                std::move(callback).Run();
                return;
              }
              FIDO_LOG(DEBUG) << "Ignoring status " << static_cast<int>(status)
                              << " from " << authenticator_id;
            },
            GetId(), std::move(callback)),
        base::BindOnce(&pin::EmptyResponse::Parse));
    return;
  }
  MakeCredential(
      MakeCredentialTask::GetTouchRequest(device()), MakeCredentialOptions(),
      base::BindOnce(
          [](std::string authenticator_id, base::OnceCallback<void()> callback,
             CtapDeviceResponseCode status,
             absl::optional<AuthenticatorMakeCredentialResponse>) {
            // If the device didn't understand/process the request it may
            // fail immediately. Rather than count that as a touch, ignore
            // those cases completely.
            if (status == CtapDeviceResponseCode::kSuccess ||
                status == CtapDeviceResponseCode::kCtap2ErrPinNotSet ||
                status == CtapDeviceResponseCode::kCtap2ErrPinInvalid ||
                status == CtapDeviceResponseCode::kCtap2ErrPinAuthInvalid) {
              std::move(callback).Run();
              return;
            }
            FIDO_LOG(DEBUG) << "Ignoring status " << static_cast<int>(status)
                            << " from " << authenticator_id;
          },
          GetId(), std::move(callback)));
}

void FidoDeviceAuthenticator::GetPinRetries(GetRetriesCallback callback) {
  DCHECK(options_.client_pin_availability !=
         ClientPinAvailability::kNotSupported);
  DCHECK(chosen_pin_uv_auth_protocol_);

  RunOperation<pin::PinRetriesRequest, pin::RetriesResponse>(
      pin::PinRetriesRequest{*chosen_pin_uv_auth_protocol_},
      std::move(callback),
      base::BindOnce(&pin::RetriesResponse::ParsePinRetries));
}

void FidoDeviceAuthenticator::GetEphemeralKey(
    GetEphemeralKeyCallback callback) {
  DCHECK(options_.client_pin_availability !=
             ClientPinAvailability::kNotSupported ||
         options_.supports_pin_uv_auth_token || options_.supports_hmac_secret);
  DCHECK(chosen_pin_uv_auth_protocol_);

  RunOperation<pin::KeyAgreementRequest, pin::KeyAgreementResponse>(
      pin::KeyAgreementRequest{*chosen_pin_uv_auth_protocol_},
      std::move(callback), base::BindOnce(&pin::KeyAgreementResponse::Parse));
}

void FidoDeviceAuthenticator::GetPINToken(
    std::string pin,
    std::vector<pin::Permissions> permissions,
    absl::optional<std::string> rp_id,
    GetTokenCallback callback) {
  DCHECK(options_.client_pin_availability !=
         ClientPinAvailability::kNotSupported);
  DCHECK_NE(permissions.size(), 0u);
  DCHECK(!((base::Contains(permissions, pin::Permissions::kMakeCredential)) ||
           base::Contains(permissions, pin::Permissions::kGetAssertion)) ||
         rp_id);

  GetEphemeralKey(base::BindOnce(
      &FidoDeviceAuthenticator::OnHaveEphemeralKeyForGetPINToken,
      weak_factory_.GetWeakPtr(), std::move(pin), std::move(permissions),
      std::move(rp_id), std::move(callback)));
}

void FidoDeviceAuthenticator::OnHaveEphemeralKeyForGetPINToken(
    std::string pin,
    std::vector<pin::Permissions> permissions,
    absl::optional<std::string> rp_id,
    GetTokenCallback callback,
    CtapDeviceResponseCode status,
    absl::optional<pin::KeyAgreementResponse> key) {
  if (status != CtapDeviceResponseCode::kSuccess) {
    std::move(callback).Run(status, absl::nullopt);
    return;
  }

  if (options_.supports_pin_uv_auth_token) {
    pin::PinTokenWithPermissionsRequest request(*chosen_pin_uv_auth_protocol_,
                                                pin, *key, permissions, rp_id);
    std::vector<uint8_t> shared_key = request.shared_key();
    RunOperation<pin::PinTokenWithPermissionsRequest, pin::TokenResponse>(
        std::move(request), std::move(callback),
        base::BindOnce(&pin::TokenResponse::Parse,
                       *chosen_pin_uv_auth_protocol_, std::move(shared_key)));
    return;
  }

  pin::PinTokenRequest request(*chosen_pin_uv_auth_protocol_, pin, *key);
  std::vector<uint8_t> shared_key = request.shared_key();
  RunOperation<pin::PinTokenRequest, pin::TokenResponse>(
      std::move(request), std::move(callback),
      base::BindOnce(&pin::TokenResponse::Parse, *chosen_pin_uv_auth_protocol_,
                     std::move(shared_key)));
}

void FidoDeviceAuthenticator::SetPIN(const std::string& pin,
                                     SetPINCallback callback) {
  DCHECK(options_.client_pin_availability !=
         ClientPinAvailability::kNotSupported);

  GetEphemeralKey(base::BindOnce(
      &FidoDeviceAuthenticator::OnHaveEphemeralKeyForSetPIN,
      weak_factory_.GetWeakPtr(), std::move(pin), std::move(callback)));
}

void FidoDeviceAuthenticator::OnHaveEphemeralKeyForSetPIN(
    std::string pin,
    SetPINCallback callback,
    CtapDeviceResponseCode status,
    absl::optional<pin::KeyAgreementResponse> key) {
  if (status != CtapDeviceResponseCode::kSuccess) {
    std::move(callback).Run(status, absl::nullopt);
    return;
  }

  RunOperation<pin::SetRequest, pin::EmptyResponse>(
      pin::SetRequest(*chosen_pin_uv_auth_protocol_, pin, *key),
      std::move(callback), base::BindOnce(&pin::EmptyResponse::Parse));
}

void FidoDeviceAuthenticator::ChangePIN(const std::string& old_pin,
                                        const std::string& new_pin,
                                        SetPINCallback callback) {
  DCHECK(options_.client_pin_availability !=
         ClientPinAvailability::kNotSupported);

  GetEphemeralKey(
      base::BindOnce(&FidoDeviceAuthenticator::OnHaveEphemeralKeyForChangePIN,
                     weak_factory_.GetWeakPtr(), std::move(old_pin),
                     std::move(new_pin), std::move(callback)));
}

void FidoDeviceAuthenticator::OnHaveEphemeralKeyForChangePIN(
    std::string old_pin,
    std::string new_pin,
    SetPINCallback callback,
    CtapDeviceResponseCode status,
    absl::optional<pin::KeyAgreementResponse> key) {
  if (status != CtapDeviceResponseCode::kSuccess) {
    std::move(callback).Run(status, absl::nullopt);
    return;
  }

  RunOperation<pin::ChangeRequest, pin::EmptyResponse>(
      pin::ChangeRequest(*chosen_pin_uv_auth_protocol_, old_pin, new_pin, *key),
      std::move(callback), base::BindOnce(&pin::EmptyResponse::Parse));
}

FidoAuthenticator::PINUVDisposition
FidoDeviceAuthenticator::PINUVDispositionForMakeCredential(
    const CtapMakeCredentialRequest& request,
    const FidoRequestHandlerBase::Observer* observer) {
  DCHECK(initialized_);

  const bool can_collect_pin = observer && observer->SupportsPIN();
  const bool pin_supported =
      options_.client_pin_availability != ClientPinAvailability::kNotSupported;
  const bool uv_supported = options_.user_verification_availability !=
                            UserVerificationAvailability::kNotSupported;
  const bool pin_configured = options_.client_pin_availability ==
                              ClientPinAvailability::kSupportedAndPinSet;
  const bool uv_configured =
      options_.user_verification_availability ==
      UserVerificationAvailability::kSupportedAndConfigured;

  // CTAP 2.0 requires a PIN for credential creation once a PIN has been set.
  // Thus, if fallback to U2F isn't possible, a PIN will be needed if set.
  const bool u2f_fallback_possible =
      device()->device_info() &&
      device()->device_info()->versions.contains(ProtocolVersion::kU2f) &&
      IsConvertibleToU2fRegisterCommand(request) &&
      !ShouldPreferCTAP2EvenIfItNeedsAPIN(request);

  // CTAP 2.1 authenticators on the other hand can indicate that they allow
  // credential creation with PIN or UV.
  const bool can_make_ctap2_credential_without_uv =
      request.user_verification == UserVerificationRequirement::kDiscouraged &&
      options_.make_cred_uv_not_required;

  const UserVerificationRequirement uv_requirement =
      (pin_configured && !u2f_fallback_possible &&
       !can_make_ctap2_credential_without_uv)
          ? UserVerificationRequirement::kRequired
          : request.user_verification;

  if (uv_requirement == UserVerificationRequirement::kDiscouraged ||
      (uv_requirement == UserVerificationRequirement::kPreferred &&
       ((!pin_configured || !can_collect_pin) && !uv_configured &&
        // The hmac-secret extension makes uv=preferred "more" preferred so that
        // the HMAC output is stable. Otherwise later configuring UV on the
        // authenticator could cause the hmac-secret outputs to change as a
        // different seed is used for UV and non-UV assertions.
        (!request.hmac_secret || !options_.supports_hmac_secret)))) {
    if (!pin_supported && !uv_supported) {
      return PINUVDisposition::kUVNotSupportedNorRequired;
    }
    return PINUVDisposition::kNoUVRequired;
  }

  // Authenticators with built-in UV that don't support UV token should try
  // sending the request as-is with uv=true first.
  if (uv_configured && !CanGetUvToken()) {
    return (can_collect_pin && pin_supported)
               ? PINUVDisposition::kNoTokenInternalUVPINFallback
               : PINUVDisposition::kNoTokenInternalUV;
  }

  const bool can_get_token =
      (can_collect_pin && pin_supported) || CanGetUvToken();

  if (can_get_token) {
    return PINUVDisposition::kGetToken;
  }

  if (uv_requirement == UserVerificationRequirement::kPreferred) {
    return PINUVDisposition::kNoUVRequired;
  }

  return PINUVDisposition::kUnsatisfiable;
}

FidoAuthenticator::PINUVDisposition
FidoDeviceAuthenticator::PINUVDispositionForGetAssertion(
    const CtapGetAssertionRequest& request,
    const FidoRequestHandlerBase::Observer* observer) {
  // TODO(crbug.com/1149405): GetAssertion requests don't allow in-line UV
  // enrollment. Perhaps we should change this and align with MakeCredential
  // behavior.
  const bool can_collect_pin = observer && observer->SupportsPIN();
  const bool pin_supported =
      options_.client_pin_availability != ClientPinAvailability::kNotSupported;
  const bool uv_supported = options_.user_verification_availability !=
                            UserVerificationAvailability::kNotSupported;
  const bool pin_configured = options_.client_pin_availability ==
                              ClientPinAvailability::kSupportedAndPinSet;

  const bool uv_configured =
      options_.user_verification_availability ==
      UserVerificationAvailability::kSupportedAndConfigured;

  const UserVerificationRequirement uv_requirement =
      request.allow_list.empty() ? UserVerificationRequirement::kRequired
                                 : request.user_verification;

  if (uv_requirement == UserVerificationRequirement::kDiscouraged ||
      (uv_requirement == UserVerificationRequirement::kPreferred &&
       ((!pin_configured || !can_collect_pin) && !uv_configured))) {
    if (!pin_supported && !uv_supported) {
      return PINUVDisposition::kUVNotSupportedNorRequired;
    }
    return PINUVDisposition::kNoUVRequired;
  }

  // Authenticators with built-in UV that don't support UV token should try
  // sending the request as-is with uv=true first.
  if (uv_configured && !CanGetUvToken()) {
    return (can_collect_pin && pin_configured)
               ? PINUVDisposition::kNoTokenInternalUVPINFallback
               : PINUVDisposition::kNoTokenInternalUV;
  }

  if ((can_collect_pin && pin_configured) || CanGetUvToken()) {
    return PINUVDisposition::kGetToken;
  }

  return PINUVDisposition::kUnsatisfiable;
}

void FidoDeviceAuthenticator::GetCredentialsMetadata(
    const pin::TokenResponse& pin_token,
    GetCredentialsMetadataCallback callback) {
  DCHECK(options_.supports_credential_management ||
         options_.supports_credential_management_preview);
  DCHECK(chosen_pin_uv_auth_protocol_ == pin_token.protocol());

  RunOperation<CredentialManagementRequest, CredentialsMetadataResponse>(
      CredentialManagementRequest::ForGetCredsMetadata(
          GetCredentialManagementRequestVersion(options_), pin_token),
      std::move(callback), base::BindOnce(&CredentialsMetadataResponse::Parse));
}

struct FidoDeviceAuthenticator::EnumerateCredentialsState {
  explicit EnumerateCredentialsState(pin::TokenResponse pin_token_)
      : pin_token(pin_token_) {}
  EnumerateCredentialsState(EnumerateCredentialsState&&) = default;
  EnumerateCredentialsState& operator=(EnumerateCredentialsState&&) = default;

  pin::TokenResponse pin_token;

  size_t rp_count = 0;
  size_t current_rp = 0;
  size_t current_rp_credential_count = 0;

  FidoDeviceAuthenticator::EnumerateCredentialsCallback callback;
  std::vector<AggregatedEnumerateCredentialsResponse> responses;
  std::vector<std::array<uint8_t, kRpIdHashLength>> rp_id_hashes;
};

void FidoDeviceAuthenticator::EnumerateCredentials(
    const pin::TokenResponse& pin_token,
    EnumerateCredentialsCallback callback) {
  DCHECK(options_.supports_credential_management ||
         options_.supports_credential_management_preview);
  DCHECK(chosen_pin_uv_auth_protocol_ == pin_token.protocol());

  EnumerateCredentialsState state(pin_token);
  state.callback = std::move(callback);
  RunOperation<CredentialManagementRequest, EnumerateRPsResponse>(
      CredentialManagementRequest::ForEnumerateRPsBegin(
          GetCredentialManagementRequestVersion(options_), pin_token),
      base::BindOnce(&FidoDeviceAuthenticator::OnEnumerateRPsDone,
                     weak_factory_.GetWeakPtr(), std::move(state)),
      base::BindOnce(&EnumerateRPsResponse::Parse, /*expect_rp_count=*/true),
      &EnumerateRPsResponse::StringFixupPredicate);
}

// TaskClearProxy interposes |callback| and resets |task_| before it runs.
template <typename... Args>
void FidoDeviceAuthenticator::TaskClearProxy(
    base::OnceCallback<void(Args...)> callback,
    Args... args) {
  DCHECK(task_);
  DCHECK(!operation_);
  task_.reset();
  std::move(callback).Run(std::forward<Args>(args)...);
}

// OperationClearProxy interposes |callback| and resets |operation_| before it
// runs.
template <typename... Args>
void FidoDeviceAuthenticator::OperationClearProxy(
    base::OnceCallback<void(Args...)> callback,
    Args... args) {
  DCHECK(operation_);
  DCHECK(!task_);
  operation_.reset();
  std::move(callback).Run(std::forward<Args>(args)...);
}

// RunTask starts a |FidoTask| and ensures that |task_| is reset when the given
// callback is called.
template <typename Task, typename Response, typename... RequestArgs>
void FidoDeviceAuthenticator::RunTask(
    RequestArgs&&... request_args,
    base::OnceCallback<void(CtapDeviceResponseCode, absl::optional<Response>)>
        callback) {
  DCHECK(!task_);
  DCHECK(!operation_);
  DCHECK(initialized_);

  task_ = std::make_unique<Task>(
      device_.get(), std::forward<RequestArgs>(request_args)...,
      base::BindOnce(
          &FidoDeviceAuthenticator::TaskClearProxy<CtapDeviceResponseCode,
                                                   absl::optional<Response>>,
          weak_factory_.GetWeakPtr(), std::move(callback)));
}

// RunOperation starts a |Ctap2DeviceOperation| and ensures that |operation_| is
// reset when the given completion callback is called.
template <typename Request, typename Response>
void FidoDeviceAuthenticator::RunOperation(
    Request request,
    base::OnceCallback<void(CtapDeviceResponseCode, absl::optional<Response>)>
        callback,
    base::OnceCallback<
        absl::optional<Response>(const absl::optional<cbor::Value>&)> parser,
    bool (*string_fixup_predicate)(const std::vector<const cbor::Value*>&)) {
  DCHECK(!task_);
  DCHECK(!operation_);
  DCHECK(initialized_);

  operation_ = std::make_unique<Ctap2DeviceOperation<Request, Response>>(
      device_.get(), std::move(request),
      base::BindOnce(&FidoDeviceAuthenticator::OperationClearProxy<
                         CtapDeviceResponseCode, absl::optional<Response>>,
                     weak_factory_.GetWeakPtr(), std::move(callback)),
      std::move(parser), string_fixup_predicate);
  operation_->Start();
}

void FidoDeviceAuthenticator::OnEnumerateRPsDone(
    EnumerateCredentialsState state,
    CtapDeviceResponseCode status,
    absl::optional<EnumerateRPsResponse> response) {
  DCHECK_EQ(state.rp_id_hashes.size(), state.responses.size());
  DCHECK_LE(state.rp_id_hashes.size(), state.rp_count);

  if (status != CtapDeviceResponseCode::kSuccess) {
    std::move(state.callback).Run(status, absl::nullopt);
    return;
  }
  if (state.rp_count == 0) {
    if (response->rp_count == 0) {
      std::move(state.callback).Run(status, std::move(state.responses));
      return;
    }
    state.rp_count = response->rp_count;
  }
  DCHECK(response->rp);
  DCHECK(response->rp_id_hash);

  state.rp_id_hashes.push_back(*response->rp_id_hash);
  state.responses.emplace_back(*response->rp);

  if (state.rp_id_hashes.size() < state.rp_count) {
    // Get the next RP.
    RunOperation<CredentialManagementRequest, EnumerateRPsResponse>(
        CredentialManagementRequest::ForEnumerateRPsGetNext(
            GetCredentialManagementRequestVersion(options_)),
        base::BindOnce(&FidoDeviceAuthenticator::OnEnumerateRPsDone,
                       weak_factory_.GetWeakPtr(), std::move(state)),
        base::BindOnce(&EnumerateRPsResponse::Parse, /*expect_rp_count=*/false),
        &EnumerateRPsResponse::StringFixupPredicate);
    return;
  }

  auto request = CredentialManagementRequest::ForEnumerateCredentialsBegin(
      GetCredentialManagementRequestVersion(options_), state.pin_token,
      state.rp_id_hashes.front());
  RunOperation<CredentialManagementRequest, EnumerateCredentialsResponse>(
      std::move(request),
      base::BindOnce(&FidoDeviceAuthenticator::OnEnumerateCredentialsDone,
                     weak_factory_.GetWeakPtr(), std::move(state)),
      base::BindOnce(&EnumerateCredentialsResponse::Parse,
                     /*expect_credential_count=*/true),
      &EnumerateCredentialsResponse::StringFixupPredicate);
}

void FidoDeviceAuthenticator::OnEnumerateCredentialsDone(
    EnumerateCredentialsState state,
    CtapDeviceResponseCode status,
    absl::optional<EnumerateCredentialsResponse> response) {
  DCHECK_EQ(state.rp_id_hashes.size(), state.responses.size());
  DCHECK_EQ(state.rp_id_hashes.size(), state.rp_count);
  DCHECK_LT(state.current_rp, state.rp_count);

  if (status != CtapDeviceResponseCode::kSuccess) {
    std::move(state.callback).Run(status, absl::nullopt);
    return;
  }

  if (state.current_rp_credential_count == 0) {
    // First credential for this RP.
    DCHECK_GT(response->credential_count, 0u);
    state.current_rp_credential_count = response->credential_count;
  }
  AggregatedEnumerateCredentialsResponse& current_aggregated_response =
      state.responses.at(state.current_rp);
  current_aggregated_response.credentials.push_back(std::move(*response));

  if (current_aggregated_response.credentials.size() <
      state.current_rp_credential_count) {
    // Fetch the next credential for this RP.
    RunOperation<CredentialManagementRequest, EnumerateCredentialsResponse>(
        CredentialManagementRequest::ForEnumerateCredentialsGetNext(
            GetCredentialManagementRequestVersion(options_)),
        base::BindOnce(&FidoDeviceAuthenticator::OnEnumerateCredentialsDone,
                       weak_factory_.GetWeakPtr(), std::move(state)),
        base::BindOnce(&EnumerateCredentialsResponse::Parse,
                       /*expect_credential_count=*/false),
        &EnumerateCredentialsResponse::StringFixupPredicate);
    return;
  }

  if (++state.current_rp < state.rp_count) {
    // Enumerate credentials for the next RP.
    state.current_rp_credential_count = 0;
    auto request = CredentialManagementRequest::ForEnumerateCredentialsBegin(
        GetCredentialManagementRequestVersion(options_), state.pin_token,
        state.rp_id_hashes.at(state.current_rp));
    RunOperation<CredentialManagementRequest, EnumerateCredentialsResponse>(
        std::move(request),
        base::BindOnce(&FidoDeviceAuthenticator::OnEnumerateCredentialsDone,
                       weak_factory_.GetWeakPtr(), std::move(state)),
        base::BindOnce(&EnumerateCredentialsResponse::Parse,
                       /*expect_credential_count=*/true),
        &EnumerateCredentialsResponse::StringFixupPredicate);
    return;
  }

  std::move(state.callback)
      .Run(CtapDeviceResponseCode::kSuccess, std::move(state.responses));
  return;
}

void FidoDeviceAuthenticator::DeleteCredential(
    const pin::TokenResponse& pin_token,
    const PublicKeyCredentialDescriptor& credential_id,
    DeleteCredentialCallback callback) {
  DCHECK(options_.supports_credential_management ||
         options_.supports_credential_management_preview);
  DCHECK(chosen_pin_uv_auth_protocol_ == pin_token.protocol());

  RunOperation<CredentialManagementRequest, DeleteCredentialResponse>(
      CredentialManagementRequest::ForDeleteCredential(
          GetCredentialManagementRequestVersion(options_), pin_token,
          credential_id),
      std::move(callback), base::BindOnce(&DeleteCredentialResponse::Parse),
      /*string_fixup_predicate=*/nullptr);
}

bool FidoDeviceAuthenticator::SupportsUpdateUserInformation() const {
  return device_->device_info() &&
         device_->device_info()->SupportsAtLeast(Ctap2Version::kCtap2_1);
}

void FidoDeviceAuthenticator::UpdateUserInformation(
    const pin::TokenResponse& pin_token,
    const PublicKeyCredentialDescriptor& credential_id,
    const PublicKeyCredentialUserEntity& updated_user,
    UpdateUserInformationCallback callback) {
  DCHECK(options_.supports_credential_management ||
         options_.supports_credential_management_preview);
  DCHECK(chosen_pin_uv_auth_protocol_ == pin_token.protocol());

  RunOperation<CredentialManagementRequest, UpdateUserInformationResponse>(
      CredentialManagementRequest::ForUpdateUserInformation(
          GetCredentialManagementRequestVersion(options_), pin_token,
          credential_id, updated_user),
      std::move(callback),
      base::BindOnce(&UpdateUserInformationResponse::Parse),
      /*string_fixup_predicate=*/nullptr);
}

void FidoDeviceAuthenticator::GetModality(BioEnrollmentCallback callback) {
  RunOperation<BioEnrollmentRequest, BioEnrollmentResponse>(
      BioEnrollmentRequest::ForGetModality(
          GetBioEnrollmentRequestVersion(options_)),
      std::move(callback), base::BindOnce(&BioEnrollmentResponse::Parse));
}

void FidoDeviceAuthenticator::GetSensorInfo(BioEnrollmentCallback callback) {
  RunOperation<BioEnrollmentRequest, BioEnrollmentResponse>(
      BioEnrollmentRequest::ForGetSensorInfo(
          GetBioEnrollmentRequestVersion(options_)),
      std::move(callback), base::BindOnce(&BioEnrollmentResponse::Parse));
}

void FidoDeviceAuthenticator::BioEnrollFingerprint(
    const pin::TokenResponse& pin_token,
    absl::optional<std::vector<uint8_t>> template_id,
    BioEnrollmentCallback callback) {
  DCHECK(chosen_pin_uv_auth_protocol_ == pin_token.protocol());

  RunOperation<BioEnrollmentRequest, BioEnrollmentResponse>(
      template_id
          ? BioEnrollmentRequest::ForEnrollNextSample(
                GetBioEnrollmentRequestVersion(options_), std::move(pin_token),
                std::move(*template_id))
          : BioEnrollmentRequest::ForEnrollBegin(
                GetBioEnrollmentRequestVersion(options_), std::move(pin_token)),
      std::move(callback), base::BindOnce(&BioEnrollmentResponse::Parse));
}

void FidoDeviceAuthenticator::BioEnrollRename(
    const pin::TokenResponse& pin_token,
    std::vector<uint8_t> id,
    std::string name,
    BioEnrollmentCallback callback) {
  DCHECK(chosen_pin_uv_auth_protocol_ == pin_token.protocol());

  RunOperation<BioEnrollmentRequest, BioEnrollmentResponse>(
      BioEnrollmentRequest::ForRename(GetBioEnrollmentRequestVersion(options_),
                                      pin_token, std::move(id),
                                      std::move(name)),
      std::move(callback), base::BindOnce(&BioEnrollmentResponse::Parse));
}

void FidoDeviceAuthenticator::BioEnrollDelete(
    const pin::TokenResponse& pin_token,
    std::vector<uint8_t> template_id,
    BioEnrollmentCallback callback) {
  DCHECK(chosen_pin_uv_auth_protocol_ == pin_token.protocol());

  RunOperation<BioEnrollmentRequest, BioEnrollmentResponse>(
      BioEnrollmentRequest::ForDelete(GetBioEnrollmentRequestVersion(options_),
                                      pin_token, std::move(template_id)),
      std::move(callback), base::BindOnce(&BioEnrollmentResponse::Parse));
}

void FidoDeviceAuthenticator::BioEnrollCancel(BioEnrollmentCallback callback) {
  RunOperation<BioEnrollmentRequest, BioEnrollmentResponse>(
      BioEnrollmentRequest::ForCancel(GetBioEnrollmentRequestVersion(options_)),
      std::move(callback), base::BindOnce(&BioEnrollmentResponse::Parse));
}

void FidoDeviceAuthenticator::BioEnrollEnumerate(
    const pin::TokenResponse& pin_token,
    BioEnrollmentCallback callback) {
  DCHECK(chosen_pin_uv_auth_protocol_ == pin_token.protocol());

  RunOperation<BioEnrollmentRequest, BioEnrollmentResponse>(
      BioEnrollmentRequest::ForEnumerate(
          GetBioEnrollmentRequestVersion(options_), std::move(pin_token)),
      std::move(callback), base::BindOnce(&BioEnrollmentResponse::Parse));
}

void FidoDeviceAuthenticator::OnWroteLargeBlobForGetAssertion(
    std::vector<AuthenticatorGetAssertionResponse> responses,
    GetAssertionCallback callback,
    CtapDeviceResponseCode status) {
  switch (status) {
    case CtapDeviceResponseCode::kSuccess:
      LogLargeBlobResult(LargeBlobKeyWriteResult::kSuccess);
      break;
    case CtapDeviceResponseCode::kCtap2ErrRequestTooLarge:
      LogLargeBlobResult(LargeBlobKeyWriteResult::kNotEnoughSpace);
      break;
    default:
      LogLargeBlobResult(LargeBlobKeyWriteResult::kCtapError);
  }
  responses.at(0).large_blob_written =
      status == CtapDeviceResponseCode::kSuccess;
  std::move(callback).Run(CtapDeviceResponseCode::kSuccess,
                          std::move(responses));
}

void FidoDeviceAuthenticator::OnReadLargeBlobForGetAssertion(
    std::vector<AuthenticatorGetAssertionResponse> responses,
    GetAssertionCallback callback,
    CtapDeviceResponseCode status,
    absl::optional<std::vector<std::pair<LargeBlobKey, LargeBlob>>> blobs) {
  if (status != CtapDeviceResponseCode::kSuccess) {
    FIDO_LOG(ERROR) << "Reading large blob failed with code "
                    << static_cast<int>(status);
    std::move(callback).Run(CtapDeviceResponseCode::kSuccess,
                            std::move(responses));
    return;
  }
  if (blobs->empty()) {
    std::move(callback).Run(CtapDeviceResponseCode::kSuccess,
                            std::move(responses));
    return;
  }

  std::pair<LargeBlobKey, LargeBlob> next = std::move(blobs->back());
  blobs->pop_back();

  data_decoder_.Inflate(
      std::move(next.second.compressed_data), next.second.original_size,
      base::BindOnce(&FidoDeviceAuthenticator::OnBlobUncompressed,
                     weak_factory_.GetWeakPtr(), std::move(responses),
                     std::move(*blobs), std::move(next.first),
                     std::move(callback)));
}

void FidoDeviceAuthenticator::OnBlobUncompressed(
    std::vector<AuthenticatorGetAssertionResponse> responses,
    std::vector<std::pair<LargeBlobKey, LargeBlob>> blobs,
    LargeBlobKey uncompressed_key,
    GetAssertionCallback callback,
    base::expected<mojo_base::BigBuffer, std::string> result) {
  if (result.has_value()) {
    bool set_blob = false;
    for (auto& response : responses) {
      if (response.large_blob_key == uncompressed_key) {
        response.large_blob = fido_parsing_utils::Materialize(result.value());
        set_blob = true;
        break;
      }
    }
    DCHECK(set_blob);
  } else {
    FIDO_LOG(ERROR) << "Could not uncompress blob: " << result.error();
  }
  if (blobs.empty()) {
    std::move(callback).Run(CtapDeviceResponseCode::kSuccess,
                            std::move(responses));
    return;
  }
  std::pair<LargeBlobKey, LargeBlob> next = std::move(blobs.back());
  blobs.pop_back();
  data_decoder_.Inflate(
      std::move(next.second.compressed_data), next.second.original_size,
      base::BindOnce(&FidoDeviceAuthenticator::OnBlobUncompressed,
                     weak_factory_.GetWeakPtr(), std::move(responses),
                     std::move(blobs), std::move(next.first),
                     std::move(callback)));
}

void FidoDeviceAuthenticator::OnLargeBlobExtensionUncompressed(
    std::vector<AuthenticatorGetAssertionResponse> responses,
    GetAssertionCallback callback,
    base::expected<mojo_base::BigBuffer, std::string> result) {
  DCHECK_EQ(responses.size(), 1u);
  if (result.has_value()) {
    responses.at(0).large_blob =
        fido_parsing_utils::Materialize(result.value());
  } else {
    FIDO_LOG(ERROR) << "Could not uncompress blob: " << result.error();
  }
  std::move(callback).Run(CtapDeviceResponseCode::kSuccess,
                          std::move(responses));
}

void FidoDeviceAuthenticator::ReadLargeBlob(
    const std::vector<LargeBlobKey>& large_blob_keys,
    LargeBlobReadCallback callback) {
  DCHECK(!large_blob_keys.empty());
  FetchLargeBlobArray(
      LargeBlobArrayReader(),
      base::BindOnce(&FidoDeviceAuthenticator::OnHaveLargeBlobArrayForRead,
                     weak_factory_.GetWeakPtr(), large_blob_keys,
                     std::move(callback)));
}

void FidoDeviceAuthenticator::GarbageCollectLargeBlob(
    const pin::TokenResponse& pin_uv_auth_token,
    base::OnceCallback<void(CtapDeviceResponseCode)> callback) {
  EnumerateCredentials(
      pin_uv_auth_token,
      base::BindOnce(
          &FidoDeviceAuthenticator::OnCredentialsEnumeratedForGarbageCollect,
          weak_factory_.GetWeakPtr(), pin_uv_auth_token, std::move(callback)));
}

void FidoDeviceAuthenticator::FetchLargeBlobArray(
    LargeBlobArrayReader large_blob_array_reader,
    base::OnceCallback<void(CtapDeviceResponseCode,
                            absl::optional<LargeBlobArrayReader>)> callback) {
  size_t bytes_to_read = max_large_blob_fragment_length();
  LargeBlobsRequest request =
      LargeBlobsRequest::ForRead(bytes_to_read, large_blob_array_reader.size());
  RunOperation<LargeBlobsRequest, LargeBlobsResponse>(
      std::move(request),
      base::BindOnce(&FidoDeviceAuthenticator::OnReadLargeBlobFragment,
                     weak_factory_.GetWeakPtr(), bytes_to_read,
                     std::move(large_blob_array_reader), std::move(callback)),
      base::BindOnce(&LargeBlobsResponse::ParseForRead, bytes_to_read));
}

void FidoDeviceAuthenticator::OnReadLargeBlobFragment(
    const size_t bytes_requested,
    LargeBlobArrayReader large_blob_array_reader,
    base::OnceCallback<void(CtapDeviceResponseCode,
                            absl::optional<LargeBlobArrayReader>)> callback,
    CtapDeviceResponseCode status,
    absl::optional<LargeBlobsResponse> response) {
  if (status != CtapDeviceResponseCode::kSuccess) {
    std::move(callback).Run(status, absl::nullopt);
    return;
  }

  DCHECK(response && response->config());
  large_blob_array_reader.Append(*response->config());

  if (response->config()->size() == bytes_requested) {
    // More data may be available, read the next fragment.
    FetchLargeBlobArray(std::move(large_blob_array_reader),
                        std::move(callback));
    return;
  }

  std::move(callback).Run(CtapDeviceResponseCode::kSuccess,
                          std::move(large_blob_array_reader));
}

void FidoDeviceAuthenticator::OnHaveLargeBlobArrayForWrite(
    const LargeBlobKey& large_blob_key,
    const absl::optional<pin::TokenResponse> pin_uv_auth_token,
    base::OnceCallback<void(CtapDeviceResponseCode)> callback,
    CtapDeviceResponseCode status,
    absl::optional<LargeBlobArrayReader> large_blob_array_reader) {
  if (status != CtapDeviceResponseCode::kSuccess) {
    std::move(callback).Run(status);
    return;
  }

  absl::optional<cbor::Value::ArrayValue> large_blob_array =
      large_blob_array_reader->Materialize();
  if (!large_blob_array) {
    FIDO_LOG(ERROR) << "Large blob array corrupted. Replacing with a new one";
    large_blob_array.emplace();
  }

  auto existing_large_blob = base::ranges::find_if(
      *large_blob_array, [&large_blob_key](const cbor::Value& blob_cbor) {
        absl::optional<LargeBlobData> blob = LargeBlobData::Parse(blob_cbor);
        return blob && blob->Decrypt(large_blob_key).has_value();
      });

  cbor::Value new_blob =
      LargeBlobData(large_blob_key, std::move(*large_blob_)).AsCBOR();
  large_blob_.reset();

  if (existing_large_blob != large_blob_array->end()) {
    *existing_large_blob = std::move(new_blob);
  } else {
    large_blob_array->emplace_back(std::move(new_blob));
  }

  LargeBlobArrayWriter writer(std::move(*large_blob_array));
  if (writer.size() >
      device_->device_info()->max_serialized_large_blob_array.value_or(
          kMinLargeBlobSize)) {
    std::move(callback).Run(CtapDeviceResponseCode::kCtap2ErrRequestTooLarge);
    return;
  }

  WriteLargeBlobArray(std::move(pin_uv_auth_token), std::move(writer),
                      std::move(callback));
}

void FidoDeviceAuthenticator::WriteLargeBlobArray(
    const absl::optional<pin::TokenResponse> pin_uv_auth_token,
    LargeBlobArrayWriter large_blob_array_writer,
    base::OnceCallback<void(CtapDeviceResponseCode)> callback) {
  LargeBlobArrayFragment fragment =
      large_blob_array_writer.Pop(max_large_blob_fragment_length());

  LargeBlobsRequest request = LargeBlobsRequest::ForWrite(
      std::move(fragment), large_blob_array_writer.size());
  if (pin_uv_auth_token) {
    DCHECK(chosen_pin_uv_auth_protocol_ == pin_uv_auth_token->protocol());
    request.SetPinParam(*pin_uv_auth_token);
  }
  RunOperation<LargeBlobsRequest, LargeBlobsResponse>(
      std::move(request),
      base::BindOnce(&FidoDeviceAuthenticator::OnWriteLargeBlobFragment,
                     weak_factory_.GetWeakPtr(),
                     std::move(large_blob_array_writer),
                     std::move(pin_uv_auth_token), std::move(callback)),
      base::BindOnce(&LargeBlobsResponse::ParseForWrite));
}

void FidoDeviceAuthenticator::OnWriteLargeBlobFragment(
    LargeBlobArrayWriter large_blob_array_writer,
    const absl::optional<pin::TokenResponse> pin_uv_auth_token,
    base::OnceCallback<void(CtapDeviceResponseCode)> callback,
    CtapDeviceResponseCode status,
    absl::optional<LargeBlobsResponse> response) {
  if (status != CtapDeviceResponseCode::kSuccess) {
    std::move(callback).Run(status);
    return;
  }

  if (large_blob_array_writer.has_remaining_fragments()) {
    WriteLargeBlobArray(std::move(pin_uv_auth_token),
                        std::move(large_blob_array_writer),
                        std::move(callback));
    return;
  }

  std::move(callback).Run(CtapDeviceResponseCode::kSuccess);
}

void FidoDeviceAuthenticator::OnHaveLargeBlobArrayForRead(
    const std::vector<LargeBlobKey>& large_blob_keys,
    LargeBlobReadCallback callback,
    CtapDeviceResponseCode status,
    absl::optional<LargeBlobArrayReader> large_blob_array_reader) {
  if (status != CtapDeviceResponseCode::kSuccess) {
    std::move(callback).Run(status, absl::nullopt);
    return;
  }

  absl::optional<cbor::Value::ArrayValue> large_blob_array =
      large_blob_array_reader->Materialize();
  if (!large_blob_array) {
    std::move(callback).Run(CtapDeviceResponseCode::kCtap2ErrIntegrityFailure,
                            absl::nullopt);
    return;
  }

  std::vector<std::pair<LargeBlobKey, LargeBlob>> result;
  for (const cbor::Value& blob_cbor : *large_blob_array) {
    absl::optional<LargeBlobData> blob = LargeBlobData::Parse(blob_cbor);
    if (!blob.has_value()) {
      continue;
    }
    for (const LargeBlobKey& key : large_blob_keys) {
      absl::optional<LargeBlob> plaintext = blob->Decrypt(key);
      if (plaintext) {
        result.emplace_back(key, std::move(*plaintext));
        break;
      }
    }
  }

  std::move(callback).Run(CtapDeviceResponseCode::kSuccess, std::move(result));
}

void FidoDeviceAuthenticator::OnCredentialsEnumeratedForGarbageCollect(
    const pin::TokenResponse& pin_uv_auth_token,
    base::OnceCallback<void(CtapDeviceResponseCode)> callback,
    CtapDeviceResponseCode status,
    absl::optional<std::vector<AggregatedEnumerateCredentialsResponse>>
        credentials) {
  if (status == CtapDeviceResponseCode::kCtap2ErrNoCredentials) {
    credentials.emplace();
  } else if (status != CtapDeviceResponseCode::kSuccess) {
    std::move(callback).Run(status);
    return;
  }

  FetchLargeBlobArray(
      LargeBlobArrayReader(),
      base::BindOnce(
          &FidoDeviceAuthenticator::OnHaveLargeBlobArrayForGarbageCollect,
          weak_factory_.GetWeakPtr(), std::move(*credentials),
          pin_uv_auth_token, std::move(callback)));
}

void FidoDeviceAuthenticator::OnHaveLargeBlobArrayForGarbageCollect(
    std::vector<AggregatedEnumerateCredentialsResponse> credentials,
    const pin::TokenResponse& pin_uv_auth_token,
    base::OnceCallback<void(CtapDeviceResponseCode)> callback,
    CtapDeviceResponseCode status,
    absl::optional<LargeBlobArrayReader> large_blob_array_reader) {
  if (status != CtapDeviceResponseCode::kSuccess) {
    std::move(callback).Run(status);
    return;
  }

  absl::optional<cbor::Value::ArrayValue> large_blob_array =
      large_blob_array_reader->Materialize();
  if (!large_blob_array) {
    FIDO_LOG(ERROR) << "Large blob array corrupted. Replacing with a new one";
    WriteLargeBlobArray(std::move(pin_uv_auth_token), LargeBlobArrayWriter({}),
                        std::move(callback));
    return;
  }

  std::vector<std::array<uint8_t, kLargeBlobKeyLength>> large_blob_keys;
  for (const auto& cred_by_rp : credentials) {
    for (const auto& credential : cred_by_rp.credentials) {
      if (credential.large_blob_key) {
        large_blob_keys.push_back(*credential.large_blob_key);
      }
    }
  }
  bool did_erase = base::EraseIf(
      *large_blob_array, [&large_blob_keys](const cbor::Value& blob_cbor) {
        absl::optional<LargeBlobData> blob = LargeBlobData::Parse(blob_cbor);
        return blob &&
               base::ranges::none_of(
                   large_blob_keys,
                   [&blob](
                       const std::array<uint8_t, kLargeBlobKeyLength>& key) {
                     return blob->Decrypt(key);
                   });
      });

  if (!did_erase) {
    // No need to update the blob.
    std::move(callback).Run(CtapDeviceResponseCode::kSuccess);
    return;
  }

  LargeBlobArrayWriter writer(std::move(*large_blob_array));
  DCHECK_LE(writer.size(),
            device_->device_info()->max_serialized_large_blob_array.value_or(
                kMinLargeBlobSize));
  WriteLargeBlobArray(std::move(pin_uv_auth_token), std::move(writer),
                      std::move(callback));
}

void FidoDeviceAuthenticator::LogLargeBlobResult(
    LargeBlobKeyWriteResult result) {
  if (options_.large_blob_type == LargeBlobSupportType::kKey) {
    base::UmaHistogramEnumeration("WebAuthentication.LargeBlobKey.WriteResult",
                                  result);
  }
}

absl::optional<base::span<const int32_t>>
FidoDeviceAuthenticator::GetAlgorithms() {
  if (device_->supported_protocol() == ProtocolVersion::kU2f) {
    static constexpr int32_t kU2fAlgorithms[1] = {
        static_cast<int32_t>(CoseAlgorithmIdentifier::kEs256)};
    return kU2fAlgorithms;
  }

  const absl::optional<AuthenticatorGetInfoResponse>& get_info_response =
      device_->device_info();
  if (get_info_response) {
    return get_info_response->algorithms;
  }
  return absl::nullopt;
}

bool FidoDeviceAuthenticator::DiscoverableCredentialStorageFull() const {
  return device_->device_info()->remaining_discoverable_credentials == 0u;
}

void FidoDeviceAuthenticator::Reset(ResetCallback callback) {
  DCHECK(initialized_);
  RunOperation<pin::ResetRequest, pin::ResetResponse>(
      pin::ResetRequest(), std::move(callback),
      base::BindOnce(&pin::ResetResponse::Parse));
}

void FidoDeviceAuthenticator::Cancel() {
  if (operation_) {
    operation_->Cancel();
  }
  if (task_) {
    task_->Cancel();
  }
}

std::string FidoDeviceAuthenticator::GetId() const {
  return device_->GetId();
}

std::string FidoDeviceAuthenticator::GetDisplayName() const {
  return device_->GetDisplayName();
}

ProtocolVersion FidoDeviceAuthenticator::SupportedProtocol() const {
  DCHECK(initialized_);
  return device_->supported_protocol();
}

const AuthenticatorSupportedOptions& FidoDeviceAuthenticator::Options() const {
  return options_;
}

absl::optional<FidoTransportProtocol>
FidoDeviceAuthenticator::AuthenticatorTransport() const {
  return device_->DeviceTransport();
}

void FidoDeviceAuthenticator::SetTaskForTesting(
    std::unique_ptr<FidoTask> task) {
  task_ = std::move(task);
}

void FidoDeviceAuthenticator::GetUvRetries(GetRetriesCallback callback) {
  DCHECK(options_.user_verification_availability !=
         UserVerificationAvailability::kNotSupported);
  DCHECK(chosen_pin_uv_auth_protocol_);

  RunOperation<pin::UvRetriesRequest, pin::RetriesResponse>(
      pin::UvRetriesRequest{*chosen_pin_uv_auth_protocol_}, std::move(callback),
      base::BindOnce(&pin::RetriesResponse::ParseUvRetries));
}

bool FidoDeviceAuthenticator::CanGetUvToken() {
  return options_.user_verification_availability ==
             AuthenticatorSupportedOptions::UserVerificationAvailability::
                 kSupportedAndConfigured &&
         options_.supports_pin_uv_auth_token;
}

void FidoDeviceAuthenticator::GetUvToken(
    std::vector<pin::Permissions> permissions,
    absl::optional<std::string> rp_id,
    GetTokenCallback callback) {
  GetEphemeralKey(
      base::BindOnce(&FidoDeviceAuthenticator::OnHaveEphemeralKeyForUvToken,
                     weak_factory_.GetWeakPtr(), std::move(rp_id),
                     std::move(permissions), std::move(callback)));
}

uint32_t FidoDeviceAuthenticator::CurrentMinPINLength() {
  return ForcePINChange() ? kMinPinLength : NewMinPINLength();
}

uint32_t FidoDeviceAuthenticator::NewMinPINLength() {
  return device()->device_info()->min_pin_length.value_or(kMinPinLength);
}

bool FidoDeviceAuthenticator::ForcePINChange() {
  return device()->device_info()->force_pin_change.value_or(false);
}

void FidoDeviceAuthenticator::OnHaveEphemeralKeyForUvToken(
    absl::optional<std::string> rp_id,
    std::vector<pin::Permissions> permissions,
    GetTokenCallback callback,
    CtapDeviceResponseCode status,
    absl::optional<pin::KeyAgreementResponse> key) {
  if (status != CtapDeviceResponseCode::kSuccess) {
    std::move(callback).Run(status, absl::nullopt);
    return;
  }

  DCHECK(key);

  pin::UvTokenRequest request(*chosen_pin_uv_auth_protocol_, *key,
                              std::move(rp_id), permissions);
  std::vector<uint8_t> shared_key = request.shared_key();
  RunOperation<pin::UvTokenRequest, pin::TokenResponse>(
      std::move(request), std::move(callback),
      base::BindOnce(&pin::TokenResponse::Parse, *chosen_pin_uv_auth_protocol_,
                     std::move(shared_key)));
}

size_t FidoDeviceAuthenticator::max_large_blob_fragment_length() {
  return device_->device_info()->max_msg_size
             ? *device_->device_info()->max_msg_size -
                   kLargeBlobReadEncodingOverhead
             : kLargeBlobDefaultMaxFragmentLength;
}

base::WeakPtr<FidoAuthenticator> FidoDeviceAuthenticator::GetWeakPtr() {
  return weak_factory_.GetWeakPtr();
}

}  // namespace device