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

#include <array>
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <tuple>
#include <utility>
#include <vector>

#include "base/base64.h"
#include "base/containers/to_vector.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "components/device_event_log/device_event_log.h"
#include "device/fido/authenticator_get_info_response.h"
#include "device/fido/authenticator_make_credential_response.h"
#include "device/fido/authenticator_supported_options.h"
#include "device/fido/ctap_make_credential_request.h"
#include "device/fido/fido_device.h"
#include "device/fido/fido_test_data.h"
#include "device/fido/mock_fido_device.h"
#include "device/fido/public/fido_constants.h"
#include "device/fido/public/fido_types.h"
#include "device/fido/public/public_key_credential_params.h"
#include "device/fido/public/public_key_credential_rp_entity.h"
#include "device/fido/public/public_key_credential_user_entity.h"
#include "device/fido/virtual_ctap2_device.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

using ::testing::_;

namespace device {

namespace {

constexpr std::array<uint8_t, kAaguidLength> kTestDeviceAaguid = {
    {0xF8, 0xA0, 0x11, 0xF3, 0x8C, 0x0A, 0x4D, 0x15, 0x80, 0x06, 0x17, 0x11,
     0x1F, 0x9E, 0xDC, 0x7D}};

using TestMakeCredentialTaskFuture = ::base::test::TestFuture<
    CtapDeviceResponseCode,
    std::optional<AuthenticatorMakeCredentialResponse>>;

class FidoMakeCredentialTaskTest : public testing::Test {
 public:
  FidoMakeCredentialTaskTest() = default;

  std::unique_ptr<MakeCredentialTask> CreateMakeCredentialTask(
      FidoDevice* device) {
    PublicKeyCredentialRpEntity rp(test_data::kRelyingPartyId);
    PublicKeyCredentialUserEntity user(base::ToVector(test_data::kUserId));
    return std::make_unique<MakeCredentialTask>(
        device,
        CtapMakeCredentialRequest(
            test_data::kClientDataJson, std::move(rp), std::move(user),
            PublicKeyCredentialParams(
                std::vector<PublicKeyCredentialParams::CredentialInfo>(1))),
        MakeCredentialOptions(), future_.GetCallback());
  }

  TestMakeCredentialTaskFuture& make_credential_future() { return future_; }

