// 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/os_crypt/async/browser/freedesktop_secret_key_provider.h"

#include <algorithm>
#include <tuple>
#include <utility>

#include "base/base64.h"
#include "base/check.h"
#include "base/containers/span.h"
#include "base/environment.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/memory/ref_counted_memory.h"
#include "base/memory/scoped_refptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/nix/xdg_util.h"
#include "base/no_destructor.h"
#include "base/notreached.h"
#include "base/rand_util.h"
#include "components/dbus/thread_linux/dbus_thread_linux.h"
#include "components/dbus/utils/call_method.h"
#include "components/dbus/utils/connect_to_signal.h"
#include "components/dbus/utils/variant.h"
#include "components/os_crypt/async/common/algorithm.mojom.h"
#include "crypto/kdf.h"
#include "dbus/message.h"
#include "dbus/object_path.h"

namespace os_crypt_async {

namespace {

constexpr char kUmaInitStatus[] =
    "OSCrypt.FreedesktopSecretKeyProvider.InitStatus";

// These constants are duplicated from the sync backend.
constexpr char kEncryptionTag[] = "v11";
constexpr char kSalt[] = "saltysalt";
constexpr size_t kDerivedKeySizeInBits = 128;
constexpr size_t kEncryptionIterations = 1;
constexpr size_t kSecretLengthBytes = 16;

}  // namespace

// A helper class to handle a Secret Service prompt. It is templated on the
// return type expected from the prompt.
template <typename T>
class FreedesktopSecretKeyProvider::Prompter
    : public base::RefCountedThreadSafe<Prompter<T>> {
 public:
  using PromptCallback = base::OnceCallback<void(std::optional<T>)>;

  template <dbus_utils::SignatureLiteral ArgsSig,
            dbus_utils::SignatureLiteral RetsSig,
            typename... Args>
  static void Prompt(scoped_refptr<dbus::Bus> bus,
                     dbus::ObjectProxy* object_proxy,
                     const std::string& interface_name,
                     const std::string& method_name,
                     PromptCallback callback,
                     const Args&... args) {
    auto handler =
        base::MakeRefCounted<Prompter<T>>(std::move(bus), std::move(callback));
    dbus_utils::CallMethod<ArgsSig, RetsSig>(
        object_proxy, interface_name, method_name,
        base::BindOnce(&Prompter::OnReply, handler), args...);
  }

  Prompter(scoped_refptr<dbus::Bus> bus, PromptCallback callback)
      : bus_(std::move(bus)), callback_(std::move(callback)) {}

  Prompter(const Prompter&) = delete;
  Prompter& operator=(const Prompter&) = delete;

 private:
  friend class base::RefCountedThreadSafe<Prompter<T>>;

  ~Prompter() { Finish(std::nullopt); }

  void OnReply(base::expected<std::tuple<T, dbus::ObjectPath>,
                              dbus_utils::CallMethodError> reply) {
    if (!reply.has_value()) {
      Finish(std::nullopt);
      return;
    }
    auto& [value, prompt] = reply.value();
    if (prompt.value() == "/") {
      Finish(std::move(value));
    } else {
      prompt_path_ = prompt;
      StartPrompt();
    }
  }

  void StartPrompt() {
    auto* prompt_proxy = bus_->GetObjectProxy(
        FreedesktopSecretKeyProvider::kSecretServiceName, prompt_path_);
    dbus_utils::ConnectToSignal<"bv">(
        prompt_proxy, FreedesktopSecretKeyProvider::kSecretPromptInterface,
        "Completed",
        base::BindRepeating(&Prompter::OnPromptCompletedSignal, this),
        base::BindOnce(&Prompter::OnSignalConnected, this));
    dbus_utils::CallMethod<"s", "">(
        prompt_proxy, FreedesktopSecretKeyProvider::kSecretPromptInterface,
        FreedesktopSecretKeyProvider::kMethodPrompt,
        base::BindOnce(&Prompter::OnPromptResponse, this), "");
  }

  void OnPromptResponse(dbus_utils::CallMethodResult<> response) {
    if (!response.has_value()) {
      LOG(ERROR) << "Prompt call returned no response.";
      Finish(std::nullopt);
    }
  }

  void OnSignalConnected(const std::string& interface_name,
                         const std::string& signal_name,
                         bool connected) {
    if (!connected) {
      LOG(ERROR) << "Failed to connect to Prompt.Completed signal.";
      Finish(std::nullopt);
    }
  }

  void OnPromptCompletedSignal(
      dbus_utils::ConnectToSignalResultSig<"bv"> result) {
    if (!result.has_value()) {
      LOG(ERROR) << "Failed to read Prompt.Completed signal args.";
      Finish(std::nullopt);
      return;
    }

    auto& [dismissed, variant] = result.value();

    if (dismissed) {
      Finish(std::nullopt);
      return;
    }

    auto value = std::move(variant).Take<T>();
    if (!value) {
      LOG(ERROR) << "Failed to parse prompt result.";
      Finish(std::nullopt);
      return;
    }

    Finish(std::move(*value));
  }

  void Finish(std::optional<T> result) {
    if (!prompt_path_.value().empty()) {
      bus_->RemoveObjectProxy(FreedesktopSecretKeyProvider::kSecretServiceName,
                              prompt_path_, base::DoNothing());
      prompt_path_ = dbus::ObjectPath();
    }
    if (callback_) {
      std::move(callback_).Run(std::move(result));
    }
    bus_.reset();
  }

  scoped_refptr<dbus::Bus> bus_;
  PromptCallback callback_;
  dbus::ObjectPath prompt_path_;
};

FreedesktopSecretKeyProvider::FreedesktopSecretKeyProvider(
    const std::string& password_store,
    const std::string& product_name,
    scoped_refptr<dbus::Bus> bus)
    : password_store_(password_store),
      product_name_(product_name),
      bus_(std::move(bus)) {
  if (!bus_) {
    bus_ = dbus_thread_linux::GetSharedSessionBus();
  }
}

FreedesktopSecretKeyProvider::~FreedesktopSecretKeyProvider() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}

