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

#include <array>
#include <string>
#include <utility>

#include "base/bind.h"
#include "base/logging.h"
#include "base/numerics/safe_conversions.h"
#include "base/threading/thread_task_runner_handle.h"
#include "components/cbor/cbor_writer.h"
#include "crypto/ec_private_key.h"
#include "device/fido/authenticator_get_assertion_response.h"
#include "device/fido/authenticator_make_credential_response.h"
#include "device/fido/ctap_get_assertion_request.h"
#include "device/fido/ctap_make_credential_request.h"
#include "device/fido/ec_public_key.h"
#include "device/fido/fido_constants.h"
#include "device/fido/fido_parsing_utils.h"
#include "device/fido/opaque_attestation_statement.h"

namespace device {

namespace {

constexpr std::array<uint8_t, kAaguidLength> kDeviceAaguid = {
    {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04,
     0x05, 0x06, 0x07, 0x08}};

std::vector<uint8_t> ConstructResponse(CtapDeviceResponseCode response_code,
                                       base::span<const uint8_t> data) {
  std::vector<uint8_t> response{base::strict_cast<uint8_t>(response_code)};
  fido_parsing_utils::Append(&response, data);
  return response;
}

void ReturnCtap2Response(
    FidoDevice::DeviceCallback cb,
    CtapDeviceResponseCode response_code,
    base::Optional<base::span<const uint8_t>> data = base::nullopt) {
  base::ThreadTaskRunnerHandle::Get()->PostTask(
      FROM_HERE,
      base::BindOnce(std::move(cb),
                     ConstructResponse(response_code,
                                       data.value_or(std::vector<uint8_t>{}))));
}

bool AreMakeCredentialOptionsValid(const AuthenticatorSupportedOptions& options,
                                   const CtapMakeCredentialRequest& request) {
  if (request.resident_key_supported() && !options.supports_resident_key())
    return false;

  return !request.user_verification_required() ||
         options.user_verification_availability() ==
             AuthenticatorSupportedOptions::UserVerificationAvailability::
                 kSupportedAndConfigured;
}

bool AreGetAssertionOptionsValid(const AuthenticatorSupportedOptions& options,
                                 const CtapGetAssertionRequest& request) {
  if (request.user_presence_required() && !options.user_presence_required())
    return false;

  return request.user_verification() !=
             UserVerificationRequirement::kRequired ||
         options.user_verification_availability() ==
             AuthenticatorSupportedOptions::UserVerificationAvailability::
                 kSupportedAndConfigured;
}

// Checks that whether the received MakeCredential request includes EA256
// algorithm in publicKeyCredParam.
bool AreMakeCredentialParamsValid(const CtapMakeCredentialRequest& request) {
  const auto& params =
      request.public_key_credential_params().public_key_credential_params();
  return std::any_of(
      params.begin(), params.end(), [](const auto& credential_info) {
        return credential_info.algorithm ==
               base::strict_cast<int>(CoseAlgorithmIdentifier::kCoseEs256);
      });
}

std::unique_ptr<ECPublicKey> ConstructECPublicKey(
    std::string public_key_string) {
  DCHECK_EQ(64u, public_key_string.size());

  const auto public_key_x_coordinate =
      base::as_bytes(base::make_span(public_key_string)).first(32);
  const auto public_key_y_coordinate =
      base::as_bytes(base::make_span(public_key_string)).last(32);
  return std::make_unique<ECPublicKey>(
      fido_parsing_utils::kEs256,
      fido_parsing_utils::Materialize(public_key_x_coordinate),
      fido_parsing_utils::Materialize(public_key_y_coordinate));
}

std::vector<uint8_t> ConstructSignatureBuffer(
    const AuthenticatorData& authenticator_data,
    base::span<const uint8_t, kClientDataHashLength> client_data_hash) {
  std::vector<uint8_t> signature_buffer;
  fido_parsing_utils::Append(&signature_buffer,
                             authenticator_data.SerializeToByteArray());
  fido_parsing_utils::Append(&signature_buffer, client_data_hash);
  return signature_buffer;
}

std::vector<uint8_t> ConstructMakeCredentialResponse(
    base::span<const uint8_t> attestation_certificate,
    base::span<const uint8_t> signature,
    AuthenticatorData authenticator_data) {
  cbor::CBORValue::MapValue attestation_map;
  attestation_map.emplace(cbor::CBORValue("alg"), cbor::CBORValue(-7));
  attestation_map.emplace(cbor::CBORValue("sig"), cbor::CBORValue(fido_parsing_utils::Materialize(signature)));

  cbor::CBORValue::ArrayValue certificate_chain;
  certificate_chain.emplace_back(
      fido_parsing_utils::Materialize(attestation_certificate));
  attestation_map.emplace(cbor::CBORValue("x5c"), cbor::CBORValue(std::move(certificate_chain)));
  AuthenticatorMakeCredentialResponse make_credential_response(
      AttestationObject(
          std::move(authenticator_data),
          std::make_unique<OpaqueAttestationStatement>(
              "packed", cbor::CBORValue(std::move(attestation_map)))));
  return GetSerializedCtapDeviceResponse(make_credential_response);
}

std::vector<uint8_t> ConstructGetAssertionResponse(
    AuthenticatorData authenticator_data,
    base::span<const uint8_t> signature,
    base::span<const uint8_t> key_handle) {
  AuthenticatorGetAssertionResponse response(
      std::move(authenticator_data),
      fido_parsing_utils::Materialize(signature));

  response.SetCredential({CredentialType::kPublicKey,
                          fido_parsing_utils::Materialize(key_handle)});
  response.SetNumCredentials(1);
  return GetSerializedCtapDeviceResponse(response);
}

}  // namespace

VirtualCtap2Device::VirtualCtap2Device()
    : VirtualFidoDevice(),
      device_info_(AuthenticatorGetInfoResponse({ProtocolVersion::kCtap},
                                                kDeviceAaguid)),
      weak_factory_(this) {}

VirtualCtap2Device::VirtualCtap2Device(scoped_refptr<State> state)
    : VirtualFidoDevice(std::move(state)),
      device_info_(AuthenticatorGetInfoResponse({ProtocolVersion::kCtap},
                                                kDeviceAaguid)),
      weak_factory_(this) {}

VirtualCtap2Device::~VirtualCtap2Device() = default;

// As all operations for VirtualCtap2Device are synchronous and we do not wait
// for user touch, Cancel command is no-op.
void VirtualCtap2Device::Cancel() {}

void VirtualCtap2Device::DeviceTransact(std::vector<uint8_t> command,
                                        DeviceCallback cb) {
  if (command.empty()) {
    ReturnCtap2Response(std::move(cb), CtapDeviceResponseCode::kCtap2ErrOther);
    return;
  }

  auto cmd_type = command[0];
  const auto request_bytes = base::make_span(command).subspan(1);
  CtapDeviceResponseCode response_code = CtapDeviceResponseCode::kCtap2ErrOther;
  std::vector<uint8_t> response_data;

  switch (static_cast<CtapRequestCommand>(cmd_type)) {
    case CtapRequestCommand::kAuthenticatorGetInfo:
      if (!request_bytes.empty()) {
        ReturnCtap2Response(std::move(cb),
                            CtapDeviceResponseCode::kCtap2ErrOther);
        return;
      }

      response_code = OnAuthenticatorGetInfo(&response_data);
      break;
    case CtapRequestCommand::kAuthenticatorMakeCredential:
      response_code = OnMakeCredential(request_bytes, &response_data);
      break;
    case CtapRequestCommand::kAuthenticatorGetAssertion:
      response_code = OnGetAssertion(request_bytes, &response_data);
      break;
    default:
      break;
  }

  // Call |callback| via the |MessageLoop| because |AuthenticatorImpl| doesn't
  // support callback hairpinning.
  ReturnCtap2Response(std::move(cb), response_code, std::move(response_data));
}

base::WeakPtr<FidoDevice> VirtualCtap2Device::GetWeakPtr() {
  return weak_factory_.GetWeakPtr();
}

void VirtualCtap2Device::SetAuthenticatorSupportedOptions(
    AuthenticatorSupportedOptions options) {
  device_info_.SetOptions(std::move(options));
}

CtapDeviceResponseCode VirtualCtap2Device::OnMakeCredential(
    base::span<const uint8_t> request_bytes,
    std::vector<uint8_t>* response) {
  auto request = ParseCtapMakeCredentialRequest(request_bytes);
  if (!request) {
    DLOG(ERROR) << "Incorrectly formatted MakeCredential request.";
    return CtapDeviceResponseCode::kCtap2ErrOther;
  }

  if (!AreMakeCredentialOptionsValid(device_info_.options(), *request) ||
      !AreMakeCredentialParamsValid(*request)) {
    DLOG(ERROR) << "Virtual CTAP2 device does not support options required by "
                   "the request.";
    return CtapDeviceResponseCode::kCtap2ErrOther;
  }

  // Client pin is not supported.
  if (request->pin_auth()) {
    DLOG(ERROR) << "Virtual CTAP2 device does not support client pin.";
    return CtapDeviceResponseCode::kCtap2ErrPinInvalid;
  }

  // Check for already registered credentials.
  const auto rp_id_hash =
      fido_parsing_utils::CreateSHA256Hash(request->rp().rp_id());
  if (request->exclude_list()) {
    for (const auto& excluded_credential : *request->exclude_list()) {
      if (FindRegistrationData(excluded_credential.id(), rp_id_hash))
        return CtapDeviceResponseCode::kCtap2ErrCredentialExcluded;
    }
  }

  // Create key to register.
  // Note: Non-deterministic, you need to mock this out if you rely on
  // deterministic behavior.
  auto private_key = crypto::ECPrivateKey::Create();
  std::string public_key;
  bool status = private_key->ExportRawPublicKey(&public_key);
  DCHECK(status);

  // Our key handles are simple hashes of the public key.
  auto hash = fido_parsing_utils::CreateSHA256Hash(public_key);
  std::vector<uint8_t> key_handle(hash.begin(), hash.end());
  std::array<uint8_t, 2> sha256_length = {0, crypto::kSHA256Length};

  AttestedCredentialData attested_credential_data(
      kDeviceAaguid, sha256_length, key_handle,
      ConstructECPublicKey(public_key));

  auto authenticator_data = ConstructAuthenticatorData(
      rp_id_hash, 01ul, std::move(attested_credential_data));
  auto sign_buffer =
      ConstructSignatureBuffer(authenticator_data, request->client_data_hash());

  // Sign with attestation key.
  // Note: Non-deterministic, you need to mock this out if you rely on
  // deterministic behavior.
  std::vector<uint8_t> sig;
  std::unique_ptr<crypto::ECPrivateKey> attestation_private_key =
      crypto::ECPrivateKey::CreateFromPrivateKeyInfo(GetAttestationKey());
  status = Sign(attestation_private_key.get(), std::move(sign_buffer), &sig);
  DCHECK(status);

  auto attestation_cert = GenerateAttestationCertificate(
      false /* individual_attestation_requested */);
  if (!attestation_cert) {
    DLOG(ERROR) << "Failed to generate attestation certificate.";
    return CtapDeviceResponseCode::kCtap2ErrOther;
  }

  *response = ConstructMakeCredentialResponse(std::move(*attestation_cert), sig,
                                              std::move(authenticator_data));

  StoreNewKey(rp_id_hash, key_handle, std::move(private_key));
  return CtapDeviceResponseCode::kSuccess;
}

CtapDeviceResponseCode VirtualCtap2Device::OnGetAssertion(
    base::span<const uint8_t> request_bytes,
    std::vector<uint8_t>* response) {
  auto request = ParseCtapGetAssertionRequest(request_bytes);
  if (!request) {
    DLOG(ERROR) << "Incorrectly formatted GetAssertion request.";
    return CtapDeviceResponseCode::kCtap2ErrOther;
  }

  // Resident keys are not supported.
  if (!request->allow_list() || request->allow_list()->empty()) {
    DLOG(ERROR) << "Allowed credential list is empty, but Virtual CTAP2 device "
                   "does not support resident keys.";
    return CtapDeviceResponseCode::kCtap2ErrNoCredentials;
  }

  // Client pin option is not supported.
  if (request->pin_auth()) {
    DLOG(ERROR) << "Virtual CTAP2 device does not support client pin.";
    return CtapDeviceResponseCode::kCtap2ErrOther;
  }

  if (!AreGetAssertionOptionsValid(device_info_.options(), *request)) {
    DLOG(ERROR) << "Unsupported options required from the request.";
    return CtapDeviceResponseCode::kCtap2ErrOther;
  }

  const auto rp_id_hash =
      fido_parsing_utils::CreateSHA256Hash(request->rp_id());

  RegistrationData* found_data = nullptr;
  base::span<const uint8_t> credential_id;
  for (const auto& allowed_credential : *request->allow_list()) {
    if ((found_data =
             FindRegistrationData(allowed_credential.id(), rp_id_hash))) {
      credential_id = allowed_credential.id();
      break;
    }
  }

  if (!found_data)
    return CtapDeviceResponseCode::kCtap2ErrNoCredentials;

  found_data->counter++;
  auto authenticator_data =
      ConstructAuthenticatorData(rp_id_hash, found_data->counter);
  auto signature_buffer =
      ConstructSignatureBuffer(authenticator_data, request->client_data_hash());

  // Sign with the private key of the received key handle.
  std::vector<uint8_t> sig;
  auto* private_key = found_data->private_key.get();
  bool status = Sign(private_key, std::move(signature_buffer), &sig);
  DCHECK(status);

  *response = ConstructGetAssertionResponse(std::move(authenticator_data),
                                            std::move(sig), credential_id);
  return CtapDeviceResponseCode::kSuccess;
}

CtapDeviceResponseCode VirtualCtap2Device::OnAuthenticatorGetInfo(
    std::vector<uint8_t>* response) const {
  *response = EncodeToCBOR(device_info_);
  return CtapDeviceResponseCode::kSuccess;
}

AuthenticatorData VirtualCtap2Device::ConstructAuthenticatorData(
    base::span<const uint8_t, kRpIdHashLength> rp_id_hash,
    uint32_t current_signature_count,
    base::Optional<AttestedCredentialData> attested_credential_data) {
  uint8_t flag =
      base::strict_cast<uint8_t>(AuthenticatorData::Flag::kTestOfUserPresence);
  std::array<uint8_t, kSignCounterLength> signature_counter;

  // Constructing AuthenticatorData for registration operation.
  if (attested_credential_data)
    flag |= base::strict_cast<uint8_t>(AuthenticatorData::Flag::kAttestation);

  signature_counter[0] = (current_signature_count >> 24) & 0xff;
  signature_counter[1] = (current_signature_count >> 16) & 0xff;
  signature_counter[2] = (current_signature_count >> 8) & 0xff;
  signature_counter[3] = (current_signature_count)&0xff;

  return AuthenticatorData(rp_id_hash, flag, signature_counter,
                           std::move(attested_credential_data));
}

}  // namespace device
