1 // Copyright 2017 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "chrome/browser/safe_browsing/settings_reset_prompt/settings_reset_prompt_config.h"
6 
7 #include <utility>
8 
9 #include "base/json/json_reader.h"
10 #include "base/memory/ptr_util.h"
11 #include "base/metrics/field_trial_params.h"
12 #include "base/metrics/histogram_macros.h"
13 #include "base/strings/string_number_conversions.h"
14 #include "base/strings/string_piece.h"
15 #include "base/strings/string_util.h"
16 #include "base/values.h"
17 #include "components/url_formatter/url_fixer.h"
18 #include "crypto/sha2.h"
19 #include "net/base/registry_controlled_domains/registry_controlled_domain.h"
20 #include "url/gurl.h"
21 
22 namespace safe_browsing {
23 
24 namespace {
25 
26 constexpr const char kSettingsResetPromptFeatureName[] = "SettingsResetPrompt";
27 
IsPromptEnabled()28 bool IsPromptEnabled() {
29   return base::FeatureList::IsEnabled(kSettingsResetPrompt);
30 }
31 
32 }  // namespace.
33 
34 const base::Feature kSettingsResetPrompt{kSettingsResetPromptFeatureName,
35                                          base::FEATURE_DISABLED_BY_DEFAULT};
36 
37 // static
Create()38 std::unique_ptr<SettingsResetPromptConfig> SettingsResetPromptConfig::Create() {
39   if (!IsPromptEnabled())
40     return nullptr;
41 
42   auto prompt_config = base::WrapUnique(new SettingsResetPromptConfig());
43   if (!prompt_config->Init())
44     return nullptr;
45 
46   return prompt_config;
47 }
48 
SettingsResetPromptConfig()49 SettingsResetPromptConfig::SettingsResetPromptConfig() {}
50 
~SettingsResetPromptConfig()51 SettingsResetPromptConfig::~SettingsResetPromptConfig() {}
52 
UrlToResetDomainId(const GURL & url) const53 int SettingsResetPromptConfig::UrlToResetDomainId(const GURL& url) const {
54   DCHECK(IsPromptEnabled());
55 
56   // Do a best-effort to fix the URL before testing if it is valid.
57   GURL fixed_url =
58       url_formatter::FixupURL(url.possibly_invalid_spec(), std::string());
59   if (!fixed_url.is_valid())
60     return -1;
61 
62   // Get the length of the top level domain or registry of the URL. Used
63   // to guard against trying to match the (effective) TLDs themselves.
64   size_t registry_length = net::registry_controlled_domains::GetRegistryLength(
65       fixed_url, net::registry_controlled_domains::INCLUDE_UNKNOWN_REGISTRIES,
66       net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES);
67   // Do not proceed, if |fixed_url| does not have a host or consists entirely of
68   // a registry or top domain.
69   if (registry_length == 0 || registry_length == std::string::npos)
70     return -1;
71 
72   // The hashes in the prompt config are generally TLD+1 and identify
73   // only the topmost levels of URLs that we wish to prompt for. Try to
74   // match each sensible suffix of the URL host with the hashes in the
75   // prompt config. For example, if the host is
76   // "www.sub.domain.com", try hashes for:
77   // "www.sub.domain.com"
78   // "sub.domain.com"
79   // "domain.com"
80   // We Do not check top level or registry domains to guard against bad
81   // configuration data.
82   SHA256Hash hash(crypto::kSHA256Length, '\0');
83   base::StringPiece host = fixed_url.host_piece();
84   while (host.size() > registry_length) {
85     crypto::SHA256HashString(host, hash.data(), crypto::kSHA256Length);
86     auto iter = domain_hashes_.find(hash);
87     if (iter != domain_hashes_.end())
88       return iter->second;
89 
90     size_t next_start_pos = host.find('.');
91     next_start_pos = next_start_pos == base::StringPiece::npos
92                          ? base::StringPiece::npos
93                          : next_start_pos + 1;
94     host = host.substr(next_start_pos);
95   }
96 
97   return -1;
98 }
99 
delay_before_prompt() const100 base::TimeDelta SettingsResetPromptConfig::delay_before_prompt() const {
101   return delay_before_prompt_;
102 }
103 
prompt_wave() const104 int SettingsResetPromptConfig::prompt_wave() const {
105   return prompt_wave_;
106 }
107 
time_between_prompts() const108 base::TimeDelta SettingsResetPromptConfig::time_between_prompts() const {
109   return time_between_prompts_;
110 }
111 
112 // Implements the hash function for SHA256Hash objects. Simply uses the
113 // first bytes of the SHA256 hash as its own hash.
operator ()(const SHA256Hash & key) const114 size_t SettingsResetPromptConfig::SHA256HashHasher::operator()(
115     const SHA256Hash& key) const {
116   DCHECK_EQ(crypto::kSHA256Length, key.size());
117   // This is safe because |key| contains 32 bytes while a size_t is
118   // either 4 or 8 bytes.
119   return *reinterpret_cast<const size_t*>(key.data());
120 }
121 
122 // These values are written to logs. New enum values can be added, but
123 // existing enums must never be renumbered or deleted and reused. If you
124 // do add values, also update the corresponding enum definition in the
125 // histograms.xml file.
126 enum SettingsResetPromptConfig::ConfigError : int {
127   CONFIG_ERROR_OK = 1,
128   CONFIG_ERROR_MISSING_DOMAIN_HASHES_PARAM = 2,
129   CONFIG_ERROR_BAD_DOMAIN_HASHES_PARAM = 3,
130   CONFIG_ERROR_BAD_DOMAIN_HASH = 4,
131   CONFIG_ERROR_BAD_DOMAIN_ID = 5,
132   CONFIG_ERROR_DUPLICATE_DOMAIN_HASH = 6,
133   CONFIG_ERROR_BAD_DELAY_BEFORE_PROMPT_SECONDS_PARAM = 7,
134   CONFIG_ERROR_BAD_PROMPT_WAVE_PARAM = 8,
135   CONFIG_ERROR_BAD_TIME_BETWEEN_PROMPTS_SECONDS_PARAM = 9,
136   CONFIG_ERROR_MAX
137 };
138 
Init()139 bool SettingsResetPromptConfig::Init() {
140   if (!IsPromptEnabled())
141     return false;
142 
143   // Parse the domain_hashes feature parameter.
144   std::string domain_hashes_json = base::GetFieldTrialParamValueByFeature(
145       kSettingsResetPrompt, "domain_hashes");
146   ConfigError error = ParseDomainHashes(domain_hashes_json);
147   if (error != CONFIG_ERROR_OK) {
148     UMA_HISTOGRAM_ENUMERATION("SettingsResetPrompt.ConfigError", error,
149                               CONFIG_ERROR_MAX);
150     return false;
151   }
152 
153   // Get the delay_before_prompt feature parameter.
154   int delay_before_prompt_seconds = base::GetFieldTrialParamByFeatureAsInt(
155       kSettingsResetPrompt, "delay_before_prompt_seconds", -1);
156   if (delay_before_prompt_seconds < 0) {
157     UMA_HISTOGRAM_ENUMERATION(
158         "SettingsResetPrompt.ConfigError",
159         CONFIG_ERROR_BAD_DELAY_BEFORE_PROMPT_SECONDS_PARAM, CONFIG_ERROR_MAX);
160     return false;
161   }
162   delay_before_prompt_ =
163       base::TimeDelta::FromSeconds(delay_before_prompt_seconds);
164 
165   // Get the prompt_wave feature paramter.
166   prompt_wave_ = base::GetFieldTrialParamByFeatureAsInt(kSettingsResetPrompt,
167                                                         "prompt_wave", 0);
168   if (prompt_wave_ <= 0) {
169     UMA_HISTOGRAM_ENUMERATION("SettingsResetPrompt.ConfigError",
170                               CONFIG_ERROR_BAD_PROMPT_WAVE_PARAM,
171                               CONFIG_ERROR_MAX);
172     return false;
173   }
174 
175   // Get the time_between_prompts_seconds feature parameter.
176   int time_between_prompts_seconds = base::GetFieldTrialParamByFeatureAsInt(
177       kSettingsResetPrompt, "time_between_prompts_seconds", -1);
178   if (time_between_prompts_seconds < 0) {
179     UMA_HISTOGRAM_ENUMERATION(
180         "SettingsResetPrompt.ConfigError",
181         CONFIG_ERROR_BAD_TIME_BETWEEN_PROMPTS_SECONDS_PARAM, CONFIG_ERROR_MAX);
182     return false;
183   }
184   time_between_prompts_ =
185       base::TimeDelta::FromSeconds(time_between_prompts_seconds);
186 
187   UMA_HISTOGRAM_ENUMERATION("SettingsResetPrompt.ConfigError", CONFIG_ERROR_OK,
188                             CONFIG_ERROR_MAX);
189   return true;
190 }
191 
192 SettingsResetPromptConfig::ConfigError
ParseDomainHashes(const std::string & domain_hashes_json)193 SettingsResetPromptConfig::ParseDomainHashes(
194     const std::string& domain_hashes_json) {
195   if (domain_hashes_json.empty())
196     return CONFIG_ERROR_MISSING_DOMAIN_HASHES_PARAM;
197 
198   // Is the input parseable JSON?
199   std::unique_ptr<base::DictionaryValue> domains_dict =
200       base::DictionaryValue::From(
201           base::JSONReader::ReadDeprecated(domain_hashes_json));
202   if (!domains_dict || domains_dict->empty())
203     return CONFIG_ERROR_BAD_DOMAIN_HASHES_PARAM;
204 
205   // The input JSON should be a hash object with hex-encoded 32-byte
206   // hashes as keys and integer IDs as values. For example,
207   //
208   // {"2714..D7": "1", "2821..CB": "2", ...}
209   //
210   // Each key in the hash should be a 64-byte long string and each
211   // integer ID should fit in an int.
212   domain_hashes_.clear();
213   for (base::DictionaryValue::Iterator iter(*domains_dict); !iter.IsAtEnd();
214        iter.Advance()) {
215     const std::string& hash_string = iter.key();
216     if (hash_string.size() != crypto::kSHA256Length * 2)
217       return CONFIG_ERROR_BAD_DOMAIN_HASH;
218 
219     // Convert hex-encoded hash string to its numeric value as bytes.
220     SHA256Hash hash;
221     hash.reserve(crypto::kSHA256Length);
222     if (!base::HexStringToBytes(hash_string, &hash))
223       return CONFIG_ERROR_BAD_DOMAIN_HASH;
224 
225     // Convert the ID string to an integer.
226     std::string domain_id_string;
227     int domain_id = -1;
228     if (!iter.value().GetAsString(&domain_id_string) ||
229         !base::StringToInt(domain_id_string, &domain_id) || domain_id < 0) {
230       return CONFIG_ERROR_BAD_DOMAIN_ID;
231     }
232 
233     if (!domain_hashes_.insert(std::make_pair(std::move(hash), domain_id))
234              .second)
235       return CONFIG_ERROR_DUPLICATE_DOMAIN_HASH;
236   }
237 
238   return CONFIG_ERROR_OK;
239 }
240 
241 }  // namespace safe_browsing.
242