void FreedesktopSecretKeyProvider::GetKey(KeyCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(callback);
  key_callback_ = std::move(callback);

  if (!secret_for_testing_.empty()) {
    DeriveKeyFromSecret(base::as_byte_span(secret_for_testing_));
    return;
  }

  // Reset state in case GetKey is called multiple times.
  kwallet_proxy_ = nullptr;
  kwallet_handle_ = kKWalletInvalidHandle;
  kwallet_transaction_id_ = kKWalletInvalidTransactionId;
  session_opened_ = false;
  session_proxy_ = nullptr;
  default_collection_proxy_ = nullptr;

  if (password_store_ == "basic") {
    // Use PosixKeyProvider.
    FinalizeFailure(InitStatus::kDisabled);
  } else if (password_store_ == "gnome-libsecret") {
    InitializeFreedesktopSecretService();
  } else if (password_store_ == "kwallet") {
    InitializeKWallet(kKWalletDService, kKWalletDPath);
  } else if (password_store_ == "kwallet5") {
    InitializeKWallet(kKWalletD5Service, kKWalletD5Path);
  } else if (password_store_ == "kwallet6") {
    InitializeKWallet(kKWalletD6Service, kKWalletD6Path);
  } else {
    if (!password_store_.empty()) {
      LOG(ERROR) << "Unknown password store: " << password_store_;
    }
    auto env = base::Environment::Create();
    switch (base::nix::GetDesktopEnvironment(env.get())) {
      case base::nix::DESKTOP_ENVIRONMENT_KDE3:
      case base::nix::DESKTOP_ENVIRONMENT_KDE4:
        InitializeKWallet(kKWalletDService, kKWalletDPath);
        break;
      case base::nix::DESKTOP_ENVIRONMENT_KDE5:
        InitializeKWallet(kKWalletD5Service, kKWalletD5Path);
        break;
      case base::nix::DESKTOP_ENVIRONMENT_KDE6:
        InitializeKWallet(kKWalletD6Service, kKWalletD6Path);
        break;
      case base::nix::DESKTOP_ENVIRONMENT_OTHER:
      case base::nix::DESKTOP_ENVIRONMENT_CINNAMON:
      case base::nix::DESKTOP_ENVIRONMENT_DEEPIN:
      case base::nix::DESKTOP_ENVIRONMENT_GNOME:
      case base::nix::DESKTOP_ENVIRONMENT_PANTHEON:
      case base::nix::DESKTOP_ENVIRONMENT_UKUI:
      case base::nix::DESKTOP_ENVIRONMENT_UNITY:
      case base::nix::DESKTOP_ENVIRONMENT_XFCE:
      case base::nix::DESKTOP_ENVIRONMENT_LXQT:
      case base::nix::DESKTOP_ENVIRONMENT_COSMIC:
        InitializeFreedesktopSecretService();
        break;
    }
  }
}

