1 // Copyright 2014 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.
5 #include "components/password_manager/core/browser/affiliation_utils.h"
10 #include "base/base64.h"
11 #include "base/command_line.h"
12 #include "base/metrics/field_trial.h"
13 #include "base/strings/string_piece.h"
14 #include "base/strings/string_util.h"
15 #include "components/autofill/core/common/password_form.h"
16 #include "components/password_manager/core/common/password_manager_switches.h"
17 #include "components/url_formatter/url_formatter.h"
18 #include "components/variations/variations_associated_data.h"
19 #include "net/base/escape.h"
20 #include "url/third_party/mozilla/url_parse.h"
21 #include "url/url_canon_stdstring.h"
23 namespace password_manager
{
27 // The scheme used for identifying Android applications.
28 const char kAndroidAppScheme
[] = "android";
30 // The name of the field trial controlling affiliation-based matching.
31 const char kFieldTrialName
[] = "AffiliationBasedMatching";
33 // Returns a StringPiece corresponding to |component| in |uri|, or the empty
34 // string in case there is no such component.
35 base::StringPiece
ComponentString(const std::string
& uri
,
36 const url::Component
& component
) {
37 if (!component
.is_valid())
38 return base::StringPiece();
39 return base::StringPiece(uri
.c_str() + component
.begin
, component
.len
);
42 // Returns true if the passed ASCII |input| string contains nothing else than
43 // alphanumeric characters and those in |other_characters|.
44 bool ContainsOnlyAlphanumericAnd(const base::StringPiece
& input
,
45 const base::StringPiece
& other_characters
) {
46 for (char c
: input
) {
47 if (!base::IsAsciiAlpha(c
) && !base::IsAsciiDigit(c
) &&
48 other_characters
.find(c
) == base::StringPiece::npos
)
54 // Canonicalizes a Web facet URI, and returns true if canonicalization was
55 // successful and produced a valid URI.
56 bool CanonicalizeWebFacetURI(const std::string
& input_uri
,
57 const url::Parsed
& input_parsed
,
58 std::string
* canonical_uri
) {
59 url::Parsed canonical_parsed
;
60 url::StdStringCanonOutput
canonical_output(canonical_uri
);
62 bool canonicalization_succeeded
= url::CanonicalizeStandardURL(
63 input_uri
.c_str(), input_uri
.size(), input_parsed
, nullptr,
64 &canonical_output
, &canonical_parsed
);
65 canonical_output
.Complete();
67 if (canonicalization_succeeded
&& canonical_parsed
.host
.is_nonempty() &&
68 !canonical_parsed
.username
.is_valid() &&
69 !canonical_parsed
.password
.is_valid() &&
70 ComponentString(*canonical_uri
, canonical_parsed
.path
) == "/" &&
71 !canonical_parsed
.query
.is_valid() && !canonical_parsed
.ref
.is_valid()) {
72 // Get rid of the trailing slash added by url::CanonicalizeStandardURL().
73 DCHECK_EQ((size_t)canonical_parsed
.path
.begin
, canonical_uri
->size() - 1);
74 canonical_uri
->erase(canonical_parsed
.path
.begin
,
75 canonical_parsed
.path
.len
);
81 // Adds padding until the length of the base64-encoded |data| becomes a multiple
82 // of 4, and returns true if the thusly obtained |data| is now correctly padded,
83 // i.e., there are at most 2 padding characters ('=') at the very end.
84 bool CanonicalizeBase64Padding(std::string
* data
) {
85 while (data
->size() % 4u != 0u)
88 size_t first_padding
= data
->find_first_of('=');
89 return first_padding
== std::string::npos
||
90 (data
->size() - first_padding
<= 2u &&
91 data
->find_first_not_of('=', first_padding
) == std::string::npos
);
94 // Canonicalizes the username component in an Android facet URI (containing the
95 // certificate hash), and returns true if canonicalization was successful and
96 // produced a valid non-empty component.
97 bool CanonicalizeHashComponent(const base::StringPiece
& input_hash
,
98 url::CanonOutput
* canonical_output
) {
99 // Characters other than alphanumeric that are used in the "URL and filename
100 // safe" base64 alphabet; plus the padding ('=').
101 const char kBase64NonAlphanumericChars
[] = "-_=";
103 // We need net::UnescapeRule::URL_SPECIAL_CHARS to unescape the padding ('=').
104 std::string base64_encoded_hash
= net::UnescapeURLComponent(
105 input_hash
.as_string(), net::UnescapeRule::URL_SPECIAL_CHARS
);
107 if (!base64_encoded_hash
.empty() &&
108 CanonicalizeBase64Padding(&base64_encoded_hash
) &&
109 ContainsOnlyAlphanumericAnd(base64_encoded_hash
,
110 kBase64NonAlphanumericChars
)) {
111 canonical_output
->Append(base64_encoded_hash
.data(),
112 base64_encoded_hash
.size());
113 canonical_output
->push_back('@');
119 // Canonicalizes the host component in an Android facet URI (containing the
120 // package name), and returns true if canonicalization was successful and
121 // produced a valid non-empty component.
122 bool CanonicalizePackageNameComponent(
123 const base::StringPiece
& input_package_name
,
124 url::CanonOutput
* canonical_output
) {
125 // Characters other than alphanumeric that are permitted in the package names.
126 const char kPackageNameNonAlphanumericChars
[] = "._";
128 std::string package_name
= net::UnescapeURLComponent(
129 input_package_name
.as_string(), net::UnescapeRule::NORMAL
);
131 // TODO(engedy): We might want to use a regex to check this more throughly.
132 if (!package_name
.empty() &&
133 ContainsOnlyAlphanumericAnd(package_name
,
134 kPackageNameNonAlphanumericChars
)) {
135 canonical_output
->Append(package_name
.data(), package_name
.size());
141 // Canonicalizes an Android facet URI, and returns true if canonicalization was
142 // successful and produced a valid URI.
143 bool CanonicalizeAndroidFacetURI(const std::string
& input_uri
,
144 const url::Parsed
& input_parsed
,
145 std::string
* canonical_uri
) {
146 url::StdStringCanonOutput
canonical_output(canonical_uri
);
148 url::Component unused
;
149 bool success
= url::CanonicalizeScheme(
150 input_uri
.c_str(), input_parsed
.scheme
, &canonical_output
, &unused
);
152 canonical_output
.push_back('/');
153 canonical_output
.push_back('/');
155 // We cannot use url::CanonicalizeUserInfo as that would percent encode the
156 // base64 padding characters ('=').
157 success
&= CanonicalizeHashComponent(
158 ComponentString(input_uri
, input_parsed
.username
), &canonical_output
);
160 // We cannot use url::CanonicalizeHost as that would convert the package name
161 // to lower case, but the package name is case sensitive.
162 success
&= CanonicalizePackageNameComponent(
163 ComponentString(input_uri
.data(), input_parsed
.host
), &canonical_output
);
165 canonical_output
.Complete();
167 return success
&& !input_parsed
.password
.is_nonempty() &&
168 (!input_parsed
.path
.is_nonempty() ||
169 ComponentString(input_uri
, input_parsed
.path
) == "/") &&
170 !input_parsed
.port
.is_nonempty() && !input_parsed
.query
.is_valid() &&
171 !input_parsed
.ref
.is_valid();
174 // Computes the canonicalized form of |uri| into |canonical_uri|, and returns
175 // true if canonicalization was successful and produced a valid URI.
176 bool ParseAndCanonicalizeFacetURI(const std::string
& input_uri
,
177 std::string
* canonical_uri
) {
178 DCHECK(canonical_uri
);
179 canonical_uri
->clear();
180 canonical_uri
->reserve(input_uri
.size() + 32);
182 url::Parsed input_parsed
;
183 url::ParseStandardURL(input_uri
.c_str(), input_uri
.size(), &input_parsed
);
185 base::StringPiece scheme
= ComponentString(input_uri
, input_parsed
.scheme
);
186 if (base::LowerCaseEqualsASCII(scheme
, url::kHttpsScheme
)) {
187 return CanonicalizeWebFacetURI(input_uri
, input_parsed
, canonical_uri
);
188 } else if (base::LowerCaseEqualsASCII(scheme
, kAndroidAppScheme
)) {
189 return CanonicalizeAndroidFacetURI(input_uri
, input_parsed
, canonical_uri
);
197 // FacetURI -------------------------------------------------------------------
199 FacetURI::FacetURI() : is_valid_(false) {
203 FacetURI
FacetURI::FromPotentiallyInvalidSpec(const std::string
& spec
) {
204 std::string canonical_spec
;
205 bool is_valid
= ParseAndCanonicalizeFacetURI(spec
, &canonical_spec
);
206 return FacetURI(canonical_spec
, is_valid
);
210 FacetURI
FacetURI::FromCanonicalSpec(const std::string
& canonical_spec
) {
211 return FacetURI(canonical_spec
, true);
214 bool FacetURI::operator==(const FacetURI
& other
) const {
215 DCHECK(is_empty() || is_valid());
216 DCHECK(other
.is_empty() || other
.is_valid());
217 return canonical_spec_
== other
.canonical_spec_
;
220 bool FacetURI::operator!=(const FacetURI
& other
) const {
221 DCHECK(is_empty() || is_valid());
222 DCHECK(other
.is_empty() || other
.is_valid());
223 return canonical_spec_
!= other
.canonical_spec_
;
226 bool FacetURI::operator<(const FacetURI
& other
) const {
227 DCHECK(is_empty() || is_valid());
228 DCHECK(other
.is_empty() || other
.is_valid());
229 return canonical_spec_
< other
.canonical_spec_
;
232 bool FacetURI::operator>(const FacetURI
& other
) const {
233 DCHECK(is_empty() || is_valid());
234 DCHECK(other
.is_empty() || other
.is_valid());
235 return canonical_spec_
> other
.canonical_spec_
;
238 bool FacetURI::IsValidWebFacetURI() const {
239 return scheme() == url::kHttpsScheme
;
242 bool FacetURI::IsValidAndroidFacetURI() const {
243 return scheme() == kAndroidAppScheme
;
246 std::string
FacetURI::scheme() const {
248 ? ComponentString(canonical_spec_
, parsed_
.scheme
).as_string()
252 std::string
FacetURI::android_package_name() const {
253 if (!IsValidAndroidFacetURI())
255 return ComponentString(canonical_spec_
, parsed_
.host
).as_string();
258 FacetURI::FacetURI(const std::string
& canonical_spec
, bool is_valid
)
259 : is_valid_(is_valid
), canonical_spec_(canonical_spec
) {
260 // TODO(engedy): Refactor code in order to avoid to avoid parsing the URL
262 url::ParseStandardURL(canonical_spec_
.c_str(), canonical_spec_
.size(),
267 // AffiliatedFacetsWithUpdateTime ---------------------------------------------
269 AffiliatedFacetsWithUpdateTime::AffiliatedFacetsWithUpdateTime() {
272 AffiliatedFacetsWithUpdateTime::~AffiliatedFacetsWithUpdateTime() {
276 // Helpers --------------------------------------------------------------------
278 std::ostream
& operator<<(std::ostream
& os
, const FacetURI
& facet_uri
) {
279 return os
<< facet_uri
.potentially_invalid_spec();
282 bool AreEquivalenceClassesEqual(const AffiliatedFacets
& a
,
283 const AffiliatedFacets
& b
) {
284 if (a
.size() != b
.size())
287 std::vector
<FacetURI
> a_sorted(a
.begin(), a
.end());
288 std::vector
<FacetURI
> b_sorted(b
.begin(), b
.end());
289 std::sort(a_sorted
.begin(), a_sorted
.end());
290 std::sort(b_sorted
.begin(), b_sorted
.end());
291 return std::equal(a_sorted
.begin(), a_sorted
.end(), b_sorted
.begin());
294 bool IsAffiliationBasedMatchingEnabled(const base::CommandLine
& command_line
) {
295 // Note: It is important to always query the field trial state, to ensure that
296 // UMA reports the correct group.
297 const std::string group_name
=
298 base::FieldTrialList::FindFullName(kFieldTrialName
);
300 if (command_line
.HasSwitch(switches::kDisableAffiliationBasedMatching
))
302 if (command_line
.HasSwitch(switches::kEnableAffiliationBasedMatching
))
304 return base::StartsWith(group_name
, "Enabled",
305 base::CompareCase::INSENSITIVE_ASCII
);
308 bool IsPropagatingPasswordChangesToWebCredentialsEnabled(
309 const base::CommandLine
& command_line
) {
310 // Note: It is important to always query the variation param first, which, in
311 // turn, queries the field trial state, which ensures that UMA reports the
312 // correct group if it is forced by a command line flag.
313 const std::string update_enabled
= variations::GetVariationParamValue(
314 kFieldTrialName
, "propagate_password_changes_to_web");
316 if (command_line
.HasSwitch(switches::kDisableAffiliationBasedMatching
))
318 if (command_line
.HasSwitch(switches::kEnableAffiliationBasedMatching
))
320 return base::LowerCaseEqualsASCII(update_enabled
, "enabled");
323 bool IsAffiliationRequestsForDummyFacetsEnabled(
324 const base::CommandLine
& command_line
) {
325 const std::string synthesizing_enabled
= variations::GetVariationParamValue(
326 kFieldTrialName
, "affiliation_requests_for_dummy_facets");
327 if (command_line
.HasSwitch(switches::kDisableAffiliationBasedMatching
))
329 if (command_line
.HasSwitch(switches::kEnableAffiliationBasedMatching
))
331 return base::LowerCaseEqualsASCII(synthesizing_enabled
, "enabled");
334 bool IsValidAndroidFacetURI(const std::string
& url
) {
335 FacetURI facet
= FacetURI::FromPotentiallyInvalidSpec(url
);
336 return facet
.IsValidAndroidFacetURI();
339 std::string
GetHumanReadableOrigin(const autofill::PasswordForm
& password_form
,
340 const std::string
& languages
) {
341 password_manager::FacetURI facet_uri
=
342 password_manager::FacetURI::FromPotentiallyInvalidSpec(
343 password_form
.signon_realm
);
344 if (facet_uri
.IsValidAndroidFacetURI())
345 return facet_uri
.scheme() + "://" + facet_uri
.android_package_name();
346 return base::UTF16ToUTF8(
347 url_formatter::FormatUrl(password_form
.origin
, languages
));
350 } // namespace password_manager