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

#include "components/trusted_vault/recovery_key_store_connection_impl.h"

#include <memory>

#include "base/barrier_closure.h"
#include "base/functional/bind.h"
#include "base/notreached.h"
#include "base/strings/string_number_conversions.h"
#include "base/time/clock.h"
#include "base/time/time.h"
#include "base/types/expected.h"
#include "components/signin/public/identity_manager/account_info.h"
#include "components/trusted_vault/proto/recovery_key_store.pb.h"
#include "components/trusted_vault/proto_string_bytes_conversion.h"
#include "components/trusted_vault/recovery_key_store_certificate.h"
#include "components/trusted_vault/recovery_key_store_connection.h"
#include "components/trusted_vault/trusted_vault_access_token_fetcher.h"
#include "components/trusted_vault/trusted_vault_connection.h"
#include "components/trusted_vault/trusted_vault_histograms.h"
#include "components/trusted_vault/trusted_vault_request.h"
#include "net/base/url_util.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "url/gurl.h"

namespace trusted_vault {

namespace {

// The "/0" suffix is required but ignored.
constexpr char kUpdateVaultUrl[] =
    "https://cryptauthvault.googleapis.com/v1/vaults/0";

constexpr char kListVaultsUrl[] =
    "https://cryptauthvault.googleapis.com/v1/"
    "vaults?use_case=13&challenge_not_required=1";

constexpr char kRecoveryKeyStoreCertFileUrl[] =
    "https://www.gstatic.com/cryptauthvault/v0/cert.xml";

constexpr char kRecoveryKeyStoreSigFileUrl[] =
    "https://www.gstatic.com/cryptauthvault/v0/cert.sig.xml";

// The maximum number of bytes that will be downloaded from the above two URLs.
constexpr size_t kMaxRecoveryKeyStoreCertFetchBodyBytes = 128 * 1024;

static constexpr net::NetworkTrafficAnnotationTag kCertXmlTrafficAnnotation =
    net::DefineNetworkTrafficAnnotation("trusted_vault_cert_request",
                                        R"(
      semantics {
        sender: "Trusted Vault Service"
        description:
          "Request to the vault service in order to retrieve the public list "
          "of certificates to perform encryption key operations for Chrome, "
          "such as Google Password Manager passkey operations"
        trigger:
          "Periodically/upon certain non-user controlled events after user "
          "signs in Chrome profile."
        data: "None"
        destination: GOOGLE_OWNED_SERVICE
        last_reviewed: "2025-06-05"
        user_data {
          type: NONE
        }
        internal {
          contacts {
            email: "nsatragno@chromium.org"
          }
          contacts {
            email: "chrome-webauthn@google.com"
          }
        }
      }
      policy {
        cookies_allowed: NO
        setting:
          "This feature cannot be disabled in settings, but if user signs "
          "out of Chrome, this request would not be made."
        chrome_policy {
          BrowserSignin {
            BrowserSignin: 0
          }
        }
      })");

RecoveryKeyStoreStatus HttpStatusToRecoveryKeyStoreStatus(
    TrustedVaultRequest::HttpStatus http_status) {
  switch (http_status) {
    case TrustedVaultRequest::HttpStatus::kSuccess:
      return RecoveryKeyStoreStatus::kSuccess;
    case TrustedVaultRequest::HttpStatus::kBadRequest:
    case TrustedVaultRequest::HttpStatus::kNotFound:
    case TrustedVaultRequest::HttpStatus::kConflict:
    case TrustedVaultRequest::HttpStatus::kOtherError:
      return RecoveryKeyStoreStatus::kOtherError;
    case TrustedVaultRequest::HttpStatus::kNetworkError:
      return RecoveryKeyStoreStatus::kNetworkError;
    case TrustedVaultRequest::HttpStatus::kTransientAccessTokenFetchError:
      return RecoveryKeyStoreStatus::kTransientAccessTokenFetchError;
    case TrustedVaultRequest::HttpStatus::kPersistentAccessTokenFetchError:
      return RecoveryKeyStoreStatus::kPersistentAccessTokenFetchError;
    case TrustedVaultRequest::HttpStatus::
        kPrimaryAccountChangeAccessTokenFetchError:
      return RecoveryKeyStoreStatus::kPrimaryAccountChangeAccessTokenFetchError;
  }
}

void ProcessUpdateVaultResponseResponse(
    RecoveryKeyStoreConnection::UpdateRecoveryKeyStoreCallback callback,
    TrustedVaultRequest::HttpStatus http_status,
    const std::string& response_body) {
  std::move(callback).Run(HttpStatusToRecoveryKeyStoreStatus(http_status));
}

// Represents a request to the listVaults RPC endpoint.
class ListRecoveryKeyStoresRequest : public TrustedVaultConnection::Request {
 public:
  ListRecoveryKeyStoresRequest(
      const CoreAccountInfo& account_info,
      scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
      std::unique_ptr<TrustedVaultAccessTokenFetcher> access_token_fetcher,
      RecoveryKeyStoreConnection::ListRecoveryKeyStoresCallback callback)
      : account_id_(account_info.account_id),
        url_loader_factory_(std::move(url_loader_factory)),
        access_token_fetcher_(std::move(access_token_fetcher)),
        callback_(std::move(callback)) {
    StartOrContinueRequest();
  }