bool FreedesktopSecretKeyProvider::UseForEncryption() {
  return true;
}

void FreedesktopSecretKeyProvider::InitializeFreedesktopSecretService() {
  dbus_utils::CheckForServiceAndStart(
      bus_, kSecretServiceName,
      base::BindOnce(&FreedesktopSecretKeyProvider::OnServiceStarted,
                     weak_ptr_factory_.GetWeakPtr()));
}

void FreedesktopSecretKeyProvider::OnServiceStarted(
    std::optional<bool> service_started) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!service_started.value_or(false)) {
    FinalizeFailure(InitStatus::kNoService);
    return;
  }

  auto* service_proxy = bus_->GetObjectProxy(
      kSecretServiceName, dbus::ObjectPath(kSecretServicePath));
  dbus_utils::CallMethod<"s", "o">(
      service_proxy, kSecretServiceInterface, kMethodReadAlias,
      base::BindOnce(&FreedesktopSecretKeyProvider::OnReadAliasDefault,
                     weak_ptr_factory_.GetWeakPtr()),
      kDefaultAlias);
}

void FreedesktopSecretKeyProvider::OnReadAliasDefault(
    dbus_utils::CallMethodResultSig<"o"> collection_path_result) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!collection_path_result.has_value()) {
    FinalizeFailure(InitStatus::kReadAliasFailed);
    return;
  }

  const auto& collection_path = std::get<0>(collection_path_result.value());
  if (collection_path.value() != "/") {
    default_collection_proxy_ =
        bus_->GetObjectProxy(kSecretServiceName, collection_path);
    dbus_utils::CallMethod<"ss", "v">(
        default_collection_proxy_, kPropertiesInterface, kMethodGet,
        base::BindOnce(
            &FreedesktopSecretKeyProvider::OnGetCollectionLabelResponse,
            weak_ptr_factory_.GetWeakPtr()),
        kSecretCollectionInterface, kLabelProperty);
  } else {
    // No default collection, create it
    auto* service_proxy = bus_->GetObjectProxy(
        kSecretServiceName, dbus::ObjectPath(kSecretServicePath));
    std::map<std::string, dbus_utils::Variant> props;
    props.emplace(kSecretCollectionLabelProperty,
                  dbus_utils::Variant::Wrap<"s">(kDefaultCollectionLabel));

    Prompter<dbus::ObjectPath>::Prompt<"a{sv}s", "oo">(
        bus_, service_proxy, kSecretServiceInterface, kMethodCreateCollection,
        base::BindOnce(&FreedesktopSecretKeyProvider::OnCreateCollection,
                       weak_ptr_factory_.GetWeakPtr()),
        props, kDefaultAlias);
  }
}

