// Copyright 2014 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/security_interstitials/content/stateful_ssl_host_state_delegate.h"

#include <stdint.h>

#include <functional>
#include <memory>
#include <optional>
#include <set>
#include <utility>

#include "base/base64.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/json/values_util.h"
#include "base/logging.h"
#include "base/strings/string_number_conversions.h"
#include "base/time/clock.h"
#include "base/time/default_clock.h"
#include "base/time/time.h"
#include "base/values.h"
#include "build/build_config.h"
#include "components/content_settings/core/browser/content_settings_pref_provider.h"
#include "components/content_settings/core/browser/host_content_settings_map.h"
#include "components/content_settings/core/common/content_settings_types.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "components/security_interstitials/core/pref_names.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/storage_partition.h"
#include "net/base/hash_value.h"
#include "net/cert/x509_certificate.h"
#include "services/network/public/mojom/network_context.mojom.h"
#include "url/gurl.h"

namespace {

// Certificate error bypasses are remembered for one week.
const uint64_t kCertErrorBypassExpirationInSeconds = UINT64_C(604800);

// The expiration for HTTPS-First Mode bypasses is 15 days.
const uint64_t kHTTPSFirstModeBypassExpirationInSeconds = UINT64_C(1296000);

// Keys for the per-site error + certificate finger to judgment content
// settings map.
const char kSSLCertDecisionCertErrorMapKey[] = "cert_exceptions_map";
const char kSSLCertDecisionExpirationTimeKey[] = "decision_expiration_time";
const char kSSLCertDecisionVersionKey[] = "version";

const int kDefaultSSLCertDecisionVersion = 1;

// All SSL decisions are per host (and are shared arcoss schemes), so this
// canonicalizes all hosts into a secure scheme GURL to use with content
// settings. The returned GURL will be the passed in host with an empty path and
// https:// as the scheme.
GURL GetSecureGURLForHost(const std::string& host) {
  std::string url = "https://" + host;
  return GURL(url);
}

std::string GetKey(const net::X509Certificate& cert, net::Error error) {
  // Since a security decision will be made based on the fingerprint, Chrome
  // should use the SHA-256 fingerprint for the certificate.
  net::SHA256HashValue fingerprint = cert.CalculateChainFingerprint256();
  std::string base64_fingerprint = base::Base64Encode(fingerprint);
  return base::NumberToString(error) + base64_fingerprint;
}

bool HostFilterToPatternFilter(
    base::OnceCallback<bool(const std::string&)> host_filter,
    const ContentSettingsPattern& primary_pattern,
    const ContentSettingsPattern& secondary_pattern) {
  // We only ever set origin-scoped exceptions which are of the form
  // "https://<host>:443". That is a valid URL, so we can compare |host_filter|
  // against its host.
  GURL url = GURL(primary_pattern.ToString());
  DCHECK(url.is_valid());
  return std::move(host_filter).Run(url.GetHost());
}

}  // namespace

StatefulSSLHostStateDelegate::StatefulSSLHostStateDelegate(
    content::BrowserContext* browser_context,
    HostContentSettingsMap* host_content_settings_map)
    : clock_(new base::DefaultClock()),
      browser_context_(browser_context),
      host_content_settings_map_(host_content_settings_map),
      https_only_mode_allowlist_(
          host_content_settings_map,
          clock_.get(),
          base::Seconds(kHTTPSFirstModeBypassExpirationInSeconds)),
      https_only_mode_enforcelist_(host_content_settings_map_, clock_.get()),
      https_first_balanced_mode_suppressed_for_testing(false) {}

StatefulSSLHostStateDelegate::~StatefulSSLHostStateDelegate() = default;

