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

#include "extensions/browser/install_verifier.h"

#include <algorithm>
#include <string>
#include <utility>

#include "base/base_switches.h"
#include "base/check.h"
#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/metrics/field_trial.h"
#include "base/notimplemented.h"
#include "base/one_shot_event.h"
#include "base/stl_util.h"
#include "base/time/time.h"
#include "base/trace_event/trace_event.h"
#include "base/values.h"
#include "build/branding_buildflags.h"
#include "build/build_config.h"
#include "components/prefs/pref_service.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/common/content_switches.h"
#include "extensions/browser/disable_reason.h"
#include "extensions/browser/extension_management_client.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_system.h"
#include "extensions/browser/extensions_browser_client.h"
#include "extensions/browser/install_signer.h"
#include "extensions/browser/pref_names.h"
#include "extensions/buildflags/buildflags.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_id.h"
#include "extensions/common/extension_set.h"
#include "extensions/common/manifest.h"
#include "extensions/common/manifest_handlers/manifest_url_handlers.h"
#include "extensions/common/switches.h"
#include "services/network/public/mojom/url_loader_factory.mojom.h"
#include "ui/base/l10n/l10n_util.h"

static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));

namespace extensions {

namespace {

// This should only be set during tests.
ScopedInstallVerifierBypassForTest::ForceType* g_bypass_for_test = nullptr;

enum class VerifyStatus {
  NONE = 0,   // Do not request install signatures, and do not enforce them.
  BOOTSTRAP,  // Request install signatures, but do not enforce them.
  ENFORCE,    // Request install signatures, and enforce them.
  ENFORCE_STRICT,  // Same as ENFORCE, but hard fail if we can't fetch
                   // signatures.

  // This is used in histograms - do not remove or reorder entries above! Also
  // the "MAX" item below should always be the last element.
  VERIFY_STATUS_MAX
};

VerifyStatus GetExperimentStatus() {
#if BUILDFLAG(GOOGLE_CHROME_BRANDING) && \
    (BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC))
  return VerifyStatus::ENFORCE;
#else
  return VerifyStatus::NONE;
#endif  // BUILDFLAG(GOOGLE_CHROME_BRANDING)
}

VerifyStatus GetCommandLineStatus() {
  const base::CommandLine* cmdline = base::CommandLine::ForCurrentProcess();
  if (!InstallSigner::GetForcedNotFromWebstore().empty())
    return VerifyStatus::ENFORCE;

  if (cmdline->HasSwitch(switches::kExtensionsInstallVerification)) {
    std::string value =
        cmdline->GetSwitchValueASCII(switches::kExtensionsInstallVerification);
    if (value == "bootstrap")
      return VerifyStatus::BOOTSTRAP;
    else if (value == "enforce_strict")
      return VerifyStatus::ENFORCE_STRICT;
    else
      return VerifyStatus::ENFORCE;
  }

  return VerifyStatus::NONE;
}

VerifyStatus GetStatus() {
  if (g_bypass_for_test) {
    switch (*g_bypass_for_test) {
      case ScopedInstallVerifierBypassForTest::kForceOn:
        return VerifyStatus::ENFORCE_STRICT;
      case ScopedInstallVerifierBypassForTest::kForceOff:
        return VerifyStatus::NONE;
    }
  }

  return std::max(GetExperimentStatus(), GetCommandLineStatus());
}

bool ShouldFetchSignature() {
  return GetStatus() >= VerifyStatus::BOOTSTRAP;
}

bool CanUseExtensionApis(const Extension& extension) {
  return extension.is_extension() || extension.is_legacy_packaged_app();
}

}  // namespace

InstallVerifier::InstallVerifier(ExtensionPrefs* prefs,
                                 content::BrowserContext* context)
    : prefs_(prefs), context_(context), bootstrap_check_complete_(false) {}

InstallVerifier::~InstallVerifier() = default;

// static
bool InstallVerifier::ShouldEnforce() {
  return GetStatus() >= VerifyStatus::ENFORCE;
}

// static
bool InstallVerifier::NeedsVerification(const Extension& extension,
                                        content::BrowserContext* context) {
  return IsFromStore(extension, context) && CanUseExtensionApis(extension);
}