 private:
  void StartOrContinueRequest(const std::string* next_page_token = nullptr) {
    TrustedVaultRequest::RecordFetchStatusCallback record_fetch_status_to_uma =
        base::BindRepeating(
            &RecordRecoveryKeyStoreURLFetchResponse,
            RecoveryKeyStoreURLFetchReasonForUMA::kListRecoveryKeyStores);
    GURL request_url(kListVaultsUrl);
    request_ = std::make_unique<TrustedVaultRequest>(
        SecurityDomainId::kPasskeys, account_id_,
        TrustedVaultRequest::HttpMethod::kGet,
        next_page_token ? net::AppendQueryParameter(request_url, "page_token",
                                                    *next_page_token)
                        : request_url,
        /*serialized_request_proto=*/std::nullopt,
        /*max_retry_duration=*/base::Seconds(0), url_loader_factory_,
        access_token_fetcher_->Clone(), std::move(record_fetch_status_to_uma));

    // Unretained: this object owns `request_`. When `request_` is deleted, so
    // is the `SimpleURLLoader` and thus any callback is canceled.
    request_->FetchAccessTokenAndSendRequest(
        base::BindOnce(&ListRecoveryKeyStoresRequest::ProcessResponse,
                       base::Unretained(this)));
  }

  void ProcessResponse(TrustedVaultRequest::HttpStatus http_status,
                       const std::string& response_body) {
    if (http_status != TrustedVaultRequest::HttpStatus::kSuccess) {
      FinishWithResultAndMaybeDestroySelf(
          base::unexpected(HttpStatusToRecoveryKeyStoreStatus(http_status)));
      return;
    }
    trusted_vault_pb::ListVaultsResponse response;
    if (!response.ParseFromString(response_body)) {
      FinishWithResultAndMaybeDestroySelf(
          base::unexpected(RecoveryKeyStoreStatus::kOtherError));
      return;
    }
    for (const auto& vault : response.vaults()) {
      RecoveryKeyStoreEntry entry;
      entry.backend_public_key =
          ProtoStringToBytes(vault.vault_parameters().backend_public_key());
      entry.vault_handle =
          ProtoStringToBytes(vault.vault_parameters().vault_handle());
      result_.push_back(std::move(entry));
    }
    if (!response.next_page_token().empty()) {
      StartOrContinueRequest(&response.next_page_token());
      return;
    }
    FinishWithResultAndMaybeDestroySelf(std::move(result_));
  }

  void FinishWithResultAndMaybeDestroySelf(
      base::expected<std::vector<RecoveryKeyStoreEntry>, RecoveryKeyStoreStatus>
          result) {
    std::move(callback_).Run(std::move(result));
  }