void StatefulSSLHostStateDelegate::AllowCert(
    const std::string& host,
    const net::X509Certificate& cert,
    net::Error error,
    content::StoragePartition* storage_partition) {
  if (!storage_partition ||
      storage_partition != browser_context_->GetDefaultStoragePartition()) {
    // Decisions for non-default storage partitions are stored in memory only;
    // see comment on declaration of
    // |allowed_certs_for_non_default_storage_partitions_|.
    auto allowed_cert =
        AllowedCert(GetKey(cert, error), storage_partition->GetPath());
    allowed_certs_for_non_default_storage_partitions_[host].insert(
        allowed_cert);
    return;
  }

  GURL url = GetSecureGURLForHost(host);
  base::Value value(host_content_settings_map_->GetWebsiteSetting(
      url, url, ContentSettingsType::SSL_CERT_DECISIONS, nullptr));

  if (!value.is_dict())
    value = base::Value(base::Value::Type::DICT);

  base::DictValue* cert_dict =
      GetValidCertDecisionsDict(CREATE_DICTIONARY_ENTRIES, value.GetDict());
  // If a a valid certificate dictionary cannot be extracted from the content
  // setting, that means it's in an unknown format. Unfortunately, there's
  // nothing to be done in that case, so a silent fail is the only option.
  if (!cert_dict)
    return;

  value.GetDict().Set(kSSLCertDecisionVersionKey,
                      kDefaultSSLCertDecisionVersion);
  cert_dict->Set(GetKey(cert, error), ALLOWED);

  // The map takes ownership of the value, so it is released in the call to
  // SetWebsiteSettingDefaultScope.
  host_content_settings_map_->SetWebsiteSettingDefaultScope(
      url, GURL(), ContentSettingsType::SSL_CERT_DECISIONS, std::move(value));
}

void StatefulSSLHostStateDelegate::Clear(
    base::RepeatingCallback<bool(const std::string&)> host_filter) {
  // Convert host matching to content settings pattern matching. Content
  // settings deletion is done synchronously on the UI thread, so we can use
  // |host_filter| by reference.
  HostContentSettingsMap::PatternSourcePredicate pattern_filter;
  if (!host_filter.is_null()) {
    pattern_filter =
        base::BindRepeating(&HostFilterToPatternFilter, host_filter);
  }

  host_content_settings_map_->ClearSettingsForOneTypeWithPredicate(
      ContentSettingsType::SSL_CERT_DECISIONS, base::Time(), base::Time::Max(),
      pattern_filter);
  https_only_mode_allowlist_.Clear(base::Time(), base::Time::Max(),
                                   pattern_filter);

  // This might not be necessary since the engagement score of a site will be
  // cleared and the site will be removed from the enforcelist. However, there
  // could be a period between clearing and navigating where the enforcelist
  // would still be persisted on disk etc, so clear it explicitly.
  https_only_mode_enforcelist_.Clear(base::Time(), base::Time::Max(),
                                     pattern_filter);
}

content::SSLHostStateDelegate::CertJudgment
StatefulSSLHostStateDelegate::QueryPolicy(
    const std::string& host,
    const net::X509Certificate& cert,
    net::Error error,
    content::StoragePartition* storage_partition) {
  if (!storage_partition ||
      storage_partition != browser_context_->GetDefaultStoragePartition()) {
    if (allowed_certs_for_non_default_storage_partitions_.find(host) ==
        allowed_certs_for_non_default_storage_partitions_.end()) {
      return DENIED;
    }
    AllowedCert allowed_cert =
        AllowedCert(GetKey(cert, error), storage_partition->GetPath());
    if (allowed_certs_for_non_default_storage_partitions_[host].contains(
            allowed_cert)) {
      return ALLOWED;
    }
    return DENIED;
  }

  GURL url = GetSecureGURLForHost(host);
  base::Value value(host_content_settings_map_->GetWebsiteSetting(
      url, url, ContentSettingsType::SSL_CERT_DECISIONS, nullptr));

  if (!value.is_dict())
    return DENIED;

  base::DictValue* cert_error_dict = GetValidCertDecisionsDict(
      DO_NOT_CREATE_DICTIONARY_ENTRIES, value.GetDict());
  if (!cert_error_dict) {
    // This revoke is necessary to clear any old expired setting that may be
    // lingering in the case that an old decision expried.
    RevokeUserAllowExceptions(host);
    return DENIED;
  }

  std::optional<int> policy_decision =
      cert_error_dict->FindInt(GetKey(cert, error));

  // If a policy decision was successfully retrieved and it's a valid value of
  // ALLOWED, return the valid value. Otherwise, return DENIED.
  if (policy_decision.has_value() && policy_decision.value() == ALLOWED)
    return ALLOWED;

  return DENIED;
}