void FreedesktopSecretKeyProvider::OnGetCollectionLabelResponse(
    dbus_utils::CallMethodResultSig<"v"> variant_result) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!variant_result.has_value()) {
    LOG(ERROR) << "Get(Label) failed.";
    FinalizeFailure(InitStatus::kGnomeKeyringDeadlock);
    return;
  }

  auto label_variant =
      std::move(std::get<0>(variant_result.value())).Take<std::string>();
  if (!label_variant) {
    LOG(ERROR) << "Label property missing or invalid.";
    FinalizeFailure(InitStatus::kGnomeKeyringDeadlock);
    return;
  }

  // Label property read successfully
  UnlockDefaultCollection();
}

void FreedesktopSecretKeyProvider::OnCreateCollection(
    std::optional<dbus::ObjectPath> create_collection_reply) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!create_collection_reply.has_value() ||
      create_collection_reply->value() == "/") {
    FinalizeFailure(InitStatus::kCreateCollectionFailed);
    return;
  }
  default_collection_proxy_ =
      bus_->GetObjectProxy(kSecretServiceName, create_collection_reply.value());
  UnlockDefaultCollection();
}

void FreedesktopSecretKeyProvider::UnlockDefaultCollection() {
  auto* service_proxy = bus_->GetObjectProxy(
      kSecretServiceName, dbus::ObjectPath(kSecretServicePath));

  std::vector<dbus::ObjectPath> objects = {
      default_collection_proxy_->object_path()};
  Prompter<std::vector<dbus::ObjectPath>>::Prompt<"ao", "aoo">(
      bus_, service_proxy, kSecretServiceInterface, kMethodUnlock,
      base::BindOnce(&FreedesktopSecretKeyProvider::OnUnlock,
                     weak_ptr_factory_.GetWeakPtr()),
      objects);
}

void FreedesktopSecretKeyProvider::OnUnlock(
    std::optional<std::vector<dbus::ObjectPath>> unlocked_collection) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!unlocked_collection.has_value() || unlocked_collection->empty()) {
    FinalizeFailure(InitStatus::kUnlockFailed);
    return;
  }
  // Unlocked now
  OpenSession();
}

void FreedesktopSecretKeyProvider::OpenSession() {
  auto* service_proxy = bus_->GetObjectProxy(
      kSecretServiceName, dbus::ObjectPath(kSecretServicePath));
  dbus_utils::CallMethod<"sv", "vo">(
      service_proxy, kSecretServiceInterface, kMethodOpenSession,
      base::BindOnce(&FreedesktopSecretKeyProvider::OnOpenSession,
                     weak_ptr_factory_.GetWeakPtr()),
      kAlgorithmPlain, dbus_utils::Variant::Wrap<"s">(kInputPlain));
}

void FreedesktopSecretKeyProvider::OnOpenSession(
    dbus_utils::CallMethodResultSig<"vo"> session_reply) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!session_reply.has_value()) {
    FinalizeFailure(InitStatus::kSessionFailure);
    return;
  }
  const auto& [_, result] = session_reply.value();
  session_proxy_ = bus_->GetObjectProxy(kSecretServiceName, result);
  session_opened_ = true;

  std::map<std::string, std::string> search_attrs{
      {kApplicationAttributeKey, kAppName}};

  dbus_utils::CallMethod<"a{ss}", "ao">(
      default_collection_proxy_, kSecretCollectionInterface, kMethodSearchItems,
      base::BindOnce(&FreedesktopSecretKeyProvider::OnSearchItems,
                     weak_ptr_factory_.GetWeakPtr()),
      search_attrs);
}