// static
bool InstallVerifier::IsFromStore(const Extension& extension,
                                  content::BrowserContext* context) {
  return extension.from_webstore() ||
         ExtensionsBrowserClient::Get()
             ->GetExtensionManagementClient(context)
             ->UpdatesFromWebstore(extension);
}

void InstallVerifier::Init() {
  TRACE_EVENT0("browser,startup", "extensions::InstallVerifier::Init");

  std::unique_ptr<InstallSignature> signature_from_prefs =
      InstallSignature::FromDict(prefs_->GetInstallSignature());
  if (signature_from_prefs.get()) {
    if (!InstallSigner::VerifySignature(*signature_from_prefs)) {
      DVLOG(1) << "Init - ignoring invalid signature";
    } else {
      signature_ = std::move(signature_from_prefs);
      GarbageCollect();
    }
  }

  ExtensionSystem::Get(context_)->ready().Post(
      FROM_HERE, base::BindOnce(&InstallVerifier::MaybeBootstrapSelf,
                                weak_factory_.GetWeakPtr()));
}

void InstallVerifier::VerifyAllExtensions() {
  AddMany(GetExtensionsToVerify(), ADD_ALL);
}

base::Time InstallVerifier::SignatureTimestamp() {
  if (signature_.get())
    return signature_->timestamp;
  else
    return base::Time();
}

bool InstallVerifier::IsKnownId(const std::string& id) const {
  return signature_.get() &&
         (signature_->ids.contains(id) || signature_->invalid_ids.contains(id));
}

bool InstallVerifier::IsInvalid(const std::string& id) const {
  return ((signature_.get() && signature_->invalid_ids.contains(id)));
}

void InstallVerifier::VerifyExtension(const std::string& extension_id) {
  ExtensionIdSet ids;
  ids.insert(extension_id);
  AddMany(ids, ADD_SINGLE);
}

void InstallVerifier::AddMany(const ExtensionIdSet& ids, OperationType type) {
  if (!ShouldFetchSignature()) {
    OnVerificationComplete(true, type);  // considered successful.
    return;
  }

  if (signature_.get()) {
    ExtensionIdSet not_allowed_yet =
        base::STLSetDifference<ExtensionIdSet>(ids, signature_->ids);
    if (not_allowed_yet.empty()) {
      OnVerificationComplete(true, type);  // considered successful.
      return;
    }
  }

  std::unique_ptr<InstallVerifier::PendingOperation> operation(
      new InstallVerifier::PendingOperation(type));
  operation->ids.insert(ids.begin(), ids.end());

  operation_queue_.push(std::move(operation));

  // If there are no ongoing pending requests, we need to kick one off.
  if (operation_queue_.size() == 1)
    BeginFetch();
}

void InstallVerifier::AddProvisional(const ExtensionIdSet& ids) {
  provisional_.insert(ids.begin(), ids.end());
  AddMany(ids, ADD_PROVISIONAL);
}

void InstallVerifier::Remove(const std::string& id) {
  ExtensionIdSet ids;
  ids.insert(id);
  RemoveMany(ids);
}

void InstallVerifier::RemoveMany(const ExtensionIdSet& ids) {
  if (!signature_.get() || !ShouldFetchSignature())
    return;

  if (std::ranges::any_of(ids, [this](const std::string& id) {
        return signature_->ids.contains(id) ||
               signature_->invalid_ids.contains(id);
      })) {
    return;
  }

  std::unique_ptr<InstallVerifier::PendingOperation> operation(
      new InstallVerifier::PendingOperation(InstallVerifier::REMOVE));
  operation->ids = ids;

  operation_queue_.push(std::move(operation));
  if (operation_queue_.size() == 1)
    BeginFetch();
}

bool InstallVerifier::AllowedByEnterprisePolicy(const std::string& id) const {
  return ExtensionsBrowserClient::Get()
      ->GetExtensionManagementClient(context_)
      ->IsInstallationExplicitlyAllowed(id);
}

std::string InstallVerifier::GetDebugPolicyProviderName() const {
  return std::string("InstallVerifier");
}

