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

#include <utility>

#include "base/bind.h"
#include "base/threading/sequenced_task_runner_handle.h"
#include "components/apdu/apdu_response.h"
#include "device/fido/authenticator_get_assertion_response.h"
#include "device/fido/ctap_get_assertion_request.h"
#include "device/fido/device_response_converter.h"
#include "device/fido/fido_device.h"
#include "device/fido/fido_parsing_utils.h"
#include "device/fido/u2f_command_constructor.h"

namespace device {

U2fSignOperation::U2fSignOperation(FidoDevice* device,
                                   const CtapGetAssertionRequest& request,
                                   DeviceResponseCallback callback)
    : DeviceOperation(device, request, std::move(callback)),
      weak_factory_(this) {}

U2fSignOperation::~U2fSignOperation() = default;

void U2fSignOperation::Start() {
  const auto& allow_list = request().allow_list();
  if (allow_list && !allow_list->empty()) {
    const auto it = allow_list->cbegin();
    DispatchDeviceRequest(
        ConvertToU2fSignCommand(request(), ApplicationParameterType::kPrimary,
                                it->id(), true /* is_check_only */),
        base::BindOnce(&U2fSignOperation::OnCheckForKeyHandlePresence,
                       weak_factory_.GetWeakPtr(),
                       ApplicationParameterType::kPrimary, it));
  } else {
    // In order to make U2F authenticators blink on sign request with an empty
    // allow list, we send fake enrollment to the device and error out if the
    // user has provided user presence.
    SendFakeEnrollment();
  }
}

void U2fSignOperation::SendFakeEnrollment() {
  DispatchDeviceRequest(
      ConstructBogusU2fRegistrationCommand(),
      base::BindOnce(&U2fSignOperation::OnSignResponseReceived,
                     weak_factory_.GetWeakPtr(), true /* is_fake_enrollment */,
                     ApplicationParameterType::kPrimary,
                     std::vector<uint8_t>()));
}

void U2fSignOperation::RetrySign(
    bool is_fake_enrollment,
    ApplicationParameterType application_parameter_type,
    const std::vector<uint8_t>& key_handle) {
  auto cmd = is_fake_enrollment
                 ? ConstructBogusU2fRegistrationCommand()
                 : ConvertToU2fSignCommand(
                       request(), application_parameter_type, key_handle);
  DispatchDeviceRequest(
      std::move(cmd),
      base::BindOnce(&U2fSignOperation::OnSignResponseReceived,
                     weak_factory_.GetWeakPtr(), is_fake_enrollment,
                     application_parameter_type, key_handle));
}

void U2fSignOperation::OnSignResponseReceived(
    bool is_fake_enrollment,
    ApplicationParameterType application_parameter_type,
    const std::vector<uint8_t>& key_handle,
    base::Optional<std::vector<uint8_t>> device_response) {
  const auto apdu_response =
      device_response
          ? apdu::ApduResponse::CreateFromMessage(std::move(*device_response))
          : base::nullopt;
  auto return_code = apdu_response ? apdu_response->status()
                                   : apdu::ApduResponse::Status::SW_WRONG_DATA;

  switch (return_code) {
    case apdu::ApduResponse::Status::SW_NO_ERROR: {
      if (is_fake_enrollment) {
        std::move(callback())
            .Run(CtapDeviceResponseCode::kCtap2ErrNoCredentials, base::nullopt);
      } else {
        auto application_parameter =
            application_parameter_type == ApplicationParameterType::kPrimary
                ? fido_parsing_utils::CreateSHA256Hash(request().rp_id())
                : request().alternative_application_parameter().value_or(
                      std::array<uint8_t, kRpIdHashLength>());
        auto sign_response =
            AuthenticatorGetAssertionResponse::CreateFromU2fSignResponse(
                std::move(application_parameter), apdu_response->data(),
                key_handle);
        if (!sign_response) {
          std::move(callback())
              .Run(CtapDeviceResponseCode::kCtap2ErrOther, base::nullopt);
          return;
        }

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

    case apdu::ApduResponse::Status::SW_CONDITIONS_NOT_SATISFIED: {
      // Waiting for user touch. Retry after 200 milliseconds delay.
      base::SequencedTaskRunnerHandle::Get()->PostDelayedTask(
          FROM_HERE,
          base::BindOnce(&U2fSignOperation::RetrySign,
                         weak_factory_.GetWeakPtr(), is_fake_enrollment,
                         application_parameter_type, key_handle),
          kU2fRetryDelay);
      break;
    }
    default:
      // Some sort of failure occurred. Abandon this device and move on.
      std::move(callback())
          .Run(CtapDeviceResponseCode::kCtap2ErrOther, base::nullopt);
      return;
  }
}

void U2fSignOperation::OnCheckForKeyHandlePresence(
    ApplicationParameterType application_parameter_type,
    AllowedListIterator it,
    base::Optional<std::vector<uint8_t>> device_response) {
  DCHECK(request().allow_list());
  const auto apdu_response =
      device_response
          ? apdu::ApduResponse::CreateFromMessage(std::move(*device_response))
          : base::nullopt;
  auto return_code = apdu_response ? apdu_response->status()
                                   : apdu::ApduResponse::Status::SW_WRONG_DATA;

  switch (return_code) {
    case apdu::ApduResponse::Status::SW_NO_ERROR:
    case apdu::ApduResponse::Status::SW_CONDITIONS_NOT_SATISFIED: {
      DispatchDeviceRequest(
          ConvertToU2fSignCommand(request(), application_parameter_type,
                                  it->id()),
          base::BindOnce(&U2fSignOperation::OnSignResponseReceived,
                         weak_factory_.GetWeakPtr(),
                         false /* is_fake_enrollment */,
                         application_parameter_type, it->id()));
      break;
    }
    case apdu::ApduResponse::Status::SW_WRONG_DATA:
    case apdu::ApduResponse::Status::SW_WRONG_LENGTH: {
      if (application_parameter_type == ApplicationParameterType::kPrimary &&
          request().alternative_application_parameter()) {
        // |application_parameter_| failed, but there is also
        // |alternative_application_parameter_| to try.
        DispatchDeviceRequest(
            ConvertToU2fSignCommand(request(),
                                    ApplicationParameterType::kAlternative,
                                    it->id(), true /* is_check_only */),
            base::BindOnce(&U2fSignOperation::OnCheckForKeyHandlePresence,
                           weak_factory_.GetWeakPtr(),
                           ApplicationParameterType::kAlternative, it));
      } else if (++it != request().allow_list()->cend()) {
        // Key is not for this device. Try signing with the next key.
        DispatchDeviceRequest(
            ConvertToU2fSignCommand(request(),
                                    ApplicationParameterType::kPrimary,
                                    it->id(), true /* check_only */),
            base::BindOnce(&U2fSignOperation::OnCheckForKeyHandlePresence,
                           weak_factory_.GetWeakPtr(),
                           ApplicationParameterType::kPrimary, it));
      } else {
        // No provided key was accepted by this device. Send registration
        // (Fake enroll) request to device.
        SendFakeEnrollment();
      }

      break;
    }
    default:
      // Some sort of failure occurred. Abandon this device and move on.
      std::move(callback())
          .Run(CtapDeviceResponseCode::kCtap2ErrOther, base::nullopt);
      break;
  }
}

}  // namespace device