void FreedesktopSecretKeyProvider::OnSearchItems(
    dbus_utils::CallMethodResultSig<"ao"> results) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!results.has_value()) {
    FinalizeFailure(InitStatus::kSearchItemsFailed);
    return;
  }

  const auto& result_paths = std::get<0>(results.value());
  if (result_paths.empty()) {
    // No items found, create a new secret.
    CreateItem(base::MakeRefCounted<base::RefCountedString>(
        base::Base64Encode(base::RandBytesAsVector(kSecretLengthBytes))));
    return;
  }

  auto* service_proxy = bus_->GetObjectProxy(
      kSecretServiceName, dbus::ObjectPath(kSecretServicePath));

  std::vector<dbus::ObjectPath> objects = {result_paths.front()};
  Prompter<std::vector<dbus::ObjectPath>>::Prompt<"ao", "aoo">(
      bus_, service_proxy, kSecretServiceInterface, kMethodUnlock,
      base::BindOnce(&FreedesktopSecretKeyProvider::OnUnlockItems,
                     weak_ptr_factory_.GetWeakPtr(), result_paths.front()),
      objects);
}

void FreedesktopSecretKeyProvider::OnUnlockItems(
    const dbus::ObjectPath& item_path,
    std::optional<std::vector<dbus::ObjectPath>> unlocked_items) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!unlocked_items.has_value() || unlocked_items->empty()) {
    FinalizeFailure(InitStatus::kUnlockFailed);
    return;
  }

  auto* service_proxy = bus_->GetObjectProxy(
      kSecretServiceName, dbus::ObjectPath(kSecretServicePath));
  std::vector<dbus::ObjectPath> items = {item_path};
  dbus_utils::CallMethod<"aoo", "a{o(oayays)}">(
      service_proxy, kSecretServiceInterface, kMethodGetSecrets,
      base::BindOnce(&FreedesktopSecretKeyProvider::OnGetSecrets,
                     weak_ptr_factory_.GetWeakPtr(), item_path),
      items, session_proxy_->object_path());
}

void FreedesktopSecretKeyProvider::OnGetSecrets(
    dbus::ObjectPath expected_item_path,
    dbus_utils::CallMethodResultSig<"a{o(oayays)}"> secrets_reply) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!secrets_reply.has_value()) {
    FinalizeFailure(InitStatus::kGetSecretFailed);
    return;
  }

  const auto& secrets_map = std::get<0>(secrets_reply.value());
  auto it = secrets_map.find(expected_item_path);
  if (it == secrets_map.end()) {
    LOG(ERROR) << "GetSecrets reply did not contain the expected item.";
    FinalizeFailure(InitStatus::kGetSecretFailed);
    return;
  }

  const auto& [session_path, parameters, value, content_type] = it->second;

  if (value.empty()) {
    LOG(ERROR) << "GetSecrets returned an empty secret.";
    FinalizeFailure(InitStatus::kEmptySecret);
    return;
  }

  DeriveKeyFromSecret(base::span(value));
}

void FreedesktopSecretKeyProvider::InitializeKWallet(
    const char* kwallet_service,
    const char* kwallet_path) {
  kwallet_proxy_ =
      bus_->GetObjectProxy(kwallet_service, dbus::ObjectPath(kwallet_path));
  dbus_utils::CheckForServiceAndStart(
      bus_.get(), kwallet_service,
      base::BindOnce(&FreedesktopSecretKeyProvider::OnKWalletServiceStarted,
                     weak_ptr_factory_.GetWeakPtr()));
}

void FreedesktopSecretKeyProvider::OnKWalletServiceStarted(
    std::optional<bool> service_started) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!service_started.value_or(false)) {
    FinalizeFailure(InitStatus::kKWalletNoService);
    return;
  }

  dbus_utils::CallMethod<"", "b">(
      kwallet_proxy_, kKWalletInterface, kKWalletMethodIsEnabled,
      base::BindOnce(&FreedesktopSecretKeyProvider::OnKWalletIsEnabled,
                     weak_ptr_factory_.GetWeakPtr()));
}