void StatefulSSLHostStateDelegate::HostRanInsecureContent(
    const std::string& host,
    InsecureContentType content_type) {
  switch (content_type) {
    case MIXED_CONTENT:
      ran_mixed_content_hosts_.insert(host);
      return;
    case CERT_ERRORS_CONTENT:
      ran_content_with_cert_errors_hosts_.insert(host);
      return;
  }
}

bool StatefulSSLHostStateDelegate::DidHostRunInsecureContent(
    const std::string& host,
    InsecureContentType content_type) {
  switch (content_type) {
    case MIXED_CONTENT:
      return ran_mixed_content_hosts_.contains(host);
    case CERT_ERRORS_CONTENT:
      return ran_content_with_cert_errors_hosts_.contains(host);
  }
  NOTREACHED();
}

void StatefulSSLHostStateDelegate::AllowHttpForHost(
    const std::string& host,
    content::StoragePartition* storage_partition) {
  bool is_nondefault_storage =
      !storage_partition ||
      storage_partition != browser_context_->GetDefaultStoragePartition();
  https_only_mode_allowlist_.AllowHttpForHost(host, is_nondefault_storage);
}

bool StatefulSSLHostStateDelegate::IsHttpAllowedForHost(
    const std::string& host,
    content::StoragePartition* storage_partition) {
  bool is_nondefault_storage =
      !storage_partition ||
      storage_partition != browser_context_->GetDefaultStoragePartition();
  return https_only_mode_allowlist_.IsHttpAllowedForHost(host,
                                                         is_nondefault_storage);
}

void StatefulSSLHostStateDelegate::SetHttpsEnforcementForHost(
    const std::string& host,
    bool enforced,
    content::StoragePartition* storage_partition) {
  bool is_nondefault_storage =
      !storage_partition ||
      storage_partition != browser_context_->GetDefaultStoragePartition();
  if (enforced) {
    https_only_mode_enforcelist_.EnforceForHost(host, is_nondefault_storage);
  } else {
    https_only_mode_enforcelist_.UnenforceForHost(host, is_nondefault_storage);
  }
}

bool StatefulSSLHostStateDelegate::IsHttpsEnforcedForUrl(
    const GURL& url,
    content::StoragePartition* storage_partition) {
  bool is_nondefault_storage =
      !storage_partition ||
      storage_partition != browser_context_->GetDefaultStoragePartition();
  return https_only_mode_enforcelist_.IsEnforcedForUrl(url,
                                                       is_nondefault_storage);
}

std::set<GURL> StatefulSSLHostStateDelegate::GetHttpsEnforcedHosts(
    content::StoragePartition* storage_partition) const {
  bool is_nondefault_storage =
      !storage_partition ||
      storage_partition != browser_context_->GetDefaultStoragePartition();
  return https_only_mode_enforcelist_.GetHosts(is_nondefault_storage);
}

void StatefulSSLHostStateDelegate::ClearHttpsOnlyModeAllowlist() {
  https_only_mode_allowlist_.ClearAllowlist(base::Time(), base::Time::Max());
}

void StatefulSSLHostStateDelegate::ClearHttpsEnforcelist() {
  https_only_mode_enforcelist_.ClearEnforcements(base::Time(),
                                                 base::Time::Max());
}

void StatefulSSLHostStateDelegate::RevokeUserAllowExceptions(
    const std::string& host) {
  GURL url = GetSecureGURLForHost(host);
  host_content_settings_map_->SetWebsiteSettingDefaultScope(
      url, GURL(), ContentSettingsType::SSL_CERT_DECISIONS, base::Value());
  // Decisions for non-default storage partitions are stored separately in
  // memory; delete those as well.
  allowed_certs_for_non_default_storage_partitions_.erase(host);

  https_only_mode_allowlist_.RevokeUserAllowExceptions(host);
  https_only_mode_enforcelist_.RevokeEnforcements(host);
}

bool StatefulSSLHostStateDelegate::HasAllowException(
    const std::string& host,
    content::StoragePartition* storage_partition) {
  return HasCertAllowException(host, storage_partition) ||
         IsHttpAllowedForHost(host, storage_partition);
}

bool StatefulSSLHostStateDelegate::HasAllowExceptionForAnyHost(
    content::StoragePartition* storage_partition) {
  return HasCertAllowExceptionForAnyHost(storage_partition) ||
         IsHttpAllowedForAnyHost(storage_partition);
}

bool StatefulSSLHostStateDelegate::HasCertAllowExceptionForAnyHost(
    content::StoragePartition* storage_partition) {
  if (!storage_partition ||
      storage_partition != browser_context_->GetDefaultStoragePartition()) {
    return !allowed_certs_for_non_default_storage_partitions_.empty();
  }

  ContentSettingsForOneType content_settings_list =
      host_content_settings_map_->GetSettingsForOneType(
          ContentSettingsType::SSL_CERT_DECISIONS);
  return !content_settings_list.empty();
}

bool StatefulSSLHostStateDelegate::IsHttpAllowedForAnyHost(
    content::StoragePartition* storage_partition) {
  bool is_nondefault_storage =
      !storage_partition ||
      storage_partition != browser_context_->GetDefaultStoragePartition();

  return https_only_mode_allowlist_.IsHttpAllowedForAnyHost(
      is_nondefault_storage);
}

// TODO(jww): This will revoke all of the decisions in the browser context.
// However, the networking stack actually keeps track of its own list of
// exceptions per-HttpNetworkTransaction in the SSLConfig structure (see the
// allowed_bad_certs Vector in net/ssl/ssl_config.h). This dual-tracking of
// exceptions introduces a problem where the browser context can revoke a
// certificate, but if a transaction reuses a cached version of the SSLConfig
// (probably from a pooled socket), it may bypass the intestitial layer.
//
// Over time, the cached versions should expire and it should converge on
// showing the interstitial. We probably need to introduce into the networking
// stack a way revoke SSLConfig's allowed_bad_certs lists per socket.
//
// For now, RevokeUserAllowExceptionsHard is our solution for the rare case
// where it is necessary to revoke the preferences immediately. It does so by
// flushing idle sockets, thus it is a big hammer and should be wielded with
// extreme caution as it can have a big, negative impact on network performance.
void StatefulSSLHostStateDelegate::RevokeUserAllowExceptionsHard(
    const std::string& host) {
  RevokeUserAllowExceptions(host);
  auto* network_context =
      browser_context_->GetDefaultStoragePartition()->GetNetworkContext();
  network_context->CloseIdleConnections(base::NullCallback());
}

bool StatefulSSLHostStateDelegate::
    HttpsFirstBalancedModeSuppressedForTesting() {
  return https_first_balanced_mode_suppressed_for_testing;
}

void StatefulSSLHostStateDelegate::
    SetHttpsFirstBalancedModeSuppressedForTesting(bool suppressed) {
  https_first_balanced_mode_suppressed_for_testing = suppressed;
}

void StatefulSSLHostStateDelegate::SetClockForTesting(
    std::unique_ptr<base::Clock> clock) {
  // Pointers to the existing Clock object must be reset before swapping the
  // underlying Clock object, otherwise they are dangling (briefly).
  https_only_mode_allowlist_.SetClockForTesting(nullptr);    // IN-TEST
  https_only_mode_enforcelist_.SetClockForTesting(nullptr);  // IN-TEST

  clock_ = std::move(clock);
  https_only_mode_allowlist_.SetClockForTesting(clock_.get());    // IN-TEST
  https_only_mode_enforcelist_.SetClockForTesting(clock_.get());  // IN-TEST
}