 protected:
  base::test::TaskEnvironment task_environment_;
  TestMakeCredentialTaskFuture future_;
};

TEST_F(FidoMakeCredentialTaskTest, MakeCredentialSuccess) {
  auto device = MockFidoDevice::MakeCtap();
  device->ExpectCtap2CommandAndRespondWith(
      CtapRequestCommand::kAuthenticatorMakeCredential,
      test_data::kTestMakeCredentialResponse);

  const auto task = CreateMakeCredentialTask(device.get());
  EXPECT_TRUE(make_credential_future().Wait());

  EXPECT_EQ(CtapDeviceResponseCode::kSuccess,
            std::get<0>(make_credential_future().Get()));
  EXPECT_TRUE(std::get<1>(make_credential_future().Get()));
  EXPECT_EQ(device->supported_protocol(), ProtocolVersion::kCtap2);
  EXPECT_TRUE(device->device_info());
}

TEST_F(FidoMakeCredentialTaskTest, RedactLog) {
  // This is a base 64 encoded CTAP make credential response with:
  // * largeBlobKey
  // * largeBlob extension
  // * PRF extension
  // All the values intended to be redacted are set to the string "secret".
  // (The response is not valid, but that's okay here.)
  constexpr char kTestCtapMakeCredentialResponse[] =
      "AKUBZG5vbmUCWJTEbO+CrRtUZHdZHQCLCHWew+bS7LTzlHS/"
      "6mlpkl0Dt10AAAAA6puNZk0BHSE85La0jLV11AAQICd/"
      "yty9fBbIZa7eL+"
      "4WpKUBAgMmIAEhWCCm7PMJaGqWTdGQn2Tae0XwaIwSQ13NK7BDQSUwBiot5yJYIAuec8BEHT"
      "Onn3BP942qdWBtSSUxVQoAYGKjVhQzf30hA2ZzZWNyZXQFZnNlY3JldAaiY3ByZqJnZW5hYm"
      "xlZPVncmVzdWx0c6FlZmlyc3Rmc2VjcmV0aWxhcmdlQmxvYmZzZWNyZXQ=";
  auto device = MockFidoDevice::MakeCtap();
  device->ExpectCtap2CommandAndRespondWith(
      CtapRequestCommand::kAuthenticatorMakeCredential,
      *base::Base64Decode(kTestCtapMakeCredentialResponse));

  device_event_log::Initialize(/*max_entries=*/0);
  const auto task = CreateMakeCredentialTask(device.get());
  EXPECT_TRUE(make_credential_future().Wait());

  // Signature.
  std::string device_log = device_event_log::GetAsString(
      device_event_log::NEWEST_FIRST, /*format=*/"level",
      /*types=*/"fido",
      /*max_level=*/device_event_log::LOG_LEVEL_EVENT, /*max_events=*/0);
  EXPECT_THAT(device_log, testing::HasSubstr("3: \"[redacted]\""));
  // Large blob key.
  EXPECT_THAT(device_log, testing::HasSubstr("5: \"[redacted]\""));
  // PRF.
  EXPECT_THAT(
      device_log,
      testing::HasSubstr(
          "6: {\"prf\": {\"enabled\": true, \"results\": \"[redacted]\""));
  // Large blob extension.
  EXPECT_THAT(device_log, testing::HasSubstr("\"largeBlob\": \"[redacted]\""));

  // Verify that the data hasn't escaped redaction somehow.
  EXPECT_THAT(device_log, testing::Not(testing::HasSubstr("secret")));
  device_event_log::Shutdown();
}

TEST_F(FidoMakeCredentialTaskTest, TestRegisterSuccessWithFake) {
  auto device = std::make_unique<VirtualCtap2Device>();
  base::test::TestFuture<void> done_init;
  device->DiscoverSupportedProtocolAndDeviceInfo(done_init.GetCallback());
  EXPECT_TRUE(done_init.Wait());
  const auto task = CreateMakeCredentialTask(device.get());
  EXPECT_TRUE(make_credential_future().Wait());

  EXPECT_EQ(CtapDeviceResponseCode::kSuccess,
            std::get<0>(make_credential_future().Get()));

  // We don't verify the response from the fake, but do a quick sanity check.
  ASSERT_TRUE(std::get<1>(make_credential_future().Get()));
  EXPECT_EQ(32u, std::get<1>(make_credential_future().Get())
                     ->attestation_object.GetCredentialId()
                     .size());
}

TEST_F(FidoMakeCredentialTaskTest, FallbackToU2fRegisterSuccess) {
  auto device = MockFidoDevice::MakeU2f();
  device->ExpectWinkedAtLeastOnce();
  device->ExpectRequestAndRespondWith(
      test_data::kU2fRegisterCommandApdu,
      test_data::kApduEncodedNoErrorRegisterResponse);

  const auto task = CreateMakeCredentialTask(device.get());
  EXPECT_TRUE(make_credential_future().Wait());

  EXPECT_EQ(ProtocolVersion::kU2f, device->supported_protocol());
  EXPECT_EQ(CtapDeviceResponseCode::kSuccess,
            std::get<0>(make_credential_future().Get()));
}

TEST_F(FidoMakeCredentialTaskTest, DefaultToU2fWhenClientPinSet) {
  AuthenticatorGetInfoResponse device_info(
      {ProtocolVersion::kCtap2, ProtocolVersion::kU2f},
      {Ctap2Version::kCtap2_0}, kTestDeviceAaguid);
  AuthenticatorSupportedOptions options;
  options.client_pin_availability =
      AuthenticatorSupportedOptions::ClientPinAvailability::kSupportedAndPinSet;
  device_info.options = std::move(options);

  auto device = MockFidoDevice::MakeCtap(std::move(device_info));
  device->ExpectWinkedAtLeastOnce();
  device->ExpectRequestAndRespondWith(
      test_data::kU2fRegisterCommandApdu,
      test_data::kApduEncodedNoErrorRegisterResponse);

  const auto task = CreateMakeCredentialTask(device.get());
  EXPECT_TRUE(make_credential_future().Wait());
  EXPECT_EQ(CtapDeviceResponseCode::kSuccess,
            std::get<0>(make_credential_future().Get()));
  EXPECT_TRUE(std::get<1>(make_credential_future().Get()));
}

TEST_F(FidoMakeCredentialTaskTest, EnforceClientPinWhenUserVerificationSet) {
  AuthenticatorGetInfoResponse device_info(
      {ProtocolVersion::kCtap2, ProtocolVersion::kU2f},
      {Ctap2Version::kCtap2_0}, kTestDeviceAaguid);
  AuthenticatorSupportedOptions options;
  options.client_pin_availability =
      AuthenticatorSupportedOptions::ClientPinAvailability::kSupportedAndPinSet;
  device_info.options = std::move(options);

  auto device = MockFidoDevice::MakeCtap(std::move(device_info));
  device->ExpectCtap2CommandAndRespondWith(
      CtapRequestCommand::kAuthenticatorMakeCredential, std::nullopt);

  PublicKeyCredentialRpEntity rp(test_data::kRelyingPartyId);
  PublicKeyCredentialUserEntity user(base::ToVector(test_data::kUserId));
  auto request = CtapMakeCredentialRequest(
      test_data::kClientDataJson, std::move(rp), std::move(user),
      PublicKeyCredentialParams(
          std::vector<PublicKeyCredentialParams::CredentialInfo>(1)));
  request.user_verification = UserVerificationRequirement::kRequired;
  const auto task = std::make_unique<MakeCredentialTask>(
      device.get(), std::move(request), MakeCredentialOptions(),
      make_credential_future().GetCallback());

  EXPECT_TRUE(make_credential_future().Wait());
  EXPECT_EQ(CtapDeviceResponseCode::kCtap2ErrOther,
            std::get<0>(make_credential_future().Get()));
  EXPECT_FALSE(std::get<1>(make_credential_future().Get()));
}

}  // namespace
}  // namespace device