void FreedesktopSecretKeyProvider::OnKWalletIsEnabled(
    dbus_utils::CallMethodResultSig<"b"> is_enabled) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!is_enabled.has_value() || !std::get<0>(is_enabled.value())) {
    FinalizeFailure(InitStatus::kKWalletDisabled);
    return;
  }
  dbus_utils::CallMethod<"", "s">(
      kwallet_proxy_, kKWalletInterface, kKWalletMethodNetworkWallet,
      base::BindOnce(&FreedesktopSecretKeyProvider::OnKWalletNetworkWallet,
                     weak_ptr_factory_.GetWeakPtr()));
}

void FreedesktopSecretKeyProvider::OnKWalletNetworkWallet(
    dbus_utils::CallMethodResultSig<"s"> wallet_name) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!wallet_name.has_value()) {
    FinalizeFailure(InitStatus::kKWalletNoNetworkWallet);
    return;
  }

  dbus_utils::ConnectToSignal<"ii">(
      kwallet_proxy_, kKWalletInterface, kKWalletSignalWalletAsyncOpened,
      base::BindRepeating(
          &FreedesktopSecretKeyProvider::OnKWalletWalletAsyncOpened,
          weak_ptr_factory_.GetWeakPtr()),
      base::BindOnce(&FreedesktopSecretKeyProvider::OnSignalConnected,
                     weak_ptr_factory_.GetWeakPtr()));

  // handleSession = true means KWallet will take care of keeping the wallet
  // open as long as the application is running.
  constexpr bool kHandleSession = true;
  dbus_utils::CallMethod<"sxsb", "i">(
      kwallet_proxy_, kKWalletInterface, kKWalletMethodOpenAsync,
      base::BindOnce(&FreedesktopSecretKeyProvider::OnKWalletOpenAsync,
                     weak_ptr_factory_.GetWeakPtr()),
      std::get<0>(wallet_name.value()), 0, product_name_, kHandleSession);
}

void FreedesktopSecretKeyProvider::OnKWalletOpenAsync(
    dbus_utils::CallMethodResultSig<"i"> t_id) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!t_id.has_value()) {
    FinalizeFailure(InitStatus::kKWalletOpenFailed);
    return;
  }

  kwallet_transaction_id_ = std::get<0>(t_id.value());
}

void FreedesktopSecretKeyProvider::OnSignalConnected(
    const std::string& interface_name,
    const std::string& signal_name,
    bool connected) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!connected) {
    LOG(ERROR) << "Failed to connect to " << signal_name << " signal.";
    FinalizeFailure(InitStatus::kKWalletOpenFailed);
  }
}

void FreedesktopSecretKeyProvider::OnKWalletWalletAsyncOpened(
    dbus_utils::ConnectToSignalResultSig<"ii"> result) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (kwallet_transaction_id_ == kKWalletInvalidTransactionId) {
    return;
  }

  if (!result.has_value()) {
    LOG(ERROR) << "Failed to read walletAsyncOpened signal args.";
    FinalizeFailure(InitStatus::kKWalletOpenFailed);
    return;
  }

  auto& [t_id, handle] = result.value();

  if (t_id != kwallet_transaction_id_) {
    return;
  }

  // Reset transaction ID to avoid processing the signal again.
  kwallet_transaction_id_ = kKWalletInvalidTransactionId;

  OnKWalletOpen(handle);
}

void FreedesktopSecretKeyProvider::OnKWalletOpen(int32_t handle) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  kwallet_handle_ = handle;
  if (kwallet_handle_ == kKWalletInvalidHandle) {
    FinalizeFailure(InitStatus::kKWalletOpenFailed);
    return;
  }

  dbus_utils::CallMethod<"iss", "b">(
      kwallet_proxy_, kKWalletInterface, kKWalletMethodHasFolder,
      base::BindOnce(&FreedesktopSecretKeyProvider::OnKWalletHasFolder,
                     weak_ptr_factory_.GetWeakPtr()),
      kwallet_handle_, kKWalletFolder, product_name_);
}

