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