  std::unique_ptr<TrustedVaultRequest> request_;
  const CoreAccountId account_id_;
  scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory_;
  std::unique_ptr<TrustedVaultAccessTokenFetcher> access_token_fetcher_;
  RecoveryKeyStoreConnection::ListRecoveryKeyStoresCallback callback_;
  std::vector<RecoveryKeyStoreEntry> result_;
};

// Represents a request to fetch the recovery key store certificates.
class FetchRecoveryKeyStoreCertificatesRequest
    : public TrustedVaultConnection::Request {
 public:
  // `clock` must outlive this.
  FetchRecoveryKeyStoreCertificatesRequest(
      base::Clock* clock,
      scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
      RecoveryKeyStoreConnection::FetchRecoveryKeyStoreCertificatesCallback
          callback)
      : clock_(clock),
        url_loader_factory_(std::move(url_loader_factory)),
        callback_(std::move(callback)) {
    // Destroying `this` will also destroy the URL loader, which means these
    // callbacks will never complete. Thus, unretained is fine.
    file_download_closure_ = base::BarrierClosure(
        2, base::BindOnce(
               &FetchRecoveryKeyStoreCertificatesRequest::OnDownloadComplete,
               base::Unretained(this)));
    cert_xml_loader_ =
        Fetch(kRecoveryKeyStoreCertFileUrl,
              base::BindOnce(
                  &FetchRecoveryKeyStoreCertificatesRequest::OnFileReceived,
                  base::Unretained(this), &cert_xml_));
    sig_xml_loader_ =
        Fetch(kRecoveryKeyStoreSigFileUrl,
              base::BindOnce(
                  &FetchRecoveryKeyStoreCertificatesRequest::OnFileReceived,
                  base::Unretained(this), &sig_xml_));
  }

 private:
  // Fetches `url` and runs `callback` when done.
  std::unique_ptr<network::SimpleURLLoader> Fetch(
      const std::string_view url,
      base::OnceCallback<void(std::optional<std::string>)> callback) {
    auto network_request = std::make_unique<network::ResourceRequest>();
    GURL request_url(url);
    CHECK(request_url.is_valid());
    network_request->url = std::move(request_url);

    auto loader = network::SimpleURLLoader::Create(std::move(network_request),
                                                   kCertXmlTrafficAnnotation);
    loader->SetTimeoutDuration(base::Seconds(10));
    loader->SetURLLoaderFactoryOptions(
        network::mojom::kURLLoadOptionBlockAllCookies);
    loader->DownloadToString(url_loader_factory_.get(), std::move(callback),
                             kMaxRecoveryKeyStoreCertFetchBodyBytes);
    return loader;
  }

  // Called after a fetch request finishes.
  void OnFileReceived(std::string* destination,
                      std::optional<std::string> file) {
    if (!file) {
      FinishRequestAndMaybeDestroySelf(base::unexpected(
          RecoveryKeyStoreCertificateFetchStatus::kNetworkError));
      return;
    }
    *destination = std::move(*file);
    file_download_closure_.Run();
  }

  // Called after both `sig_xml_` and `cert_xml_` have been downloaded.
  void OnDownloadComplete() {
    std::optional<RecoveryKeyStoreCertificate> certificate =
        RecoveryKeyStoreCertificate::Parse(cert_xml_, sig_xml_, clock_->Now());
    if (certificate) {
      FinishRequestAndMaybeDestroySelf(std::move(*certificate));
      return;
    }
    FinishRequestAndMaybeDestroySelf(
        base::unexpected(RecoveryKeyStoreCertificateFetchStatus::kParseError));
  }

  void FinishRequestAndMaybeDestroySelf(
      base::expected<RecoveryKeyStoreCertificate,
                     RecoveryKeyStoreCertificateFetchStatus> result) {
    RecoveryKeyStoreCertificatesFetchStatusForUMA status =
        RecoveryKeyStoreCertificatesFetchStatusForUMA::kSuccess;
    if (!result.has_value()) {
      switch (result.error()) {
        case RecoveryKeyStoreCertificateFetchStatus::kNetworkError:
          status = RecoveryKeyStoreCertificatesFetchStatusForUMA::kNetworkError;
          break;
        case RecoveryKeyStoreCertificateFetchStatus::kParseError:
          status = RecoveryKeyStoreCertificatesFetchStatusForUMA::kParseError;
          break;
        case RecoveryKeyStoreCertificateFetchStatus::kSuccess:
          NOTREACHED();
      }
    }
    RecordRecoveryKeyStoreFetchCertificatesStatus(status);
    std::move(callback_).Run(std::move(result));
  }

  raw_ptr<base::Clock> clock_;
  scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory_;
  RecoveryKeyStoreConnection::FetchRecoveryKeyStoreCertificatesCallback
      callback_;
  base::RepeatingClosure file_download_closure_;
  std::unique_ptr<network::SimpleURLLoader> cert_xml_loader_;
  std::unique_ptr<network::SimpleURLLoader> sig_xml_loader_;
  std::string cert_xml_;
  std::string sig_xml_;
};

}  // namespace

RecoveryKeyStoreEntry::RecoveryKeyStoreEntry() = default;
RecoveryKeyStoreEntry::RecoveryKeyStoreEntry(RecoveryKeyStoreEntry&&) = default;
RecoveryKeyStoreEntry& RecoveryKeyStoreEntry::operator=(
    RecoveryKeyStoreEntry&&) = default;
RecoveryKeyStoreEntry::~RecoveryKeyStoreEntry() = default;
bool RecoveryKeyStoreEntry::operator==(const RecoveryKeyStoreEntry&) const =
    default;

RecoveryKeyStoreConnectionImpl::RecoveryKeyStoreConnectionImpl(
    std::unique_ptr<network::PendingSharedURLLoaderFactory>
        pending_url_loader_factory,
    std::unique_ptr<TrustedVaultAccessTokenFetcher> access_token_fetcher)
    : pending_url_loader_factory_(std::move(pending_url_loader_factory)),
      access_token_fetcher_(std::move(access_token_fetcher)) {}

RecoveryKeyStoreConnectionImpl::~RecoveryKeyStoreConnectionImpl() = default;

std::unique_ptr<RecoveryKeyStoreConnectionImpl::Request>
RecoveryKeyStoreConnectionImpl::UpdateRecoveryKeyStore(
    const CoreAccountInfo& account_info,
    const trusted_vault_pb::Vault& vault,
    UpdateRecoveryKeyStoreCallback callback) {
  TrustedVaultRequest::RecordFetchStatusCallback record_fetch_status_to_uma =
      base::BindRepeating(
          &RecordRecoveryKeyStoreURLFetchResponse,
          RecoveryKeyStoreURLFetchReasonForUMA::kUpdateRecoveryKeyStore);
  auto request = std::make_unique<TrustedVaultRequest>(
      SecurityDomainId::kPasskeys, account_info.account_id,
      TrustedVaultRequest::HttpMethod::kPatch, GURL(kUpdateVaultUrl),
      vault.SerializeAsString(),
      /*max_retry_duration=*/base::Seconds(0), URLLoaderFactory(),
      access_token_fetcher_->Clone(), std::move(record_fetch_status_to_uma));
  request->FetchAccessTokenAndSendRequest(
      base::BindOnce(&ProcessUpdateVaultResponseResponse, std::move(callback)));
  return request;
}

std::unique_ptr<RecoveryKeyStoreConnectionImpl::Request>
RecoveryKeyStoreConnectionImpl::ListRecoveryKeyStores(
    const CoreAccountInfo& account_info,
    ListRecoveryKeyStoresCallback callback) {
  return std::make_unique<ListRecoveryKeyStoresRequest>(
      account_info, URLLoaderFactory(), access_token_fetcher_->Clone(),
      std::move(callback));
}

std::unique_ptr<RecoveryKeyStoreConnectionImpl::Request>
RecoveryKeyStoreConnectionImpl::FetchRecoveryKeyStoreCertificates(
    base::Clock* clock,
    FetchRecoveryKeyStoreCertificatesCallback callback) {
  return std::make_unique<FetchRecoveryKeyStoreCertificatesRequest>(
      clock, URLLoaderFactory(), std::move(callback));
}

scoped_refptr<network::SharedURLLoaderFactory>
RecoveryKeyStoreConnectionImpl::URLLoaderFactory() {
  // `url_loader_factory_` is created lazily, because it needs to be done on
  // the backend sequence, while this class ctor is called on UI thread.
  if (!url_loader_factory_) {
    CHECK(pending_url_loader_factory_);
    url_loader_factory_ = network::SharedURLLoaderFactory::Create(
        std::move(pending_url_loader_factory_));
  }
  return url_loader_factory_;
}

}  // namespace trusted_vault