void FreedesktopSecretKeyProvider::OnKWalletHasFolder(
    dbus_utils::CallMethodResultSig<"b"> has_folder) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!has_folder.has_value()) {
    FinalizeFailure(InitStatus::kKWalletFolderCheckFailed);
    return;
  }

  if (std::get<0>(has_folder.value())) {
    dbus_utils::CallMethod<"isss", "b">(
        kwallet_proxy_, kKWalletInterface, kKWalletMethodHasEntry,
        base::BindOnce(&FreedesktopSecretKeyProvider::OnKWalletHasEntry,
                       weak_ptr_factory_.GetWeakPtr()),
        kwallet_handle_, kKWalletFolder, kKeyName, product_name_);
  } else {
    dbus_utils::CallMethod<"iss", "b">(
        kwallet_proxy_, kKWalletInterface, kKWalletMethodCreateFolder,
        base::BindOnce(&FreedesktopSecretKeyProvider::OnKWalletCreateFolder,
                       weak_ptr_factory_.GetWeakPtr()),
        kwallet_handle_, kKWalletFolder, product_name_);
  }
}

void FreedesktopSecretKeyProvider::OnKWalletCreateFolder(
    dbus_utils::CallMethodResultSig<"b"> success) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!success.has_value() || !std::get<0>(success.value())) {
    FinalizeFailure(InitStatus::kKWalletFolderCreationFailed);
    return;
  }
  GenerateAndWriteKWalletPassword();
}

void FreedesktopSecretKeyProvider::OnKWalletHasEntry(
    dbus_utils::CallMethodResultSig<"b"> has_entry) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!has_entry.has_value()) {
    FinalizeFailure(InitStatus::kKWalletEntryCheckFailed);
    return;
  }

  if (std::get<0>(has_entry.value())) {
    dbus_utils::CallMethod<"isss", "s">(
        kwallet_proxy_, kKWalletInterface, kKWalletMethodReadPassword,
        base::BindOnce(&FreedesktopSecretKeyProvider::OnKWalletReadPassword,
                       weak_ptr_factory_.GetWeakPtr()),
        kwallet_handle_, kKWalletFolder, kKeyName, product_name_);
  } else {
    GenerateAndWriteKWalletPassword();
  }
}

void FreedesktopSecretKeyProvider::OnKWalletReadPassword(
    dbus_utils::CallMethodResultSig<"s"> secret_reply) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (!secret_reply.has_value()) {
    FinalizeFailure(InitStatus::kKWalletReadFailed);
    return;
  }

  std::string secret = std::move(std::get<0>(secret_reply.value()));
  if (secret.empty()) {
    // The synchronous KWallet backend generates a new password if the
    // existing one is empty, so that logic is duplicated here.
    LOG(ERROR) << "Existing KWallet password is empty. Generating a new one.";
    GenerateAndWriteKWalletPassword();
    return;
  }

  DeriveKeyFromSecret(base::as_byte_span(secret));
}

void FreedesktopSecretKeyProvider::GenerateAndWriteKWalletPassword() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  auto secret = base::MakeRefCounted<base::RefCountedString>(
      base::Base64Encode(base::RandBytesAsVector(kSecretLengthBytes)));
  dbus_utils::CallMethod<"issss", "i">(
      kwallet_proxy_, kKWalletInterface, kKWalletMethodWritePassword,
      base::BindOnce(&FreedesktopSecretKeyProvider::OnKWalletWritePassword,
                     weak_ptr_factory_.GetWeakPtr(), secret),
      kwallet_handle_, kKWalletFolder, kKeyName, secret->as_string(),
      product_name_);
}

void FreedesktopSecretKeyProvider::OnKWalletWritePassword(
    scoped_refptr<base::RefCountedMemory> generated_secret,
    dbus_utils::CallMethodResultSig<"i"> return_code) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!return_code.has_value()) {
    FinalizeFailure(InitStatus::kKWalletWriteFailed);
    return;
  }

  int32_t kwallet_code = std::get<0>(return_code.value());
  if (kwallet_code != 0) {
    LOG(ERROR) << "KWallet writePassword failed with code: " << kwallet_code;
    FinalizeFailure(InitStatus::kKWalletWriteFailed);
    return;
  }

  DeriveKeyFromSecret(*generated_secret);
}

