Commit 33340286 authored by Maxim Kolosovskiy's avatar Maxim Kolosovskiy Committed by Commit Bot

[Password Manager] Download the list of password scripts from gstatic

Bug: 1095627
Change-Id: If0b2ce840067480b4847a558b57b04c82ae8abff
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2310529
Commit-Queue: Maxim Kolosovskiy  <kolos@chromium.org>
Reviewed-by: default avatarDominic Battré <battre@chromium.org>
Reviewed-by: default avatarRamin Halavati <rhalavati@chromium.org>
Cr-Commit-Position: refs/heads/master@{#791666}
parent f5725445
......@@ -2800,6 +2800,8 @@ static_library("browser") {
"password_manager/android/update_password_infobar_delegate_android.h",
"password_manager/biometric_authenticator_android.cc",
"password_manager/biometric_authenticator_android.h",
"password_manager/password_scripts_fetcher_factory.cc",
"password_manager/password_scripts_fetcher_factory.h",
"payments/android/can_make_payment_query_android.cc",
"payments/android/journey_logger_android.cc",
"payments/android/journey_logger_android.h",
......
......@@ -62,6 +62,7 @@
#include "components/password_manager/core/browser/password_manager_metrics_util.h"
#include "components/password_manager/core/browser/password_manager_util.h"
#include "components/password_manager/core/browser/password_requirements_service.h"
#include "components/password_manager/core/browser/password_scripts_fetcher.h"
#include "components/password_manager/core/browser/store_metrics_reporter.h"
#include "components/password_manager/core/common/credential_manager_types.h"
#include "components/password_manager/core/common/password_manager_features.h"
......@@ -121,6 +122,7 @@
#include "chrome/browser/password_manager/android/password_manager_launcher_android.h"
#include "chrome/browser/password_manager/android/save_password_infobar_delegate_android.h"
#include "chrome/browser/password_manager/android/update_password_infobar_delegate_android.h"
#include "chrome/browser/password_manager/password_scripts_fetcher_factory.h"
#include "chrome/browser/touch_to_fill/touch_to_fill_controller.h"
#include "chrome/browser/ui/android/passwords/onboarding_dialog_view.h"
#include "components/infobars/core/infobar.h"
......@@ -561,6 +563,15 @@ void ChromePasswordManagerClient::NotifyUserCredentialsWereLeaked(
if (base::FeatureList::IsEnabled(
password_manager::features::kPasswordChange)) {
was_leak_dialog_shown_ = true;
// Prefetch list of scripts whose execution is possible.
// TODO(crbug.com/1108692): Make sure the list is only prefetched in case
// the password-change-in-settings flag is enabled.
if (IsSavingAndFillingEnabled(origin) &&
GetPasswordFeatureManager()->IsGenerationEnabled()) {
PasswordScriptsFetcherFactory::GetInstance()
->GetForBrowserContext(web_contents()->GetBrowserContext())
->PrewarmCache();
}
}
HideSavePasswordInfobar(web_contents());
(new CredentialLeakControllerAndroid(
......
// Copyright 2020 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/password_manager/password_scripts_fetcher_factory.h"
#include "base/no_destructor.h"
#include "components/keyed_service/content/browser_context_dependency_manager.h"
#include "components/password_manager/core/browser/password_scripts_fetcher_impl.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/storage_partition.h"
PasswordScriptsFetcherFactory::PasswordScriptsFetcherFactory()
: BrowserContextKeyedServiceFactory(
"PasswordScriptsFetcher",
BrowserContextDependencyManager::GetInstance()) {}
PasswordScriptsFetcherFactory::~PasswordScriptsFetcherFactory() = default;
// static
PasswordScriptsFetcherFactory* PasswordScriptsFetcherFactory::GetInstance() {
static base::NoDestructor<PasswordScriptsFetcherFactory> instance;
return instance.get();
}
// static
password_manager::PasswordScriptsFetcher*
PasswordScriptsFetcherFactory::GetForBrowserContext(
content::BrowserContext* browser_context) {
return static_cast<password_manager::PasswordScriptsFetcher*>(
GetInstance()->GetServiceForBrowserContext(browser_context, true));
}
KeyedService* PasswordScriptsFetcherFactory::BuildServiceInstanceFor(
content::BrowserContext* browser_context) const {
return new password_manager::PasswordScriptsFetcherImpl(
content::BrowserContext::GetDefaultStoragePartition(browser_context)
->GetURLLoaderFactoryForBrowserProcess());
}
// Copyright 2020 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_PASSWORD_MANAGER_PASSWORD_SCRIPTS_FETCHER_FACTORY_H_
#define CHROME_BROWSER_PASSWORD_MANAGER_PASSWORD_SCRIPTS_FETCHER_FACTORY_H_
#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
namespace password_manager {
class PasswordScriptsFetcher;
}
namespace content {
class BrowserContext;
}
// Creates instances of |PasswordScriptsFetcher| per |BrowserContext|.
class PasswordScriptsFetcherFactory : public BrowserContextKeyedServiceFactory {
public:
PasswordScriptsFetcherFactory();
~PasswordScriptsFetcherFactory() override;
static PasswordScriptsFetcherFactory* GetInstance();
static password_manager::PasswordScriptsFetcher* GetForBrowserContext(
content::BrowserContext* browser_context);
private:
KeyedService* BuildServiceInstanceFor(
content::BrowserContext* browser_context) const override;
};
#endif // CHROME_BROWSER_PASSWORD_MANAGER_PASSWORD_SCRIPTS_FETCHER_FACTORY_H_
......@@ -307,6 +307,14 @@ jumbo_static_library("browser") {
"//url",
]
if (is_android) {
sources += [
"password_scripts_fetcher.h",
"password_scripts_fetcher_impl.cc",
"password_scripts_fetcher_impl.h",
]
}
if (!is_ios) {
sources += [
"hsts_query.cc",
......@@ -608,6 +616,9 @@ source_set("unit_tests") {
"vote_uploads_test_matchers.h",
"votes_uploader_unittest.cc",
]
if (is_android) {
sources += [ "password_scripts_fetcher_impl_unittests.cc" ]
}
if (is_mac) {
sources -= [ "password_store_default_unittest.cc" ]
}
......
// Copyright 2020 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 COMPONENTS_PASSWORD_MANAGER_CORE_BROWSER_PASSWORD_SCRIPTS_FETCHER_H_
#define COMPONENTS_PASSWORD_MANAGER_CORE_BROWSER_PASSWORD_SCRIPTS_FETCHER_H_
#include "base/callback_forward.h"
#include "components/keyed_service/core/keyed_service.h"
namespace url {
class Origin;
}
namespace password_manager {
// Abstract interface to fetch the list of password scripts.
class PasswordScriptsFetcher : public KeyedService {
public:
using ResponseCallback = base::OnceCallback<void(bool)>;
// Triggers pre-fetching the list of scripts.
virtual void PrewarmCache() = 0;
// Returns whether there is a password change script for |origin| via
// |callback|. If the cache was never set or is stale, it triggers a new
// network request (but doesn't trigger a duplicate request if another request
// is in-flight) and enqueues |callback|. Otherwise, it runs the callback
// immediately. In case of an network error, the verdict will default to no
// script being available.
virtual void GetPasswordScriptAvailability(const url::Origin& origin,
ResponseCallback callback) = 0;
};
} // namespace password_manager
#endif // COMPONENTS_PASSWORD_MANAGER_CORE_BROWSER_PASSWORD_SCRIPTS_FETCHER_H_
// Copyright 2020 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/password_manager/core/browser/password_scripts_fetcher_impl.h"
#include "base/callback.h"
#include "base/containers/flat_map.h"
#include "base/json/json_reader.h"
#include "base/no_destructor.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "url/gurl.h"
#include "url/origin.h"
namespace {
constexpr int kCacheTimeoutInMinutes = 5;
constexpr int kFetchTimeoutInSeconds = 3;
constexpr int kMaxDownloadSizeInBytes = 10 * 1024;
} // namespace
namespace password_manager {
constexpr char kChangePasswordScriptsListUrl[] =
"https://www.gstatic.com/chrome/duplex/change_password_scripts.json";
PasswordScriptsFetcherImpl::PasswordScriptsFetcherImpl(
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory)
: url_loader_factory_(std::move(url_loader_factory)) {}
PasswordScriptsFetcherImpl::~PasswordScriptsFetcherImpl() = default;
void PasswordScriptsFetcherImpl::PrewarmCache() {
if (IsCacheStale())
StartFetch();
}
void PasswordScriptsFetcherImpl::GetPasswordScriptAvailability(
const url::Origin& origin,
ResponseCallback callback) {
if (IsCacheStale()) {
pending_callbacks_.emplace_back(
std::make_pair(origin, std::move(callback)));
StartFetch();
return;
}
RunResponseCallback(origin, std::move(callback));
}
void PasswordScriptsFetcherImpl::StartFetch() {
static const base::NoDestructor<base::TimeDelta> kFetchTimeout(
base::TimeDelta::FromMinutes(kFetchTimeoutInSeconds));
if (url_loader_)
return;
auto resource_request = std::make_unique<network::ResourceRequest>();
resource_request->url = GURL(kChangePasswordScriptsListUrl);
resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;
net::NetworkTrafficAnnotationTag traffic_annotation =
net::DefineNetworkTrafficAnnotation("gstatic_change_password_scripts",
R"(
semantics {
sender: "Password Manager"
description:
"A JSON file hosted by gstatic containing a map of password change"
"scripts to optional parameters for those scripts."
trigger:
"When the user visits chrome://settings/passwords/check or "
"makes Safety Check in settings or sees a leak warning."
data:
"The request body is empty. No user data is included."
destination: GOOGLE_OWNED_SERVICE
}
policy {
cookies_allowed: NO
setting:
"The user can enable or disable automatic password leak checks in "
"Chrome's security settings. The feature is enabled by default."
})");
url_loader_ = network::SimpleURLLoader::Create(std::move(resource_request),
traffic_annotation);
url_loader_->SetTimeoutDuration(*kFetchTimeout);
url_loader_->DownloadToString(
url_loader_factory_.get(),
base::BindOnce(&PasswordScriptsFetcherImpl::OnFetchComplete,
base::Unretained(this)),
kMaxDownloadSizeInBytes);
}
void PasswordScriptsFetcherImpl::OnFetchComplete(
std::unique_ptr<std::string> response_body) {
url_loader_.reset();
last_fetch_timestamp_ = base::TimeTicks::Now();
password_change_domains_.clear();
if (response_body) {
base::Optional<base::Value> data = base::JSONReader::Read(*response_body);
if (data != base::nullopt && data->is_dict()) {
for (const auto& it : data->DictItems()) {
// |it.second| is not used at the moment and reserved for
// domain-specific parameters.
GURL url(it.first);
if (url.is_valid()) {
url::Origin origin = url::Origin::Create(url);
password_change_domains_.insert(origin);
}
}
}
}
for (auto& callback : std::exchange(pending_callbacks_, {}))
RunResponseCallback(std::move(callback.first), std::move(callback.second));
}
bool PasswordScriptsFetcherImpl::IsCacheStale() const {
static const base::NoDestructor<base::TimeDelta> kCacheTimeout(
base::TimeDelta::FromMinutes(kCacheTimeoutInMinutes));
return last_fetch_timestamp_.is_null() ||
base::TimeTicks::Now() - last_fetch_timestamp_ >= *kCacheTimeout;
}
void PasswordScriptsFetcherImpl::RunResponseCallback(
url::Origin origin,
ResponseCallback callback) {
DCHECK(!url_loader_); // Fetching is not running.
DCHECK(!IsCacheStale()); // Cache is ready.
bool has_script =
password_change_domains_.find(origin) != password_change_domains_.end();
std::move(callback).Run(has_script);
}
} // namespace password_manager
// Copyright 2020 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 COMPONENTS_PASSWORD_MANAGER_CORE_BROWSER_PASSWORD_SCRIPTS_FETCHER_IMPL_H_
#define COMPONENTS_PASSWORD_MANAGER_CORE_BROWSER_PASSWORD_SCRIPTS_FETCHER_IMPL_H_
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "base/callback_forward.h"
#include "base/containers/flat_set.h"
#include "base/memory/scoped_refptr.h"
#include "base/time/time.h"
#include "components/password_manager/core/browser/password_scripts_fetcher.h"
#include "services/network/public/cpp/simple_url_loader.h"
namespace url {
class Origin;
}
namespace network {
class SharedURLLoaderFactory;
}
namespace password_manager {
extern const char kChangePasswordScriptsListUrl[];
class PasswordScriptsFetcherImpl
: public password_manager::PasswordScriptsFetcher {
public:
explicit PasswordScriptsFetcherImpl(
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory);
~PasswordScriptsFetcherImpl() override;
// PasswordScriptsFetcher:
void PrewarmCache() override;
void GetPasswordScriptAvailability(const url::Origin& origin,
ResponseCallback callback) override;
#if defined(UNIT_TEST)
void make_cache_stale_for_testing() {
last_fetch_timestamp_ =
base::TimeTicks::Now() - base::TimeDelta::FromDays(1);
}
#endif
private:
// Sends new request to gstatic.
void StartFetch();
// Callback for the request to gstatic.
void OnFetchComplete(std::unique_ptr<std::string> response_body);
// Returns whether a re-fetch is needed.
bool IsCacheStale() const;
// Runs |callback| immediately with the script availability for |origin|.
void RunResponseCallback(url::Origin origin, ResponseCallback callback);
// Parsed set of domains from gstatic.
base::flat_set<url::Origin> password_change_domains_;
// Timestamp of the last finished request.
base::TimeTicks last_fetch_timestamp_;
// Stores the callbacks that are waiting for the request to finish.
std::vector<std::pair<url::Origin, ResponseCallback>> pending_callbacks_;
// URL loader object for the gstatic request. If |url_loader_| is not null, a
// request is currently in flight.
std::unique_ptr<network::SimpleURLLoader> url_loader_;
// Used for the gstatic requests.
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory_;
};
} // namespace password_manager
#endif // COMPONENTS_PASSWORD_MANAGER_CORE_BROWSER_PASSWORD_SCRIPTS_FETCHER_IMPL_H_
// Copyright 2020 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/password_manager/core/browser/password_scripts_fetcher_impl.h"
#include "base/test/task_environment.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
#include "services/network/test/test_url_loader_factory.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
using ::testing::Pair;
using ::testing::UnorderedElementsAre;
namespace {
constexpr char kOriginWithScript1[] = "https://example.com";
constexpr char kOriginWithScript2[] = "https://test.com";
constexpr char kOriginWithoutScript[] = "https://no-script.com";
constexpr char kTestResponseContent[] =
"{\"https://example.com\" : {}, \"https://test.com\" : {}}";
url::Origin GetOriginWithScript1() {
return url::Origin::Create(GURL(kOriginWithScript1));
}
url::Origin GetOriginWithScript2() {
return url::Origin::Create(GURL(kOriginWithScript2));
}
url::Origin GetOriginWithoutScript() {
return url::Origin::Create(GURL(kOriginWithoutScript));
}
} // namespace
namespace password_manager {
class PasswordScriptsFetcherImplTest : public ::testing::Test {
public:
void SetUp() override {
// Recreate all classes as they are stateful.
test_url_loader_factory_ =
std::make_unique<network::TestURLLoaderFactory>();
test_shared_loader_factory_ =
base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
test_url_loader_factory_.get());
fetcher_ = std::make_unique<PasswordScriptsFetcherImpl>(
test_shared_loader_factory_);
}
void TearDown() override { EXPECT_EQ(0, GetNumberOfPendingRequests()); }
void SimulateResponse() { SimulateResponseWithContent(kTestResponseContent); }
void SimulateResponseWithContent(const std::string& content) {
EXPECT_TRUE(test_url_loader_factory_->SimulateResponseForPendingRequest(
kChangePasswordScriptsListUrl, content));
}
void SimulateFailedResponse() {
EXPECT_TRUE(test_url_loader_factory_->SimulateResponseForPendingRequest(
kChangePasswordScriptsListUrl, kTestResponseContent,
net::HttpStatusCode::HTTP_BAD_REQUEST));
}
void RequestAllScriptsAvailability() {
RequestSingleScriptAvailability(GetOriginWithScript1());
RequestSingleScriptAvailability(GetOriginWithScript2());
RequestSingleScriptAvailability(GetOriginWithoutScript());
}
int GetNumberOfPendingRequests() {
return test_url_loader_factory_->NumPending();
}
base::flat_map<url::Origin, bool>& recorded_responses() {
return recorded_responses_;
}
PasswordScriptsFetcherImpl* fetcher() { return fetcher_.get(); }
private:
void RequestSingleScriptAvailability(const url::Origin& origin) {
fetcher_->GetPasswordScriptAvailability(origin,
GenerateResponseCallback(origin));
}
void RecordResponse(url::Origin origin, bool has_script) {
const auto& it = recorded_responses_.find(origin);
if (it != recorded_responses_.end()) {
EXPECT_EQ(recorded_responses_[origin], has_script)
<< "Responses for " << origin << " differ";
} else {
recorded_responses_[origin] = has_script;
}
}
PasswordScriptsFetcher::ResponseCallback GenerateResponseCallback(
url::Origin origin) {
return base::BindOnce(&PasswordScriptsFetcherImplTest::RecordResponse,
base::Unretained(this), origin);
}
base::test::SingleThreadTaskEnvironment task_environment_;
base::flat_map<url::Origin, bool> recorded_responses_;
std::unique_ptr<PasswordScriptsFetcherImpl> fetcher_;
std::unique_ptr<network::TestURLLoaderFactory> test_url_loader_factory_;
scoped_refptr<network::SharedURLLoaderFactory> test_shared_loader_factory_;
};
TEST_F(PasswordScriptsFetcherImplTest, PrewarmCache) {
fetcher()->PrewarmCache();
EXPECT_EQ(1, GetNumberOfPendingRequests());
SimulateResponse();
EXPECT_EQ(0, GetNumberOfPendingRequests());
// The cache is not stale yet. So, no new request is expected.
fetcher()->PrewarmCache();
EXPECT_EQ(0, GetNumberOfPendingRequests());
RequestAllScriptsAvailability();
EXPECT_THAT(recorded_responses(),
UnorderedElementsAre(Pair(GetOriginWithScript1(), true),
Pair(GetOriginWithScript2(), true),
Pair(GetOriginWithoutScript(), false)));
EXPECT_EQ(0, GetNumberOfPendingRequests());
// Make cache stale and re-fetch the map.
fetcher()->make_cache_stale_for_testing();
recorded_responses().clear();
RequestAllScriptsAvailability();
EXPECT_EQ(1, GetNumberOfPendingRequests());
// OriginWithScript2 (test.com) is not available anymore.
SimulateResponseWithContent("{\"https://example.com\" : {}}");
base::RunLoop().RunUntilIdle();
EXPECT_THAT(recorded_responses(),
UnorderedElementsAre(Pair(GetOriginWithScript1(), true),
Pair(GetOriginWithScript2(), false),
Pair(GetOriginWithoutScript(), false)));
EXPECT_EQ(0, GetNumberOfPendingRequests());
}
TEST_F(PasswordScriptsFetcherImplTest, NoPrewarmCache) {
RequestAllScriptsAvailability(); // Without preceding |PrewarmCache|.
EXPECT_EQ(1, GetNumberOfPendingRequests());
SimulateResponse();
base::RunLoop().RunUntilIdle();
EXPECT_THAT(recorded_responses(),
UnorderedElementsAre(Pair(GetOriginWithScript1(), true),
Pair(GetOriginWithScript2(), true),
Pair(GetOriginWithoutScript(), false)));
EXPECT_EQ(0, GetNumberOfPendingRequests());
}
TEST_F(PasswordScriptsFetcherImplTest, InvalidJson) {
const char* const kTestCases[] = {"", "{{{", "[\"1\", \"2\"]"};
for (auto* test_case : kTestCases) {
SCOPED_TRACE(testing::Message() << "test_case=" << test_case);
fetcher()->make_cache_stale_for_testing();
recorded_responses().clear();
RequestAllScriptsAvailability();
SimulateResponseWithContent(test_case);
base::RunLoop().RunUntilIdle();
EXPECT_THAT(recorded_responses(),
UnorderedElementsAre(Pair(GetOriginWithScript1(), false),
Pair(GetOriginWithScript2(), false),
Pair(GetOriginWithoutScript(), false)));
}
}
TEST_F(PasswordScriptsFetcherImplTest, ServerError) {
RequestAllScriptsAvailability();
SimulateFailedResponse();
base::RunLoop().RunUntilIdle();
EXPECT_THAT(recorded_responses(),
UnorderedElementsAre(Pair(GetOriginWithScript1(), false),
Pair(GetOriginWithScript2(), false),
Pair(GetOriginWithoutScript(), false)));
}
} // namespace password_manager
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment