| // Copyright 2017 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/safe_browsing/password_protection/password_protection_service.h" |
| |
| #include <stddef.h> |
| |
| #include <memory> |
| #include <string> |
| |
| #include "base/base64.h" |
| #include "base/bind.h" |
| #include "base/callback.h" |
| #include "base/stl_util.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_split.h" |
| #include "base/strings/string_util.h" |
| #include "components/content_settings/core/browser/host_content_settings_map.h" |
| #include "components/history/core/browser/history_service.h" |
| #include "components/password_manager/core/browser/password_reuse_detector.h" |
| #include "components/safe_browsing/common/utils.h" |
| #include "components/safe_browsing/db/database_manager.h" |
| #include "components/safe_browsing/db/whitelist_checker_client.h" |
| #include "components/safe_browsing/features.h" |
| #include "components/safe_browsing/password_protection/password_protection_navigation_throttle.h" |
| #include "components/safe_browsing/password_protection/password_protection_request.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/navigation_handle.h" |
| #include "content/public/browser/web_contents.h" |
| #include "google_apis/google_api_keys.h" |
| #include "net/base/escape.h" |
| #include "net/base/url_util.h" |
| |
| using content::BrowserThread; |
| using content::WebContents; |
| using history::HistoryService; |
| using password_manager::metrics_util::PasswordType; |
| |
| namespace safe_browsing { |
| |
| using PasswordReuseEvent = LoginReputationClientRequest::PasswordReuseEvent; |
| |
| namespace { |
| |
| // Keys for storing password protection verdict into a DictionaryValue. |
| const char kCacheCreationTime[] = "cache_creation_time"; |
| const char kVerdictProto[] = "verdict_proto"; |
| const int kRequestTimeoutMs = 10000; |
| const char kPasswordProtectionRequestUrl[] = |
| "https://sb-ssl.google.com/safebrowsing/clientreport/login"; |
| const char kPasswordOnFocusCacheKey[] = "password_on_focus_cache_key"; |
| |
| // Helper function to determine if the given origin matches content settings |
| // map's patterns. |
| bool OriginMatchPrimaryPattern( |
| const GURL& origin, |
| const ContentSettingsPattern& primary_pattern, |
| const ContentSettingsPattern& secondary_pattern_unused) { |
| return ContentSettingsPattern::FromURLNoWildcard(origin) == primary_pattern; |
| } |
| |
| // Returns the number of path segments in |cache_expression_path|. |
| // For example, return 0 for "/", since there is no path after the leading |
| // slash; return 3 for "/abc/def/gh.html". |
| size_t GetPathDepth(const std::string& cache_expression_path) { |
| return base::SplitString(base::StringPiece(cache_expression_path), "/", |
| base::KEEP_WHITESPACE, base::SPLIT_WANT_NONEMPTY) |
| .size(); |
| } |
| |
| // Given a URL of either http or https scheme, return its http://hostname. |
| // e.g., "https://www.foo.com:80/bar/test.cgi" -> "http://www.foo.com". |
| GURL GetHostNameWithHTTPScheme(const GURL& url) { |
| DCHECK(url.SchemeIsHTTPOrHTTPS()); |
| std::string result(url::kHttpScheme); |
| result.append(url::kStandardSchemeSeparator).append(url.host()); |
| return GURL(result); |
| } |
| |
| } // namespace |
| |
| PasswordProtectionService::PasswordProtectionService( |
| const scoped_refptr<SafeBrowsingDatabaseManager>& database_manager, |
| scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory, |
| HistoryService* history_service, |
| HostContentSettingsMap* host_content_settings_map) |
| : stored_verdict_count_password_on_focus_(-1), |
| stored_verdict_count_password_entry_(-1), |
| database_manager_(database_manager), |
| url_loader_factory_(url_loader_factory), |
| history_service_observer_(this), |
| content_settings_(host_content_settings_map), |
| weak_factory_(this) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| if (history_service) |
| history_service_observer_.Add(history_service); |
| |
| // TODO(jialiul): Remove this code when migration is done. |
| MigrateCachedVerdicts(); |
| } |
| |
| PasswordProtectionService::~PasswordProtectionService() { |
| tracker_.TryCancelAll(); |
| CancelPendingRequests(); |
| history_service_observer_.RemoveAll(); |
| weak_factory_.InvalidateWeakPtrs(); |
| } |
| |
| bool PasswordProtectionService::CanGetReputationOfURL(const GURL& url) { |
| if (!url.is_valid() || !url.SchemeIsHTTPOrHTTPS() || net::IsLocalhost(url)) |
| return false; |
| |
| const std::string hostname = url.HostNoBrackets(); |
| return !net::IsHostnameNonUnique(hostname) && |
| hostname.find('.') != std::string::npos; |
| } |
| |
| bool PasswordProtectionService::ShouldShowModalWarning( |
| LoginReputationClientRequest::TriggerType trigger_type, |
| PasswordReuseEvent::ReusedPasswordType password_type, |
| LoginReputationClientResponse::VerdictType verdict_type) { |
| if (trigger_type != LoginReputationClientRequest::PASSWORD_REUSE_EVENT || |
| !IsSupportedPasswordTypeForModalWarning(password_type)) { |
| return false; |
| } |
| |
| // Shows modal warning for sync password reuse only if user's currently logged |
| // in. |
| if (password_type == PasswordReuseEvent::SIGN_IN_PASSWORD && |
| GetSyncAccountType() == PasswordReuseEvent::NOT_SIGNED_IN) { |
| return false; |
| } |
| |
| return (verdict_type == LoginReputationClientResponse::PHISHING || |
| verdict_type == LoginReputationClientResponse::LOW_REPUTATION) && |
| IsWarningEnabled(); |
| } |
| |
| // We cache both types of pings under the same content settings type ( |
| // CONTENT_SETTINGS_TYPE_PASSWORD_PROTECTION). Since UNFAMILIAR_LOGIN_PAGE |
| // verdicts are only enabled on extended reporting users, we cache them one |
| // layer lower in the content setting DictionaryValue than PASSWORD_REUSE_EVENT |
| // verdicts. |
| // In other words, to cache a PASSWORD_REUSE_EVENT verdict we needs three levels |
| // of keys: (1) origin, (2) password type, (3) cache expression |
| // returned in verdict. |
| // To cache a UNFAMILIAR_LOGIN_PAGE, three levels of keys are used: |
| // (1) origin, (2) 2nd level key is always |kPasswordOnFocusCacheKey|, |
| // (3) cache expression. |
| LoginReputationClientResponse::VerdictType |
| PasswordProtectionService::GetCachedVerdict( |
| const GURL& url, |
| LoginReputationClientRequest::TriggerType trigger_type, |
| ReusedPasswordType password_type, |
| LoginReputationClientResponse* out_response) { |
| DCHECK(trigger_type == LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE || |
| trigger_type == LoginReputationClientRequest::PASSWORD_REUSE_EVENT); |
| |
| if (!url.is_valid()) |
| return LoginReputationClientResponse::VERDICT_TYPE_UNSPECIFIED; |
| |
| GURL hostname = GetHostNameWithHTTPScheme(url); |
| std::unique_ptr<base::DictionaryValue> cache_dictionary = |
| base::DictionaryValue::From(content_settings_->GetWebsiteSetting( |
| hostname, GURL(), CONTENT_SETTINGS_TYPE_PASSWORD_PROTECTION, |
| std::string(), nullptr)); |
| |
| if (!cache_dictionary || cache_dictionary->empty()) |
| return LoginReputationClientResponse::VERDICT_TYPE_UNSPECIFIED; |
| |
| base::Value* verdict_dictionary = nullptr; |
| if (trigger_type == LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE) { |
| // All UNFAMILIAR_LOGIN_PAGE verdicts (a.k.a password on focus ping) |
| // are cached under |kPasswordOnFocusCacheKey|. |
| verdict_dictionary = cache_dictionary->FindKey(kPasswordOnFocusCacheKey); |
| if (!verdict_dictionary) |
| return LoginReputationClientResponse::VERDICT_TYPE_UNSPECIFIED; |
| } else { |
| verdict_dictionary = |
| cache_dictionary->FindKey(base::NumberToString(password_type)); |
| if (!verdict_dictionary) |
| return LoginReputationClientResponse::VERDICT_TYPE_UNSPECIFIED; |
| } |
| |
| std::vector<std::string> paths; |
| GeneratePathVariantsWithoutQuery(url, &paths); |
| int max_path_depth = -1; |
| LoginReputationClientResponse::VerdictType most_matching_verdict = |
| LoginReputationClientResponse::VERDICT_TYPE_UNSPECIFIED; |
| // For all the verdicts of the same origin, we key them by |cache_expression|. |
| // Its corresponding value is a DictionaryValue contains its creation time and |
| // the serialized verdict proto. |
| for (const auto& item : verdict_dictionary->DictItems()) { |
| int verdict_received_time; |
| LoginReputationClientResponse verdict; |
| // Ignore any entry that we cannot parse. These invalid entries will be |
| // cleaned up during shutdown. |
| if (!ParseVerdictEntry(&item.second, &verdict_received_time, &verdict)) |
| continue; |
| // Since password protection content settings are keyed by origin, we only |
| // need to compare the path part of the cache_expression and the given url. |
| std::string cache_expression_path = |
| GetCacheExpressionPath(verdict.cache_expression()); |
| |
| // Finds the most specific match. |
| int path_depth = static_cast<int>(GetPathDepth(cache_expression_path)); |
| if (path_depth > max_path_depth && |
| PathVariantsMatchCacheExpression(paths, cache_expression_path)) { |
| max_path_depth = path_depth; |
| // If the most matching verdict is expired, set the result to |
| // VERDICT_TYPE_UNSPECIFIED. |
| most_matching_verdict = |
| IsCacheExpired(verdict_received_time, verdict.cache_duration_sec()) |
| ? LoginReputationClientResponse::VERDICT_TYPE_UNSPECIFIED |
| : verdict.verdict_type(); |
| out_response->CopyFrom(verdict); |
| } |
| } |
| return most_matching_verdict; |
| } |
| |
| void PasswordProtectionService::CacheVerdict( |
| const GURL& url, |
| LoginReputationClientRequest::TriggerType trigger_type, |
| ReusedPasswordType password_type, |
| LoginReputationClientResponse* verdict, |
| const base::Time& receive_time) { |
| DCHECK(verdict); |
| DCHECK(content_settings_); |
| DCHECK(trigger_type == LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE || |
| trigger_type == LoginReputationClientRequest::PASSWORD_REUSE_EVENT); |
| |
| GURL hostname = GetHostNameWithHTTPScheme(url); |
| int* stored_verdict_count = |
| trigger_type == LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE |
| ? &stored_verdict_count_password_on_focus_ |
| : &stored_verdict_count_password_entry_; |
| std::unique_ptr<base::DictionaryValue> cache_dictionary = |
| base::DictionaryValue::From(content_settings_->GetWebsiteSetting( |
| hostname, GURL(), CONTENT_SETTINGS_TYPE_PASSWORD_PROTECTION, |
| std::string(), nullptr)); |
| |
| if (!cache_dictionary || !cache_dictionary) |
| cache_dictionary = std::make_unique<base::DictionaryValue>(); |
| |
| std::unique_ptr<base::DictionaryValue> verdict_entry( |
| CreateDictionaryFromVerdict(verdict, receive_time)); |
| |
| base::Value* verdict_dictionary = nullptr; |
| if (trigger_type == LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE) { |
| // All UNFAMILIAR_LOGIN_PAGE verdicts (a.k.a password on focus ping) |
| // are cached under |kPasswordOnFocusCacheKey|. |
| verdict_dictionary = cache_dictionary->FindKeyOfType( |
| kPasswordOnFocusCacheKey, base::Value::Type::DICTIONARY); |
| if (!verdict_dictionary) { |
| verdict_dictionary = cache_dictionary->SetKey( |
| kPasswordOnFocusCacheKey, base::Value(base::Value::Type::DICTIONARY)); |
| } |
| } else { |
| std::string password_type_key = base::NumberToString(password_type); |
| verdict_dictionary = cache_dictionary->FindKeyOfType( |
| password_type_key, base::Value::Type::DICTIONARY); |
| if (!verdict_dictionary) { |
| verdict_dictionary = cache_dictionary->SetKey( |
| password_type_key, base::Value(base::Value::Type::DICTIONARY)); |
| } |
| } |
| |
| // Increases stored verdict count if we haven't seen this cache expression |
| // before. |
| if (!verdict_dictionary->FindKey(verdict->cache_expression())) |
| *stored_verdict_count = GetStoredVerdictCount(trigger_type) + 1; |
| |
| // If same cache_expression is already in this verdict_dictionary, we simply |
| // override it. |
| verdict_dictionary->SetKey( |
| verdict->cache_expression(), |
| base::Value::FromUniquePtrValue(std::move(verdict_entry))); |
| content_settings_->SetWebsiteSettingDefaultScope( |
| hostname, GURL(), CONTENT_SETTINGS_TYPE_PASSWORD_PROTECTION, |
| std::string(), std::move(cache_dictionary)); |
| } |
| |
| void PasswordProtectionService::CleanUpExpiredVerdicts() { |
| DCHECK(content_settings_); |
| |
| if (GetStoredVerdictCount( |
| LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE) <= 0 && |
| GetStoredVerdictCount( |
| LoginReputationClientRequest::PASSWORD_REUSE_EVENT) <= 0) |
| return; |
| |
| ContentSettingsForOneType password_protection_settings; |
| content_settings_->GetSettingsForOneType( |
| CONTENT_SETTINGS_TYPE_PASSWORD_PROTECTION, std::string(), |
| &password_protection_settings); |
| |
| for (const ContentSettingPatternSource& source : |
| password_protection_settings) { |
| GURL primary_pattern_url = GURL(source.primary_pattern.ToString()); |
| // Find all verdicts associated with this origin. |
| std::unique_ptr<base::DictionaryValue> cache_dictionary = |
| base::DictionaryValue::From(content_settings_->GetWebsiteSetting( |
| primary_pattern_url, GURL(), |
| CONTENT_SETTINGS_TYPE_PASSWORD_PROTECTION, std::string(), nullptr)); |
| bool has_expired_password_on_focus_entry = RemoveExpiredVerdicts( |
| LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE, |
| cache_dictionary.get()); |
| bool has_expired_password_reuse_entry = RemoveExpiredVerdicts( |
| LoginReputationClientRequest::PASSWORD_REUSE_EVENT, |
| cache_dictionary.get()); |
| |
| if (cache_dictionary->size() == 0u) { |
| content_settings_->ClearSettingsForOneTypeWithPredicate( |
| CONTENT_SETTINGS_TYPE_PASSWORD_PROTECTION, base::Time(), |
| base::Time::Max(), |
| base::BindRepeating(&OriginMatchPrimaryPattern, primary_pattern_url)); |
| } else if (has_expired_password_on_focus_entry || |
| has_expired_password_reuse_entry) { |
| // Set the website setting of this origin with the updated |
| // |cache_dictionary|. |
| content_settings_->SetWebsiteSettingDefaultScope( |
| primary_pattern_url, GURL(), |
| CONTENT_SETTINGS_TYPE_PASSWORD_PROTECTION, std::string(), |
| std::move(cache_dictionary)); |
| } |
| } |
| } |
| |
| void PasswordProtectionService::StartRequest( |
| WebContents* web_contents, |
| const GURL& main_frame_url, |
| const GURL& password_form_action, |
| const GURL& password_form_frame_url, |
| ReusedPasswordType reused_password_type, |
| const std::vector<std::string>& matching_domains, |
| LoginReputationClientRequest::TriggerType trigger_type, |
| bool password_field_exists) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| scoped_refptr<PasswordProtectionRequest> request( |
| new PasswordProtectionRequest( |
| web_contents, main_frame_url, password_form_action, |
| password_form_frame_url, reused_password_type, matching_domains, |
| trigger_type, password_field_exists, this, GetRequestTimeoutInMS())); |
| |
| request->Start(); |
| pending_requests_.insert(std::move(request)); |
| } |
| |
| void PasswordProtectionService::MaybeStartPasswordFieldOnFocusRequest( |
| WebContents* web_contents, |
| const GURL& main_frame_url, |
| const GURL& password_form_action, |
| const GURL& password_form_frame_url) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| RequestOutcome reason; |
| if (CanSendPing(LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE, |
| main_frame_url, |
| PasswordReuseEvent::REUSED_PASSWORD_TYPE_UNKNOWN, &reason)) { |
| StartRequest(web_contents, main_frame_url, password_form_action, |
| password_form_frame_url, |
| PasswordReuseEvent::REUSED_PASSWORD_TYPE_UNKNOWN, |
| {}, /* matching_domains: not used for this type */ |
| LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE, true); |
| } |
| } |
| |
| void PasswordProtectionService::MaybeStartProtectedPasswordEntryRequest( |
| WebContents* web_contents, |
| const GURL& main_frame_url, |
| ReusedPasswordType reused_password_type, |
| const std::vector<std::string>& matching_domains, |
| bool password_field_exists) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| if (!IsSupportedPasswordTypeForPinging(reused_password_type)) |
| return; |
| |
| RequestOutcome reason; |
| if (CanSendPing(LoginReputationClientRequest::PASSWORD_REUSE_EVENT, |
| main_frame_url, reused_password_type, &reason)) { |
| StartRequest(web_contents, main_frame_url, GURL(), GURL(), |
| reused_password_type, matching_domains, |
| LoginReputationClientRequest::PASSWORD_REUSE_EVENT, |
| password_field_exists); |
| } else { |
| MaybeLogPasswordReuseLookupEvent(web_contents, reason, nullptr); |
| if (CanShowInterstitial(reason, reused_password_type, main_frame_url)) |
| ShowInterstitial(web_contents, reused_password_type); |
| } |
| } |
| |
| bool PasswordProtectionService::CanSendPing( |
| LoginReputationClientRequest::TriggerType trigger_type, |
| const GURL& main_frame_url, |
| ReusedPasswordType password_type, |
| RequestOutcome* reason) { |
| *reason = RequestOutcome::URL_NOT_VALID_FOR_REPUTATION_COMPUTING; |
| if (IsPingingEnabled(trigger_type, reason) && |
| !IsURLWhitelistedForPasswordEntry(main_frame_url, reason) && |
| CanGetReputationOfURL(main_frame_url)) { |
| return true; |
| } |
| LogNoPingingReason(trigger_type, *reason, password_type, |
| GetSyncAccountType()); |
| return false; |
| } |
| |
| void PasswordProtectionService::RequestFinished( |
| PasswordProtectionRequest* request, |
| bool already_cached, |
| std::unique_ptr<LoginReputationClientResponse> response) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| DCHECK(request); |
| |
| if (response) { |
| if (!already_cached) { |
| CacheVerdict(request->main_frame_url(), request->trigger_type(), |
| request->reused_password_type(), response.get(), |
| base::Time::Now()); |
| } |
| if (ShouldShowModalWarning(request->trigger_type(), |
| request->reused_password_type(), |
| response->verdict_type())) { |
| ShowModalWarning(request->web_contents(), response->verdict_token(), |
| request->reused_password_type()); |
| request->set_is_modal_warning_showing(true); |
| } |
| } |
| |
| request->HandleDeferredNavigations(); |
| |
| // Remove request from |pending_requests_| list. If it triggers warning, add |
| // it into the !warning_reqeusts_| list. |
| for (auto it = pending_requests_.begin(); it != pending_requests_.end(); |
| it++) { |
| if (it->get() == request) { |
| if (request->is_modal_warning_showing()) |
| warning_requests_.insert(std::move(request)); |
| pending_requests_.erase(it); |
| break; |
| } |
| } |
| } |
| |
| void PasswordProtectionService::CancelPendingRequests() { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| for (auto it = pending_requests_.begin(); it != pending_requests_.end();) { |
| PasswordProtectionRequest* request = it->get(); |
| // These are the requests for whom we're still waiting for verdicts. |
| // We need to advance the iterator before we cancel because canceling |
| // the request will invalidate it when RequestFinished is called. |
| it++; |
| request->Cancel(false); |
| } |
| DCHECK(pending_requests_.empty()); |
| } |
| |
| scoped_refptr<SafeBrowsingDatabaseManager> |
| PasswordProtectionService::database_manager() { |
| return database_manager_; |
| } |
| |
| GURL PasswordProtectionService::GetPasswordProtectionRequestUrl() { |
| GURL url(kPasswordProtectionRequestUrl); |
| std::string api_key = google_apis::GetAPIKey(); |
| DCHECK(!api_key.empty()); |
| return url.Resolve("?key=" + net::EscapeQueryParamValue(api_key, true)); |
| } |
| |
| int PasswordProtectionService::GetStoredVerdictCount( |
| LoginReputationClientRequest::TriggerType trigger_type) { |
| DCHECK(content_settings_); |
| DCHECK(trigger_type == LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE || |
| trigger_type == LoginReputationClientRequest::PASSWORD_REUSE_EVENT); |
| int* stored_verdict_count = |
| trigger_type == LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE |
| ? &stored_verdict_count_password_on_focus_ |
| : &stored_verdict_count_password_entry_; |
| // If we have already computed this, return its value. |
| if (*stored_verdict_count >= 0) |
| return *stored_verdict_count; |
| |
| ContentSettingsForOneType password_protection_settings; |
| content_settings_->GetSettingsForOneType( |
| CONTENT_SETTINGS_TYPE_PASSWORD_PROTECTION, std::string(), |
| &password_protection_settings); |
| stored_verdict_count_password_on_focus_ = 0; |
| stored_verdict_count_password_entry_ = 0; |
| if (password_protection_settings.empty()) |
| return 0; |
| |
| for (const ContentSettingPatternSource& source : |
| password_protection_settings) { |
| std::unique_ptr<base::DictionaryValue> cache_dictionary = |
| base::DictionaryValue::From(content_settings_->GetWebsiteSetting( |
| GURL(source.primary_pattern.ToString()), GURL(), |
| CONTENT_SETTINGS_TYPE_PASSWORD_PROTECTION, std::string(), nullptr)); |
| if (cache_dictionary.get() && !cache_dictionary->empty()) { |
| for (const auto& item : cache_dictionary->DictItems()) { |
| if (item.first == base::StringPiece(kPasswordOnFocusCacheKey)) { |
| stored_verdict_count_password_on_focus_ += item.second.DictSize(); |
| } else { |
| stored_verdict_count_password_entry_ += item.second.DictSize(); |
| } |
| } |
| } |
| } |
| return *stored_verdict_count; |
| } |
| |
| int PasswordProtectionService::GetRequestTimeoutInMS() { |
| return kRequestTimeoutMs; |
| } |
| |
| void PasswordProtectionService::FillUserPopulation( |
| LoginReputationClientRequest::TriggerType trigger_type, |
| LoginReputationClientRequest* request_proto) { |
| ChromeUserPopulation* user_population = request_proto->mutable_population(); |
| user_population->set_user_population( |
| IsExtendedReporting() ? ChromeUserPopulation::EXTENDED_REPORTING |
| : ChromeUserPopulation::SAFE_BROWSING); |
| user_population->set_profile_management_status( |
| GetProfileManagementStatus(GetBrowserPolicyConnector())); |
| user_population->set_is_history_sync_enabled(IsHistorySyncEnabled()); |
| } |
| |
| void PasswordProtectionService::OnURLsDeleted( |
| history::HistoryService* history_service, |
| const history::DeletionInfo& deletion_info) { |
| BrowserThread::PostTask( |
| BrowserThread::UI, FROM_HERE, |
| base::BindRepeating( |
| &PasswordProtectionService::RemoveContentSettingsOnURLsDeleted, |
| GetWeakPtr(), deletion_info.IsAllHistory(), |
| deletion_info.deleted_rows())); |
| BrowserThread::PostTask( |
| BrowserThread::UI, FROM_HERE, |
| base::BindRepeating(&PasswordProtectionService:: |
| RemoveUnhandledSyncPasswordReuseOnURLsDeleted, |
| GetWeakPtr(), deletion_info.IsAllHistory(), |
| deletion_info.deleted_rows())); |
| } |
| |
| void PasswordProtectionService::HistoryServiceBeingDeleted( |
| history::HistoryService* history_service) { |
| history_service_observer_.RemoveAll(); |
| } |
| |
| void PasswordProtectionService::RemoveContentSettingsOnURLsDeleted( |
| bool all_history, |
| const history::URLRows& deleted_rows) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| DCHECK(content_settings_); |
| |
| if (all_history) { |
| content_settings_->ClearSettingsForOneType( |
| CONTENT_SETTINGS_TYPE_PASSWORD_PROTECTION); |
| stored_verdict_count_password_on_focus_ = 0; |
| stored_verdict_count_password_entry_ = 0; |
| return; |
| } |
| |
| // For now, if a URL is deleted from history, we simply remove all the |
| // cached verdicts of the same origin. This is a pretty aggressive deletion. |
| // We might revisit this logic later to decide if we want to only delete the |
| // cached verdict whose cache expression matches this URL. |
| for (const history::URLRow& row : deleted_rows) { |
| if (!row.url().SchemeIsHTTPOrHTTPS()) |
| continue; |
| |
| GURL url_key = GetHostNameWithHTTPScheme(row.url()); |
| stored_verdict_count_password_on_focus_ = |
| GetStoredVerdictCount( |
| LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE) - |
| GetVerdictCountForURL( |
| url_key, LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE); |
| stored_verdict_count_password_entry_ = |
| GetStoredVerdictCount( |
| LoginReputationClientRequest::PASSWORD_REUSE_EVENT) - |
| GetVerdictCountForURL( |
| url_key, LoginReputationClientRequest::PASSWORD_REUSE_EVENT); |
| content_settings_->ClearSettingsForOneTypeWithPredicate( |
| CONTENT_SETTINGS_TYPE_PASSWORD_PROTECTION, base::Time(), |
| base::Time::Max(), |
| base::BindRepeating(&OriginMatchPrimaryPattern, url_key)); |
| } |
| } |
| |
| int PasswordProtectionService::GetVerdictCountForURL( |
| const GURL& url, |
| LoginReputationClientRequest::TriggerType trigger_type) { |
| DCHECK(trigger_type == LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE || |
| trigger_type == LoginReputationClientRequest::PASSWORD_REUSE_EVENT); |
| std::unique_ptr<base::DictionaryValue> cache_dictionary = |
| base::DictionaryValue::From(content_settings_->GetWebsiteSetting( |
| url, GURL(), CONTENT_SETTINGS_TYPE_PASSWORD_PROTECTION, std::string(), |
| nullptr)); |
| if (!cache_dictionary || cache_dictionary->empty()) |
| return 0; |
| |
| int verdict_cnt = 0; |
| if (trigger_type == LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE) { |
| base::Value* password_on_focus_dict = nullptr; |
| password_on_focus_dict = |
| cache_dictionary->FindKey(kPasswordOnFocusCacheKey); |
| verdict_cnt += |
| password_on_focus_dict ? password_on_focus_dict->DictSize() : 0; |
| } else { |
| for (const auto& item : cache_dictionary->DictItems()) { |
| if (item.first == kPasswordOnFocusCacheKey) |
| continue; |
| verdict_cnt += item.second.DictSize(); |
| } |
| } |
| return verdict_cnt; |
| } |
| |
| bool PasswordProtectionService::RemoveExpiredVerdicts( |
| LoginReputationClientRequest::TriggerType trigger_type, |
| base::DictionaryValue* cache_dictionary) { |
| DCHECK(trigger_type == LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE || |
| trigger_type == LoginReputationClientRequest::PASSWORD_REUSE_EVENT); |
| if (!cache_dictionary || cache_dictionary->empty()) |
| return false; |
| |
| size_t verdicts_removed = 0; |
| std::vector<std::string> empty_keys; |
| for (auto item : cache_dictionary->DictItems()) { |
| if (trigger_type == LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE && |
| item.first == std::string(kPasswordOnFocusCacheKey)) { |
| size_t removed_cnt = RemoveExpiredEntries(&item.second); |
| verdicts_removed += removed_cnt; |
| stored_verdict_count_password_on_focus_ -= removed_cnt; |
| } else { |
| size_t removed_cnt = RemoveExpiredEntries(&item.second); |
| verdicts_removed += removed_cnt; |
| stored_verdict_count_password_entry_ -= removed_cnt; |
| } |
| |
| if (item.second.DictSize() == 0U) |
| empty_keys.push_back(item.first); |
| } |
| for (const auto& key : empty_keys) |
| cache_dictionary->RemoveKey(key); |
| |
| return verdicts_removed > 0U; |
| } |
| |
| size_t PasswordProtectionService::RemoveExpiredEntries( |
| base::Value* verdict_dictionary) { |
| std::vector<std::string> expired_keys; |
| for (const auto& item : verdict_dictionary->DictItems()) { |
| int verdict_received_time; |
| LoginReputationClientResponse verdict; |
| if (!PasswordProtectionService::ParseVerdictEntry( |
| &item.second, &verdict_received_time, &verdict) || |
| PasswordProtectionService::IsCacheExpired( |
| verdict_received_time, verdict.cache_duration_sec())) { |
| expired_keys.push_back(item.first); |
| } |
| } |
| |
| for (const std::string& key : expired_keys) |
| verdict_dictionary->RemoveKey(key); |
| |
| return expired_keys.size(); |
| } |
| |
| // static |
| bool PasswordProtectionService::ParseVerdictEntry( |
| base::Value* verdict_entry, |
| int* out_verdict_received_time, |
| LoginReputationClientResponse* out_verdict) { |
| std::string serialized_verdict_proto; |
| if (!verdict_entry || !out_verdict) |
| return false; |
| base::Value* cache_creation_time_value = |
| verdict_entry->FindKey(kCacheCreationTime); |
| |
| if (!cache_creation_time_value || !cache_creation_time_value->is_int()) |
| return false; |
| *out_verdict_received_time = cache_creation_time_value->GetInt(); |
| |
| base::Value* verdict_proto_value = verdict_entry->FindKey(kVerdictProto); |
| if (!verdict_proto_value || !verdict_proto_value->is_string()) |
| return false; |
| serialized_verdict_proto = verdict_proto_value->GetString(); |
| |
| return base::Base64Decode(serialized_verdict_proto, |
| &serialized_verdict_proto) && |
| out_verdict->ParseFromString(serialized_verdict_proto); |
| } |
| |
| bool PasswordProtectionService::PathVariantsMatchCacheExpression( |
| const std::vector<std::string>& generated_paths, |
| const std::string& cache_expression_path) { |
| return base::ContainsValue(generated_paths, cache_expression_path); |
| } |
| |
| bool PasswordProtectionService::IsCacheExpired(int cache_creation_time, |
| int cache_duration) { |
| // TODO(jialiul): For now, we assume client's clock is accurate or almost |
| // accurate. Need some logic to handle cases where client's clock is way |
| // off. |
| return base::Time::Now().ToDoubleT() > |
| static_cast<double>(cache_creation_time + cache_duration); |
| } |
| |
| // Generate path variants of the given URL. |
| void PasswordProtectionService::GeneratePathVariantsWithoutQuery( |
| const GURL& url, |
| std::vector<std::string>* paths) { |
| std::string canonical_path; |
| V4ProtocolManagerUtil::CanonicalizeUrl(url, nullptr, &canonical_path, |
| nullptr); |
| V4ProtocolManagerUtil::GeneratePathVariantsToCheck(canonical_path, |
| std::string(), paths); |
| } |
| |
| // Return the path of the cache expression. e.g.: |
| // "www.google.com" -> "" |
| // "www.google.com/abc" -> "/abc" |
| // "foo.com/foo/bar/" -> "/foo/bar/" |
| std::string PasswordProtectionService::GetCacheExpressionPath( |
| const std::string& cache_expression) { |
| DCHECK(!cache_expression.empty()); |
| size_t first_slash_pos = cache_expression.find_first_of("/"); |
| if (first_slash_pos == std::string::npos) |
| return ""; |
| return cache_expression.substr(first_slash_pos); |
| } |
| |
| // Convert a LoginReputationClientResponse proto into a DictionaryValue. |
| std::unique_ptr<base::DictionaryValue> |
| PasswordProtectionService::CreateDictionaryFromVerdict( |
| const LoginReputationClientResponse* verdict, |
| const base::Time& receive_time) { |
| std::unique_ptr<base::DictionaryValue> result = |
| std::make_unique<base::DictionaryValue>(); |
| result->SetInteger(kCacheCreationTime, |
| static_cast<int>(receive_time.ToDoubleT())); |
| std::string serialized_proto(verdict->SerializeAsString()); |
| // Performs a base64 encoding on the serialized proto. |
| base::Base64Encode(serialized_proto, &serialized_proto); |
| result->SetString(kVerdictProto, serialized_proto); |
| return result; |
| } |
| |
| std::unique_ptr<PasswordProtectionNavigationThrottle> |
| PasswordProtectionService::MaybeCreateNavigationThrottle( |
| content::NavigationHandle* navigation_handle) { |
| if (!navigation_handle->IsRendererInitiated()) |
| return nullptr; |
| |
| content::WebContents* web_contents = navigation_handle->GetWebContents(); |
| for (scoped_refptr<PasswordProtectionRequest> request : pending_requests_) { |
| if (request->web_contents() == web_contents && |
| request->trigger_type() == |
| safe_browsing::LoginReputationClientRequest::PASSWORD_REUSE_EVENT && |
| IsSupportedPasswordTypeForModalWarning( |
| request->reused_password_type())) { |
| return std::make_unique<PasswordProtectionNavigationThrottle>( |
| navigation_handle, request, /*is_warning_showing=*/false); |
| } |
| } |
| |
| for (scoped_refptr<PasswordProtectionRequest> request : warning_requests_) { |
| if (request->web_contents() == web_contents) { |
| return std::make_unique<PasswordProtectionNavigationThrottle>( |
| navigation_handle, request, /*is_warning_showing=*/true); |
| } |
| } |
| return nullptr; |
| } |
| |
| void PasswordProtectionService::RemoveWarningRequestsByWebContents( |
| content::WebContents* web_contents) { |
| for (auto it = warning_requests_.begin(); it != warning_requests_.end();) { |
| if (it->get()->web_contents() == web_contents) |
| it = warning_requests_.erase(it); |
| else |
| ++it; |
| } |
| } |
| |
| bool PasswordProtectionService::IsModalWarningShowingInWebContents( |
| content::WebContents* web_contents) { |
| for (const auto& request : warning_requests_) { |
| if (request->web_contents() == web_contents) |
| return true; |
| } |
| return false; |
| } |
| |
| bool PasswordProtectionService::IsWarningEnabled() { |
| return GetPasswordProtectionWarningTriggerPref() == PHISHING_REUSE; |
| } |
| |
| bool PasswordProtectionService::IsEventLoggingEnabled() { |
| return GetSyncAccountType() != PasswordReuseEvent::NOT_SIGNED_IN; |
| } |
| |
| // static |
| ReusedPasswordType |
| PasswordProtectionService::GetPasswordProtectionReusedPasswordType( |
| password_manager::metrics_util::PasswordType password_type) { |
| switch (password_type) { |
| case PasswordType::SAVED_PASSWORD: |
| return PasswordReuseEvent::SAVED_PASSWORD; |
| case PasswordType::SYNC_PASSWORD: |
| return PasswordReuseEvent::SIGN_IN_PASSWORD; |
| case PasswordType::OTHER_GAIA_PASSWORD: |
| return PasswordReuseEvent::OTHER_GAIA_PASSWORD; |
| case PasswordType::ENTERPRISE_PASSWORD: |
| return PasswordReuseEvent::ENTERPRISE_PASSWORD; |
| case PasswordType::PASSWORD_TYPE_COUNT: |
| break; |
| } |
| NOTREACHED(); |
| return PasswordReuseEvent::REUSED_PASSWORD_TYPE_UNKNOWN; |
| } |
| |
| bool PasswordProtectionService::IsSupportedPasswordTypeForPinging( |
| ReusedPasswordType reused_password_type) const { |
| switch (reused_password_type) { |
| case PasswordReuseEvent::SAVED_PASSWORD: |
| return true; |
| case PasswordReuseEvent::SIGN_IN_PASSWORD: |
| return GetSyncAccountType() != PasswordReuseEvent::NOT_SIGNED_IN; |
| case PasswordReuseEvent::OTHER_GAIA_PASSWORD: |
| return false; |
| case PasswordReuseEvent::ENTERPRISE_PASSWORD: |
| return base::FeatureList::IsEnabled(kEnterprisePasswordProtectionV1); |
| case PasswordReuseEvent::REUSED_PASSWORD_TYPE_UNKNOWN: |
| break; |
| } |
| NOTREACHED(); |
| return false; |
| } |
| |
| bool PasswordProtectionService::IsSupportedPasswordTypeForModalWarning( |
| ReusedPasswordType reused_password_type) const { |
| return reused_password_type == PasswordReuseEvent::SIGN_IN_PASSWORD || |
| reused_password_type == PasswordReuseEvent::ENTERPRISE_PASSWORD; |
| } |
| |
| void PasswordProtectionService::MigrateCachedVerdicts() { |
| // |content_settings_| can be null in tests. |
| if (!content_settings_) |
| return; |
| |
| ContentSettingsForOneType password_protection_settings; |
| content_settings_->GetSettingsForOneType( |
| CONTENT_SETTINGS_TYPE_PASSWORD_PROTECTION, std::string(), |
| &password_protection_settings); |
| |
| size_t verdicts_migrated = 0; |
| for (const ContentSettingPatternSource& source : |
| password_protection_settings) { |
| GURL primary_pattern_url = GURL(source.primary_pattern.ToString()); |
| // Find all verdicts associated with this origin. |
| std::unique_ptr<base::DictionaryValue> cache_dictionary = |
| base::DictionaryValue::From(content_settings_->GetWebsiteSetting( |
| primary_pattern_url, GURL(), |
| CONTENT_SETTINGS_TYPE_PASSWORD_PROTECTION, std::string(), nullptr)); |
| |
| std::vector<std::string> removed_keys; |
| for (const auto& item : cache_dictionary->DictItems()) { |
| int password_type_int = -1; |
| if (item.first == kPasswordOnFocusCacheKey || |
| (base::StringToInt(item.first, &password_type_int) && |
| password_type_int >= PasswordReuseEvent::ReusedPasswordType_MIN && |
| password_type_int <= PasswordReuseEvent::ReusedPasswordType_MAX)) { |
| continue; |
| } |
| // Removes value if its key is not kPasswordOnFocusCacheKey or a valid |
| // reused password type. |
| removed_keys.push_back(item.first); |
| } |
| |
| verdicts_migrated += removed_keys.size(); |
| for (const std::string& key : removed_keys) |
| cache_dictionary->RemoveKey(key); |
| |
| if (cache_dictionary->size() == 0u) { |
| content_settings_->ClearSettingsForOneTypeWithPredicate( |
| CONTENT_SETTINGS_TYPE_PASSWORD_PROTECTION, base::Time(), |
| base::Time::Max(), |
| base::BindRepeating(&OriginMatchPrimaryPattern, primary_pattern_url)); |
| } else { |
| // Set the website setting of this origin with the updated |
| // |cache_dictionary|. |
| content_settings_->SetWebsiteSettingDefaultScope( |
| primary_pattern_url, GURL(), |
| CONTENT_SETTINGS_TYPE_PASSWORD_PROTECTION, std::string(), |
| std::move(cache_dictionary)); |
| } |
| } |
| LogNumberOfVerdictMigrated(verdicts_migrated); |
| } |
| |
| } // namespace safe_browsing |