bool StatefulSSLHostStateDelegate::HasCertAllowException(
    const std::string& host,
    content::StoragePartition* storage_partition) {
  if (!storage_partition ||
      storage_partition != browser_context_->GetDefaultStoragePartition()) {
    return allowed_certs_for_non_default_storage_partitions_.contains(host);
  }

  GURL url = GetSecureGURLForHost(host);
  const ContentSettingsPattern pattern =
      ContentSettingsPattern::FromURLNoWildcard(url);

  const base::Value value(host_content_settings_map_->GetWebsiteSetting(
      url, url, ContentSettingsType::SSL_CERT_DECISIONS, nullptr));

  if (!value.is_dict())
    return false;

  for (const auto pair : value.GetDict()) {
    if (!pair.second.is_int())
      continue;

    if (static_cast<CertJudgment>(pair.second.GetInt()) == ALLOWED)
      return true;
  }

  return false;
}

// This helper function gets the dictionary of certificate fingerprints to
// errors of certificates that have been accepted by the user from the content
// dictionary that has been passed in. The returned pointer is owned by the the
// argument dict that is passed in.
//
// If create_entries is set to |DO_NOT_CREATE_DICTIONARY_ENTRIES|,
// GetValidCertDecisionsDict will return nullptr if there is anything invalid
// about the setting, such as an invalid version or invalid value types (in
// addition to there not being any values in the dictionary). If create_entries
// is set to |CREATE_DICTIONARY_ENTRIES|, if no dictionary is found or the
// decisions are expired, a new dictionary will be created.
base::DictValue* StatefulSSLHostStateDelegate::GetValidCertDecisionsDict(
    CreateDictionaryEntriesDisposition create_entries,
    base::DictValue& dict) {
  // Extract the version of the certificate decision structure from the content
  // setting.
  std::optional<int> version = dict.FindInt(kSSLCertDecisionVersionKey);
  if (!version) {
    if (create_entries == DO_NOT_CREATE_DICTIONARY_ENTRIES)
      return nullptr;

    dict.Set(kSSLCertDecisionVersionKey, kDefaultSSLCertDecisionVersion);
    version = std::make_optional<int>(kDefaultSSLCertDecisionVersion);
  }

  // If the version is somehow a newer version than Chrome can handle, there's
  // really nothing to do other than fail silently and pretend it doesn't exist
  // (or is malformed).
  if (*version > kDefaultSSLCertDecisionVersion) {
    LOG(ERROR) << "Failed to parse a certificate error exception that is in a "
               << "newer version format (" << *version << ") than is supported"
               << "(" << kDefaultSSLCertDecisionVersion << ")";
    return nullptr;
  }

  // Extract the certificate decision's expiration time from the content
  // setting. If there is no expiration time, that means it should never expire
  // and it should reset only at session restart, so skip all of the expiration
  // checks.
  bool expired = false;
  base::Time now = clock_->Now();
  auto* decision_expiration_value =
      dict.Find(kSSLCertDecisionExpirationTimeKey);
  auto decision_expiration = base::ValueToTime(decision_expiration_value);

  // Check to see if the user's certificate decision has expired.
  // - Expired and |create_entries| is DO_NOT_CREATE_DICTIONARY_ENTRIES, return
  // nullptr.
  // - Expired and |create_entries| is CREATE_DICTIONARY_ENTRIES, update the
  // expiration time.
  if (decision_expiration <= now) {
    if (create_entries == DO_NOT_CREATE_DICTIONARY_ENTRIES)
      return nullptr;

    expired = true;
    base::Time expiration_time =
        now + base::Seconds(kCertErrorBypassExpirationInSeconds);
    // Unfortunately, JSON (and thus content settings) doesn't support int64_t
    // values, only doubles. Since this mildly depends on precision, it is
    // better to store the value as a string.
    dict.Set(kSSLCertDecisionExpirationTimeKey,
             base::TimeToValue(expiration_time));
  }

  // Extract the map of certificate fingerprints to errors from the setting.
  base::DictValue* cert_error_dict =
      dict.FindDict(kSSLCertDecisionCertErrorMapKey);
  if (expired || !cert_error_dict) {
    if (create_entries == DO_NOT_CREATE_DICTIONARY_ENTRIES)
      return nullptr;

    cert_error_dict = dict.EnsureDict(kSSLCertDecisionCertErrorMapKey);
    cert_error_dict->clear();
  }

  return cert_error_dict;
}
