| // 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 <string> |
| |
| #include "base/base64.h" |
| #include "base/bind.h" |
| #include "base/callback.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/metrics/field_trial.h" |
| #include "base/metrics/field_trial_params.h" |
| #include "base/metrics/histogram_macros.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/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; |
| |
| namespace safe_browsing { |
| |
| 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 |
| |
| const char kPasswordOnFocusRequestOutcomeHistogram[] = |
| "PasswordProtection.RequestOutcome.PasswordFieldOnFocus"; |
| // Matches sync and/or saved password |
| const char kAnyPasswordEntryRequestOutcomeHistogram[] = |
| "PasswordProtection.RequestOutcome.AnyPasswordEntry"; |
| // Matches sync and maybe also saved password |
| const char kSyncPasswordEntryRequestOutcomeHistogram[] = |
| "PasswordProtection.RequestOutcome.SyncPasswordEntry"; |
| // Matches saved but NOT sync password |
| const char kProtectedPasswordEntryRequestOutcomeHistogram[] = |
| "PasswordProtection.RequestOutcome.ProtectedPasswordEntry"; |
| const char kSyncPasswordWarningDialogHistogram[] = |
| "PasswordProtection.ModalWarningDialogAction.SyncPasswordEntry"; |
| const char kSyncPasswordPageInfoHistogram[] = |
| "PasswordProtection.PageInfoAction.SyncPasswordEntry"; |
| const char kSyncPasswordChromeSettingsHistogram[] = |
| "PasswordProtection.ChromeSettingsAction.SyncPasswordEntry"; |
| |
| PasswordProtectionService::PasswordProtectionService( |
| const scoped_refptr<SafeBrowsingDatabaseManager>& database_manager, |
| scoped_refptr<net::URLRequestContextGetter> request_context_getter, |
| 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), |
| request_context_getter_(request_context_getter), |
| 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); |
| } |
| |
| PasswordProtectionService::~PasswordProtectionService() { |
| tracker_.TryCancelAll(); |
| CancelPendingRequests(); |
| history_service_observer_.RemoveAll(); |
| weak_factory_.InvalidateWeakPtrs(); |
| } |
| |
| bool PasswordProtectionService::CanGetReputationOfURL(const GURL& url) { |
| if (!url.is_valid() || !url.SchemeIsHTTPOrHTTPS()) |
| return false; |
| |
| const std::string hostname = url.HostNoBrackets(); |
| return !net::IsLocalhost(hostname) && !net::IsHostnameNonUnique(hostname) && |
| hostname.find('.') != std::string::npos; |
| } |
| |
| void PasswordProtectionService::RecordWarningAction(WarningUIType ui_type, |
| WarningAction action) { |
| switch (ui_type) { |
| case PAGE_INFO: |
| UMA_HISTOGRAM_ENUMERATION(kSyncPasswordPageInfoHistogram, action, |
| MAX_ACTION); |
| break; |
| case MODAL_DIALOG: |
| UMA_HISTOGRAM_ENUMERATION(kSyncPasswordWarningDialogHistogram, action, |
| MAX_ACTION); |
| break; |
| case CHROME_SETTINGS: |
| UMA_HISTOGRAM_ENUMERATION(kSyncPasswordChromeSettingsHistogram, action, |
| MAX_ACTION); |
| break; |
| case NOT_USED: |
| case MAX_UI_TYPE: |
| NOTREACHED(); |
| break; |
| } |
| } |
| |
| // static |
| bool PasswordProtectionService::ShouldShowModalWarning( |
| LoginReputationClientRequest::TriggerType trigger_type, |
| bool matches_sync_password, |
| LoginReputationClientRequest::PasswordReuseEvent::SyncAccountType |
| account_type, |
| LoginReputationClientResponse::VerdictType verdict_type) { |
| return base::FeatureList::IsEnabled(kGoogleBrandedPhishingWarning) && |
| trigger_type == LoginReputationClientRequest::PASSWORD_REUSE_EVENT && |
| matches_sync_password && |
| account_type == |
| LoginReputationClientRequest::PasswordReuseEvent::GMAIL && |
| (verdict_type == LoginReputationClientResponse::PHISHING || |
| (verdict_type == LoginReputationClientResponse::LOW_REPUTATION && |
| base::GetFieldTrialParamByFeatureAsBool( |
| kGoogleBrandedPhishingWarning, "warn_on_low_reputation", |
| false))); |
| } |
| |
| bool PasswordProtectionService::ShouldShowSofterWarning() { |
| return base::GetFieldTrialParamByFeatureAsBool(kGoogleBrandedPhishingWarning, |
| "softer_warning", false); |
| } |
| |
| // 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 UNFAMILIAR_LOGIN_PAGE verdict we needs two levels |
| // of keys: (1) origin, (2) cache expression returned in verdict. |
| // To cache a PASSWORD_REUSE_EVENT, 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, |
| 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.get() || cache_dictionary->empty()) |
| return LoginReputationClientResponse::VERDICT_TYPE_UNSPECIFIED; |
| |
| base::DictionaryValue* 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|. |
| if (!cache_dictionary->GetDictionaryWithoutPathExpansion( |
| base::StringPiece(kPasswordOnFocusCacheKey), &verdict_dictionary)) { |
| return LoginReputationClientResponse::VERDICT_TYPE_UNSPECIFIED; |
| } |
| } else { |
| verdict_dictionary = cache_dictionary.get(); |
| } |
| |
| 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 (base::DictionaryValue::Iterator it(*verdict_dictionary); !it.IsAtEnd(); |
| it.Advance()) { |
| if (trigger_type == LoginReputationClientRequest::PASSWORD_REUSE_EVENT && |
| it.key() == base::StringPiece(kPasswordOnFocusCacheKey)) { |
| continue; |
| } |
| base::DictionaryValue* verdict_entry = nullptr; |
| verdict_dictionary->GetDictionaryWithoutPathExpansion( |
| it.key() /* cache_expression */, &verdict_entry); |
| int verdict_received_time; |
| LoginReputationClientResponse verdict; |
| // Ignore any entry that we cannot parse. These invalid entries will be |
| // cleaned up during shutdown. |
| if (!ParseVerdictEntry(verdict_entry, &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, |
| 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.get()) |
| cache_dictionary = base::MakeUnique<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 { |
| verdict_dictionary = cache_dictionary.get(); |
| } |
| |
| // 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::Bind(&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 |
| // |verdict_diectionary|. |
| 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, |
| bool matches_sync_password, |
| 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, matches_sync_password, 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); |
| if (CanSendPing(LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE, |
| main_frame_url, false)) { |
| StartRequest(web_contents, main_frame_url, password_form_action, |
| password_form_frame_url, |
| false, /* matches_sync_password: not used for this type */ |
| {}, /* matching_domains: not used for this type */ |
| LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE, true); |
| } |
| } |
| |
| void PasswordProtectionService::MaybeStartProtectedPasswordEntryRequest( |
| WebContents* web_contents, |
| const GURL& main_frame_url, |
| bool matches_sync_password, |
| const std::vector<std::string>& matching_domains, |
| bool password_field_exists) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| if (CanSendPing(LoginReputationClientRequest::PASSWORD_REUSE_EVENT, |
| main_frame_url, matches_sync_password)) { |
| StartRequest(web_contents, main_frame_url, GURL(), GURL(), |
| matches_sync_password, matching_domains, |
| LoginReputationClientRequest::PASSWORD_REUSE_EVENT, |
| password_field_exists); |
| } |
| } |
| |
| bool PasswordProtectionService::CanSendPing( |
| LoginReputationClientRequest::TriggerType trigger_type, |
| const GURL& main_frame_url, |
| bool matches_sync_password) { |
| RequestOutcome request_outcome = URL_NOT_VALID_FOR_REPUTATION_COMPUTING; |
| if (IsPingingEnabled(trigger_type, &request_outcome) && |
| CanGetReputationOfURL(main_frame_url)) { |
| return true; |
| } |
| RecordNoPingingReason(trigger_type, request_outcome, matches_sync_password); |
| 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(), |
| response.get(), base::Time::Now()); |
| } |
| if (ShouldShowModalWarning( |
| request->trigger_type(), request->matches_sync_password(), |
| GetSyncAccountType(), response->verdict_type())) { |
| ShowModalWarning(request->web_contents(), response->verdict_token()); |
| 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()) { |
| stored_verdict_count_password_entry_ += |
| static_cast<int>(cache_dictionary->size()); |
| base::DictionaryValue* password_on_focus_dict = nullptr; |
| if (cache_dictionary->GetDictionaryWithoutPathExpansion( |
| base::StringPiece(kPasswordOnFocusCacheKey), |
| &password_on_focus_dict)) { |
| // Substracts 1 from password_entry count if |kPasswordOnFocusCacheKey| |
| // presents. |
| stored_verdict_count_password_entry_ -= 1; |
| stored_verdict_count_password_on_focus_ += |
| static_cast<int>(password_on_focus_dict->size()); |
| } |
| } |
| } |
| 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_is_history_sync_enabled(IsHistorySyncEnabled()); |
| } |
| |
| void PasswordProtectionService::OnURLsDeleted( |
| history::HistoryService* history_service, |
| bool all_history, |
| bool expired, |
| const history::URLRows& deleted_rows, |
| const std::set<GURL>& favicon_urls) { |
| BrowserThread::PostTask( |
| BrowserThread::UI, FROM_HERE, |
| base::Bind(&PasswordProtectionService::RemoveContentSettingsOnURLsDeleted, |
| GetWeakPtr(), all_history, deleted_rows)); |
| BrowserThread::PostTask( |
| BrowserThread::UI, FROM_HERE, |
| base::Bind(&PasswordProtectionService:: |
| RemoveUnhandledSyncPasswordReuseOnURLsDeleted, |
| GetWeakPtr(), all_history, 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::Bind(&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.get() || cache_dictionary->empty()) |
| return 0; |
| |
| if (trigger_type == LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE) { |
| base::DictionaryValue* password_on_focus_dict = nullptr; |
| return cache_dictionary->GetDictionaryWithoutPathExpansion( |
| base::StringPiece(kPasswordOnFocusCacheKey), |
| &password_on_focus_dict) |
| ? password_on_focus_dict->size() |
| : 0; |
| } else { |
| return cache_dictionary->HasKey(base::StringPiece(kPasswordOnFocusCacheKey)) |
| ? cache_dictionary->size() - 1 |
| : cache_dictionary->size(); |
| } |
| } |
| |
| bool PasswordProtectionService::RemoveExpiredVerdicts( |
| LoginReputationClientRequest::TriggerType trigger_type, |
| base::DictionaryValue* cache_dictionary) { |
| DCHECK(trigger_type == LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE || |
| trigger_type == LoginReputationClientRequest::PASSWORD_REUSE_EVENT); |
| base::DictionaryValue* verdict_dictionary = nullptr; |
| if (trigger_type == LoginReputationClientRequest::PASSWORD_REUSE_EVENT) { |
| verdict_dictionary = cache_dictionary; |
| } else { |
| if (!cache_dictionary->GetDictionaryWithoutPathExpansion( |
| base::StringPiece(kPasswordOnFocusCacheKey), &verdict_dictionary)) { |
| return false; |
| } |
| } |
| |
| if (!verdict_dictionary || verdict_dictionary->empty()) |
| return false; |
| |
| std::vector<std::string> expired_keys; |
| for (base::DictionaryValue::Iterator it(*verdict_dictionary); !it.IsAtEnd(); |
| it.Advance()) { |
| if (trigger_type == LoginReputationClientRequest::PASSWORD_REUSE_EVENT && |
| it.key() == std::string(kPasswordOnFocusCacheKey)) |
| continue; |
| |
| base::DictionaryValue* verdict_entry = nullptr; |
| verdict_dictionary->GetDictionaryWithoutPathExpansion(it.key(), |
| &verdict_entry); |
| int verdict_received_time; |
| LoginReputationClientResponse verdict; |
| |
| if (!ParseVerdictEntry(verdict_entry, &verdict_received_time, &verdict) || |
| IsCacheExpired(verdict_received_time, verdict.cache_duration_sec())) { |
| // Since DictionaryValue::Iterator cannot be used to modify the |
| // dictionary, we record the keys of expired verdicts in |expired_keys| |
| // and remove them in the next for-loop. |
| expired_keys.push_back(it.key()); |
| } |
| } |
| |
| for (const std::string& key : expired_keys) { |
| verdict_dictionary->RemoveWithoutPathExpansion(key, nullptr); |
| if (trigger_type == LoginReputationClientRequest::PASSWORD_REUSE_EVENT) |
| stored_verdict_count_password_entry_--; |
| else |
| stored_verdict_count_password_on_focus_--; |
| } |
| |
| if (trigger_type == LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE && |
| verdict_dictionary->size() == 0U) { |
| cache_dictionary->RemoveWithoutPathExpansion( |
| base::StringPiece(kPasswordOnFocusCacheKey), nullptr); |
| } |
| |
| return expired_keys.size() > 0U; |
| } |
| |
| // static |
| bool PasswordProtectionService::ParseVerdictEntry( |
| base::DictionaryValue* verdict_entry, |
| int* out_verdict_received_time, |
| LoginReputationClientResponse* out_verdict) { |
| std::string serialized_verdict_proto; |
| return verdict_entry && out_verdict && |
| verdict_entry->GetInteger(kCacheCreationTime, |
| out_verdict_received_time) && |
| verdict_entry->GetString(kVerdictProto, &serialized_verdict_proto) && |
| 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 std::find(generated_paths.begin(), generated_paths.end(), |
| cache_expression_path) != generated_paths.end(); |
| } |
| |
| 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 = |
| base::MakeUnique<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; |
| } |
| |
| void PasswordProtectionService::RecordNoPingingReason( |
| LoginReputationClientRequest::TriggerType trigger_type, |
| RequestOutcome reason, |
| bool matches_sync_password) { |
| DCHECK(trigger_type == LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE || |
| trigger_type == LoginReputationClientRequest::PASSWORD_REUSE_EVENT); |
| |
| if (trigger_type == LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE) { |
| UMA_HISTOGRAM_ENUMERATION(kPasswordOnFocusRequestOutcomeHistogram, reason, |
| MAX_OUTCOME); |
| return; |
| } |
| |
| LogPasswordEntryRequestOutcome(reason, matches_sync_password); |
| } |
| |
| // static |
| void PasswordProtectionService::LogPasswordEntryRequestOutcome( |
| RequestOutcome reason, |
| bool matches_sync_password) { |
| UMA_HISTOGRAM_ENUMERATION(kAnyPasswordEntryRequestOutcomeHistogram, reason, |
| MAX_OUTCOME); |
| if (matches_sync_password) { |
| UMA_HISTOGRAM_ENUMERATION(kSyncPasswordEntryRequestOutcomeHistogram, reason, |
| MAX_OUTCOME); |
| } else { |
| UMA_HISTOGRAM_ENUMERATION(kProtectedPasswordEntryRequestOutcomeHistogram, |
| reason, MAX_OUTCOME); |
| } |
| } |
| |
| 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 && |
| request->matches_sync_password()) { |
| return base::MakeUnique<PasswordProtectionNavigationThrottle>( |
| navigation_handle, request, /*is_warning_showing=*/false); |
| } |
| } |
| |
| for (scoped_refptr<PasswordProtectionRequest> request : warning_requests_) { |
| if (request->web_contents() == web_contents) { |
| return base::MakeUnique<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; |
| } |
| |
| } // namespace safe_browsing |