| // 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 "chrome/browser/safe_browsing/chrome_password_protection_service.h" |
| |
| #include <memory> |
| #include <string> |
| |
| #include "base/bind.h" |
| #include "base/containers/contains.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/metrics/user_metrics.h" |
| #include "base/metrics/user_metrics_action.h" |
| #include "base/rand_util.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_piece.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/content_settings/host_content_settings_map_factory.h" |
| #include "chrome/browser/extensions/api/safe_browsing_private/safe_browsing_private_event_router.h" |
| #include "chrome/browser/extensions/api/safe_browsing_private/safe_browsing_private_event_router_factory.h" |
| #include "chrome/browser/history/history_service_factory.h" |
| #include "chrome/browser/password_manager/account_password_store_factory.h" |
| #include "chrome/browser/password_manager/password_reuse_manager_factory.h" |
| #include "chrome/browser/policy/chrome_browser_policy_connector.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/safe_browsing/advanced_protection_status_manager.h" |
| #include "chrome/browser/safe_browsing/advanced_protection_status_manager_factory.h" |
| #include "chrome/browser/safe_browsing/chrome_user_population_helper.h" |
| #include "chrome/browser/safe_browsing/safe_browsing_metrics_collector_factory.h" |
| #include "chrome/browser/safe_browsing/safe_browsing_navigation_observer_manager_factory.h" |
| #include "chrome/browser/safe_browsing/safe_browsing_service.h" |
| #include "chrome/browser/safe_browsing/verdict_cache_manager_factory.h" |
| #include "chrome/browser/signin/identity_manager_factory.h" |
| #include "chrome/browser/sync/sync_service_factory.h" |
| #include "chrome/browser/sync/user_event_service_factory.h" |
| #include "chrome/common/pref_names.h" |
| #include "chrome/common/url_constants.h" |
| #include "components/content_settings/core/browser/host_content_settings_map.h" |
| #include "components/google/core/common/google_util.h" |
| #include "components/omnibox/common/omnibox_features.h" |
| #include "components/password_manager/core/browser/form_parsing/form_parser.h" |
| #include "components/password_manager/core/browser/insecure_credentials_helper.h" |
| #include "components/password_manager/core/browser/leak_detection_dialog_utils.h" |
| #include "components/password_manager/core/browser/ui/password_check_referrer.h" |
| #include "components/prefs/pref_change_registrar.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/prefs/scoped_user_pref_update.h" |
| #include "components/safe_browsing/content/browser/password_protection/password_protection_navigation_throttle.h" |
| #include "components/safe_browsing/content/browser/password_protection/password_protection_request_content.h" |
| #include "components/safe_browsing/content/browser/safe_browsing_navigation_observer_manager.h" |
| #include "components/safe_browsing/content/browser/triggers/trigger_throttler.h" |
| #include "components/safe_browsing/content/browser/ui_manager.h" |
| #include "components/safe_browsing/content/browser/web_ui/safe_browsing_ui.h" |
| #include "components/safe_browsing/core/browser/db/database_manager.h" |
| #include "components/safe_browsing/core/browser/realtime/policy_engine.h" |
| #include "components/safe_browsing/core/browser/safe_browsing_metrics_collector.h" |
| #include "components/safe_browsing/core/browser/sync/safe_browsing_primary_account_token_fetcher.h" |
| #include "components/safe_browsing/core/browser/verdict_cache_manager.h" |
| #include "components/safe_browsing/core/common/features.h" |
| #include "components/safe_browsing/core/common/proto/csd.pb.h" |
| #include "components/safe_browsing/core/common/safe_browsing_prefs.h" |
| #include "components/safe_browsing/core/common/safebrowsing_constants.h" |
| #include "components/safe_browsing/core/common/utils.h" |
| #include "components/security_interstitials/content/unsafe_resource_util.h" |
| #include "components/security_interstitials/core/unsafe_resource.h" |
| #include "components/signin/public/identity_manager/account_info.h" |
| #include "components/signin/public/identity_manager/consent_level.h" |
| #include "components/signin/public/identity_manager/identity_manager.h" |
| #include "components/strings/grit/components_strings.h" |
| #include "components/sync/driver/sync_service.h" |
| #include "components/sync/protocol/user_event_specifics.pb.h" |
| #include "components/sync_user_events/user_event_service.h" |
| #include "components/unified_consent/pref_names.h" |
| #include "components/variations/service/variations_service.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/global_routing_id.h" |
| #include "content/public/browser/navigation_entry.h" |
| #include "content/public/browser/navigation_handle.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/render_process_host.h" |
| #include "content/public/browser/storage_partition.h" |
| #include "content/public/browser/web_contents.h" |
| #include "google_apis/gaia/gaia_auth_util.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/gfx/geometry/size.h" |
| #include "url/gurl.h" |
| #include "url/url_util.h" |
| |
| #if BUILDFLAG(FULL_SAFE_BROWSING) |
| #include "chrome/browser/ui/browser_finder.h" |
| #include "chrome/browser/ui/chrome_pages.h" |
| #include "chrome/browser/ui/views/frame/browser_view.h" |
| #endif |
| |
| #if defined(OS_ANDROID) |
| #include "chrome/browser/password_manager/android/password_checkup_launcher_helper.h" |
| #include "chrome/browser/safe_browsing/android/password_reuse_controller_android.h" |
| #include "chrome/browser/safe_browsing/android/safe_browsing_referring_app_bridge_android.h" |
| #include "ui/android/window_android.h" |
| #else |
| #include "chrome/browser/ui/browser_list.h" |
| #endif |
| |
| using base::RecordAction; |
| using base::UserMetricsAction; |
| using content::BrowserThread; |
| using sync_pb::GaiaPasswordReuse; |
| using sync_pb::UserEventSpecifics; |
| using GaiaPasswordCaptured = UserEventSpecifics::GaiaPasswordCaptured; |
| using PasswordReuseDialogInteraction = |
| GaiaPasswordReuse::PasswordReuseDialogInteraction; |
| using PasswordReuseLookup = GaiaPasswordReuse::PasswordReuseLookup; |
| using PasswordReuseEvent = |
| safe_browsing::LoginReputationClientRequest::PasswordReuseEvent; |
| using SafeBrowsingStatus = |
| GaiaPasswordReuse::PasswordReuseDetected::SafeBrowsingStatus; |
| |
| namespace safe_browsing { |
| |
| using ReusedPasswordAccountType = |
| LoginReputationClientRequest::PasswordReuseEvent::ReusedPasswordAccountType; |
| |
| namespace { |
| |
| // The number of user gestures we trace back for login event attribution. |
| const int kPasswordEventAttributionUserGestureLimit = 2; |
| |
| // Probability for sending password protection reports for domains on the |
| // allowlist for users opted into extended reporting, from non-incognito window. |
| const float kProbabilityForSendingReportsFromSafeURLs = 0.01; |
| |
| // If user specifically mark a site as legitimate, we will keep this decision |
| // for 2 days. |
| const int kOverrideVerdictCacheDurationSec = 2 * 24 * 60 * 60; |
| |
| // Frequency to log PasswordCapture event log. Random 24-28 days. |
| const int kPasswordCaptureEventLogFreqDaysMin = 24; |
| const int kPasswordCaptureEventLogFreqDaysExtra = 4; |
| |
| int64_t GetMicrosecondsSinceWindowsEpoch(base::Time time) { |
| return (time - base::Time()).InMicroseconds(); |
| } |
| |
| PasswordReuseLookup::ReputationVerdict GetVerdictToLogFromResponse( |
| LoginReputationClientResponse::VerdictType response_verdict) { |
| switch (response_verdict) { |
| case LoginReputationClientResponse::SAFE: |
| return PasswordReuseLookup::SAFE; |
| case LoginReputationClientResponse::LOW_REPUTATION: |
| return PasswordReuseLookup::LOW_REPUTATION; |
| case LoginReputationClientResponse::PHISHING: |
| return PasswordReuseLookup::PHISHING; |
| case LoginReputationClientResponse::VERDICT_TYPE_UNSPECIFIED: |
| return PasswordReuseLookup::VERDICT_UNSPECIFIED; |
| } |
| NOTREACHED() << "Unexpected response_verdict: " << response_verdict; |
| return PasswordReuseLookup::VERDICT_UNSPECIFIED; |
| } |
| |
| // Records changes in the phished status of saved credential. |
| void LogCredentialPhishedStatusChanged(CredentialPhishedStatus status) { |
| base::UmaHistogramEnumeration("SafeBrowsing.CredentialPhishedStatusChange", |
| status); |
| } |
| |
| // Given a |web_contents|, returns the navigation id of its last committed |
| // navigation. |
| int64_t GetLastCommittedNavigationID(content::WebContents* web_contents) { |
| if (!web_contents) |
| return 0; |
| content::NavigationEntry* navigation = |
| web_contents->GetController().GetLastCommittedEntry(); |
| return navigation |
| ? GetMicrosecondsSinceWindowsEpoch(navigation->GetTimestamp()) |
| : 0; |
| } |
| |
| // Opens a |url| from |current_web_contents| with |referrer|. |in_new_tab| |
| // indicates if opening in a new foreground tab or in current tab. |
| void OpenUrl(content::WebContents* current_web_contents, |
| const GURL& url, |
| const content::Referrer& referrer, |
| bool in_new_tab) { |
| content::OpenURLParams params(url, referrer, |
| in_new_tab |
| ? WindowOpenDisposition::NEW_FOREGROUND_TAB |
| : WindowOpenDisposition::CURRENT_TAB, |
| ui::PAGE_TRANSITION_LINK, |
| /*is_renderer_initiated=*/false); |
| current_web_contents->OpenURL(params); |
| } |
| |
| int64_t GetNavigationIDFromPrefsByOrigin(PrefService* prefs, |
| const Origin& origin) { |
| const base::DictionaryValue* unhandled_sync_password_reuses = |
| prefs->GetDictionary(prefs::kSafeBrowsingUnhandledGaiaPasswordReuses); |
| if (!unhandled_sync_password_reuses) |
| return 0; |
| |
| const base::Value* navigation_id_value = |
| unhandled_sync_password_reuses->FindKey(origin.Serialize()); |
| |
| int64_t navigation_id; |
| return navigation_id_value && |
| base::StringToInt64(navigation_id_value->GetString(), |
| &navigation_id) |
| ? navigation_id |
| : 0; |
| } |
| |
| // Return a new UserEventSpecifics w/o the navigation_id populated |
| std::unique_ptr<UserEventSpecifics> GetNewUserEventSpecifics() { |
| auto specifics = std::make_unique<UserEventSpecifics>(); |
| specifics->set_event_time_usec( |
| GetMicrosecondsSinceWindowsEpoch(base::Time::Now())); |
| return specifics; |
| } |
| |
| // Return a new UserEventSpecifics w/ the navigation_id populated |
| std::unique_ptr<UserEventSpecifics> GetUserEventSpecificsWithNavigationId( |
| int64_t navigation_id) { |
| if (navigation_id <= 0) |
| return nullptr; |
| |
| auto specifics = GetNewUserEventSpecifics(); |
| specifics->set_navigation_id(navigation_id); |
| return specifics; |
| } |
| |
| // Return a new UserEventSpecifics populated from the web_contents |
| std::unique_ptr<UserEventSpecifics> GetUserEventSpecifics( |
| content::WebContents* web_contents) { |
| return GetUserEventSpecificsWithNavigationId( |
| GetLastCommittedNavigationID(web_contents)); |
| } |
| |
| } // namespace |
| |
| ChromePasswordProtectionService::ChromePasswordProtectionService( |
| SafeBrowsingService* sb_service, |
| Profile* profile) |
| : PasswordProtectionService( |
| sb_service->database_manager(), |
| sb_service->GetURLLoaderFactory(profile), |
| HistoryServiceFactory::GetForProfile( |
| profile, |
| ServiceAccessType::EXPLICIT_ACCESS), |
| profile->GetPrefs(), |
| std::make_unique<SafeBrowsingPrimaryAccountTokenFetcher>( |
| IdentityManagerFactory::GetForProfile(profile)), |
| profile->IsOffTheRecord(), |
| IdentityManagerFactory::GetForProfile(profile), |
| /*try_token_fetch=*/true, |
| SafeBrowsingMetricsCollectorFactory::GetForProfile(profile)), |
| ui_manager_(sb_service->ui_manager()), |
| trigger_manager_(sb_service->trigger_manager()), |
| profile_(profile), |
| pref_change_registrar_(new PrefChangeRegistrar), |
| cache_manager_(VerdictCacheManagerFactory::GetForProfile(profile)) { |
| pref_change_registrar_->Init(profile_->GetPrefs()); |
| |
| password_manager::PasswordReuseManager* reuse_manager = |
| PasswordReuseManagerFactory::GetForProfile(profile_); |
| // Reuse manager can be null in tests. |
| if (reuse_manager) { |
| // Subscribe to gaia hash password changes change notifications. |
| hash_password_manager_subscription_ = |
| reuse_manager->RegisterStateCallbackOnHashPasswordManager( |
| base::BindRepeating(&ChromePasswordProtectionService:: |
| CheckGaiaPasswordChangeForAllSignedInUsers, |
| base::Unretained(this))); |
| } |
| pref_change_registrar_->Add( |
| prefs::kPasswordProtectionWarningTrigger, |
| base::BindRepeating( |
| &ChromePasswordProtectionService::OnWarningTriggerChanged, |
| base::Unretained(this))); |
| pref_change_registrar_->Add( |
| prefs::kPasswordProtectionLoginURLs, |
| base::BindRepeating( |
| &ChromePasswordProtectionService::OnEnterprisePasswordUrlChanged, |
| base::Unretained(this))); |
| pref_change_registrar_->Add( |
| prefs::kPasswordProtectionChangePasswordURL, |
| base::BindRepeating( |
| &ChromePasswordProtectionService::OnEnterprisePasswordUrlChanged, |
| base::Unretained(this))); |
| |
| add_phished_credentials_ = |
| base::BindRepeating(&password_manager::AddPhishedCredentials); |
| remove_phished_credentials_ = |
| base::BindRepeating(&password_manager::RemovePhishedCredentials); |
| // TODO(nparker) Move the rest of the above code into Init() |
| // without crashing unittests. |
| Init(); |
| } |
| |
| void ChromePasswordProtectionService::Init() { |
| // The following code is disabled on Android. RefreshTokenIsAvailable cannot be |
| // used in unit tests, because it needs to interact with system accounts. |
| // Considering avoid running it during unit tests. See: crbug.com/1009957. |
| #if !defined(OS_ANDROID) |
| // This code is shared by the normal ctor and testing ctor. |
| |
| sync_password_hash_ = GetSyncPasswordHashFromPrefs(); |
| if (!sync_password_hash_.empty()) { |
| // Set a timer for when next to log the PasswordCapture event. The timer |
| // value is stored in a pref to carry across restarts. |
| base::TimeDelta delay = |
| GetDelayFromPref(profile_->GetPrefs(), |
| prefs::kSafeBrowsingNextPasswordCaptureEventLogTime); |
| |
| // Bound it between 1 min and 28 days. Handles clock-resets. We wait |
| // 1 min to not slowdown browser-startup, and to improve the |
| // probability that the sync system is initialized. |
| base::TimeDelta min_delay = base::Minutes(1); |
| base::TimeDelta max_delay = |
| base::Days(kPasswordCaptureEventLogFreqDaysMin + |
| kPasswordCaptureEventLogFreqDaysExtra); |
| if (delay < min_delay) |
| delay = min_delay; |
| else if (delay > max_delay) |
| delay = max_delay; |
| SetLogPasswordCaptureTimer(delay); |
| } |
| #endif |
| } |
| |
| void ChromePasswordProtectionService::Shutdown() { |
| if (pref_change_registrar_) |
| pref_change_registrar_->RemoveAll(); |
| hash_password_manager_subscription_ = {}; |
| } |
| |
| ChromePasswordProtectionService::~ChromePasswordProtectionService() = default; |
| |
| // static |
| ChromePasswordProtectionService* |
| ChromePasswordProtectionService::GetPasswordProtectionService( |
| Profile* profile) { |
| if (g_browser_process && g_browser_process->safe_browsing_service()) { |
| return static_cast<safe_browsing::ChromePasswordProtectionService*>( |
| g_browser_process->safe_browsing_service() |
| ->GetPasswordProtectionService(profile)); |
| } |
| return nullptr; |
| } |
| |
| // static |
| bool ChromePasswordProtectionService::ShouldShowPasswordReusePageInfoBubble( |
| content::WebContents* web_contents, |
| PasswordType password_type) { |
| Profile* profile = |
| Profile::FromBrowserContext(web_contents->GetBrowserContext()); |
| ChromePasswordProtectionService* service = |
| ChromePasswordProtectionService::GetPasswordProtectionService(profile); |
| |
| // |service| could be null if safe browsing service is disabled. |
| if (!service) |
| return false; |
| |
| if (password_type == PasswordType::ENTERPRISE_PASSWORD) |
| return service->HasUnhandledEnterprisePasswordReuse(web_contents); |
| |
| bool enable_warning_for_non_sync_users = base::FeatureList::IsEnabled( |
| safe_browsing::kPasswordProtectionForSignedInUsers); |
| DCHECK(password_type == PasswordType::PRIMARY_ACCOUNT_PASSWORD || |
| password_type == PasswordType::SAVED_PASSWORD || |
| (enable_warning_for_non_sync_users && |
| password_type == PasswordType::OTHER_GAIA_PASSWORD)); |
| // Otherwise, checks if there's any unhandled sync password reuses matches |
| // this origin. |
| auto* unhandled_sync_password_reuses = profile->GetPrefs()->GetDictionary( |
| prefs::kSafeBrowsingUnhandledGaiaPasswordReuses); |
| return unhandled_sync_password_reuses |
| ? (unhandled_sync_password_reuses->FindKey( |
| Origin::Create(web_contents->GetLastCommittedURL()) |
| .Serialize()) != nullptr) |
| : false; |
| } |
| |
| safe_browsing::LoginReputationClientRequest::UrlDisplayExperiment |
| ChromePasswordProtectionService::GetUrlDisplayExperiment() const { |
| safe_browsing::LoginReputationClientRequest::UrlDisplayExperiment experiment; |
| experiment.set_simplified_url_display_enabled( |
| base::FeatureList::IsEnabled(safe_browsing::kSimplifiedUrlDisplay)); |
| // Delayed warnings parameters: |
| experiment.set_delayed_warnings_enabled( |
| base::FeatureList::IsEnabled(safe_browsing::kDelayedWarnings)); |
| experiment.set_delayed_warnings_mouse_clicks_enabled( |
| safe_browsing::kDelayedWarningsEnableMouseClicks.Get()); |
| return experiment; |
| } |
| |
| void ChromePasswordProtectionService::ShowModalWarning( |
| PasswordProtectionRequest* request, |
| LoginReputationClientResponse::VerdictType verdict_type, |
| const std::string& verdict_token, |
| ReusedPasswordAccountType password_type) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| DCHECK(password_type.account_type() == ReusedPasswordAccountType::GMAIL || |
| password_type.account_type() == ReusedPasswordAccountType::GSUITE || |
| password_type.account_type() == |
| ReusedPasswordAccountType::NON_GAIA_ENTERPRISE || |
| (password_type.account_type() == |
| ReusedPasswordAccountType::SAVED_PASSWORD)); |
| PasswordProtectionRequestContent* request_content = |
| static_cast<PasswordProtectionRequestContent*>(request); |
| content::WebContents* web_contents = request_content->web_contents(); |
| RequestOutcome outcome = request->request_outcome(); |
| // Don't show warning again if there is already a modal warning showing. |
| if (IsModalWarningShowingInWebContents(web_contents)) |
| return; |
| |
| // Exit fullscreen if this |web_contents| is showing in fullscreen mode. |
| if (web_contents->IsFullscreen()) |
| web_contents->ExitFullscreen(true); |
| |
| #if defined(OS_ANDROID) |
| (new PasswordReuseControllerAndroid( |
| web_contents, this, password_type, |
| base::BindOnce(&ChromePasswordProtectionService::OnUserAction, |
| base::Unretained(this), web_contents, password_type, |
| outcome, verdict_type, verdict_token, |
| WarningUIType::MODAL_DIALOG))) |
| ->ShowDialog(); |
| #else // !defined(OS_ANDROID) |
| ShowPasswordReuseModalWarningDialog( |
| web_contents, this, password_type, |
| base::BindOnce(&ChromePasswordProtectionService::OnUserAction, |
| base::Unretained(this), web_contents, password_type, |
| outcome, verdict_type, verdict_token, |
| WarningUIType::MODAL_DIALOG)); |
| #endif // defined(OS_ANDROID) |
| |
| LogWarningAction(WarningUIType::MODAL_DIALOG, WarningAction::SHOWN, |
| password_type); |
| switch (password_type.account_type()) { |
| case ReusedPasswordAccountType::SAVED_PASSWORD: |
| OnModalWarningShownForSavedPassword(web_contents, password_type, |
| verdict_token); |
| break; |
| case ReusedPasswordAccountType::GMAIL: |
| case ReusedPasswordAccountType::GSUITE: |
| OnModalWarningShownForGaiaPassword(web_contents, password_type, |
| verdict_token); |
| break; |
| case ReusedPasswordAccountType::NON_GAIA_ENTERPRISE: |
| OnModalWarningShownForEnterprisePassword(web_contents, password_type, |
| verdict_token); |
| break; |
| default: |
| return; |
| } |
| } |
| |
| void ChromePasswordProtectionService::OnModalWarningShownForSavedPassword( |
| content::WebContents* web_contents, |
| ReusedPasswordAccountType password_type, |
| const std::string& verdict_token) { |
| UpdateSecurityState(SB_THREAT_TYPE_SAVED_PASSWORD_REUSE, password_type, |
| web_contents); |
| // Starts preparing post-warning report. |
| MaybeStartThreatDetailsCollection(web_contents, verdict_token, password_type); |
| } |
| |
| void ChromePasswordProtectionService::OnModalWarningShownForGaiaPassword( |
| content::WebContents* web_contents, |
| ReusedPasswordAccountType password_type, |
| const std::string& verdict_token) { |
| if (!IsIncognito()) { |
| DictionaryPrefUpdate update( |
| profile_->GetPrefs(), prefs::kSafeBrowsingUnhandledGaiaPasswordReuses); |
| // Since base::Value doesn't support int64_t type, we convert the navigation |
| // ID to string format and store it in the preference dictionary. |
| update->SetKey( |
| Origin::Create(web_contents->GetLastCommittedURL()).Serialize(), |
| base::Value( |
| base::NumberToString(GetLastCommittedNavigationID(web_contents)))); |
| } |
| SBThreatType threat_type; |
| if (password_type.is_account_syncing()) { |
| threat_type = SB_THREAT_TYPE_SIGNED_IN_SYNC_PASSWORD_REUSE; |
| } else { |
| threat_type = SB_THREAT_TYPE_SIGNED_IN_NON_SYNC_PASSWORD_REUSE; |
| } |
| UpdateSecurityState(threat_type, password_type, web_contents); |
| |
| // Starts preparing post-warning report. |
| MaybeStartThreatDetailsCollection(web_contents, verdict_token, password_type); |
| } |
| |
| void ChromePasswordProtectionService::OnModalWarningShownForEnterprisePassword( |
| content::WebContents* web_contents, |
| ReusedPasswordAccountType password_type, |
| const std::string& verdict_token) { |
| web_contents_with_unhandled_enterprise_reuses_.insert(web_contents); |
| UpdateSecurityState(SB_THREAT_TYPE_ENTERPRISE_PASSWORD_REUSE, password_type, |
| web_contents); |
| // Starts preparing post-warning report. |
| MaybeStartThreatDetailsCollection(web_contents, verdict_token, password_type); |
| } |
| |
| void ChromePasswordProtectionService::ShowInterstitial( |
| content::WebContents* web_contents, |
| ReusedPasswordAccountType password_type) { |
| DCHECK(password_type.account_type() == |
| ReusedPasswordAccountType::NON_GAIA_ENTERPRISE || |
| password_type.account_type() == ReusedPasswordAccountType::GSUITE); |
| // Exit fullscreen if this |web_contents| is showing in fullscreen mode. |
| if (web_contents->IsFullscreen()) |
| web_contents->ExitFullscreen(/*will_cause_resize=*/true); |
| |
| content::OpenURLParams params( |
| GURL(chrome::kChromeUIResetPasswordURL), content::Referrer(), |
| WindowOpenDisposition::NEW_FOREGROUND_TAB, ui::PAGE_TRANSITION_LINK, |
| /*is_renderer_initiated=*/false); |
| std::string post_data = |
| base::NumberToString(static_cast<std::underlying_type_t<PasswordType>>( |
| ConvertReusedPasswordAccountTypeToPasswordType(password_type))); |
| |
| params.post_data = network::ResourceRequestBody::CreateFromBytes( |
| post_data.data(), post_data.size()); |
| web_contents->OpenURL(params); |
| |
| LogWarningAction(WarningUIType::INTERSTITIAL, WarningAction::SHOWN, |
| password_type); |
| } |
| |
| void ChromePasswordProtectionService::OnUserAction( |
| content::WebContents* web_contents, |
| ReusedPasswordAccountType password_type, |
| RequestOutcome outcome, |
| LoginReputationClientResponse::VerdictType verdict_type, |
| const std::string& verdict_token, |
| WarningUIType ui_type, |
| WarningAction action) { |
| // Only log modal warning dialog action for all password types except for |
| // signed-in non-syncing type for now. We log for signed-in non-syncing type |
| // only when we are about to send the event to SecurityEventRecorder because |
| // we don't want to count non-unconsented primary accounts. |
| bool is_signed_in_non_syncing = |
| !password_type.is_account_syncing() && |
| (password_type.account_type() == ReusedPasswordAccountType::GMAIL || |
| password_type.account_type() == ReusedPasswordAccountType::GSUITE); |
| if (!is_signed_in_non_syncing) |
| LogWarningAction(ui_type, action, password_type); |
| |
| switch (ui_type) { |
| case WarningUIType::PAGE_INFO: |
| HandleUserActionOnPageInfo(web_contents, password_type, action); |
| break; |
| case WarningUIType::MODAL_DIALOG: |
| HandleUserActionOnModalWarning(web_contents, password_type, outcome, |
| verdict_type, verdict_token, action); |
| break; |
| case WarningUIType::INTERSTITIAL: |
| DCHECK_EQ(WarningAction::CHANGE_PASSWORD, action); |
| HandleResetPasswordOnInterstitial(web_contents, action); |
| break; |
| default: |
| NOTREACHED(); |
| break; |
| } |
| } |
| |
| void ChromePasswordProtectionService::AddObserver(Observer* observer) { |
| observer_list_.AddObserver(observer); |
| } |
| |
| void ChromePasswordProtectionService::RemoveObserver(Observer* observer) { |
| observer_list_.RemoveObserver(observer); |
| } |
| |
| void ChromePasswordProtectionService::MaybeStartThreatDetailsCollection( |
| content::WebContents* web_contents, |
| const std::string& token, |
| ReusedPasswordAccountType password_type) { |
| // |trigger_manager_| can be null in test. |
| if (!trigger_manager_) |
| return; |
| |
| const content::GlobalRenderFrameHostId primary_main_frame_id = |
| web_contents->GetMainFrame()->GetGlobalId(); |
| security_interstitials::UnsafeResource resource; |
| if (password_type.account_type() == |
| ReusedPasswordAccountType::NON_GAIA_ENTERPRISE) { |
| resource.threat_type = SB_THREAT_TYPE_ENTERPRISE_PASSWORD_REUSE; |
| } else if (password_type.account_type() == |
| ReusedPasswordAccountType::SAVED_PASSWORD) { |
| resource.threat_type = SB_THREAT_TYPE_SAVED_PASSWORD_REUSE; |
| } else if (password_type.is_account_syncing()) { |
| resource.threat_type = SB_THREAT_TYPE_SIGNED_IN_SYNC_PASSWORD_REUSE; |
| } else { |
| resource.threat_type = SB_THREAT_TYPE_SIGNED_IN_NON_SYNC_PASSWORD_REUSE; |
| } |
| resource.url = web_contents->GetLastCommittedURL(); |
| resource.render_process_id = primary_main_frame_id.child_id; |
| resource.render_frame_id = primary_main_frame_id.frame_routing_id; |
| resource.token = token; |
| scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory = |
| profile_->GetDefaultStoragePartition() |
| ->GetURLLoaderFactoryForBrowserProcess(); |
| // Ignores the return of |StartCollectingThreatDetails()| here and |
| // let TriggerManager decide whether it should start data |
| // collection. |
| trigger_manager_->StartCollectingThreatDetails( |
| safe_browsing::TriggerType::GAIA_PASSWORD_REUSE, web_contents, resource, |
| url_loader_factory, /*history_service=*/nullptr, |
| SafeBrowsingNavigationObserverManagerFactory::GetForBrowserContext( |
| profile_), |
| TriggerManager::GetSBErrorDisplayOptions(*profile_->GetPrefs(), |
| web_contents)); |
| } |
| |
| void ChromePasswordProtectionService::MaybeFinishCollectingThreatDetails( |
| content::WebContents* web_contents, |
| bool did_proceed) { |
| // |trigger_manager_| can be null in test. |
| if (!trigger_manager_) |
| return; |
| |
| // Since we don't keep track the threat details in progress, it is safe to |
| // ignore the result of |FinishCollectingThreatDetails()|. TriggerManager will |
| // take care of whether report should be sent. |
| trigger_manager_->FinishCollectingThreatDetails( |
| safe_browsing::TriggerType::GAIA_PASSWORD_REUSE, web_contents, |
| base::Milliseconds(0), did_proceed, /*num_visit=*/0, |
| TriggerManager::GetSBErrorDisplayOptions(*profile_->GetPrefs(), |
| web_contents)); |
| } |
| |
| void ChromePasswordProtectionService::MaybeLogPasswordReuseDetectedEvent( |
| content::WebContents* web_contents) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| if (IsIncognito() && !WebUIInfoSingleton::HasListener()) |
| return; |
| |
| syncer::UserEventService* user_event_service = |
| browser_sync::UserEventServiceFactory::GetForProfile(profile_); |
| if (!user_event_service) |
| return; |
| |
| std::unique_ptr<UserEventSpecifics> specifics = |
| GetUserEventSpecifics(web_contents); |
| if (!specifics) |
| return; |
| |
| auto* const status = specifics->mutable_gaia_password_reuse_event() |
| ->mutable_reuse_detected() |
| ->mutable_status(); |
| status->set_enabled(IsSafeBrowsingEnabled()); |
| |
| ExtendedReportingLevel erl = GetExtendedReportingLevel(*GetPrefs()); |
| switch (erl) { |
| case SBER_LEVEL_OFF: |
| status->set_safe_browsing_reporting_population(SafeBrowsingStatus::NONE); |
| break; |
| case SBER_LEVEL_LEGACY: |
| status->set_safe_browsing_reporting_population( |
| SafeBrowsingStatus::EXTENDED_REPORTING); |
| break; |
| case SBER_LEVEL_SCOUT: |
| status->set_safe_browsing_reporting_population(SafeBrowsingStatus::SCOUT); |
| break; |
| } |
| |
| WebUIInfoSingleton::GetInstance()->AddToPGEvents(*specifics); |
| user_event_service->RecordUserEvent(std::move(specifics)); |
| } |
| |
| void ChromePasswordProtectionService::MaybeLogPasswordReuseDialogInteraction( |
| int64_t navigation_id, |
| PasswordReuseDialogInteraction::InteractionResult interaction_result) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| if (IsIncognito() && !WebUIInfoSingleton::HasListener()) |
| return; |
| |
| syncer::UserEventService* user_event_service = |
| browser_sync::UserEventServiceFactory::GetForProfile(profile_); |
| if (!user_event_service) |
| return; |
| |
| std::unique_ptr<UserEventSpecifics> specifics = |
| GetUserEventSpecificsWithNavigationId(navigation_id); |
| if (!specifics) |
| return; |
| |
| PasswordReuseDialogInteraction* const dialog_interaction = |
| specifics->mutable_gaia_password_reuse_event() |
| ->mutable_dialog_interaction(); |
| dialog_interaction->set_interaction_result(interaction_result); |
| |
| WebUIInfoSingleton::GetInstance()->AddToPGEvents(*specifics); |
| user_event_service->RecordUserEvent(std::move(specifics)); |
| } |
| |
| void ChromePasswordProtectionService::MaybeLogPasswordReuseLookupResult( |
| content::WebContents* web_contents, |
| PasswordReuseLookup::LookupResult result) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| if (IsIncognito() && !WebUIInfoSingleton::HasListener()) |
| return; |
| |
| syncer::UserEventService* user_event_service = |
| browser_sync::UserEventServiceFactory::GetForProfile(profile_); |
| if (!user_event_service) |
| return; |
| |
| std::unique_ptr<UserEventSpecifics> specifics = |
| GetUserEventSpecifics(web_contents); |
| if (!specifics) |
| return; |
| |
| auto* const reuse_lookup = |
| specifics->mutable_gaia_password_reuse_event()->mutable_reuse_lookup(); |
| reuse_lookup->set_lookup_result(result); |
| WebUIInfoSingleton::GetInstance()->AddToPGEvents(*specifics); |
| user_event_service->RecordUserEvent(std::move(specifics)); |
| } |
| |
| void ChromePasswordProtectionService:: |
| MaybeLogPasswordReuseLookupResultWithVerdict( |
| content::WebContents* web_contents, |
| PasswordType password_type, |
| PasswordReuseLookup::LookupResult result, |
| PasswordReuseLookup::ReputationVerdict verdict, |
| const std::string& verdict_token) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| if (IsIncognito() && !WebUIInfoSingleton::HasListener()) |
| return; |
| |
| PasswordReuseLookup reuse_lookup; |
| reuse_lookup.set_lookup_result(result); |
| reuse_lookup.set_verdict(verdict); |
| reuse_lookup.set_verdict_token(verdict_token); |
| |
| // If password_type == OTHER_GAIA_PASSWORD, the account is not syncing. |
| // Therefore, we have to use the security event recorder to log events to mark |
| // the account at risk. |
| if (password_type == PasswordType::OTHER_GAIA_PASSWORD) { |
| sync_pb::GaiaPasswordReuse gaia_password_reuse_event; |
| *gaia_password_reuse_event.mutable_reuse_lookup() = reuse_lookup; |
| |
| auto* identity_manager = IdentityManagerFactory::GetForProfileIfExists( |
| profile_->GetOriginalProfile()); |
| if (identity_manager) { |
| CoreAccountInfo unconsented_primary_account_info = |
| identity_manager->GetPrimaryAccountInfo( |
| signin::ConsentLevel::kSignin); |
| // SecurityEventRecorder only supports unconsented primary accounts. |
| if (gaia::AreEmailsSame(unconsented_primary_account_info.email, |
| username_for_last_shown_warning())) { |
| // We currently only send a security event recorder ONLY when a |
| // signed-in non-syncing user clicks on "Protect Account" button. |
| LogWarningAction(WarningUIType::MODAL_DIALOG, |
| WarningAction::CHANGE_PASSWORD, |
| GetPasswordProtectionReusedPasswordAccountType( |
| password_type, username_for_last_shown_warning())); |
| WebUIInfoSingleton::GetInstance()->AddToSecurityEvents( |
| gaia_password_reuse_event); |
| SecurityEventRecorderFactory::GetForProfile(profile_) |
| ->RecordGaiaPasswordReuse(gaia_password_reuse_event); |
| } |
| } |
| } else { |
| syncer::UserEventService* user_event_service = |
| browser_sync::UserEventServiceFactory::GetForProfile(profile_); |
| if (!user_event_service) |
| return; |
| |
| std::unique_ptr<UserEventSpecifics> specifics = |
| GetUserEventSpecifics(web_contents); |
| if (!specifics) |
| return; |
| |
| *(specifics->mutable_gaia_password_reuse_event())->mutable_reuse_lookup() = |
| reuse_lookup; |
| WebUIInfoSingleton::GetInstance()->AddToPGEvents(*specifics); |
| user_event_service->RecordUserEvent(std::move(specifics)); |
| } |
| } |
| |
| void ChromePasswordProtectionService::MaybeLogPasswordReuseLookupEvent( |
| content::WebContents* web_contents, |
| RequestOutcome outcome, |
| PasswordType password_type, |
| const LoginReputationClientResponse* response) { |
| switch (outcome) { |
| case RequestOutcome::MATCHED_ALLOWLIST: |
| MaybeLogPasswordReuseLookupResult(web_contents, |
| PasswordReuseLookup::WHITELIST_HIT); |
| break; |
| case RequestOutcome::RESPONSE_ALREADY_CACHED: |
| MaybeLogPasswordReuseLookupResultWithVerdict( |
| web_contents, password_type, PasswordReuseLookup::CACHE_HIT, |
| GetVerdictToLogFromResponse(response->verdict_type()), |
| response->verdict_token()); |
| break; |
| case RequestOutcome::SUCCEEDED: |
| MaybeLogPasswordReuseLookupResultWithVerdict( |
| web_contents, password_type, PasswordReuseLookup::REQUEST_SUCCESS, |
| GetVerdictToLogFromResponse(response->verdict_type()), |
| response->verdict_token()); |
| break; |
| case RequestOutcome::URL_NOT_VALID_FOR_REPUTATION_COMPUTING: |
| MaybeLogPasswordReuseLookupResult(web_contents, |
| PasswordReuseLookup::URL_UNSUPPORTED); |
| break; |
| case RequestOutcome::MATCHED_ENTERPRISE_ALLOWLIST: |
| case RequestOutcome::MATCHED_ENTERPRISE_LOGIN_URL: |
| case RequestOutcome::MATCHED_ENTERPRISE_CHANGE_PASSWORD_URL: |
| MaybeLogPasswordReuseLookupResult( |
| web_contents, PasswordReuseLookup::ENTERPRISE_WHITELIST_HIT); |
| break; |
| case RequestOutcome::PASSWORD_ALERT_MODE: |
| case RequestOutcome::TURNED_OFF_BY_ADMIN: |
| MaybeLogPasswordReuseLookupResult( |
| web_contents, PasswordReuseLookup::TURNED_OFF_BY_POLICY); |
| break; |
| case RequestOutcome::CANCELED: |
| case RequestOutcome::TIMEDOUT: |
| case RequestOutcome::DISABLED_DUE_TO_INCOGNITO: |
| case RequestOutcome::REQUEST_MALFORMED: |
| case RequestOutcome::FETCH_FAILED: |
| case RequestOutcome::RESPONSE_MALFORMED: |
| case RequestOutcome::SERVICE_DESTROYED: |
| case RequestOutcome::DISABLED_DUE_TO_FEATURE_DISABLED: |
| case RequestOutcome::DISABLED_DUE_TO_USER_POPULATION: |
| case RequestOutcome::SAFE_BROWSING_DISABLED: |
| case RequestOutcome::USER_NOT_SIGNED_IN: |
| case RequestOutcome::EXCLUDED_COUNTRY: |
| MaybeLogPasswordReuseLookupResult(web_contents, |
| PasswordReuseLookup::REQUEST_FAILURE); |
| break; |
| case RequestOutcome::UNKNOWN: |
| case RequestOutcome::DEPRECATED_NO_EXTENDED_REPORTING: |
| NOTREACHED() << __FUNCTION__ |
| << ": outcome: " << static_cast<int>(outcome); |
| break; |
| } |
| } |
| |
| void ChromePasswordProtectionService:: |
| CheckGaiaPasswordChangeForAllSignedInUsers(const std::string& username) { |
| // If the sync password has changed, report the change. |
| std::string new_sync_password_hash = GetSyncPasswordHashFromPrefs(); |
| if (sync_password_hash_ != new_sync_password_hash) { |
| sync_password_hash_ = new_sync_password_hash; |
| OnGaiaPasswordChanged(username, /*is_other_gaia_password=*/false); |
| return; |
| } |
| |
| // For non sync password changes, we have to loop through all the password |
| // hashes and find the hash associated with the username. |
| password_manager::HashPasswordManager hash_password_manager; |
| hash_password_manager.set_prefs(profile_->GetPrefs()); |
| for (const auto& hash_data : |
| hash_password_manager.RetrieveAllPasswordHashes()) { |
| if (password_manager::AreUsernamesSame( |
| hash_data.username, /*is_username1_gaia_account=*/true, username, |
| /*is_username2_gaia_account=*/true)) { |
| OnGaiaPasswordChanged(username, /*is_other_gaia_password=*/true); |
| break; |
| } |
| } |
| } |
| |
| void ChromePasswordProtectionService::OnGaiaPasswordChanged( |
| const std::string& username, |
| bool is_other_gaia_password) { |
| DictionaryPrefUpdate unhandled_gaia_password_reuses( |
| profile_->GetPrefs(), prefs::kSafeBrowsingUnhandledGaiaPasswordReuses); |
| LogNumberOfReuseBeforeSyncPasswordChange( |
| unhandled_gaia_password_reuses->DictSize()); |
| unhandled_gaia_password_reuses->Clear(); |
| if (!is_other_gaia_password) |
| MaybeLogPasswordCapture(/*did_log_in=*/true); |
| for (auto& observer : observer_list_) |
| observer.OnGaiaPasswordChanged(); |
| |
| // Disabled on Android, because enterprise reporting extension is not supported. |
| #if !defined(OS_ANDROID) |
| // Only report if the current password changed is the primary account and it's |
| // not a Gmail account or if the current password changed is a content area |
| // account and it's not a Gmail account. |
| if (!IsAccountGmail(username)) |
| ReportPasswordChanged(); |
| #endif |
| } |
| |
| GURL ChromePasswordProtectionService::GetEnterpriseChangePasswordURL() const { |
| // If change password URL is specified in preferences, returns the |
| // corresponding pref value. |
| GURL enterprise_change_password_url = |
| GetPasswordProtectionChangePasswordURLPref(*profile_->GetPrefs()); |
| if (!enterprise_change_password_url.is_empty()) |
| return enterprise_change_password_url; |
| |
| return GetDefaultChangePasswordURL(); |
| } |
| |
| GURL ChromePasswordProtectionService::GetDefaultChangePasswordURL() const { |
| // Computes the default GAIA change password URL. |
| const AccountInfo account_info = GetAccountInfo(); |
| std::string account_email = account_info.email; |
| // This page will prompt for re-auth and then will prompt for a new password. |
| std::string account_url = |
| "https://myaccount.google.com/signinoptions/" |
| "password?utm_source=Google&utm_campaign=PhishGuard"; |
| url::RawCanonOutputT<char> percent_encoded_email; |
| url::RawCanonOutputT<char> percent_encoded_account_url; |
| url::EncodeURIComponent(account_email.c_str(), account_email.length(), |
| &percent_encoded_email); |
| url::EncodeURIComponent(account_url.c_str(), account_url.length(), |
| &percent_encoded_account_url); |
| GURL change_password_url = GURL(base::StringPrintf( |
| "https://accounts.google.com/" |
| "AccountChooser?Email=%s&continue=%s", |
| std::string(percent_encoded_email.data(), percent_encoded_email.length()) |
| .c_str(), |
| std::string(percent_encoded_account_url.data(), |
| percent_encoded_account_url.length()) |
| .c_str())); |
| return google_util::AppendGoogleLocaleParam( |
| change_password_url, g_browser_process->GetApplicationLocale()); |
| } |
| |
| void ChromePasswordProtectionService::HandleUserActionOnModalWarning( |
| content::WebContents* web_contents, |
| ReusedPasswordAccountType password_type, |
| RequestOutcome outcome, |
| LoginReputationClientResponse::VerdictType verdict_type, |
| const std::string& verdict_token, |
| WarningAction action) { |
| const Origin origin = Origin::Create(web_contents->GetLastCommittedURL()); |
| int64_t navigation_id = |
| GetNavigationIDFromPrefsByOrigin(profile_->GetPrefs(), origin); |
| |
| if (action == WarningAction::IGNORE_WARNING) { |
| AddModelWarningBypasstoPref(); |
| } |
| |
| if (action == WarningAction::CHANGE_PASSWORD) { |
| RecordAction(UserMetricsAction( |
| "PasswordProtection.ModalWarning.ChangePasswordButtonClicked")); |
| LogDialogMetricsOnChangePassword(web_contents, password_type, navigation_id, |
| outcome, verdict_type, verdict_token); |
| OpenChangePasswordUrl(web_contents, password_type); |
| } else if (action == WarningAction::IGNORE_WARNING && |
| password_type.is_account_syncing()) { |
| RecordAction(UserMetricsAction( |
| "PasswordProtection.ModalWarning.IgnoreButtonClicked")); |
| // No need to change state. |
| MaybeLogPasswordReuseDialogInteraction( |
| navigation_id, PasswordReuseDialogInteraction::WARNING_ACTION_IGNORED); |
| } else if (action == WarningAction::CLOSE && |
| password_type.is_account_syncing()) { |
| RecordAction( |
| UserMetricsAction("PasswordProtection.ModalWarning.CloseWarning")); |
| // No need to change state. |
| MaybeLogPasswordReuseDialogInteraction( |
| navigation_id, PasswordReuseDialogInteraction::WARNING_UI_IGNORED); |
| } |
| RemoveWarningRequestsByWebContents(web_contents); |
| MaybeFinishCollectingThreatDetails( |
| web_contents, |
| /*did_proceed=*/action == WarningAction::CHANGE_PASSWORD); |
| } |
| |
| void ChromePasswordProtectionService::LogDialogMetricsOnChangePassword( |
| content::WebContents* web_contents, |
| ReusedPasswordAccountType password_type, |
| int64_t navigation_id, |
| RequestOutcome outcome, |
| LoginReputationClientResponse::VerdictType verdict_type, |
| const std::string& verdict_token) { |
| if (password_type.is_account_syncing() || |
| password_type.account_type() == |
| ReusedPasswordAccountType::SAVED_PASSWORD) { |
| MaybeLogPasswordReuseDialogInteraction( |
| navigation_id, PasswordReuseDialogInteraction::WARNING_ACTION_TAKEN); |
| } else { |
| // |outcome| is only recorded as succeeded or response_already_cached. |
| MaybeLogPasswordReuseLookupResultWithVerdict( |
| web_contents, PasswordType::OTHER_GAIA_PASSWORD, |
| outcome == RequestOutcome::SUCCEEDED |
| ? PasswordReuseLookup::REQUEST_SUCCESS |
| : PasswordReuseLookup::CACHE_HIT, |
| GetVerdictToLogFromResponse(verdict_type), verdict_token); |
| } |
| } |
| |
| void ChromePasswordProtectionService::AddModelWarningBypasstoPref() { |
| auto* metrics_collector = |
| SafeBrowsingMetricsCollectorFactory::GetForProfile(profile_); |
| if (metrics_collector) { |
| metrics_collector->AddSafeBrowsingEventToPref( |
| SafeBrowsingMetricsCollector::EventType::PASSWORD_REUSE_MODAL_BYPASS); |
| } |
| } |
| |
| void ChromePasswordProtectionService::OpenChangePasswordUrl( |
| content::WebContents* web_contents, |
| ReusedPasswordAccountType password_type) { |
| if (password_type.account_type() == |
| ReusedPasswordAccountType::NON_GAIA_ENTERPRISE) { |
| // Directly open enterprise change password page for enterprise password |
| // reuses. |
| OpenUrl(web_contents, GetEnterpriseChangePasswordURL(), content::Referrer(), |
| /*in_new_tab=*/true); |
| web_contents_with_unhandled_enterprise_reuses_.erase(web_contents); |
| } else if (password_type.account_type() != |
| ReusedPasswordAccountType::SAVED_PASSWORD) { |
| // Opens accounts.google.com in a new tab. |
| OpenUrl(web_contents, GetDefaultChangePasswordURL(), content::Referrer(), |
| /*in_new_tab=*/true); |
| } else { |
| #if defined(OS_ANDROID) |
| if (base::FeatureList::IsEnabled( |
| safe_browsing:: |
| kSafeBrowsingPasswordCheckIntegrationForSavedPasswordsAndroid)) { |
| JNIEnv* env = base::android::AttachCurrentThread(); |
| |
| PasswordCheckupLauncherHelper:: |
| LaunchLocalCheckupFromPhishGuardWarningDialog( |
| env, web_contents->GetTopLevelNativeWindow()->GetJavaObject()); |
| } |
| #endif |
| #if BUILDFLAG(FULL_SAFE_BROWSING) |
| // Opens chrome://settings/passwords/check in a new tab. |
| chrome::ShowPasswordCheck(chrome::FindBrowserWithWebContents(web_contents)); |
| password_manager::LogPasswordCheckReferrer( |
| password_manager::PasswordCheckReferrer::kPhishGuardDialog); |
| #endif |
| } |
| } |
| |
| void ChromePasswordProtectionService::HandleUserActionOnPageInfo( |
| content::WebContents* web_contents, |
| ReusedPasswordAccountType password_type, |
| WarningAction action) { |
| GURL url = web_contents->GetLastCommittedURL(); |
| const Origin origin = Origin::Create(url); |
| |
| if (action == WarningAction::CHANGE_PASSWORD) { |
| RecordAction(UserMetricsAction( |
| "PasswordProtection.PageInfo.ChangePasswordButtonClicked")); |
| OpenChangePasswordUrl(web_contents, password_type); |
| return; |
| } |
| |
| if (action == WarningAction::MARK_AS_LEGITIMATE) { |
| RecordAction( |
| UserMetricsAction("PasswordProtection.PageInfo.MarkSiteAsLegitimate")); |
| // TODO(vakh): There's no good enum to report this dialog interaction. |
| // This needs to be investigated. |
| UpdateSecurityState(SB_THREAT_TYPE_SAFE, password_type, web_contents); |
| if (password_type.account_type() == |
| ReusedPasswordAccountType::NON_GAIA_ENTERPRISE) { |
| web_contents_with_unhandled_enterprise_reuses_.erase(web_contents); |
| } else { |
| DictionaryPrefUpdate update( |
| profile_->GetPrefs(), |
| prefs::kSafeBrowsingUnhandledGaiaPasswordReuses); |
| update->RemoveKey(origin.Serialize()); |
| } |
| |
| // If the site is marked as legitimate and the phished password is |
| // a saved password, remove the matching saved password credentials |
| // from the compromised credentials table as the user has considered |
| // the site safe. |
| if (password_type.account_type() == |
| ReusedPasswordAccountType::SAVED_PASSWORD) { |
| RemovePhishedSavedPasswordCredential( |
| saved_passwords_matching_reused_credentials()); |
| } |
| for (auto& observer : observer_list_) |
| observer.OnMarkingSiteAsLegitimate(url); |
| return; |
| } |
| |
| NOTREACHED(); |
| } |
| |
| void ChromePasswordProtectionService::HandleResetPasswordOnInterstitial( |
| content::WebContents* web_contents, |
| WarningAction action) { |
| RecordAction( |
| UserMetricsAction("PasswordProtection.Interstitial.ResetPassword")); |
| // Opens enterprise change password page in current tab for user to change |
| // password. |
| OpenUrl(web_contents, GetEnterpriseChangePasswordURL(), |
| content::Referrer(web_contents->GetLastCommittedURL(), |
| network::mojom::ReferrerPolicy::kDefault), |
| /*in_new_tab=*/false); |
| } |
| |
| std::u16string ChromePasswordProtectionService::GetWarningDetailText( |
| ReusedPasswordAccountType password_type) const { |
| DCHECK(password_type.account_type() == ReusedPasswordAccountType::GSUITE || |
| password_type.account_type() == ReusedPasswordAccountType::GMAIL || |
| password_type.account_type() == |
| ReusedPasswordAccountType::NON_GAIA_ENTERPRISE || |
| (password_type.account_type() == |
| ReusedPasswordAccountType::SAVED_PASSWORD)); |
| if (password_type.account_type() == |
| ReusedPasswordAccountType::NON_GAIA_ENTERPRISE) { |
| return l10n_util::GetStringUTF16( |
| IDS_PAGE_INFO_CHANGE_PASSWORD_DETAILS_ENTERPRISE); |
| } |
| |
| if (password_type.account_type() == |
| ReusedPasswordAccountType::SAVED_PASSWORD) { |
| return l10n_util::GetStringUTF16( |
| IDS_PAGE_INFO_CHANGE_PASSWORD_DETAILS_SAVED); |
| } |
| |
| bool enable_warning_for_non_sync_users = base::FeatureList::IsEnabled( |
| safe_browsing::kPasswordProtectionForSignedInUsers); |
| if (enable_warning_for_non_sync_users && |
| !password_type.is_account_syncing()) { |
| return l10n_util::GetStringUTF16( |
| IDS_PAGE_INFO_CHANGE_PASSWORD_DETAILS_SIGNED_IN_NON_SYNC); |
| } |
| if (password_type.account_type() != ReusedPasswordAccountType::GSUITE) { |
| return l10n_util::GetStringUTF16( |
| IDS_PAGE_INFO_CHANGE_PASSWORD_DETAILS_SYNC); |
| } |
| |
| std::string org_name = GetOrganizationName(password_type); |
| if (!org_name.empty()) { |
| return l10n_util::GetStringFUTF16( |
| IDS_PAGE_INFO_CHANGE_PASSWORD_DETAILS_ENTERPRISE_WITH_ORG_NAME, |
| base::UTF8ToUTF16(org_name)); |
| } |
| return l10n_util::GetStringUTF16( |
| IDS_PAGE_INFO_CHANGE_PASSWORD_DETAILS_ENTERPRISE); |
| } |
| |
| std::string ChromePasswordProtectionService::GetOrganizationName( |
| ReusedPasswordAccountType password_type) const { |
| if (password_type.account_type() != ReusedPasswordAccountType::GSUITE) { |
| return std::string(); |
| } |
| |
| std::string email = |
| password_type.is_account_syncing() |
| ? GetAccountInfo().email |
| : GetAccountInfoForUsername(username_for_last_shown_warning()).email; |
| return email.empty() ? std::string() : gaia::ExtractDomainName(email); |
| } |
| |
| // Disabled on Android, because enterprise reporting extension is not supported. |
| #if !defined(OS_ANDROID) |
| void ChromePasswordProtectionService::MaybeReportPasswordReuseDetected( |
| PasswordProtectionRequest* request, |
| const std::string& username, |
| PasswordType password_type, |
| bool is_phishing_url) { |
| auto reused_password_account_type = |
| GetPasswordProtectionReusedPasswordAccountType(password_type, username); |
| if (reused_password_account_type.account_type() == |
| ReusedPasswordAccountType::UNKNOWN) { |
| return; |
| } |
| |
| // When a PasswordFieldFocus event is sent, a PasswordProtectionRequest is |
| // sent which means the password reuse type is unknown. We do not want to |
| // report these events as PasswordReuse events. Also do not send reports for |
| // Gmail accounts. |
| bool can_log_password_reuse_event = |
| (password_type == PasswordType::ENTERPRISE_PASSWORD || |
| reused_password_account_type.account_type() == |
| ReusedPasswordAccountType::GSUITE) && |
| (password_type != PasswordType::PASSWORD_TYPE_UNKNOWN); |
| if (!IsIncognito() && can_log_password_reuse_event) { |
| // User name should only be empty when MaybeStartPasswordFieldOnFocusRequest |
| // is called. |
| std::string username_or_email = |
| username.empty() ? GetAccountInfo().email : username; |
| auto* router = |
| extensions::SafeBrowsingPrivateEventRouterFactory::GetForProfile( |
| profile_); |
| if (router) { |
| PasswordProtectionRequestContent* request_content = |
| static_cast<PasswordProtectionRequestContent*>(request); |
| router->OnPolicySpecifiedPasswordReuseDetected( |
| request_content->web_contents()->GetLastCommittedURL(), |
| username_or_email, is_phishing_url); |
| } |
| } |
| } |
| |
| void ChromePasswordProtectionService::ReportPasswordChanged() { |
| if (!IsIncognito()) { |
| auto* router = |
| extensions::SafeBrowsingPrivateEventRouterFactory::GetForProfile( |
| profile_); |
| if (router) { |
| router->OnPolicySpecifiedPasswordChanged(GetAccountInfo().email); |
| } |
| } |
| } |
| #endif |
| |
| bool ChromePasswordProtectionService::HasUnhandledEnterprisePasswordReuse( |
| content::WebContents* web_contents) const { |
| return web_contents_with_unhandled_enterprise_reuses_.find(web_contents) != |
| web_contents_with_unhandled_enterprise_reuses_.end(); |
| } |
| |
| void ChromePasswordProtectionService::OnWarningTriggerChanged() { |
| const base::Value* pref_value = pref_change_registrar_->prefs()->Get( |
| prefs::kPasswordProtectionWarningTrigger); |
| // If password protection is not turned off, do nothing. |
| if (static_cast<PasswordProtectionTrigger>(pref_value->GetInt()) != |
| PASSWORD_PROTECTION_OFF) { |
| return; |
| } |
| |
| // Clears captured enterprise password hashes or GSuite sync password hashes. |
| password_manager::PasswordReuseManager* reuse_manager = |
| GetPasswordReuseManager(); |
| |
| reuse_manager->ClearAllNonGmailPasswordHash(); |
| reuse_manager->ClearAllEnterprisePasswordHash(); |
| } |
| |
| void ChromePasswordProtectionService::OnEnterprisePasswordUrlChanged() { |
| GetPasswordReuseManager()->ScheduleEnterprisePasswordURLUpdate(); |
| } |
| |
| bool ChromePasswordProtectionService::CanShowInterstitial( |
| ReusedPasswordAccountType password_type, |
| const GURL& main_frame_url) { |
| bool is_supported_password_type = |
| password_type.account_type() == ReusedPasswordAccountType::GSUITE || |
| password_type.account_type() == |
| ReusedPasswordAccountType::NON_GAIA_ENTERPRISE; |
| return IsInPasswordAlertMode(password_type) && is_supported_password_type && |
| !IsURLAllowlistedForPasswordEntry(main_frame_url); |
| } |
| |
| void ChromePasswordProtectionService::SetLogPasswordCaptureTimer( |
| const base::TimeDelta& delay) { |
| // This will replace any pending timer. |
| log_password_capture_timer_.Start( |
| FROM_HERE, delay, |
| base::BindOnce(&ChromePasswordProtectionService::MaybeLogPasswordCapture, |
| base::Unretained(this), false)); |
| } |
| |
| void ChromePasswordProtectionService::MaybeLogPasswordCapture(bool did_log_in) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| // We skip this event and not set a timer if the profile is in incognito. When |
| // the user logs in in the future, MaybeLogPasswordCapture() will be called |
| // immediately then and will restart the timer. |
| if (IsIncognito() || sync_password_hash_.empty()) |
| return; |
| |
| syncer::UserEventService* user_event_service = |
| browser_sync::UserEventServiceFactory::GetForProfile(profile_); |
| if (!user_event_service) |
| return; |
| |
| std::unique_ptr<UserEventSpecifics> specifics = GetNewUserEventSpecifics(); |
| auto* const password_captured = |
| specifics->mutable_gaia_password_captured_event(); |
| password_captured->set_event_trigger( |
| did_log_in ? GaiaPasswordCaptured::USER_LOGGED_IN |
| : GaiaPasswordCaptured::EXPIRED_28D_TIMER); |
| |
| WebUIInfoSingleton::GetInstance()->AddToPGEvents(*specifics); |
| user_event_service->RecordUserEvent(std::move(specifics)); |
| |
| // Set a timer to log it again in 24-28 days. Spread it to avoid hammering the |
| // backend with fixed cycle after this code lands in Stable. |
| base::TimeDelta delay = |
| base::Days((kPasswordCaptureEventLogFreqDaysMin + |
| base::RandInt(0, kPasswordCaptureEventLogFreqDaysExtra))); |
| SetLogPasswordCaptureTimer(delay); |
| |
| // Write the deadline to a pref to carry over restarts. |
| SetDelayInPref(profile_->GetPrefs(), |
| prefs::kSafeBrowsingNextPasswordCaptureEventLogTime, delay); |
| } |
| |
| void ChromePasswordProtectionService::UpdateSecurityState( |
| SBThreatType threat_type, |
| ReusedPasswordAccountType password_type, |
| content::WebContents* web_contents) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| const GURL url = web_contents->GetLastCommittedURL(); |
| if (!url.is_valid()) |
| return; |
| |
| const GURL url_with_empty_path = url.GetWithEmptyPath(); |
| if (threat_type == SB_THREAT_TYPE_SAFE) { |
| ui_manager_->RemoveAllowlistUrlSet(url_with_empty_path, web_contents, |
| /*from_pending_only=*/false); |
| // Overrides cached verdicts. |
| LoginReputationClientResponse verdict; |
| GetCachedVerdict(url, LoginReputationClientRequest::PASSWORD_REUSE_EVENT, |
| password_type, &verdict); |
| verdict.set_verdict_type(LoginReputationClientResponse::SAFE); |
| verdict.set_cache_duration_sec(kOverrideVerdictCacheDurationSec); |
| CacheVerdict(url, LoginReputationClientRequest::PASSWORD_REUSE_EVENT, |
| password_type, verdict, base::Time::Now()); |
| return; |
| } |
| |
| SBThreatType current_threat_type = SB_THREAT_TYPE_UNUSED; |
| // If user already click-through interstitial warning, or if there's already |
| // a dangerous security state showing, we'll override it. |
| if (ui_manager_->IsUrlAllowlistedOrPendingForWebContents( |
| url_with_empty_path, /*is_subresource=*/false, |
| web_contents->GetController().GetLastCommittedEntry(), web_contents, |
| /*allowlist_only=*/false, ¤t_threat_type)) { |
| DCHECK_NE(SB_THREAT_TYPE_UNUSED, current_threat_type); |
| if (current_threat_type == threat_type) |
| return; |
| // Resets previous threat type. |
| ui_manager_->RemoveAllowlistUrlSet(url_with_empty_path, web_contents, |
| /*from_pending_only=*/false); |
| } |
| ui_manager_->AddToAllowlistUrlSet(url_with_empty_path, web_contents, |
| /*is_pending=*/true, threat_type); |
| } |
| |
| void ChromePasswordProtectionService::FillReferrerChain( |
| const GURL& event_url, |
| SessionID event_tab_id, |
| LoginReputationClientRequest::Frame* frame) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| SafeBrowsingNavigationObserverManager* navigation_observer_manager = |
| SafeBrowsingNavigationObserverManagerFactory::GetForBrowserContext( |
| profile_); |
| SafeBrowsingNavigationObserverManager::AttributionResult result = |
| navigation_observer_manager->IdentifyReferrerChainByEventURL( |
| event_url, event_tab_id, kPasswordEventAttributionUserGestureLimit, |
| frame->mutable_referrer_chain()); |
| size_t referrer_chain_length = frame->referrer_chain().size(); |
| UMA_HISTOGRAM_COUNTS_100( |
| "SafeBrowsing.ReferrerURLChainSize.PasswordEventAttribution", |
| referrer_chain_length); |
| UMA_HISTOGRAM_ENUMERATION( |
| "SafeBrowsing.ReferrerAttributionResult.PasswordEventAttribution", result, |
| SafeBrowsingNavigationObserverManager::ATTRIBUTION_FAILURE_TYPE_MAX); |
| |
| // Determines how many recent navigation events to append to referrer chain. |
| size_t recent_navigations_to_collect = |
| profile_ ? SafeBrowsingNavigationObserverManager:: |
| CountOfRecentNavigationsToAppend( |
| profile_, profile_->GetPrefs(), result) |
| : 0u; |
| navigation_observer_manager->AppendRecentNavigations( |
| recent_navigations_to_collect, frame->mutable_referrer_chain()); |
| } |
| |
| std::string ChromePasswordProtectionService::GetSyncPasswordHashFromPrefs() { |
| if (!sync_password_hash_provider_for_testing_.is_null()) |
| return sync_password_hash_provider_for_testing_.Run(); |
| |
| password_manager::HashPasswordManager hash_password_manager; |
| hash_password_manager.set_prefs(profile_->GetPrefs()); |
| absl::optional<password_manager::PasswordHashData> sync_hash_data = |
| hash_password_manager.RetrievePasswordHash(GetAccountInfo().email, |
| /*is_gaia_password=*/true); |
| return sync_hash_data ? base::NumberToString(sync_hash_data->hash) |
| : std::string(); |
| } |
| |
| PrefService* ChromePasswordProtectionService::GetPrefs() { |
| return profile_->GetPrefs(); |
| } |
| |
| bool ChromePasswordProtectionService::IsSafeBrowsingEnabled() { |
| return ::safe_browsing::IsSafeBrowsingEnabled(*GetPrefs()); |
| } |
| |
| bool ChromePasswordProtectionService::IsExtendedReporting() { |
| return IsExtendedReportingEnabled(*GetPrefs()); |
| } |
| |
| bool ChromePasswordProtectionService::IsIncognito() { |
| return profile_->IsOffTheRecord(); |
| } |
| |
| bool ChromePasswordProtectionService::IsInPasswordAlertMode( |
| ReusedPasswordAccountType password_type) { |
| return GetPasswordProtectionWarningTriggerPref(password_type) == |
| PASSWORD_REUSE; |
| } |
| |
| bool ChromePasswordProtectionService::IsPingingEnabled( |
| LoginReputationClientRequest::TriggerType trigger_type, |
| ReusedPasswordAccountType password_type) { |
| if (!IsSafeBrowsingEnabled()) { |
| return false; |
| } |
| bool extended_reporting_enabled = IsExtendedReporting(); |
| if (trigger_type == LoginReputationClientRequest::PASSWORD_REUSE_EVENT) { |
| if (password_type.account_type() == |
| ReusedPasswordAccountType::SAVED_PASSWORD) { |
| return true; |
| } |
| |
| // Only override policy if password protection is off for Gmail users. |
| if (GetPasswordProtectionWarningTriggerPref(password_type) == |
| PASSWORD_PROTECTION_OFF) { |
| return false; |
| } |
| // If the account type is UNKNOWN (i.e. AccountInfo fields could not be |
| // retrieved from server), pings should be gated by SBER. |
| if (password_type.account_type() == ReusedPasswordAccountType::UNKNOWN) { |
| return extended_reporting_enabled; |
| } |
| |
| // Only saved password and GAIA password reuse warnings are shown to users on |
| // Android, so other types of password reuse events should be gated by Safe |
| // Browsing extended reporting. |
| #if defined(OS_ANDROID) |
| if (password_type.account_type() == |
| ReusedPasswordAccountType::SAVED_PASSWORD || |
| IsSyncingGMAILPasswordWithSignedInProtectionEnabled(password_type)) { |
| return true; |
| } |
| |
| return extended_reporting_enabled; |
| #else |
| return true; |
| #endif |
| } |
| |
| return !IsIncognito() && extended_reporting_enabled; |
| } |
| |
| RequestOutcome ChromePasswordProtectionService::GetPingNotSentReason( |
| LoginReputationClientRequest::TriggerType trigger_type, |
| const GURL& url, |
| ReusedPasswordAccountType password_type) { |
| DCHECK(!CanSendPing(trigger_type, url, password_type)); |
| if (IsInExcludedCountry()) { |
| return RequestOutcome::EXCLUDED_COUNTRY; |
| } |
| if (!IsSafeBrowsingEnabled()) { |
| return RequestOutcome::SAFE_BROWSING_DISABLED; |
| } |
| if (IsIncognito()) { |
| return RequestOutcome::DISABLED_DUE_TO_INCOGNITO; |
| } |
| if (trigger_type == LoginReputationClientRequest::PASSWORD_REUSE_EVENT && |
| password_type.account_type() != |
| ReusedPasswordAccountType::SAVED_PASSWORD && |
| GetPasswordProtectionWarningTriggerPref(password_type) == |
| PASSWORD_PROTECTION_OFF) { |
| return RequestOutcome::TURNED_OFF_BY_ADMIN; |
| } |
| PrefService* prefs = profile_->GetPrefs(); |
| if (IsURLAllowlistedByPolicy(url, *prefs)) { |
| return RequestOutcome::MATCHED_ENTERPRISE_ALLOWLIST; |
| } |
| if (MatchesPasswordProtectionChangePasswordURL(url, *prefs)) { |
| return RequestOutcome::MATCHED_ENTERPRISE_CHANGE_PASSWORD_URL; |
| } |
| if (MatchesPasswordProtectionLoginURL(url, *prefs)) { |
| return RequestOutcome::MATCHED_ENTERPRISE_LOGIN_URL; |
| } |
| if (IsInPasswordAlertMode(password_type)) { |
| return RequestOutcome::PASSWORD_ALERT_MODE; |
| } |
| return RequestOutcome::DISABLED_DUE_TO_USER_POPULATION; |
| } |
| |
| void ChromePasswordProtectionService::FillUserPopulation( |
| const GURL& main_frame_url, |
| LoginReputationClientRequest* request_proto) { |
| *request_proto->mutable_population() = GetUserPopulationForProfile(profile_); |
| |
| if (!base::FeatureList::IsEnabled(kSafeBrowsingPageLoadToken)) { |
| return; |
| } |
| ChromeUserPopulation::PageLoadToken token = |
| cache_manager_->GetPageLoadToken(main_frame_url); |
| if (RealTimePolicyEngine::CanPerformFullURLLookup( |
| profile_->GetPrefs(), profile_->IsOffTheRecord(), |
| g_browser_process->variations_service())) { |
| base::UmaHistogramBoolean( |
| "SafeBrowsing.PageLoadToken.PasswordProtectionHasToken", |
| token.has_token_value()); |
| } |
| // It's possible that the token is not found because real time URL check is |
| // not performed for this navigation. Create a new page load token in this |
| // case. |
| if (!token.has_token_value()) { |
| token = cache_manager_->CreatePageLoadToken(main_frame_url); |
| } |
| request_proto->mutable_population()->mutable_page_load_tokens()->Add()->Swap( |
| &token); |
| } |
| |
| bool ChromePasswordProtectionService::IsPrimaryAccountSyncing() const { |
| syncer::SyncService* sync = SyncServiceFactory::GetForProfile(profile_); |
| return sync && sync->IsSyncFeatureActive() && !sync->IsLocalSyncEnabled(); |
| } |
| |
| bool ChromePasswordProtectionService::IsPrimaryAccountSignedIn() const { |
| return !GetAccountInfo().account_id.empty() && |
| !GetAccountInfo().hosted_domain.empty(); |
| } |
| |
| bool ChromePasswordProtectionService::IsAccountGmail( |
| const std::string& username) const { |
| return GetAccountInfoForUsername(username).hosted_domain == |
| kNoHostedDomainFound; |
| } |
| |
| AccountInfo ChromePasswordProtectionService::GetAccountInfoForUsername( |
| const std::string& username) const { |
| auto* identity_manager = IdentityManagerFactory::GetForProfileIfExists( |
| profile_->GetOriginalProfile()); |
| |
| if (!identity_manager) |
| return AccountInfo(); |
| |
| std::vector<CoreAccountInfo> signed_in_accounts = |
| identity_manager->GetAccountsWithRefreshTokens(); |
| auto account_iterator = |
| std::find_if(signed_in_accounts.begin(), signed_in_accounts.end(), |
| [username](const auto& account) { |
| return password_manager::AreUsernamesSame( |
| account.email, |
| /*is_username1_gaia_account=*/true, username, |
| /*is_username2_gaia_account=*/true); |
| }); |
| if (account_iterator == signed_in_accounts.end()) |
| return AccountInfo(); |
| |
| return identity_manager->FindExtendedAccountInfo(*account_iterator); |
| } |
| |
| bool ChromePasswordProtectionService::IsInExcludedCountry() { |
| variations::VariationsService* variations_service = |
| g_browser_process->variations_service(); |
| if (!variations_service) |
| return false; |
| return base::Contains(GetExcludedCountries(), |
| variations_service->GetStoredPermanentCountry()); |
| } |
| |
| PasswordReuseEvent::SyncAccountType |
| ChromePasswordProtectionService::GetSyncAccountType() const { |
| const AccountInfo account_info = GetAccountInfo(); |
| if (!IsPrimaryAccountSignedIn()) { |
| return PasswordReuseEvent::NOT_SIGNED_IN; |
| } |
| |
| // For gmail or googlemail account, the hosted_domain will always be |
| // kNoHostedDomainFound. |
| return account_info.hosted_domain == kNoHostedDomainFound |
| ? PasswordReuseEvent::GMAIL |
| : PasswordReuseEvent::GSUITE; |
| } |
| |
| void ChromePasswordProtectionService:: |
| RemoveUnhandledSyncPasswordReuseOnURLsDeleted( |
| bool all_history, |
| const history::URLRows& deleted_rows) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| DictionaryPrefUpdate unhandled_sync_password_reuses( |
| profile_->GetPrefs(), prefs::kSafeBrowsingUnhandledGaiaPasswordReuses); |
| if (all_history) { |
| unhandled_sync_password_reuses->Clear(); |
| return; |
| } |
| |
| for (const history::URLRow& row : deleted_rows) { |
| if (!row.url().SchemeIsHTTPOrHTTPS()) |
| continue; |
| unhandled_sync_password_reuses->RemoveKey( |
| Origin::Create(row.url()).Serialize()); |
| } |
| } |
| |
| bool ChromePasswordProtectionService::UserClickedThroughSBInterstitial( |
| PasswordProtectionRequest* request) { |
| PasswordProtectionRequestContent* request_content = |
| static_cast<PasswordProtectionRequestContent*>(request); |
| content::WebContents* web_contents = request_content->web_contents(); |
| SBThreatType current_threat_type; |
| if (!ui_manager_->IsUrlAllowlistedOrPendingForWebContents( |
| web_contents->GetLastCommittedURL().GetWithEmptyPath(), |
| /*is_subresource=*/false, |
| web_contents->GetController().GetLastCommittedEntry(), web_contents, |
| /*allowlist_only=*/true, ¤t_threat_type)) { |
| return false; |
| } |
| return current_threat_type == SB_THREAT_TYPE_URL_PHISHING || |
| current_threat_type == SB_THREAT_TYPE_URL_MALWARE || |
| current_threat_type == SB_THREAT_TYPE_URL_UNWANTED || |
| current_threat_type == SB_THREAT_TYPE_URL_CLIENT_SIDE_PHISHING || |
| current_threat_type == SB_THREAT_TYPE_URL_CLIENT_SIDE_MALWARE; |
| } |
| |
| AccountInfo ChromePasswordProtectionService::GetAccountInfo() const { |
| auto* identity_manager = IdentityManagerFactory::GetForProfileIfExists( |
| profile_->GetOriginalProfile()); |
| if (!identity_manager) |
| return AccountInfo(); |
| |
| return identity_manager->FindExtendedAccountInfo( |
| identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSync)); |
| } |
| |
| ChromeUserPopulation::UserPopulation |
| ChromePasswordProtectionService::GetUserPopulationPref() const { |
| return ::safe_browsing::GetUserPopulationPref(profile_->GetPrefs()); |
| } |
| |
| ChromePasswordProtectionService::ChromePasswordProtectionService( |
| Profile* profile, |
| scoped_refptr<SafeBrowsingUIManager> ui_manager, |
| StringProvider sync_password_hash_provider, |
| VerdictCacheManager* cache_manager, |
| ChangePhishedCredentialsCallback add_phished_credentials, |
| ChangePhishedCredentialsCallback remove_phished_credentials) |
| : PasswordProtectionService( |
| nullptr, |
| nullptr, |
| nullptr, |
| nullptr, |
| nullptr, |
| false, |
| nullptr, |
| /*try_token_fetch=*/false, |
| SafeBrowsingMetricsCollectorFactory::GetForProfile(profile)), |
| ui_manager_(ui_manager), |
| trigger_manager_(nullptr), |
| profile_(profile), |
| cache_manager_(cache_manager), |
| add_phished_credentials_(std::move(add_phished_credentials)), |
| remove_phished_credentials_(std::move(remove_phished_credentials)), |
| sync_password_hash_provider_for_testing_(sync_password_hash_provider) { |
| Init(); |
| } |
| |
| std::unique_ptr<PasswordProtectionNavigationThrottle> |
| MaybeCreateNavigationThrottle(content::NavigationHandle* navigation_handle) { |
| Profile* profile = Profile::FromBrowserContext( |
| navigation_handle->GetWebContents()->GetBrowserContext()); |
| ChromePasswordProtectionService* service = |
| ChromePasswordProtectionService::GetPasswordProtectionService(profile); |
| // |service| can be null in tests. |
| return service ? service->MaybeCreateNavigationThrottle(navigation_handle) |
| : nullptr; |
| } |
| |
| PasswordProtectionTrigger |
| ChromePasswordProtectionService::GetPasswordProtectionWarningTriggerPref( |
| ReusedPasswordAccountType password_type) const { |
| if (password_type.account_type() == ReusedPasswordAccountType::GMAIL || |
| (password_type.account_type() == |
| ReusedPasswordAccountType::SAVED_PASSWORD)) |
| return PHISHING_REUSE; |
| |
| bool is_policy_managed = profile_->GetPrefs()->HasPrefPath( |
| prefs::kPasswordProtectionWarningTrigger); |
| PasswordProtectionTrigger trigger_level = |
| static_cast<PasswordProtectionTrigger>(profile_->GetPrefs()->GetInteger( |
| prefs::kPasswordProtectionWarningTrigger)); |
| return is_policy_managed ? trigger_level : PHISHING_REUSE; |
| } |
| |
| bool ChromePasswordProtectionService::IsURLAllowlistedForPasswordEntry( |
| const GURL& url) const { |
| if (!profile_) |
| return false; |
| |
| PrefService* prefs = profile_->GetPrefs(); |
| bool is_url_allowlisted_by_policy = IsURLAllowlistedByPolicy(url, *prefs); |
| bool matches_change_password_url = |
| MatchesPasswordProtectionChangePasswordURL(url, *prefs); |
| bool matches_login_url = MatchesPasswordProtectionLoginURL(url, *prefs); |
| |
| CRSBLOG << __func__ << " URL that is being checked if allowlisted: " << url |
| << " matches URL allowlist? " << is_url_allowlisted_by_policy |
| << " matches password protection change password URL? " |
| << matches_change_password_url |
| << " matches password protection login URL? " << matches_login_url; |
| return is_url_allowlisted_by_policy || matches_change_password_url || |
| matches_login_url; |
| } |
| |
| void ChromePasswordProtectionService::PersistPhishedSavedPasswordCredential( |
| const std::vector<password_manager::MatchingReusedCredential>& |
| matching_reused_credentials) { |
| if (!profile_) |
| return; |
| |
| for (const auto& credential : matching_reused_credentials) { |
| password_manager::PasswordStoreInterface* password_store = |
| GetStoreForReusedCredential(credential); |
| // Password store can be null in tests. |
| if (!password_store) { |
| continue; |
| } |
| LogCredentialPhishedStatusChanged( |
| CredentialPhishedStatus::kMarkedAsPhished); |
| add_phished_credentials_.Run(password_store, credential); |
| } |
| } |
| |
| void ChromePasswordProtectionService::RemovePhishedSavedPasswordCredential( |
| const std::vector<password_manager::MatchingReusedCredential>& |
| matching_reused_credentials) { |
| if (!profile_) |
| return; |
| |
| for (const auto& credential : matching_reused_credentials) { |
| password_manager::PasswordStoreInterface* password_store = |
| GetStoreForReusedCredential(credential); |
| // Password store can be null in tests. |
| if (!password_store) { |
| continue; |
| } |
| LogCredentialPhishedStatusChanged( |
| CredentialPhishedStatus::kSiteMarkedAsLegitimate); |
| remove_phished_credentials_.Run(password_store, credential); |
| } |
| } |
| |
| #if defined(OS_ANDROID) |
| LoginReputationClientRequest::ReferringAppInfo |
| ChromePasswordProtectionService::GetReferringAppInfo( |
| content::WebContents* web_contents) { |
| return safe_browsing::GetReferringAppInfo(web_contents); |
| } |
| #endif |
| |
| password_manager::PasswordReuseManager* |
| ChromePasswordProtectionService::GetPasswordReuseManager() const { |
| return PasswordReuseManagerFactory::GetForProfile(profile_); |
| } |
| |
| password_manager::PasswordStoreInterface* |
| ChromePasswordProtectionService::GetProfilePasswordStore() const { |
| // Always use EXPLICIT_ACCESS as the password manager checks IsIncognito |
| // itself when it shouldn't access the PasswordStoreInterface. |
| return PasswordStoreFactory::GetForProfile(profile_, |
| ServiceAccessType::EXPLICIT_ACCESS) |
| .get(); |
| } |
| |
| password_manager::PasswordStoreInterface* |
| ChromePasswordProtectionService::GetAccountPasswordStore() const { |
| // Always use EXPLICIT_ACCESS as the password manager checks IsIncognito |
| // itself when it shouldn't access the PasswordStoreInterface. |
| return AccountPasswordStoreFactory::GetForProfile( |
| profile_, ServiceAccessType::EXPLICIT_ACCESS) |
| .get(); |
| } |
| |
| void ChromePasswordProtectionService::SanitizeReferrerChain( |
| ReferrerChain* referrer_chain) { |
| SafeBrowsingNavigationObserverManager::SanitizeReferrerChain(referrer_chain); |
| } |
| |
| bool ChromePasswordProtectionService::CanSendSamplePing() { |
| // Send a sample ping only 1% of the time. |
| return IsExtendedReporting() && !IsIncognito() && |
| (bypass_probability_for_tests_ || |
| base::RandDouble() <= kProbabilityForSendingReportsFromSafeURLs); |
| } |
| |
| // Stores |verdict| in |settings| based on its |trigger_type|, |url|, |
| // reused |password_type|, |verdict| and |receive_time|. |
| void ChromePasswordProtectionService::CacheVerdict( |
| const GURL& url, |
| LoginReputationClientRequest::TriggerType trigger_type, |
| ReusedPasswordAccountType password_type, |
| const LoginReputationClientResponse& verdict, |
| const base::Time& receive_time) { |
| if (!CanGetReputationOfURL(url) || IsIncognito()) |
| return; |
| cache_manager_->CachePhishGuardVerdict(trigger_type, password_type, verdict, |
| receive_time); |
| } |
| |
| // Looks up |settings| to find the cached verdict response. If verdict is not |
| // available or is expired, return VERDICT_TYPE_UNSPECIFIED. Can be called on |
| // any thread. |
| LoginReputationClientResponse::VerdictType |
| ChromePasswordProtectionService::GetCachedVerdict( |
| const GURL& url, |
| LoginReputationClientRequest::TriggerType trigger_type, |
| ReusedPasswordAccountType password_type, |
| LoginReputationClientResponse* out_response) { |
| if (!url.is_valid() || !CanGetReputationOfURL(url)) |
| return LoginReputationClientResponse::VERDICT_TYPE_UNSPECIFIED; |
| return cache_manager_->GetCachedPhishGuardVerdict( |
| url, trigger_type, password_type, out_response); |
| } |
| |
| int ChromePasswordProtectionService::GetStoredVerdictCount( |
| LoginReputationClientRequest::TriggerType trigger_type) { |
| return cache_manager_->GetStoredPhishGuardVerdictCount(trigger_type); |
| } |
| |
| #if BUILDFLAG(FULL_SAFE_BROWSING) |
| gfx::Size ChromePasswordProtectionService::GetCurrentContentAreaSize() const { |
| return BrowserView::GetBrowserViewForBrowser( |
| BrowserList::GetInstance()->GetLastActive()) |
| ->GetContentsSize(); |
| } |
| #endif // FULL_SAFE_BROWSING |
| |
| password_manager::PasswordStoreInterface* |
| ChromePasswordProtectionService::GetStoreForReusedCredential( |
| const password_manager::MatchingReusedCredential& reused_credential) { |
| if (!profile_) |
| return nullptr; |
| return reused_credential.in_store == |
| password_manager::PasswordForm::Store::kAccountStore |
| ? GetAccountPasswordStore() |
| : GetProfilePasswordStore(); |
| } |
| |
| } // namespace safe_browsing |