| // Copyright 2018 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/safe_browsing/advanced_protection_status_manager.h" |
| |
| #include "base/bind.h" |
| #include "base/feature_list.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/safe_browsing/advanced_protection_status_manager_factory.h" |
| #include "chrome/browser/signin/identity_manager_factory.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/safe_browsing/common/safe_browsing_prefs.h" |
| #include "components/safe_browsing/features.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "google_apis/gaia/gaia_constants.h" |
| #include "google_apis/gaia/oauth2_id_token_decoder.h" |
| #include "services/identity/public/cpp/accounts_mutator.h" |
| #include "services/identity/public/cpp/primary_account_access_token_fetcher.h" |
| |
| using content::BrowserThread; |
| |
| namespace safe_browsing { |
| |
| namespace { |
| |
| const base::TimeDelta kRefreshAdvancedProtectionDelay = |
| base::TimeDelta::FromDays(1); |
| const base::TimeDelta kRetryDelay = base::TimeDelta::FromMinutes(5); |
| const base::TimeDelta kMinimumRefreshDelay = base::TimeDelta::FromMinutes(1); |
| } // namespace |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // AdvancedProtectionStatusManager |
| //////////////////////////////////////////////////////////////////////////////// |
| AdvancedProtectionStatusManager::AdvancedProtectionStatusManager( |
| Profile* profile) |
| : profile_(profile), |
| identity_manager_(nullptr), |
| access_token_fetcher_(nullptr), |
| is_under_advanced_protection_(false), |
| minimum_delay_(kMinimumRefreshDelay) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| if (profile_->IsOffTheRecord()) |
| return; |
| |
| Initialize(); |
| MaybeRefreshOnStartUp(); |
| } |
| |
| void AdvancedProtectionStatusManager::Initialize() { |
| identity_manager_ = IdentityManagerFactory::GetForProfile(profile_); |
| SubscribeToSigninEvents(); |
| } |
| |
| void AdvancedProtectionStatusManager::MaybeRefreshOnStartUp() { |
| // Retrieves advanced protection service status from primary account's info. |
| CoreAccountInfo core_info = identity_manager_->GetPrimaryAccountInfo(); |
| if (core_info.account_id.empty()) |
| return; |
| |
| is_under_advanced_protection_ = core_info.is_under_advanced_protection; |
| |
| if (profile_->GetPrefs()->HasPrefPath( |
| prefs::kAdvancedProtectionLastRefreshInUs)) { |
| last_refreshed_ = base::Time::FromDeltaSinceWindowsEpoch( |
| base::TimeDelta::FromMicroseconds(profile_->GetPrefs()->GetInt64( |
| prefs::kAdvancedProtectionLastRefreshInUs))); |
| if (is_under_advanced_protection_) |
| ScheduleNextRefresh(); |
| } else { |
| // User's advanced protection status is unknown, refresh in |
| // |minimum_delay_|. |
| timer_.Start( |
| FROM_HERE, minimum_delay_, this, |
| &AdvancedProtectionStatusManager::RefreshAdvancedProtectionStatus); |
| } |
| } |
| |
| void AdvancedProtectionStatusManager::Shutdown() { |
| CancelFutureRefresh(); |
| UnsubscribeFromSigninEvents(); |
| } |
| |
| AdvancedProtectionStatusManager::~AdvancedProtectionStatusManager() {} |
| |
| void AdvancedProtectionStatusManager::SubscribeToSigninEvents() { |
| IdentityManagerFactory::GetForProfile(profile_)->AddObserver(this); |
| } |
| |
| void AdvancedProtectionStatusManager::UnsubscribeFromSigninEvents() { |
| IdentityManagerFactory::GetForProfile(profile_)->RemoveObserver(this); |
| } |
| |
| bool AdvancedProtectionStatusManager::IsRefreshScheduled() { |
| return timer_.IsRunning(); |
| } |
| |
| void AdvancedProtectionStatusManager::OnExtendedAccountInfoUpdated( |
| const AccountInfo& info) { |
| // Ignore update if |profile_| is in incognito mode, or the updated account |
| // is not the primary account. |
| if (profile_->IsOffTheRecord() || !IsPrimaryAccount(info)) |
| return; |
| |
| if (info.is_under_advanced_protection) { |
| // User just enrolled into advanced protection. |
| OnAdvancedProtectionEnabled(); |
| } else { |
| // User's no longer in advanced protection. |
| OnAdvancedProtectionDisabled(); |
| } |
| } |
| |
| void AdvancedProtectionStatusManager::OnExtendedAccountInfoRemoved( |
| const AccountInfo& info) { |
| if (profile_->IsOffTheRecord()) |
| return; |
| |
| // If user signed out primary account, cancel refresh. |
| std::string primary_account_id = GetPrimaryAccountId(); |
| if (!primary_account_id.empty() && primary_account_id == info.account_id) { |
| is_under_advanced_protection_ = false; |
| OnAdvancedProtectionDisabled(); |
| } |
| } |
| |
| void AdvancedProtectionStatusManager::OnPrimaryAccountSet( |
| const CoreAccountInfo& account_info) { |
| // TODO(crbug.com/926204): remove IdentityManager ensures that primary account |
| // always has valid refresh token when it is set. |
| if (account_info.is_under_advanced_protection) |
| OnAdvancedProtectionEnabled(); |
| } |
| |
| void AdvancedProtectionStatusManager::OnPrimaryAccountCleared( |
| const CoreAccountInfo& account_info) { |
| OnAdvancedProtectionDisabled(); |
| } |
| |
| void AdvancedProtectionStatusManager::OnAdvancedProtectionEnabled() { |
| is_under_advanced_protection_ = true; |
| UpdateLastRefreshTime(); |
| ScheduleNextRefresh(); |
| } |
| |
| void AdvancedProtectionStatusManager::OnAdvancedProtectionDisabled() { |
| is_under_advanced_protection_ = false; |
| UpdateLastRefreshTime(); |
| CancelFutureRefresh(); |
| } |
| |
| void AdvancedProtectionStatusManager::OnAccessTokenFetchComplete( |
| std::string account_id, |
| GoogleServiceAuthError error, |
| identity::AccessTokenInfo token_info) { |
| DCHECK(access_token_fetcher_); |
| |
| if (is_under_advanced_protection_) { |
| // Those already known to be under AP should have much lower error rates. |
| UMA_HISTOGRAM_ENUMERATION( |
| "SafeBrowsing.AdvancedProtection.APTokenFetchStatus", error.state(), |
| GoogleServiceAuthError::NUM_STATES); |
| } |
| |
| if (error.state() == GoogleServiceAuthError::NONE) |
| OnGetIDToken(account_id, token_info.id_token); |
| |
| UMA_HISTOGRAM_ENUMERATION("SafeBrowsing.AdvancedProtection.TokenFetchStatus", |
| error.state(), GoogleServiceAuthError::NUM_STATES); |
| |
| access_token_fetcher_.reset(); |
| |
| // If failure is transient, we'll retry in 5 minutes. |
| if (error.IsTransientError()) { |
| timer_.Start( |
| FROM_HERE, kRetryDelay, this, |
| &AdvancedProtectionStatusManager::RefreshAdvancedProtectionStatus); |
| } |
| } |
| |
| void AdvancedProtectionStatusManager::RefreshAdvancedProtectionStatus() { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| std::string primary_account_id = GetPrimaryAccountId(); |
| if (!identity_manager_ || primary_account_id.empty()) |
| return; |
| |
| // If there's already a request going on, do nothing. |
| if (access_token_fetcher_) |
| return; |
| |
| // Refresh OAuth access token. |
| identity::ScopeSet scopes; |
| scopes.insert(GaiaConstants::kOAuth1LoginScope); |
| |
| access_token_fetcher_ = |
| std::make_unique<identity::PrimaryAccountAccessTokenFetcher>( |
| "advanced_protection_status_manager", identity_manager_, scopes, |
| base::BindOnce( |
| &AdvancedProtectionStatusManager::OnAccessTokenFetchComplete, |
| base::Unretained(this), primary_account_id), |
| identity::PrimaryAccountAccessTokenFetcher::Mode::kImmediate); |
| } |
| |
| void AdvancedProtectionStatusManager::ScheduleNextRefresh() { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| CancelFutureRefresh(); |
| base::Time now = base::Time::Now(); |
| const base::TimeDelta time_since_last_refresh = |
| now > last_refreshed_ ? now - last_refreshed_ : base::TimeDelta::Max(); |
| base::TimeDelta delay = |
| time_since_last_refresh > kRefreshAdvancedProtectionDelay |
| ? minimum_delay_ |
| : std::max(minimum_delay_, |
| kRefreshAdvancedProtectionDelay - time_since_last_refresh); |
| timer_.Start( |
| FROM_HERE, delay, this, |
| &AdvancedProtectionStatusManager::RefreshAdvancedProtectionStatus); |
| } |
| void AdvancedProtectionStatusManager::CancelFutureRefresh() { |
| if (timer_.IsRunning()) |
| timer_.Stop(); |
| } |
| |
| void AdvancedProtectionStatusManager::UpdateLastRefreshTime() { |
| last_refreshed_ = base::Time::Now(); |
| profile_->GetPrefs()->SetInt64( |
| prefs::kAdvancedProtectionLastRefreshInUs, |
| last_refreshed_.ToDeltaSinceWindowsEpoch().InMicroseconds()); |
| } |
| |
| // static |
| bool AdvancedProtectionStatusManager::IsUnderAdvancedProtection( |
| Profile* profile) { |
| Profile* original_profile = |
| profile->IsOffTheRecord() ? profile->GetOriginalProfile() : profile; |
| |
| return original_profile && |
| AdvancedProtectionStatusManagerFactory::GetInstance() |
| ->GetForBrowserContext( |
| static_cast<content::BrowserContext*>(original_profile)) |
| ->is_under_advanced_protection(); |
| } |
| |
| // static |
| bool AdvancedProtectionStatusManager::RequestsAdvancedProtectionVerdicts( |
| Profile* profile) { |
| Profile* original_profile = |
| profile->IsOffTheRecord() ? profile->GetOriginalProfile() : profile; |
| |
| if (!original_profile) |
| return false; |
| |
| bool is_under_advanced_protection = |
| AdvancedProtectionStatusManagerFactory::GetInstance() |
| ->GetForBrowserContext( |
| static_cast<content::BrowserContext*>(original_profile)) |
| ->is_under_advanced_protection(); |
| |
| static bool force_enabled = |
| base::FeatureList::IsEnabled(kForceUseAPDownloadProtection); |
| static bool enabled = base::FeatureList::IsEnabled(kUseAPDownloadProtection); |
| |
| return force_enabled || (is_under_advanced_protection && enabled); |
| } |
| |
| bool AdvancedProtectionStatusManager::IsPrimaryAccount( |
| const CoreAccountInfo& account_info) { |
| return !account_info.account_id.empty() && |
| account_info.account_id == GetPrimaryAccountId(); |
| } |
| |
| void AdvancedProtectionStatusManager::OnGetIDToken( |
| const std::string& account_id, |
| const std::string& id_token) { |
| // Skips if the ID token is not for the primary account. Or user is no longer |
| // signed in. |
| std::string primary_account_id = GetPrimaryAccountId(); |
| if (primary_account_id.empty() || account_id != primary_account_id) |
| return; |
| |
| gaia::TokenServiceFlags service_flags = gaia::ParseServiceFlags(id_token); |
| |
| // If there's a change in advanced protection status, updates account info. |
| // This also triggers |OnAccountUpdated()|. |
| if (is_under_advanced_protection_ != |
| service_flags.is_under_advanced_protection) { |
| identity_manager_->GetAccountsMutator()->UpdateAccountInfo( |
| GetPrimaryAccountId(), false, |
| service_flags.is_under_advanced_protection); |
| } else if (service_flags.is_under_advanced_protection) { |
| OnAdvancedProtectionEnabled(); |
| } else { |
| OnAdvancedProtectionDisabled(); |
| } |
| } |
| |
| AdvancedProtectionStatusManager::AdvancedProtectionStatusManager( |
| Profile* profile, |
| const base::TimeDelta& min_delay) |
| : profile_(profile), |
| identity_manager_(nullptr), |
| is_under_advanced_protection_(false), |
| minimum_delay_(min_delay) { |
| if (profile_->IsOffTheRecord()) |
| return; |
| Initialize(); |
| MaybeRefreshOnStartUp(); |
| } |
| |
| std::string AdvancedProtectionStatusManager::GetPrimaryAccountId() const { |
| // TODO(triploblastic@): Remove explicit conversion once |
| // AdvancedProtectionStatusManager has been fixed to use CoreAccountId. |
| return identity_manager_ ? identity_manager_->GetPrimaryAccountId().id |
| : std::string(); |
| } |
| |
| } // namespace safe_browsing |