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

#import <Foundation/Foundation.h>

#include <algorithm>
#include <string>

#include "base/apple/foundation_util.h"
#include "base/apple/osstatus_logging.h"
#include "base/apple/scoped_cftyperef.h"
#include "base/functional/bind.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "components/device_event_log/device_event_log.h"
#include "device/fido/attestation_statement_formats.h"
#include "device/fido/attested_credential_data.h"
#include "device/fido/authenticator_data.h"
#include "device/fido/mac/credential_metadata.h"
#include "device/fido/mac/credential_store.h"
#include "device/fido/mac/util.h"
#include "device/fido/public/features.h"
#include "device/fido/public/fido_constants.h"
#include "device/fido/public/fido_transport_protocol.h"
#include "device/fido/public_key.h"
#include "device/fido/strings/grit/fido_strings.h"
#include "ui/base/l10n/l10n_util.h"

namespace device::fido::mac {

MakeCredentialOperation::MakeCredentialOperation(
    CtapMakeCredentialRequest request,
    TouchIdCredentialStore* credential_store,
    Callback callback)
    : request_(std::move(request)),
      credential_store_(credential_store),
      callback_(std::move(callback)) {}

MakeCredentialOperation::~MakeCredentialOperation() = default;

void MakeCredentialOperation::Run() {
  if (!std::ranges::contains(
          request_.public_key_credential_params.public_key_credential_params(),
          static_cast<int>(CoseAlgorithmIdentifier::kEs256),
          &PublicKeyCredentialParams::CredentialInfo::algorithm)) {
    FIDO_LOG(ERROR) << "No supported algorithm found";
    std::move(callback_).Run(MakeCredentialStatus::kNoCommonAlgorithms,
                             std::nullopt);
    return;
  }

  const bool require_uv = ProfileAuthenticatorWillDoUserVerification(
      request_.user_verification,
      device::fido::mac::DeviceHasBiometricsAvailable());
  if (require_uv) {
    touch_id_context_->PromptTouchId(
        l10n_util::GetStringFUTF16(IDS_WEBAUTHN_TOUCH_ID_PROMPT_REASON,
                                   base::UTF8ToUTF16(request_.rp.id)),
        base::BindOnce(&MakeCredentialOperation::PromptTouchIdDone,
                       base::Unretained(this)));
    return;
  }

  CreateCredential(/*has_uv=*/false);
}

void MakeCredentialOperation::PromptTouchIdDone(bool success) {
  if (!success) {
    std::move(callback_).Run(MakeCredentialStatus::kUserConsentDenied,
                             std::nullopt);
    return;
  }

  // Setting an authentication context authorizes credentials returned from the
  // credential store for signing without triggering yet another Touch ID
  // prompt.
  credential_store_->SetAuthenticationContext(
      touch_id_context_->authentication_context());

  CreateCredential(/*has_uv=*/true);
}

void MakeCredentialOperation::CreateCredential(bool has_uv) {
  if (!request_.exclude_list.empty()) {
    std::optional<std::list<Credential>> credentials =
        credential_store_->FindCredentialsFromCredentialDescriptorList(
            request_.rp.id, request_.exclude_list);
    if (!credentials) {
      FIDO_LOG(ERROR) << "Failed to check for excluded credentials";
      std::move(callback_).Run(
          MakeCredentialStatus::kAuthenticatorResponseInvalid, std::nullopt);
      return;
    }
    if (!credentials->empty()) {
      std::move(callback_).Run(
          MakeCredentialStatus::kUserConsentButCredentialExcluded,
          std::nullopt);
      return;
    }
  }

  // Delete the key pair for this RP + user handle if one already exists.
  if (!credential_store_->DeleteCredentialsForUserId(request_.rp.id,
                                                     request_.user.id)) {
    FIDO_LOG(ERROR) << "DeleteCredentialsForUserId() failed";
    std::move(callback_).Run(
        MakeCredentialStatus::kAuthenticatorResponseInvalid, std::nullopt);
    return;
  }

  // Generate the new key pair.
  //
  // New credentials are always discoverable. But older non-discoverable
  // credentials may exist.
  std::optional<std::pair<Credential, base::apple::ScopedCFTypeRef<SecKeyRef>>>
      credential_result = credential_store_->CreateCredential(
          request_.rp.id, request_.user, TouchIdCredentialStore::kDiscoverable);
  if (!credential_result) {
    FIDO_LOG(ERROR) << "CreateCredential() failed";
    std::move(callback_).Run(
        MakeCredentialStatus::kAuthenticatorResponseInvalid, std::nullopt);
    return;
  }
  auto [credential, sec_key_ref] = std::move(*credential_result);

  // Create attestation object. There is no separate attestation key pair, so
  // we perform self-attestation.
  std::optional<AttestedCredentialData> attested_credential_data =
      MakeAttestedCredentialData(credential.credential_id,
                                 SecKeyRefToECPublicKey(sec_key_ref.get()));
  if (!attested_credential_data) {
    FIDO_LOG(ERROR) << "MakeAttestedCredentialData failed";
    std::move(callback_).Run(
        MakeCredentialStatus::kAuthenticatorResponseInvalid, std::nullopt);
    return;
  }
  AuthenticatorData authenticator_data = MakeAuthenticatorData(
      credential.metadata.sign_counter_type, request_.rp.id,
      std::move(*attested_credential_data), has_uv);
  std::optional<std::vector<uint8_t>> signature =
      GenerateSignature(authenticator_data, request_.client_data_hash,
                        credential.private_key.get());
  if (!signature) {
    FIDO_LOG(ERROR) << "MakeSignature failed";
    std::move(callback_).Run(
        MakeCredentialStatus::kAuthenticatorResponseInvalid, std::nullopt);
    return;
  }
  AuthenticatorMakeCredentialResponse response(
      FidoTransportProtocol::kInternal,
      AttestationObject(
          std::move(authenticator_data),
          std::make_unique<PackedAttestationStatement>(
              CoseAlgorithmIdentifier::kEs256, std::move(*signature),
              /*x509_certificates=*/std::vector<std::vector<uint8_t>>())));
  // New credentials are always discoverable.
  response.is_resident_key = true;
  response.transports.emplace();
  response.transports->insert(FidoTransportProtocol::kInternal);
  std::move(callback_).Run(MakeCredentialStatus::kSuccess, std::move(response));
}

}  // namespace device::fido::mac