void FreedesktopSecretKeyProvider::CreateItem(
    scoped_refptr<base::RefCountedMemory> secret) {
  std::map<std::string, std::string> attributes{
      {kApplicationAttributeKey, kAppName},
      {kSchemaAttributeKey, kSchemaAttributeValue}};

  std::map<std::string, dbus_utils::Variant> props;
  props.emplace(kSecretItemAttributesProperty,
                dbus_utils::Variant::Wrap<"a{ss}">(std::move(attributes)));
  props.emplace(kSecretItemLabelProperty,
                dbus_utils::Variant::Wrap<"s">(kKeyName));

  std::vector<uint8_t> secret_bytes(secret->begin(), secret->end());
  auto secret_struct =
      std::make_tuple(session_proxy_->object_path(), std::vector<uint8_t>(),
                      std::move(secret_bytes), kMimePlain);

  auto* collection_proxy = bus_->GetObjectProxy(
      kSecretServiceName, default_collection_proxy_->object_path());
  Prompter<dbus::ObjectPath>::Prompt<"a{sv}(oayays)b", "oo">(
      bus_, collection_proxy, kSecretCollectionInterface, kMethodCreateItem,
      base::BindOnce(&FreedesktopSecretKeyProvider::OnCreateItem,
                     weak_ptr_factory_.GetWeakPtr(), secret),
      props, secret_struct, false);
}

void FreedesktopSecretKeyProvider::OnCreateItem(
    scoped_refptr<base::RefCountedMemory> secret,
    std::optional<dbus::ObjectPath> created_item) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!created_item.has_value() || created_item->value().empty()) {
    FinalizeFailure(InitStatus::kCreateItemFailed);
    return;
  }
  DeriveKeyFromSecret(*secret);
}

void FreedesktopSecretKeyProvider::DeriveKeyFromSecret(
    base::span<const uint8_t> secret) {
  static_assert(kDerivedKeySizeInBits % 8 == 0);
  std::array<uint8_t, kDerivedKeySizeInBits / 8> key_bytes;
  crypto::kdf::Pbkdf2HmacSha1(
      {kEncryptionIterations}, secret,
      base::as_byte_span(base::span_from_cstring(kSalt)), key_bytes,
      crypto::SubtlePassKey{});
  Encryptor::Key key(key_bytes, mojom::Algorithm::kAES128CBC);
  FinalizeSuccess(std::move(key));
}

void FreedesktopSecretKeyProvider::FinalizeSuccess(Encryptor::Key key) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  base::UmaHistogramEnumeration(kUmaInitStatus, InitStatus::kSuccess);
  std::move(key_callback_).Run(kEncryptionTag, std::move(key));
  CloseSession();
}

void FreedesktopSecretKeyProvider::FinalizeFailure(InitStatus status) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!key_callback_) {
    return;
  }
  base::UmaHistogramEnumeration(kUmaInitStatus, status);
  std::move(key_callback_)
      .Run(kEncryptionTag,
           base::unexpected(KeyProvider::KeyError::kPermanentlyUnavailable));
  CloseSession();
}

void FreedesktopSecretKeyProvider::CloseSession() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (session_opened_) {
    dbus_utils::CallMethod<"", "">(session_proxy_, kSecretSessionInterface,
                                   kMethodClose, base::DoNothing());
  }
  if (kwallet_handle_ != kKWalletInvalidHandle) {
    dbus_utils::CallMethod<"ibs", "i">(kwallet_proxy_, kKWalletInterface,
                                       kKWalletMethodClose, base::DoNothing(),
                                       kwallet_handle_, false, product_name_);
    kwallet_handle_ = kKWalletInvalidHandle;
  }
}

}  // namespace os_crypt_async
