// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/policy/messaging_layer/upload/configuration_file_controller.h"

#include <algorithm>
#include <cstdint>
#include <limits>
#include <optional>
#include <string_view>
#include <vector>

#include "base/containers/span.h"
#include "base/feature_list.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_functions.h"
#include "base/numerics/byte_conversions.h"
#include "base/strings/string_view_util.h"
#include "chrome/browser/policy/messaging_layer/upload/record_upload_request_builder.h"
#include "components/reporting/encryption/primitives.h"
#include "components/reporting/encryption/verification.h"
#include "components/reporting/proto/synced/record.pb.h"
#include "components/reporting/util/status.h"
#include "components/version_info/version_info.h"

namespace reporting {
namespace {
// Checks if two lists contain the same destinations.
bool DestinationListsEqual(ListOfBlockedDestinations list_a,
                           ListOfBlockedDestinations list_b) {
  if (list_a.destinations_size() != list_b.destinations_size()) {
    return false;
  }
  return std::equal(list_a.destinations().begin(), list_a.destinations().end(),
                    list_b.destinations().begin());
}
}  // namespace

// Only used in testing. Feature for testing the configuration file in our
// automated tests. Not exposed on the UI.
BASE_FEATURE(kReportingConfigurationFileTestSignature,
             base::FEATURE_DISABLED_BY_DEFAULT);

ConfigurationFileController::~ConfigurationFileController() = default;

// The default constructor used for this class takes as a parameter a callback
// that sends information to missive after being verified by this class if
// needed. It also creates the signature verifier that will be used to verify
// the signature of the configuration file using the well-known prod key stored
// in Chrome.
ConfigurationFileController::ConfigurationFileController(
    UploadClient::UpdateConfigInMissiveCallback update_config_in_missive_cb)
    : ConfigurationFileController(std::move(update_config_in_missive_cb),
                                  ListOfBlockedDestinations(),
                                  version_info::GetMajorVersionNumberAsInt()) {}

int32_t ConfigurationFileController::HandleConfigurationFile(
    reporting::ConfigFile config_file) {
  // Sanity check to verify that the experiment is enabled.
  if (!base::FeatureList::IsEnabled(kShouldRequestConfigurationFile)) {
    // Returns -1 since this is not an actual error with the file provided.
    return kFeatureDisabled;
  }

  // Check if we got the same version that we were using already.
  // This might happen if the server returns the configuration file two times
  // with the same version because of two requests made close to each other, we
  // just ignore them if that's the case and return the current version. We also
  // just ignore the config file if the version is negative.
  if (config_file.version() == current_config_file_version_ ||
      config_file.version() < 0) {
    return current_config_file_version_;
  }

  // Verify signature.
  const auto signature_verification_status = VerifySignature(config_file);
  if (!signature_verification_status.ok()) {
    base::UmaHistogramEnumeration(
        "Browser.ERP.ConfigFileSignatureVerificationError",
        signature_verification_status.code(), error::Code::MAX_VALUE);
    return kConfigurationFileCorrupted;
  }

  // Check if we should send the list of blocked destinations to missive.
  if (HandleBlockedEventConfigs(config_file.blocked_event_configs())) {
    update_config_in_missive_cb_.Run(destinations_list_);
  }

  // If we reached here it means that we just got a new version of the config
  // file from the server so we update the version being sent on the payload and
  // the one stored by this class.
  current_config_file_version_ = config_file.version();
  return current_config_file_version_;
}

Status ConfigurationFileController::VerifySignature(ConfigFile config_file) {
  // We don't check the signature in our tests. This flag is only used inside
  // the tast tests.
  if (base::FeatureList::IsEnabled(kReportingConfigurationFileTestSignature)) {
    return Status::StatusOK();
  }

  // Sanity checks, this should never fail.
  if (!config_file.has_version()) {
    return Status{error::INVALID_ARGUMENT,
                  "Missing version information for config file"};
  }

  if (!config_file.has_config_file_signature()) {
    return Status{error::INVALID_ARGUMENT,
                  "Missing signature information for config file"};
  }

  // Verify the value signed on the server using the big-endian representation
  // of the configuration file version.
  return verifier_.Verify(
      base::as_string_view(base::U32ToBigEndian(config_file.version())),
      config_file.config_file_signature());
}

// static
std::unique_ptr<ConfigurationFileController>
ConfigurationFileController::CreateForTesting(
    UploadClient::UpdateConfigInMissiveCallback update_config_in_missive_cb,
    ListOfBlockedDestinations destinations_list,
    int os_version) {
  return base::WrapUnique(new ConfigurationFileController(
      std::move(update_config_in_missive_cb), std::move(destinations_list),
      os_version));
}

ConfigurationFileController::ConfigurationFileController(
    UploadClient::UpdateConfigInMissiveCallback update_config_in_missive_cb,
    ListOfBlockedDestinations destinations_list,
    int os_version)
    : update_config_in_missive_cb_(std::move(update_config_in_missive_cb)),
      verifier_(SignatureVerifier(SignatureVerifier::VerificationKey())),
      destinations_list_(std::move(destinations_list)),
      current_os_version_(os_version) {}

bool ConfigurationFileController::HandleBlockedEventConfigs(
    google::protobuf::RepeatedPtrField<EventConfig> blocked_event_configs) {
  // If the incoming list is empty and the stored list is also empty
  // we return false to signal to the parent function that it should not
  // send the list to missive.
  if (blocked_event_configs.empty() &&
      destinations_list_.destinations().empty()) {
    return false;
  }

  // If the configuration fetched from the server is empty and the stored list
  // is not empty we return true to signal to the parent function that it
  // should update the list in missive to an empty one.
  if (blocked_event_configs.empty() &&
      !destinations_list_.destinations().empty()) {
    destinations_list_ = ListOfBlockedDestinations();
    return true;
  }

  ListOfBlockedDestinations current_list;
  //  Check all the destinations, if they have version information
  //  we check whether it should be blocked or not.
  for (const auto& current_event : blocked_event_configs) {
    // If there is no minimum/maximum release version specified we add it
    // directly since it should be blocked on all versions.
    if (!current_event.has_minimum_release_version()) {
      current_list.add_destinations(current_event.destination());
      continue;
    }

    // Check if it should be blocked for the current version or not.
    std::optional<int32_t> maximum_version;
    maximum_version =
        current_event.has_maximum_release_version()
            ? std::optional<int32_t>(current_event.maximum_release_version())
            : std::nullopt;
    if (ShouldBeBlocked(current_event.minimum_release_version(),
                        maximum_version)) {
      current_list.add_destinations(current_event.destination());
    }
  }

  // Compare to see if the destinations list that we have right now is the same
  // as the one that is incoming from the server, if not then we swap the lists
  // and we notify the parent function that it should send the new list to
  // missive.
  if (DestinationListsEqual(current_list, destinations_list_)) {
    return false;
  }

  destinations_list_ = std::move(current_list);
  return true;
}

bool ConfigurationFileController::ShouldBeBlocked(
    int32_t minimum_version,
    std::optional<int32_t> maximum_version) const {
  if (maximum_version.has_value()) {
    return current_os_version_ >= minimum_version &&
           current_os_version_ <= maximum_version.value();
  }
  return current_os_version_ >= minimum_version;
}

}  // namespace reporting
