blob: 2c065752666ecfcc24c82a3b63af817014fccfe5 [file] [log] [blame]
// Copyright 2018 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 "components/autofill/core/browser/password_requirements_spec_fetcher_impl.h"
#include "base/logging.h"
#include "base/md5.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/stringprintf.h"
#include "base/time/time.h"
#include "components/autofill/core/browser/password_requirements_spec_printer.h"
#include "components/autofill/core/browser/proto/password_requirements.pb.h"
#include "components/autofill/core/browser/proto/password_requirements_shard.pb.h"
#include "net/base/load_flags.h"
#include "net/base/net_errors.h"
#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "url/url_canon.h"
namespace autofill {
PasswordRequirementsSpecFetcherImpl::PasswordRequirementsSpecFetcherImpl(
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
int version,
size_t prefix_length,
int timeout)
: url_loader_factory_(std::move(url_loader_factory)),
version_(version),
prefix_length_(prefix_length),
timeout_(timeout) {
DCHECK_GE(version_, 0);
DCHECK_LE(prefix_length_, 32u);
DCHECK_GE(timeout_, 0);
}
PasswordRequirementsSpecFetcherImpl::~PasswordRequirementsSpecFetcherImpl() =
default;
PasswordRequirementsSpecFetcherImpl::LookupInFlight::LookupInFlight() = default;
PasswordRequirementsSpecFetcherImpl::LookupInFlight::~LookupInFlight() =
default;
namespace {
// Hashes the eTLD+1 of |origin| via MD5 and returns a filename with the first
// |prefix_length| bits populated. The returned value corresponds to the first
// 4 bytes of the truncated MD5 prefix in hex notation.
// For example:
// "https://www.example.com" has a eTLD+1 of "example.com".
// The MD5SUM of that is 5ababd603b22780302dd8d83498e5172.
// Stripping this to the first 8 bits (prefix_length = 8) gives
// 500000000000000000000000000000000. The file name is always cut to the first
// four bytes, i.e. 5000 in this example.
std::string GetHashPrefix(const GURL& origin, size_t prefix_length) {
DCHECK_LE(prefix_length, 32u);
std::string domain_and_registry =
net::registry_controlled_domains::GetDomainAndRegistry(
origin, net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES);
base::MD5Digest digest;
base::MD5Sum(domain_and_registry.data(), domain_and_registry.size(), &digest);
for (size_t i = 0; i < base::size(digest.a); ++i) {
if (prefix_length >= 8) {
prefix_length -= 8;
continue;
} else {
// Determine the |prefix_length| most significant bits by calculating
// the 8 - |prefix_length| least significant bits and inverting the
// result.
digest.a[i] &= ~((1 << (8 - prefix_length)) - 1);
prefix_length = 0;
}
}
return base::MD5DigestToBase16(digest).substr(0, 4);
}
// Returns the URL on gstatic.com where the passwords spec file can be found
// that contains data for |hash_prefix|.
GURL GetUrlForRequirementsSpec(int version, const std::string& hash_prefix) {
return GURL(base::StringPrintf(
"https://www.gstatic.com/chrome/autofill/password_generation_specs/%d/%s",
version, hash_prefix.c_str()));
}
} // namespace
void PasswordRequirementsSpecFetcherImpl::Fetch(
GURL origin,
FetchCallback callback) {
DCHECK(origin.is_valid());
VLOG(1) << "Fetching password requirements spec for " << origin;
if (!url_loader_factory_) {
VLOG(1) << "No url_logger_factory_ available";
TriggerCallback(std::move(callback), ResultCode::kErrorNoUrlLoader,
PasswordRequirementsSpec());
return;
}
if (!url_loader_factory_) {
TriggerCallback(std::move(callback), ResultCode::kErrorNoUrlLoader,
PasswordRequirementsSpec());
return;
}
if (!url_loader_factory_) {
TriggerCallback(std::move(callback), ResultCode::kErrorNoUrlLoader,
PasswordRequirementsSpec());
return;
}
if (!origin.is_valid() || origin.HostIsIPAddress() ||
!origin.SchemeIsHTTPOrHTTPS()) {
VLOG(1) << "No valid origin";
TriggerCallback(std::move(callback), ResultCode::kErrorInvalidOrigin,
PasswordRequirementsSpec());
return;
}
// Canonicalize away trailing periods in hostname.
while (!origin.host().empty() && origin.host().back() == '.') {
std::string new_host = origin.host().substr(0, origin.host().length() - 1);
url::Replacements<char> replacements;
replacements.SetHost(new_host.c_str(),
url::Component(0, new_host.length()));
origin = origin.ReplaceComponents(replacements);
}
std::string hash_prefix = GetHashPrefix(origin, prefix_length_);
// If a lookup is happening already, just register another callback.
auto iter = lookups_in_flight_.find(hash_prefix);
if (iter != lookups_in_flight_.end()) {
iter->second->callbacks.push_back(
std::make_pair(origin, std::move(callback)));
VLOG(1) << "Lookup already in flight";
return;
}
// Start another lookup otherwise.
auto lookup = std::make_unique<LookupInFlight>();
lookup->callbacks.push_back(std::make_pair(origin, std::move(callback)));
lookup->start_of_request = base::TimeTicks::Now();
net::NetworkTrafficAnnotationTag traffic_annotation =
net::DefineNetworkTrafficAnnotation("password_requirements_spec_fetch",
R"(
semantics {
sender: "Password requirements specification fetcher"
description:
"Fetches the password requirements for a set of domains whose origin "
"hash starts with a certain prefix."
trigger:
"When the user triggers a password generation (this can happen by "
"just focussing a password field)."
data:
"The URL encodes a hash prefix from which it is not possible to "
"derive the original origin. No user information is sent."
destination: WEBSITE
}
policy {
cookies_allowed: NO
setting: "Unconditionally enabled."
policy_exception_justification:
"Not implemented, considered not useful."
})");
auto resource_request = std::make_unique<network::ResourceRequest>();
resource_request->url = GetUrlForRequirementsSpec(version_, hash_prefix);
resource_request->load_flags = net::LOAD_DO_NOT_SAVE_COOKIES |
net::LOAD_DO_NOT_SEND_COOKIES |
net::LOAD_DO_NOT_SEND_AUTH_DATA;
lookup->url_loader = network::SimpleURLLoader::Create(
std::move(resource_request), traffic_annotation);
lookup->url_loader->DownloadToStringOfUnboundedSizeUntilCrashAndDie(
url_loader_factory_.get(),
base::BindOnce(&PasswordRequirementsSpecFetcherImpl::OnFetchComplete,
base::Unretained(this), hash_prefix));
lookup->download_timer.Start(
FROM_HERE, base::TimeDelta::FromMilliseconds(timeout_),
base::BindRepeating(&PasswordRequirementsSpecFetcherImpl::OnFetchTimeout,
base::Unretained(this), hash_prefix));
lookups_in_flight_[hash_prefix] = std::move(lookup);
}
void PasswordRequirementsSpecFetcherImpl::OnFetchComplete(
const std::string& hash_prefix,
std::unique_ptr<std::string> response_body) {
std::unique_ptr<LookupInFlight> lookup = RemoveLookupInFlight(hash_prefix);
lookup->download_timer.Stop();
UMA_HISTOGRAM_TIMES("PasswordManager.RequirementsSpecFetcher.NetworkDuration",
base::TimeTicks::Now() - lookup->start_of_request);
base::UmaHistogramSparse(
"PasswordManager.RequirementsSpecFetcher.NetErrorCode",
lookup->url_loader->NetError());
if (lookup->url_loader->ResponseInfo() &&
lookup->url_loader->ResponseInfo()->headers) {
base::UmaHistogramSparse(
"PasswordManager.RequirementsSpecFetcher.HttpResponseCode",
lookup->url_loader->ResponseInfo()->headers->response_code());
}
if (!response_body || lookup->url_loader->NetError() != net::Error::OK) {
VLOG(1) << "Fetch for " << hash_prefix << ": failed to fetch "
<< lookup->url_loader->NetError();
TriggerCallbackToAll(&lookup->callbacks, ResultCode::kErrorFailedToFetch,
PasswordRequirementsSpec());
return;
}
PasswordRequirementsShard shard;
if (!shard.ParseFromString(*response_body)) {
VLOG(1) << "Fetch for " << hash_prefix << ": failed to parse response";
TriggerCallbackToAll(&lookup->callbacks, ResultCode::kErrorFailedToParse,
PasswordRequirementsSpec());
return;
}
for (auto& callback_pair : lookup->callbacks) {
const GURL& origin = callback_pair.first;
FetchCallback& callback_function = callback_pair.second;
// Search shard for matches for origin by looking up the (canonicalized)
// host name and then stripping domain prefixes until the eTLD+1 is reached.
DCHECK(!origin.HostIsIPAddress());
// |host| is a std::string instead of StringPiece as the protbuf::Map
// implementation does not support StringPieces as parameters for find.
std::string host = origin.host();
auto host_iter = shard.specs().find(host);
if (host_iter != shard.specs().end()) {
const PasswordRequirementsSpec& spec = host_iter->second;
VLOG(1) << "Found for " << host << ": " << spec;
TriggerCallback(std::move(callback_function), ResultCode::kFoundSpec,
spec);
continue;
}
bool found_entry = false;
const std::string domain_and_registry =
net::registry_controlled_domains::GetDomainAndRegistry(
origin,
net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES);
while (host.length() > 0 && host != domain_and_registry) {
size_t pos = host.find('.');
if (pos != std::string::npos) { // strip prefix
host = host.substr(pos + 1);
} else {
break;
}
// If an entry has ben found, exit with that.
auto it = shard.specs().find(host);
if (it != shard.specs().end()) {
const PasswordRequirementsSpec& spec = it->second;
found_entry = true;
VLOG(1) << "Found for " << host << ": " << spec;
TriggerCallback(std::move(callback_function), ResultCode::kFoundSpec,
spec);
break;
}
}
if (!found_entry) {
VLOG(1) << "Found no entry for " << host;
TriggerCallback(std::move(callback_function), ResultCode::kFoundNoSpec,
PasswordRequirementsSpec());
}
}
}
void PasswordRequirementsSpecFetcherImpl::OnFetchTimeout(
const std::string& hash_prefix) {
std::unique_ptr<LookupInFlight> lookup = RemoveLookupInFlight(hash_prefix);
UMA_HISTOGRAM_TIMES("PasswordManager.RequirementsSpecFetcher.NetworkDuration",
base::TimeTicks::Now() - lookup->start_of_request);
TriggerCallbackToAll(&lookup->callbacks, ResultCode::kErrorTimeout,
PasswordRequirementsSpec());
}
void PasswordRequirementsSpecFetcherImpl::TriggerCallbackToAll(
std::list<std::pair<GURL, FetchCallback>>* callbacks,
ResultCode result,
const PasswordRequirementsSpec& spec) {
for (auto& callback_pair : *callbacks) {
TriggerCallback(std::move(callback_pair.second), result, spec);
}
}
void PasswordRequirementsSpecFetcherImpl::TriggerCallback(
FetchCallback callback,
ResultCode result,
const PasswordRequirementsSpec& spec) {
UMA_HISTOGRAM_ENUMERATION("PasswordManager.RequirementsSpecFetcher.Result",
result);
std::move(callback).Run(spec);
}
std::unique_ptr<PasswordRequirementsSpecFetcherImpl::LookupInFlight>
PasswordRequirementsSpecFetcherImpl::RemoveLookupInFlight(
const std::string& hash_prefix) {
DCHECK(lookups_in_flight_.find(hash_prefix) != lookups_in_flight_.end());
std::unique_ptr<LookupInFlight> lookup;
std::swap(lookup, lookups_in_flight_[hash_prefix]);
lookups_in_flight_.erase(hash_prefix);
return lookup;
}
} // namespace autofill