bool InstallVerifier::MustRemainDisabled(
    const Extension* extension,
    disable_reason::DisableReason* reason) const {
  CHECK(extension);
  if (!CanUseExtensionApis(*extension))
    return false;
  if (Manifest::IsUnpackedLocation(extension->location()))
    return false;
  if (extension->location() == mojom::ManifestLocation::kComponent)
    return false;
  if (AllowedByEnterprisePolicy(extension->id()) &&
      !ExtensionsBrowserClient::Get()
           ->GetExtensionManagementClient(context_)
           ->IsForceInstalledInLowTrustEnvironment(*extension)) {
    return false;
  }

  bool verified = true;
  if (InstallSigner::GetForcedNotFromWebstore().contains(extension->id())) {
    verified = false;
  } else if (!IsFromStore(*extension, context_)) {
    verified = false;
  } else if (!signature_ && (!bootstrap_check_complete_ ||
                             GetStatus() < VerifyStatus::ENFORCE_STRICT)) {
    // If we don't have a signature yet, we'll temporarily consider every
    // extension from the webstore verified to avoid false positives on existing
    // profiles hitting this code for the first time. The InstallVerifier
    // will bootstrap itself once the ExtensionsSystem is ready.
    // |verified| is already set to true.
  } else if (!IsVerified(extension->id())) {
    // Transient network failures can create a stale signature missing recently
    // added extension ids. To avoid false positives, consider all extensions to
    // be from the webstore unless the signature explicitly lists the extension
    // as invalid.
    if (!signature_ || signature_->invalid_ids.contains(extension->id()) ||
        GetStatus() >= VerifyStatus::ENFORCE_STRICT) {
      verified = false;
    }
  }

  if (!verified && !ShouldEnforce())
    verified = true;

  if (!verified) {
    DLOG(WARNING) << "Disabling extension " << extension->id() << " ('"
                  << extension->name()
                  << "') due to install verification failure. In tests you "
                  << "might want to use a ScopedInstallVerifierBypassForTest "
                  << "instance to prevent this.";

    if (reason) {
      *reason = disable_reason::DISABLE_NOT_VERIFIED;
    }
  }
  return !verified;
}

InstallVerifier::PendingOperation::PendingOperation(OperationType type)
    : type(type) {}

InstallVerifier::PendingOperation::~PendingOperation() = default;

ExtensionIdSet InstallVerifier::GetExtensionsToVerify() const {
  ExtensionIdSet result;
  const ExtensionSet extensions =
      ExtensionRegistry::Get(context_)->GenerateInstalledExtensionsSet();
  for (const auto& extension : extensions) {
    if (NeedsVerification(*extension, context_)) {
      result.insert(extension->id());
    }
  }
  return result;
}

void InstallVerifier::MaybeBootstrapSelf() {
  ExtensionIdSet extension_ids = GetExtensionsToVerify();
  if ((signature_.get() == nullptr && ShouldFetchSignature()) ||
      std::ranges::any_of(extension_ids, [this](const std::string& id) {
        return !IsKnownId(id);
      })) {
    AddMany(extension_ids, ADD_ALL_BOOTSTRAP);
  } else {
    bootstrap_check_complete_ = true;
  }
}

void InstallVerifier::OnVerificationComplete(bool success, OperationType type) {
  switch (type) {
    case ADD_ALL:
    case ADD_ALL_BOOTSTRAP:
      bootstrap_check_complete_ = true;
      if (success) {
        // Iterate through the extensions and, if any are newly-verified and
        // should have the DISABLE_NOT_VERIFIED reason lifted, do so.
        const ExtensionSet& disabled_extensions =
            ExtensionRegistry::Get(context_)->disabled_extensions();
        for (ExtensionSet::const_iterator iter = disabled_extensions.begin();
             iter != disabled_extensions.end(); ++iter) {
          if (prefs_->HasDisableReason((*iter)->id(),
                                       disable_reason::DISABLE_NOT_VERIFIED) &&
              !MustRemainDisabled(iter->get(), nullptr)) {
            prefs_->RemoveDisableReason((*iter)->id(),
                                        disable_reason::DISABLE_NOT_VERIFIED);
          }
        }
      }
      if (success || GetStatus() == VerifyStatus::ENFORCE_STRICT) {
        ExtensionsBrowserClient::Get()->CheckManagementPolicy(context_);
      }
      break;
    // We don't need to check disable reasons for provisional adds or removals.
    case ADD_PROVISIONAL:
    case ADD_SINGLE:
    case REMOVE:
      break;
  }
}

