| // Copyright 2021 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/ssl/https_first_mode_settings_tracker.h" |
| |
| #include <string_view> |
| |
| #include "base/feature_list.h" |
| #include "base/functional/bind.h" |
| #include "base/json/values_util.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/task/thread_pool.h" |
| #include "base/time/default_clock.h" |
| #include "base/values.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/content_settings/host_content_settings_map_factory.h" |
| #include "chrome/browser/metrics/chrome_metrics_service_accessor.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/safe_browsing/advanced_protection_status_manager_factory.h" |
| #include "chrome/browser/ssl/chrome_security_blocking_page_factory.h" |
| #include "chrome/browser/ssl/https_upgrades_interceptor.h" |
| #include "chrome/browser/ssl/https_upgrades_util.h" |
| #include "chrome/common/chrome_features.h" |
| #include "chrome/common/pref_names.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/security_interstitials/content/https_only_mode_blocking_page.h" |
| #include "components/security_interstitials/content/stateful_ssl_host_state_delegate.h" |
| #include "components/site_engagement/content/site_engagement_service.h" |
| #include "components/variations/synthetic_trials.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/storage_partition.h" |
| #include "net/base/url_util.h" |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| #include "chrome/browser/ash/profiles/profile_helper.h" |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| |
| // Minimum score of an HTTPS origin to enable HFM on its hostname. |
| const base::FeatureParam<int> kHttpsAddThreshold{ |
| &features::kHttpsFirstModeV2ForEngagedSites, "https-add-threshold", 80}; |
| |
| // Maximum score of an HTTP origin to enable HFM on its hostname. |
| const base::FeatureParam<int> kHttpsRemoveThreshold{ |
| &features::kHttpsFirstModeV2ForEngagedSites, "https-remove-threshold", 75}; |
| |
| // If HTTPS score goes below kHttpsRemoveThreshold or HTTP score goes above |
| // kHttpRemoveThreshold, disable HFM on this hostname. |
| const base::FeatureParam<int> kHttpAddThreshold{ |
| &features::kHttpsFirstModeV2ForEngagedSites, "http-add-threshold", 1}; |
| const base::FeatureParam<int> kHttpRemoveThreshold{ |
| &features::kHttpsFirstModeV2ForEngagedSites, "http-remove-threshold", 5}; |
| |
| // Parameters for Typically Secure User heuristic: |
| |
| // The rolling window size during which we check for HTTPS-Upgrades fallback |
| // entries. If the number of fallback entries is smaller than |
| // kMaxRecentFallbackEntryCount, we may automatically enable HTTPS-First Mode. |
| const base::TimeDelta kFallbackEntriesRollingWindowSize = base::Days(7); |
| |
| // Maximum number of past HTTPS-Upgrade fallback events (i.e. would-be warnings) |
| // to auto-enable HFM, including the current fallback event that's being added |
| // to the events list. |
| const size_t kMaxRecentFallbackEntryCount = 2; |
| |
| // Minimum age of the current browser profile to automatically enable HFM. This |
| // prevents auto-enabling HFM immediately upon first launch. |
| const base::TimeDelta kMinTypicallySecureProfileAge = base::Days(15); |
| |
| // We should observe HTTPS-Upgrade and HFM navigations at least for this long |
| // before enabling HFM. |
| const base::TimeDelta kMinTypicallySecureObservationTime = base::Days(7); |
| |
| // Minimum total score for a user to be considered typically secure. If the user |
| // doesn't have at least this much engagement score over all sites, they might |
| // not have used Chrome sufficiently for us to auto-enable HFM. |
| const base::FeatureParam<int> kMinTotalEngagementPointsForTypicallySecureUser{ |
| &features::kHttpsFirstModeV2ForTypicallySecureUsers, |
| "min-total-site-engagement-score", 50}; |
| |
| // Rolling window size in days to count recent navigations. Navigations within |
| // this window will be counted to be used for the Typically Secure heuristic. |
| // Navigations older than this many days will be discarded from the count. |
| const base::FeatureParam<int> kNavigationCounterRollingWindowSizeInDays{ |
| &features::kHttpsFirstModeV2ForTypicallySecureUsers, |
| "navigation-counts-rolling-window-size-in-days", 15}; |
| |
| // Minimum number of main frame navigations counted in this profile during a |
| // rolling window of kNavigationCounterDefaultRollingWindowSizeInDays for a user |
| // to be considered typically secure. If the user doesn't have at least this |
| // many navigations counted, they might not have used Chrome sufficiently for us |
| // to auto-enable HFM. A default value of 1500 is 100 navigations per day during |
| // the 15 day rolling window. |
| const base::FeatureParam<int> kMinRecentNavigationsForTypicallySecureUser{ |
| &features::kHttpsFirstModeV2ForTypicallySecureUsers, |
| "min-recent-navigations", 1500}; |
| |
| // The key for the fallback events in the base preference. |
| constexpr char kFallbackEventsKey[] = "fallback_events"; |
| |
| // The key for the start timestamp in the base preference. This is the time |
| // when we started observing the profile with the Typically Secure User |
| // heuristic. |
| constexpr char kHeuristicStartTimestampKey[] = "heuristic_start_timestamp"; |
| |
| // The key in each fallback event for the fallback event timestamp. A fallback |
| // event is evicted from the list if this timestamp is older than |
| // kFallbackEntriesRollingWindowSize. |
| constexpr char kFallbackEventsPrefTimestampKey[] = "timestamp"; |
| |
| constexpr int kNavigationCounterDefaultSaveInterval = 10; |
| |
| namespace { |
| |
| using security_interstitials::https_only_mode::SiteEngagementHeuristicState; |
| |
| const char kHttpsFirstModeServiceName[] = "HttpsFirstModeService"; |
| const char kHttpsFirstModeSyntheticFieldTrialName[] = |
| "HttpsFirstModeClientSetting"; |
| const char kHttpsFirstModeSyntheticFieldTrialEnabledGroup[] = "Enabled"; |
| const char kHttpsFirstModeSyntheticFieldTrialBalancedGroup[] = "Balanced"; |
| const char kHttpsFirstModeSyntheticFieldTrialDisabledGroup[] = "Disabled"; |
| |
| // We don't need to protect this with a lock since it's only set while |
| // single-threaded in tests. |
| base::Clock* g_clock = nullptr; |
| |
| base::Clock* GetClock() { |
| return g_clock ? g_clock : base::DefaultClock::GetInstance(); |
| } |
| |
| // Returns the HTTP URL from `http_url` using the test port numbers, if any. |
| // TODO(crbug.com/40904694): Refactor and merge with UpgradeUrlToHttps(). |
| GURL GetHttpUrlFromHttps(const GURL& https_url) { |
| DCHECK(https_url.SchemeIsCryptographic()); |
| |
| // Replace scheme with HTTP. |
| GURL::Replacements upgrade_url; |
| upgrade_url.SetSchemeStr(url::kHttpScheme); |
| |
| // For tests that use the EmbeddedTestServer, the server's port needs to be |
| // specified as it can't use the default ports. |
| int http_port_for_testing = HttpsUpgradesInterceptor::GetHttpPortForTesting(); |
| // `port_str` must be in scope for the call to ReplaceComponents() below. |
| const std::string port_str = base::NumberToString(http_port_for_testing); |
| if (http_port_for_testing) { |
| // Only reached in testing, where the original URL will always have a |
| // non-default port. One of the tests navigates to Google support pages, so |
| // exclude that. |
| // TODO(crbug.com/40904694): Remove this exception. |
| if (https_url != GURL(security_interstitials::HttpsOnlyModeBlockingPage:: |
| kLearnMoreLink)) { |
| DCHECK(!https_url.port().empty()); |
| upgrade_url.SetPortStr(port_str); |
| } |
| } |
| |
| return https_url.ReplaceComponents(upgrade_url); |
| } |
| |
| // Returns the HTTPS URL from `http_url` using the test port numbers, if any. |
| // TODO(crbug.com/40904694): Refactor and merge with UpgradeUrlToHttps(). |
| GURL GetHttpsUrlFromHttp(const GURL& http_url) { |
| DCHECK(!http_url.SchemeIsCryptographic()); |
| |
| // Replace scheme with HTTPS. |
| GURL::Replacements upgrade_url; |
| upgrade_url.SetSchemeStr(url::kHttpsScheme); |
| |
| // For tests that use the EmbeddedTestServer, the server's port needs to be |
| // specified as it can't use the default ports. |
| int https_port_for_testing = |
| HttpsUpgradesInterceptor::GetHttpsPortForTesting(); |
| // `port_str` must be in scope for the call to ReplaceComponents() below. |
| const std::string port_str = base::NumberToString(https_port_for_testing); |
| if (https_port_for_testing) { |
| // Only reached in testing, where the original URL will always have a |
| // non-default port. |
| DCHECK(!http_url.port().empty()); |
| upgrade_url.SetPortStr(port_str); |
| } |
| |
| return http_url.ReplaceComponents(upgrade_url); |
| } |
| |
| std::unique_ptr<KeyedService> BuildService(content::BrowserContext* context) { |
| Profile* profile = Profile::FromBrowserContext(context); |
| #if BUILDFLAG(IS_CHROMEOS) |
| // Explicitly check for ChromeOS sign-in profiles (which would cause |
| // double-counting of at-startup metrics for ChromeOS restarts) which are not |
| // covered by the `IsRegularProfile()` check. |
| if (ash::ProfileHelper::IsSigninProfile(profile)) { |
| return nullptr; |
| } |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| return std::make_unique<HttpsFirstModeService>(profile, GetClock()); |
| } |
| |
| base::Time GetTimestamp(const base::Value::Dict& dict, const char* key) { |
| const auto* timestamp_string = dict.Find(key); |
| if (timestamp_string) { |
| const auto timestamp = base::ValueToTime(timestamp_string); |
| if (timestamp) { |
| return *timestamp; |
| } |
| } |
| return base::Time(); |
| } |
| |
| std::string GetSyntheticFieldTrialGroupName(HttpsFirstModeSetting setting) { |
| switch (setting) { |
| case HttpsFirstModeSetting::kEnabledFull: |
| return kHttpsFirstModeSyntheticFieldTrialEnabledGroup; |
| case HttpsFirstModeSetting::kEnabledBalanced: |
| return kHttpsFirstModeSyntheticFieldTrialBalancedGroup; |
| case HttpsFirstModeSetting::kDisabled: |
| return kHttpsFirstModeSyntheticFieldTrialDisabledGroup; |
| default: |
| NOTREACHED(); |
| } |
| } |
| |
| } // namespace |
| |
| HttpsFirstModeService::HttpsFirstModeService(Profile* profile, |
| base::Clock* clock) |
| : profile_(profile), clock_(clock) { |
| pref_change_registrar_.Init(profile_->GetPrefs()); |
| // Using base::Unretained() here is safe as the PrefChangeRegistrar is owned |
| // by `this`. |
| pref_change_registrar_.Add( |
| prefs::kHttpsOnlyModeEnabled, |
| base::BindRepeating(&HttpsFirstModeService::OnHttpsFirstModePrefChanged, |
| base::Unretained(this))); |
| pref_change_registrar_.Add( |
| prefs::kHttpsFirstBalancedMode, |
| base::BindRepeating(&HttpsFirstModeService::OnHttpsFirstModePrefChanged, |
| base::Unretained(this))); |
| |
| // Track Advanced Protection status. |
| if (base::FeatureList::IsEnabled( |
| features::kHttpsFirstModeForAdvancedProtectionUsers)) { |
| obs_.Observe( |
| safe_browsing::AdvancedProtectionStatusManagerFactory::GetForProfile( |
| profile_)); |
| // On startup, AdvancedProtectionStatusManager runs before this class so we |
| // don't get called back. Run the callback to get the AP setting. |
| OnAdvancedProtectionStatusChanged( |
| safe_browsing::AdvancedProtectionStatusManagerFactory::GetForProfile( |
| profile_) |
| ->IsUnderAdvancedProtection()); |
| } |
| |
| // Make sure the pref state is logged and the synthetic field trial state is |
| // created at startup (as the pref may never change over the session). |
| HttpsFirstModeSetting setting = GetCurrentSetting(); |
| base::UmaHistogramEnumeration( |
| "Security.HttpsFirstMode.SettingEnabledAtStartup2", setting); |
| ChromeMetricsServiceAccessor::RegisterSyntheticFieldTrial( |
| kHttpsFirstModeSyntheticFieldTrialName, |
| GetSyntheticFieldTrialGroupName(setting)); |
| |
| // Restore navigation counts from the pref to be used in the Typically Secure |
| // heuristic. |
| navigation_counts_dict_ = |
| profile_->GetPrefs()->GetDict(prefs::kHttpsUpgradeNavigations).Clone(); |
| navigation_counter_ = std::make_unique<DailyNavigationCounter>( |
| &navigation_counts_dict_, clock_, |
| kNavigationCounterRollingWindowSizeInDays.Get(), |
| kNavigationCounterDefaultSaveInterval); |
| |
| content::GetUIThreadTaskRunner({base::TaskPriority::BEST_EFFORT}) |
| ->PostTask(FROM_HERE, base::BindOnce(&HttpsFirstModeService::AfterStartup, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| void HttpsFirstModeService::AfterStartup() { |
| CheckUserIsTypicallySecureAndMaybeEnableHttpsFirstBalancedMode(); |
| MaybeEnableHttpsFirstModeForEngagedSites(base::OnceClosure()); |
| } |
| |
| void HttpsFirstModeService:: |
| CheckUserIsTypicallySecureAndMaybeEnableHttpsFirstBalancedMode() { |
| if (MustDisableTypicallySecureUserHeuristic(profile_)) { |
| return; |
| } |
| |
| // If HFM or the auto-enable prefs were previously set, do not modify them. |
| if (profile_->GetPrefs()->HasPrefPath(prefs::kHttpsOnlyModeEnabled) || |
| profile_->GetPrefs()->HasPrefPath(prefs::kHttpsFirstBalancedMode) || |
| profile_->GetPrefs()->HasPrefPath(prefs::kHttpsOnlyModeAutoEnabled)) { |
| return; |
| } |
| if (!IsUserTypicallySecure()) { |
| return; |
| } |
| // The prefs must be set in this order, as setting kHttpsFirstBalancedMode |
| // will cause kHttpsFirstBalancedModeEnabledByTypicallySecureHeuristic to be |
| // reset to false. |
| // TODO(crbug.com/349860796): Consider having the typically-secure heuristic |
| // turn on Balanced Mode instead. |
| keep_http_allowlist_on_next_pref_change_ = true; |
| profile_->GetPrefs()->SetBoolean(prefs::kHttpsFirstBalancedMode, true); |
| profile_->GetPrefs()->SetBoolean(prefs::kHttpsOnlyModeAutoEnabled, true); |
| } |
| |
| HttpsFirstModeService::~HttpsFirstModeService() = default; |
| |
| void HttpsFirstModeService::OnHttpsFirstModePrefChanged() { |
| HttpsFirstModeSetting setting = GetCurrentSetting(); |
| // Update synthetic field trial group registration. |
| ChromeMetricsServiceAccessor::RegisterSyntheticFieldTrial( |
| kHttpsFirstModeSyntheticFieldTrialName, |
| GetSyntheticFieldTrialGroupName(setting)); |
| |
| // Reset the HTTP allowlist and HTTPS enforcelist when the pref changes. |
| // A user going from HTTPS-Upgrades to HTTPS-First Mode shouldn't inherit the |
| // set of allowlisted sites (or vice versa). |
| if (!keep_http_allowlist_on_next_pref_change_) { |
| StatefulSSLHostStateDelegate* state = |
| static_cast<StatefulSSLHostStateDelegate*>( |
| profile_->GetSSLHostStateDelegate()); |
| state->ClearHttpsOnlyModeAllowlist(); |
| state->ClearHttpsEnforcelist(); |
| } |
| keep_http_allowlist_on_next_pref_change_ = false; |
| |
| // Since the user modified the UI pref, explicitly disable any automatic |
| // HTTPS-First Mode heuristic. |
| profile_->GetPrefs()->SetBoolean(prefs::kHttpsOnlyModeAutoEnabled, false); |
| } |
| |
| void HttpsFirstModeService::OnAdvancedProtectionStatusChanged(bool enabled) { |
| DCHECK(base::FeatureList::IsEnabled( |
| features::kHttpsFirstModeForAdvancedProtectionUsers)); |
| // Override the pref if AP is enabled. We explicitly don't unset the pref if |
| // the user is no longer under Advanced Protection. |
| if (enabled && |
| !profile_->GetPrefs()->GetBoolean(prefs::kHttpsOnlyModeEnabled)) { |
| profile_->GetPrefs()->SetBoolean(prefs::kHttpsOnlyModeEnabled, true); |
| } |
| } |
| |
| bool HttpsFirstModeService:: |
| IsInterstitialEnabledByTypicallySecureUserHeuristic() const { |
| return !MustDisableTypicallySecureUserHeuristic(profile_) && |
| profile_->GetPrefs()->GetBoolean(prefs::kHttpsOnlyModeAutoEnabled) && |
| profile_->GetPrefs()->GetBoolean(prefs::kHttpsFirstBalancedMode); |
| } |
| |
| void HttpsFirstModeService::RecordHttpsUpgradeFallbackEvent() { |
| UpdateFallbackEntries(/*add_new_entry=*/true); |
| } |
| |
| bool HttpsFirstModeService::IsUserTypicallySecure() { |
| return UpdateFallbackEntries(/*add_new_entry=*/false); |
| } |
| |
| bool HttpsFirstModeService::UpdateFallbackEntries(bool add_new_entry) { |
| if (!base::FeatureList::IsEnabled( |
| features::kHttpsFirstModeV2ForTypicallySecureUsers) || |
| !IsBalancedModeAvailable()) { |
| // Normally we'd use MustDisableTypicallySecureUserHeuristic() here, but |
| // we want to record fallback entries even on enterprise devices. Otherwise, |
| // if an enterprise managed device becomes unmanaged, the heuristic would |
| // have zero fallback entries recorded. It would then try to enable the |
| // interstitial because the user would appear typically secure. |
| return false; |
| } |
| // Profile shouldn't be too new. |
| if ((clock_->Now() - profile_->GetCreationTime()) < |
| kMinTypicallySecureProfileAge) { |
| return false; |
| } |
| base::Time now = clock_->Now(); |
| const base::Value::Dict& base_pref = |
| profile_->GetPrefs()->GetDict(prefs::kHttpsUpgradeFallbacks); |
| |
| base::Value::List new_entries; |
| const base::Value::List* fallback_events = |
| base_pref.FindList(kFallbackEventsKey); |
| base::Time latest_fallback_timestamp; |
| if (fallback_events) { |
| for (const auto& event : *fallback_events) { |
| const base::Value::Dict* fallback_event = event.GetIfDict(); |
| if (!fallback_event) { |
| continue; |
| } |
| auto* event_timestamp_string = |
| fallback_event->Find(kFallbackEventsPrefTimestampKey); |
| if (!event_timestamp_string) { |
| continue; |
| } |
| auto event_timestamp = base::ValueToTime(event_timestamp_string); |
| if (!event_timestamp.has_value()) { |
| // Invalid entry, ignore. |
| continue; |
| } |
| if (event_timestamp.value() > now) { |
| // Invalid timestamp, ignore. |
| continue; |
| } |
| if (event_timestamp.value() < now - kFallbackEntriesRollingWindowSize) { |
| // Old event, ignore. |
| continue; |
| } |
| new_entries.Append(fallback_event->Clone()); |
| if (event_timestamp.value() > latest_fallback_timestamp) { |
| latest_fallback_timestamp = event_timestamp.value(); |
| } |
| } |
| } |
| |
| // Add the new fallback entry. |
| if (add_new_entry) { |
| base::Value::Dict new_event; |
| new_event.Set(kFallbackEventsPrefTimestampKey, base::TimeToValue(now)); |
| new_entries.Append(std::move(new_event)); |
| } |
| |
| size_t recent_warning_count = new_entries.size(); |
| |
| base::Time heuristic_start_timestamp = |
| GetTimestamp(base_pref, kHeuristicStartTimestampKey); |
| if (heuristic_start_timestamp.is_null()) { |
| // This can happen in a new profile or if a previous version of Chrome |
| // wrote the pref but didn't have this value. |
| heuristic_start_timestamp = now; |
| } |
| |
| auto* engagement_svc = site_engagement::SiteEngagementService::Get(profile_); |
| bool enable_https_first_mode = |
| ((now - heuristic_start_timestamp) > |
| kMinTypicallySecureObservationTime) && |
| (recent_warning_count <= kMaxRecentFallbackEntryCount) && |
| (engagement_svc->GetTotalEngagementPoints() >= |
| kMinTotalEngagementPointsForTypicallySecureUser.Get()) && |
| (now - latest_fallback_timestamp > base::Days(1)) && |
| (static_cast<int>(GetRecentNavigationCount()) >= |
| kMinRecentNavigationsForTypicallySecureUser.Get()); |
| |
| // Update the pref with the new fallback events. |
| base::Value::Dict new_base_pref; |
| new_base_pref.Set(kFallbackEventsKey, std::move(new_entries)); |
| new_base_pref.Set(kHeuristicStartTimestampKey, |
| base::TimeToValue(heuristic_start_timestamp)); |
| profile_->GetPrefs()->SetDict(prefs::kHttpsUpgradeFallbacks, |
| std::move(new_base_pref)); |
| return enable_https_first_mode; |
| } |
| |
| void HttpsFirstModeService::MaybeEnableHttpsFirstModeForEngagedSites( |
| base::OnceClosure done_callback) { |
| // If HFM or the auto-enable prefs were previously set, do not modify HFM |
| // status. |
| if (MustDisableSiteEngagementHeuristic(profile_) || |
| profile_->GetPrefs()->HasPrefPath(prefs::kHttpsOnlyModeEnabled) || |
| profile_->GetPrefs()->HasPrefPath(prefs::kHttpsFirstBalancedMode) || |
| profile_->GetPrefs()->HasPrefPath(prefs::kHttpsOnlyModeAutoEnabled)) { |
| if (!done_callback.is_null()) { |
| std::move(done_callback).Run(); |
| } |
| return; |
| } |
| // Ideal parameter order is kHttpsAddThreshold > kHttpsRemoveThreshold > |
| // kHttpRemoveThreshold > kHttpAddThreshold. |
| if (!(kHttpsAddThreshold.Get() > kHttpsRemoveThreshold.Get() && |
| kHttpsRemoveThreshold.Get() > kHttpRemoveThreshold.Get() && |
| kHttpRemoveThreshold.Get() > kHttpAddThreshold.Get())) { |
| if (!done_callback.is_null()) { |
| std::move(done_callback).Run(); |
| } |
| return; |
| } |
| base::ThreadPool::PostTaskAndReplyWithResult( |
| FROM_HERE, |
| {base::TaskPriority::USER_BLOCKING, |
| base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN}, |
| base::BindOnce( |
| &site_engagement::SiteEngagementService::GetAllDetailsInBackground, |
| clock_->Now(), |
| base::WrapRefCounted( |
| HostContentSettingsMapFactory::GetForProfile(profile_)), |
| site_engagement::SiteEngagementService::URLSets::HTTP), |
| base::BindOnce(&HttpsFirstModeService::ProcessEngagedSitesList, |
| weak_factory_.GetWeakPtr(), std::move(done_callback))); |
| } |
| |
| void HttpsFirstModeService::ProcessEngagedSitesList( |
| base::OnceClosure done_callback, |
| const std::vector<site_engagement::mojom::SiteEngagementDetails>& details) { |
| DCHECK(IsBalancedModeAvailable()); |
| |
| StatefulSSLHostStateDelegate* state = |
| static_cast<StatefulSSLHostStateDelegate*>( |
| profile_->GetSSLHostStateDelegate()); |
| // StatefulSSLHostStateDelegate can be null during tests. In that case, we |
| // can't save the site setting. |
| if (!state) { |
| return; |
| } |
| auto* engagement_service = |
| site_engagement::SiteEngagementService::Get(profile_); |
| |
| // Get all hostnames that have HTTPS enforced on them at some point. Some |
| // hostnames may no longer have a site engagement score thus be missing from |
| // `details`. We still want to process those hostnames because we want to |
| // unenforce HTTPS on these hostnames if the conditions no longer hold. |
| std::set<GURL> origins = |
| state->GetHttpsEnforcedHosts(profile_->GetDefaultStoragePartition()); |
| for (const site_engagement::mojom::SiteEngagementDetails& detail : details) { |
| origins.insert(detail.origin); |
| } |
| |
| for (const GURL& origin : origins) { |
| if (origin.SchemeIsHTTPOrHTTPS() && origin.port().empty()) { |
| MaybeEnableHttpsFirstModeForUrl(origin, engagement_service, state); |
| } |
| } |
| |
| if (!done_callback.is_null()) { |
| std::move(done_callback).Run(); |
| } |
| } |
| |
| void HttpsFirstModeService::MaybeEnableHttpsFirstModeForUrl( |
| const GURL& url, |
| site_engagement::SiteEngagementService* engagement_service, |
| StatefulSSLHostStateDelegate* state) { |
| DCHECK(IsBalancedModeAvailable()); |
| |
| DCHECK(url.port().empty()) << "Url should have a default port"; |
| bool enforced = |
| state->IsHttpsEnforcedForUrl(url, profile_->GetDefaultStoragePartition()); |
| GURL https_url = url.SchemeIsCryptographic() ? url : GetHttpsUrlFromHttp(url); |
| GURL http_url = !url.SchemeIsCryptographic() ? url : GetHttpUrlFromHttps(url); |
| |
| // If a non-unique hostname is in the enforcement list, it must have been |
| // added by a previous version of Chrome, so remove it. Otherwise, ignore |
| // non-unique hostnames. |
| if (net::IsHostnameNonUnique(url.host())) { |
| if (enforced) { |
| state->SetHttpsEnforcementForHost(url.host(), |
| /*enforced=*/false, |
| profile_->GetDefaultStoragePartition()); |
| } |
| return; |
| } |
| |
| double https_score = engagement_service->GetScore(https_url); |
| double http_score = engagement_service->GetScore(http_url); |
| bool should_enable = https_score >= kHttpsAddThreshold.Get() && |
| http_score <= kHttpAddThreshold.Get(); |
| |
| if (!enforced && should_enable) { |
| state->SetHttpsEnforcementForHost(url.host(), |
| /*enforced=*/true, |
| profile_->GetDefaultStoragePartition()); |
| return; |
| } |
| |
| bool should_disable = https_score <= kHttpsRemoveThreshold.Get() || |
| http_score >= kHttpRemoveThreshold.Get(); |
| if (enforced && should_disable) { |
| state->SetHttpsEnforcementForHost(url.host(), |
| /*enforced=*/false, |
| profile_->GetDefaultStoragePartition()); |
| return; |
| } |
| // Don't change the state otherwise. |
| } |
| |
| HttpsFirstModeSetting HttpsFirstModeService::GetCurrentSetting() const { |
| if (profile_->GetPrefs()->GetBoolean(prefs::kHttpsOnlyModeEnabled)) { |
| return HttpsFirstModeSetting::kEnabledFull; |
| } |
| if (IsBalancedModeEnabled(profile_->GetPrefs())) { |
| return HttpsFirstModeSetting::kEnabledBalanced; |
| } |
| return HttpsFirstModeSetting::kDisabled; |
| } |
| |
| bool HttpsFirstModeService::UpdatePrefs( |
| const HttpsFirstModeSetting& selection) { |
| if (selection != HttpsFirstModeSetting::kDisabled && |
| selection != HttpsFirstModeSetting::kEnabledBalanced && |
| selection != HttpsFirstModeSetting::kEnabledFull) { |
| return false; |
| } |
| |
| if (!IsBalancedModeAvailable() && |
| selection == HttpsFirstModeSetting::kEnabledBalanced) { |
| return false; |
| } |
| |
| // Update both HTTPS-First Mode preferences to match the selection. |
| // |
| // Note that the HttpsFirstModeSetting::kEnabledBalanced is not available by |
| // default. If the feature flag is disabled, then the kEnabledFull and |
| // kDisabled settings will only be mapped to the kHttpsOnlyModeEnabled pref. |
| // |
| // Note: The Security.HttpsFirstMode.SettingChanged* histograms are logged |
| // here instead of in HttpsFirstModeService::OnHttpsFirstModePrefChanged() |
| // because this will fire the pref observer _twice_, so logging the histogram |
| // in the pref observer would cause double counting. |
| if (IsBalancedModeAvailable()) { |
| switch (selection) { |
| case HttpsFirstModeSetting::kDisabled: |
| base::UmaHistogramEnumeration("Security.HttpsFirstMode.SettingChanged2", |
| HttpsFirstModeSetting::kDisabled); |
| profile_->GetPrefs()->SetBoolean(prefs::kHttpsOnlyModeEnabled, false); |
| profile_->GetPrefs()->SetBoolean(prefs::kHttpsFirstBalancedMode, false); |
| break; |
| case HttpsFirstModeSetting::kEnabledBalanced: |
| base::UmaHistogramEnumeration("Security.HttpsFirstMode.SettingChanged2", |
| HttpsFirstModeSetting::kEnabledBalanced); |
| profile_->GetPrefs()->SetBoolean(prefs::kHttpsOnlyModeEnabled, false); |
| profile_->GetPrefs()->SetBoolean(prefs::kHttpsFirstBalancedMode, true); |
| break; |
| case HttpsFirstModeSetting::kEnabledFull: |
| base::UmaHistogramEnumeration("Security.HttpsFirstMode.SettingChanged2", |
| HttpsFirstModeSetting::kEnabledFull); |
| profile_->GetPrefs()->SetBoolean(prefs::kHttpsOnlyModeEnabled, true); |
| profile_->GetPrefs()->SetBoolean(prefs::kHttpsFirstBalancedMode, false); |
| break; |
| } |
| } else { |
| // TODO(crbug.com/349860796): Remove old settings path once Balanced Mode |
| // is launched. |
| base::UmaHistogramBoolean("Security.HttpsFirstMode.SettingChanged", |
| selection == HttpsFirstModeSetting::kEnabledFull); |
| profile_->GetPrefs()->SetBoolean( |
| prefs::kHttpsOnlyModeEnabled, |
| selection == HttpsFirstModeSetting::kEnabledFull); |
| } |
| return true; |
| } |
| |
| void HttpsFirstModeService::IncrementRecentNavigationCount() { |
| if (navigation_counter_->Increment()) { |
| profile_->GetPrefs()->SetDict(prefs::kHttpsUpgradeNavigations, |
| navigation_counts_dict_.Clone()); |
| } |
| } |
| |
| size_t HttpsFirstModeService::GetRecentNavigationCount() const { |
| return navigation_counter_->GetTotal(); |
| } |
| |
| void HttpsFirstModeService::SetClockForTesting(base::Clock* clock) { |
| clock_ = clock; |
| } |
| |
| size_t HttpsFirstModeService::GetFallbackEntryCountForTesting() const { |
| const base::Value::Dict& base_pref = |
| profile_->GetPrefs()->GetDict(prefs::kHttpsUpgradeFallbacks); |
| const base::Value::List* fallback_events = |
| base_pref.FindList(kFallbackEventsKey); |
| return fallback_events ? fallback_events->size() : 0; |
| } |
| |
| // static |
| HttpsFirstModeService* HttpsFirstModeServiceFactory::GetForProfile( |
| Profile* profile) { |
| return static_cast<HttpsFirstModeService*>( |
| GetInstance()->GetServiceForBrowserContext(profile, /*create=*/true)); |
| } |
| |
| // static |
| HttpsFirstModeServiceFactory* HttpsFirstModeServiceFactory::GetInstance() { |
| return base::Singleton<HttpsFirstModeServiceFactory>::get(); |
| } |
| |
| // static |
| BrowserContextKeyedServiceFactory::TestingFactory |
| HttpsFirstModeServiceFactory::GetDefaultFactoryForTesting() { |
| return base::BindRepeating(&BuildService); |
| } |
| |
| HttpsFirstModeServiceFactory::HttpsFirstModeServiceFactory() |
| : ProfileKeyedServiceFactory( |
| kHttpsFirstModeServiceName, |
| // Don't create a service for non-regular profiles. This includes |
| // Incognito (which uses the settings of the main profile) and Guest |
| // Mode. |
| ProfileSelections::Builder() |
| .WithRegular(ProfileSelection::kOriginalOnly) |
| // TODO(crbug.com/41488885): Check if this service is needed for |
| // Ash Internals. |
| .WithAshInternals(ProfileSelection::kOriginalOnly) |
| .Build()) { |
| DependsOn( |
| safe_browsing::AdvancedProtectionStatusManagerFactory::GetInstance()); |
| } |
| |
| HttpsFirstModeServiceFactory::~HttpsFirstModeServiceFactory() = default; |
| |
| std::unique_ptr<KeyedService> |
| HttpsFirstModeServiceFactory::BuildServiceInstanceForBrowserContext( |
| content::BrowserContext* context) const { |
| return BuildService(context); |
| } |
| |
| // static |
| base::Clock* HttpsFirstModeServiceFactory::SetClockForTesting( |
| base::Clock* clock) { |
| return std::exchange(g_clock, clock); |
| } |