diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn index 7e106bd2e64a13..dbd5e6d9dfc30d 100644 --- a/chrome/browser/BUILD.gn +++ b/chrome/browser/BUILD.gn @@ -1082,6 +1082,8 @@ split_static_library("browser") { "resources_util.h", "safe_browsing/safe_browsing_tab_observer.cc", "safe_browsing/safe_browsing_tab_observer.h", + "safe_browsing/settings_reset_prompt/settings_reset_prompt_config.cc", + "safe_browsing/settings_reset_prompt/settings_reset_prompt_config.h", "safe_browsing/srt_client_info_win.cc", "safe_browsing/srt_client_info_win.h", "safe_browsing/srt_fetcher_win.cc", diff --git a/chrome/browser/safe_browsing/settings_reset_prompt/settings_reset_prompt_config.cc b/chrome/browser/safe_browsing/settings_reset_prompt/settings_reset_prompt_config.cc new file mode 100644 index 00000000000000..17601d090ed4c0 --- /dev/null +++ b/chrome/browser/safe_browsing/settings_reset_prompt/settings_reset_prompt_config.cc @@ -0,0 +1,189 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/safe_browsing/settings_reset_prompt/settings_reset_prompt_config.h" + +#include + +#include "base/json/json_reader.h" +#include "base/memory/ptr_util.h" +#include "base/metrics/histogram_macros.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_piece.h" +#include "base/strings/string_util.h" +#include "base/values.h" +#include "components/url_formatter/url_fixer.h" +#include "components/variations/variations_associated_data.h" +#include "crypto/sha2.h" +#include "net/base/registry_controlled_domains/registry_controlled_domain.h" +#include "url/gurl.h" + +namespace safe_browsing { + +namespace { + +const char kSettingsResetPromptFeatureName[] = "SettingsResetPrompt"; +const char kDomainHashesParamName[] = "domain_hashes"; + +} // namespace. + +const base::Feature kSettingsResetPrompt{kSettingsResetPromptFeatureName, + base::FEATURE_DISABLED_BY_DEFAULT}; + +// static +bool SettingsResetPromptConfig::IsPromptEnabled() { + // TODO(alito): Add prefs to local state to track when the user was + // last prompted and ensure that we only prompt once per reset prompt + // wave. + return base::FeatureList::IsEnabled(kSettingsResetPrompt); +} + +// static +std::unique_ptr SettingsResetPromptConfig::Create() { + if (!IsPromptEnabled()) + return nullptr; + + auto prompt_config = base::WrapUnique(new SettingsResetPromptConfig()); + if (!prompt_config->Init()) + return nullptr; + + return prompt_config; +} + +SettingsResetPromptConfig::SettingsResetPromptConfig() {} + +SettingsResetPromptConfig::~SettingsResetPromptConfig() {} + +int SettingsResetPromptConfig::UrlToResetDomainId(const GURL& url) const { + DCHECK(IsPromptEnabled()); + + // Do a best-effort to fix the URL before testing if it is valid. + GURL fixed_url = + url_formatter::FixupURL(url.possibly_invalid_spec(), std::string()); + if (!fixed_url.is_valid()) + return -1; + + // Get the length of the top level domain or registry of the URL. Used + // to guard against trying to match the (effective) TLDs themselves. + size_t registry_length = net::registry_controlled_domains::GetRegistryLength( + fixed_url, net::registry_controlled_domains::INCLUDE_UNKNOWN_REGISTRIES, + net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES); + // Do not proceed, if |fixed_url| does not have a host or consists entirely of + // a registry or top domain. + if (registry_length == 0 || registry_length == std::string::npos) + return -1; + + // The hashes in the prompt config are generally TLD+1 and identify + // only the topmost levels of URLs that we wish to prompt for. Try to + // match each sensible suffix of the URL host with the hashes in the + // prompt config. For example, if the host is + // "www.sub.domain.com", try hashes for: + // "www.sub.domain.com" + // "sub.domain.com" + // "domain.com" + // We Do not check top level or registry domains to guard against bad + // configuration data. + SHA256Hash hash(crypto::kSHA256Length, '\0'); + base::StringPiece host = fixed_url.host_piece(); + while (host.size() > registry_length) { + crypto::SHA256HashString(host, hash.data(), crypto::kSHA256Length); + auto iter = domain_hashes_.find(hash); + if (iter != domain_hashes_.end()) + return iter->second; + + size_t next_start_pos = host.find('.'); + next_start_pos = next_start_pos == base::StringPiece::npos + ? base::StringPiece::npos + : next_start_pos + 1; + host = host.substr(next_start_pos); + } + + return -1; +} + +// Implements the hash function for SHA256Hash objects. Simply uses the +// first bytes of the SHA256 hash as its own hash. +size_t SettingsResetPromptConfig::SHA256HashHasher::operator()( + const SHA256Hash& key) const { + DCHECK_EQ(crypto::kSHA256Length, key.size()); + // This is safe because |key| contains 32 bytes while a size_t is + // either 4 or 8 bytes. + return *reinterpret_cast(key.data()); +} + +// These values are written to logs. New enum values can be added, but +// existing enums must never be renumbered or deleted and reused. If you +// do add values, also update the corresponding enum definition in the +// histograms.xml file. +enum SettingsResetPromptConfig::ConfigError : int { + CONFIG_ERROR_OK = 1, + CONFIG_ERROR_MISSING_DOMAIN_HASHES_PARAM = 2, + CONFIG_ERROR_BAD_DOMAIN_HASHES_PARAM = 3, + CONFIG_ERROR_BAD_DOMAIN_HASH = 4, + CONFIG_ERROR_BAD_DOMAIN_ID = 5, + CONFIG_ERROR_DUPLICATE_DOMAIN_HASH = 6, + CONFIG_ERROR_MAX +}; + +bool SettingsResetPromptConfig::Init() { + if (!IsPromptEnabled()) + return false; + + std::string domain_hashes_json = variations::GetVariationParamValueByFeature( + kSettingsResetPrompt, kDomainHashesParamName); + ConfigError error = ParseDomainHashes(domain_hashes_json); + UMA_HISTOGRAM_ENUMERATION("SettingsResetPrompt.ConfigError", error, + CONFIG_ERROR_MAX); + return error == CONFIG_ERROR_OK; +} + +SettingsResetPromptConfig::ConfigError +SettingsResetPromptConfig::ParseDomainHashes( + const std::string& domain_hashes_json) { + if (domain_hashes_json.empty()) + return CONFIG_ERROR_MISSING_DOMAIN_HASHES_PARAM; + + // Is the input parseable JSON? + std::unique_ptr domains_dict = + base::DictionaryValue::From(base::JSONReader::Read(domain_hashes_json)); + if (!domains_dict || domains_dict->empty()) + return CONFIG_ERROR_BAD_DOMAIN_HASHES_PARAM; + + // The input JSON should be a hash object with hex-encoded 32-byte + // hashes as keys and integer IDs as values. For example, + // + // {"2714..D7": "1", "2821..CB": "2", ...} + // + // Each key in the hash should be a 64-byte long string and each + // integer ID should fit in an int. + domain_hashes_.clear(); + for (base::DictionaryValue::Iterator iter(*domains_dict); !iter.IsAtEnd(); + iter.Advance()) { + const std::string& hash_string = iter.key(); + if (hash_string.size() != crypto::kSHA256Length * 2) + return CONFIG_ERROR_BAD_DOMAIN_HASH; + + // Convert hex-encoded hash string to its numeric value as bytes. + SHA256Hash hash; + hash.reserve(crypto::kSHA256Length); + if (!base::HexStringToBytes(hash_string, &hash)) + return CONFIG_ERROR_BAD_DOMAIN_HASH; + + // Convert the ID string to an integer. + std::string domain_id_string; + int domain_id = -1; + if (!iter.value().GetAsString(&domain_id_string) || + !base::StringToInt(domain_id_string, &domain_id) || domain_id < 0) { + return CONFIG_ERROR_BAD_DOMAIN_ID; + } + + if (!domain_hashes_.insert(std::make_pair(std::move(hash), domain_id)) + .second) + return CONFIG_ERROR_DUPLICATE_DOMAIN_HASH; + } + + return CONFIG_ERROR_OK; +} + +} // namespace safe_browsing. diff --git a/chrome/browser/safe_browsing/settings_reset_prompt/settings_reset_prompt_config.h b/chrome/browser/safe_browsing/settings_reset_prompt/settings_reset_prompt_config.h new file mode 100644 index 00000000000000..d5cc67a6cdf97f --- /dev/null +++ b/chrome/browser/safe_browsing/settings_reset_prompt/settings_reset_prompt_config.h @@ -0,0 +1,65 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_SAFE_BROWSING_SETTINGS_RESET_PROMPT_SETTINGS_RESET_PROMPT_CONFIG_H_ +#define CHROME_BROWSER_SAFE_BROWSING_SETTINGS_RESET_PROMPT_SETTINGS_RESET_PROMPT_CONFIG_H_ + +#include +#include +#include +#include +#include + +#include "base/feature_list.h" +#include "base/macros.h" + +class GURL; + +namespace safe_browsing { + +// Exposed for testing. +extern const base::Feature kSettingsResetPrompt; + +// Encapsulates the state of the reset prompt experiment as well as +// associated data. +class SettingsResetPromptConfig { + public: + // Returns true if the settings reset prompt study is enabled. + static bool IsPromptEnabled(); + // Factory method for creating instances of SettingsResetPromptConfig. + // Returns nullptr if |IsPromptEnabled()| is false or if something is wrong + // with the config parameters. + static std::unique_ptr Create(); + + ~SettingsResetPromptConfig(); + + // Returns a non-negative integer ID if |url| should trigger a + // settings reset prompt and a negative integer otherwise. The IDs + // identify the domains or entities that we want to prompt the user + // for and can be used for metrics reporting. + int UrlToResetDomainId(const GURL& url) const; + + // TODO(alito): parameterize the set of things that we want to reset + // for so that we can control it from the finch config. For example, + // with functions like HomepageResetAllowed() etc. + private: + typedef std::vector SHA256Hash; + struct SHA256HashHasher { + size_t operator()(const SHA256Hash& key) const; + }; + enum ConfigError : int; + + SettingsResetPromptConfig(); + bool Init(); + ConfigError ParseDomainHashes(const std::string& domain_hashes_json); + + // Map of 32 byte SHA256 hashes to integer domain IDs. + std::unordered_map domain_hashes_; + + DISALLOW_COPY_AND_ASSIGN(SettingsResetPromptConfig); +}; + +} // namespace safe_browsing + +#endif // CHROME_BROWSER_SAFE_BROWSING_SETTINGS_RESET_PROMPT_SETTINGS_RESET_PROMPT_CONFIG_H_ diff --git a/chrome/browser/safe_browsing/settings_reset_prompt/settings_reset_prompt_config_unittest.cc b/chrome/browser/safe_browsing/settings_reset_prompt/settings_reset_prompt_config_unittest.cc new file mode 100644 index 00000000000000..e9ed56ca3b795f --- /dev/null +++ b/chrome/browser/safe_browsing/settings_reset_prompt/settings_reset_prompt_config_unittest.cc @@ -0,0 +1,189 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/safe_browsing/settings_reset_prompt/settings_reset_prompt_config.h" + +#include +#include + +#include "base/strings/stringprintf.h" +#include "base/test/scoped_feature_list.h" +#include "components/variations/variations_params_manager.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" + +namespace safe_browsing { + +const char kTrialName[] = "trial"; +// A SHA256 hash for "mydomain.com". +const char kDomainHash[] = + "0a79eaf6adb7b1e60d3fa548aa63105f525a00448efbb59ee965b9351a90ac31"; + +// Test class that initializes a ScopedFeatureList so that all tests +// start off with all features disabled. +class SettingsResetPromptConfigTest : public ::testing::Test { + protected: + typedef std::map Parameters; + + // Sets the settings reset prompt feature parameters, which has the + // side-effect of also enabling the feature. + void SetFeatureParams(const Parameters& params) { + static std::set features = {kSettingsResetPrompt.name}; + + params_manager_.ClearAllVariationParams(); + params_manager_.SetVariationParamsWithFeatureAssociations(kTrialName, + params, features); + } + + void SetDefaultFeatureParams() { + Parameters default_params = { + {"domain_hashes", base::StringPrintf("{\"%s\": \"1\"}", kDomainHash)}}; + SetFeatureParams(default_params); + } + + variations::testing::VariationParamsManager params_manager_; + base::test::ScopedFeatureList scoped_feature_list_; +}; + +TEST_F(SettingsResetPromptConfigTest, IsPromptEnabled) { + EXPECT_FALSE(SettingsResetPromptConfig::IsPromptEnabled()); + + SetDefaultFeatureParams(); + EXPECT_TRUE(SettingsResetPromptConfig::IsPromptEnabled()); +} + +TEST_F(SettingsResetPromptConfigTest, Create) { + // Should return nullptr when feature is not enabled. + EXPECT_FALSE(SettingsResetPromptConfig::Create()); + + scoped_feature_list_.InitAndEnableFeature(kSettingsResetPrompt); + + // Check cases where |Create()| should return nullptr because of bad + // domain_hashes parameter. + + // Parameter is missing. + EXPECT_FALSE(SettingsResetPromptConfig::Create()); + SetFeatureParams(Parameters()); + EXPECT_FALSE(SettingsResetPromptConfig::Create()); + + // Parameter is an empty string. + SetFeatureParams(Parameters({{"domain_hashes", ""}})); + EXPECT_FALSE(SettingsResetPromptConfig::Create()); + + // Invalid JSON. + SetFeatureParams(Parameters({{"domain_hashes", "bad json"}})); + EXPECT_FALSE(SettingsResetPromptConfig::Create()); + + // Parameter is not a JSON dictionary. + SetFeatureParams(Parameters({{"domain_hashes", "[3]"}})); + EXPECT_FALSE(SettingsResetPromptConfig::Create()); + + // Bad dictionary key. + SetFeatureParams(Parameters({{"domain_hashes", "\"bad key\": \"1\""}})); + EXPECT_FALSE(SettingsResetPromptConfig::Create()); + + // Dictionary key is too short. + SetFeatureParams(Parameters({{"domain_hashes", "\"1234abc\": \"1\""}})); + EXPECT_FALSE(SettingsResetPromptConfig::Create()); + + // Dictionary key has correct length, but is not a hex string. + std::string non_hex_key(64, 'x'); + SetFeatureParams( + Parameters({{"domain_hashes", base::StringPrintf("{\"%s\": \"1\"}", + non_hex_key.c_str())}})); + EXPECT_FALSE(SettingsResetPromptConfig::Create()); + + // Correct key but non-integer value. + SetFeatureParams(Parameters( + {{"domain_hashes", + base::StringPrintf("{\"%s\": \"not integer\"}", kDomainHash)}})); + EXPECT_FALSE(SettingsResetPromptConfig::Create()); + + // Correct key but integer value that is too big. + std::string too_big_int(99, '1'); + SetFeatureParams(Parameters( + {{"domain_hashes", base::StringPrintf("{\"%s\": \"%s\"}", kDomainHash, + too_big_int.c_str())}})); + EXPECT_FALSE(SettingsResetPromptConfig::Create()); + + // Correct key but negative integer value. + SetFeatureParams( + Parameters({{"domain_hashes", + base::StringPrintf("{\"%s\": \"-2\"}", kDomainHash)}})); + EXPECT_FALSE(SettingsResetPromptConfig::Create()); + + // Should return non-nullptr with a correct set of parameters. + SetDefaultFeatureParams(); + EXPECT_TRUE(SettingsResetPromptConfig::Create()); +} + +TEST_F(SettingsResetPromptConfigTest, UrlToResetDomainId) { + SetDefaultFeatureParams(); + auto config = SettingsResetPromptConfig::Create(); + ASSERT_TRUE(config); + + // Should return negative value for URL with no match in the config. + EXPECT_LT(config->UrlToResetDomainId(GURL("http://www.hello.com")), 0); + + // Should return 1, which is "mydomain.com"'s ID. + EXPECT_EQ(config->UrlToResetDomainId(GURL("http://www.sub.mydomain.com")), 1); + EXPECT_EQ(config->UrlToResetDomainId(GURL("http://www.mydomain.com")), 1); + EXPECT_EQ(config->UrlToResetDomainId(GURL("http://mydomain.com")), 1); + + // These URLs should not match "mydomain.com". + EXPECT_LT(config->UrlToResetDomainId(GURL("http://mydomain")), 0); + EXPECT_LT(config->UrlToResetDomainId(GURL("http://mydomain.org")), 0); + EXPECT_LT(config->UrlToResetDomainId(GURL("http://prefixmydomain.com")), 0); + EXPECT_LT(config->UrlToResetDomainId(GURL("http://mydomain.com.com")), 0); + EXPECT_LT(config->UrlToResetDomainId(GURL("http://www.mydomain.com.com")), 0); + + // Should return negative value for invalid URLs. + EXPECT_LT(config->UrlToResetDomainId(GURL("htp://mydomain.com")), 0); + EXPECT_LT(config->UrlToResetDomainId(GURL("http://mydomain com")), 0); +} + +TEST_F(SettingsResetPromptConfigTest, UrlToResetDomainIdTLDs) { + // Ensure that we do not match top level or registry domains even in the + // presence of faulty config data that would match those. + + // Hash for "com". + const char kTLDHash1[] = + "71b4f3a3748cd6843c01e293e701fce769f52381821e21daf2ff4fe9ea57a6f3"; + // Hash for "co.uk". + const char kTLDHash2[] = + "ad4fad2f5e5fb480ff7f9f648c6b20bbd5e44362d86821e29d30e65e626299b0"; + // Hash for "uk". + const char kTLDHash3[] = + "83116acf18e4dc4414762f584ff43d9979ff2c2b0e9e48fbc97b21e23d7004ec"; + // Hash for "com.br". + const char kTLDHash4[] = + "1d9c4ffc5429a9b4529abf6fbe9f20b52b7401c8f0fed46c7ed67b1e3153932c"; + // Hash for private registry domain "appspot.com". + const char kTLDHash5[] = + "bffd48c8162466106a84f42945bfbbcfe501c9f0931219e02ce46e275f05ba51"; + + SetFeatureParams(Parameters( + {{"domain_hashes", + base::StringPrintf( + "{\"%s\": \"1\", \"%s\": \"2\", \"%s\": \"3\", \"%s\": \"4\", " + "\"%s\": \"5\"}", + kTLDHash1, kTLDHash2, kTLDHash3, kTLDHash4, kTLDHash5)}})); + auto config = SettingsResetPromptConfig::Create(); + ASSERT_TRUE(config); + + EXPECT_LT(config->UrlToResetDomainId(GURL("http://something.com")), 0); + EXPECT_LT(config->UrlToResetDomainId(GURL("http://something.co.uk")), 0); + EXPECT_LT(config->UrlToResetDomainId(GURL("http://something.uk")), 0); + EXPECT_LT(config->UrlToResetDomainId(GURL("http://something.com.br")), 0); + EXPECT_LT(config->UrlToResetDomainId(GURL("http://something.appspot.com")), + 0); + EXPECT_LT(config->UrlToResetDomainId(GURL("http://com")), 0); + EXPECT_LT(config->UrlToResetDomainId(GURL("http://co.uk")), 0); + EXPECT_LT(config->UrlToResetDomainId(GURL("http://uk")), 0); + EXPECT_LT(config->UrlToResetDomainId(GURL("http://com.br")), 0); + EXPECT_LT(config->UrlToResetDomainId(GURL("http://appspot.com")), 0); +} + +} // namespace safe_browsing diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn index d277409749113b..f4e47cef05f208 100644 --- a/chrome/test/BUILD.gn +++ b/chrome/test/BUILD.gn @@ -3270,6 +3270,7 @@ test("unit_tests") { "../browser/push_messaging/push_messaging_service_unittest.cc", "../browser/renderer_host/chrome_render_widget_host_view_mac_history_swiper_unit_test.mm", "../browser/resources_util_unittest.cc", + "../browser/safe_browsing/settings_reset_prompt/settings_reset_prompt_config_unittest.cc", "../browser/search/contextual_search_policy_handler_android_unittest.cc", "../browser/search/iframe_source_unittest.cc", "../browser/search/thumbnail_source_unittest.cc", diff --git a/tools/metrics/histograms/histograms.xml b/tools/metrics/histograms/histograms.xml index dfeba25829b626..9f2e0b15e8bdca 100644 --- a/tools/metrics/histograms/histograms.xml +++ b/tools/metrics/histograms/histograms.xml @@ -60917,6 +60917,15 @@ http://cs/file:chrome/histograms.xml - but prefer this file for new entries. + + alito@chromium.org + + Indicates if an error was detected in the settings reset prompt config data + while initializing the reset prompt configuration. + + + grt@chromium.org @@ -103757,6 +103766,15 @@ value. + + + + + + + + + A collection of sections from chrome://settings. Used for metrics about