void InstallVerifier::GarbageCollect() {
  if (!ShouldFetchSignature()) {
    return;
  }
  CHECK(signature_.get());
  ExtensionIdSet leftovers = signature_->ids;
  leftovers.insert(signature_->invalid_ids.begin(),
                   signature_->invalid_ids.end());
  for (const auto& extension_id : prefs_->GetExtensions()) {
    leftovers.erase(extension_id);
  }
  if (!leftovers.empty()) {
    RemoveMany(leftovers);
  }
}

bool InstallVerifier::IsVerified(const std::string& id) const {
  return ((signature_.get() && signature_->ids.contains(id)) ||
          provisional_.contains(id));
}

void InstallVerifier::BeginFetch() {
  DCHECK(ShouldFetchSignature());

  // TODO(asargent) - It would be possible to coalesce all operations in the
  // queue into one fetch - we'd probably just need to change the queue to
  // hold (set of ids, list of operation type) pairs.
  CHECK(!operation_queue_.empty());
  const PendingOperation& operation = *operation_queue_.front();

  ExtensionIdSet ids_to_sign;
  if (signature_.get()) {
    ids_to_sign.insert(signature_->ids.begin(), signature_->ids.end());
  }
  if (operation.type == InstallVerifier::REMOVE) {
    for (const std::string& id : operation.ids) {
      if (ids_to_sign.contains(id))
        ids_to_sign.erase(id);
    }
  } else {  // All other operation types are some form of "ADD".
    ids_to_sign.insert(operation.ids.begin(), operation.ids.end());
  }

  auto url_loader_factory = context_->GetDefaultStoragePartition()
                                ->GetURLLoaderFactoryForBrowserProcess();
  signer_ = std::make_unique<InstallSigner>(url_loader_factory, ids_to_sign);
  signer_->GetSignature(base::BindOnce(&InstallVerifier::SignatureCallback,
                                       weak_factory_.GetWeakPtr()));
}

void InstallVerifier::SaveToPrefs() {
  if (signature_.get())
    DCHECK(InstallSigner::VerifySignature(*signature_));

  if (!signature_.get() || signature_->ids.empty()) {
    DVLOG(1) << "SaveToPrefs - saving NULL";
    prefs_->SetInstallSignature(nullptr);
  } else {
    base::DictValue pref = signature_->ToDict();
    if (VLOG_IS_ON(1)) {
      DVLOG(1) << "SaveToPrefs - saving";

      DCHECK(InstallSigner::VerifySignature(*signature_));
      std::unique_ptr<InstallSignature> rehydrated =
          InstallSignature::FromDict(pref);
      DCHECK(InstallSigner::VerifySignature(*rehydrated));
    }
    prefs_->SetInstallSignature(&pref);
  }
}

void InstallVerifier::SignatureCallback(
    std::unique_ptr<InstallSignature> signature) {
  std::unique_ptr<PendingOperation> operation =
      std::move(operation_queue_.front());
  operation_queue_.pop();

  bool success = signature.get() && InstallSigner::VerifySignature(*signature);
  if (success) {
    signature_ = std::move(signature);
    SaveToPrefs();

    if (!provisional_.empty()) {
      // Update |provisional_| to remove ids that were successfully signed.
      provisional_ =
          base::STLSetDifference<ExtensionIdSet>(provisional_, signature_->ids);
    }
  }

  // TODO(asargent) - if this was something like a network error, we need to
  // do retries with exponential back off.
  OnVerificationComplete(success, operation->type);
  if (!operation_queue_.empty())
    BeginFetch();
}

ScopedInstallVerifierBypassForTest::ScopedInstallVerifierBypassForTest(
    ForceType force_type)
    : value_(force_type), old_value_(g_bypass_for_test) {
  g_bypass_for_test = &value_;
}

ScopedInstallVerifierBypassForTest::~ScopedInstallVerifierBypassForTest() {
  g_bypass_for_test = old_value_;
}

}  // namespace extensions
