| // Copyright 2020 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/extensions/api/passwords_private/password_check_delegate.h" |
| |
| #include <stddef.h> |
| |
| #include <algorithm> |
| #include <iterator> |
| #include <map> |
| #include <memory> |
| #include <utility> |
| |
| #include "base/containers/flat_set.h" |
| #include "base/memory/ref_counted.h" |
| #include "base/numerics/safe_conversions.h" |
| #include "base/optional.h" |
| #include "base/stl_util.h" |
| #include "base/strings/string16.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/threading/sequenced_task_runner_handle.h" |
| #include "base/time/time.h" |
| #include "chrome/browser/extensions/api/passwords_private/passwords_private_event_router.h" |
| #include "chrome/browser/extensions/api/passwords_private/passwords_private_event_router_factory.h" |
| #include "chrome/browser/extensions/api/passwords_private/passwords_private_utils.h" |
| #include "chrome/browser/password_manager/bulk_leak_check_service_factory.h" |
| #include "chrome/browser/password_manager/password_store_factory.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/common/extensions/api/passwords_private.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "components/autofill/core/common/password_form.h" |
| #include "components/keyed_service/core/service_access_type.h" |
| #include "components/password_manager/core/browser/android_affiliation/affiliation_utils.h" |
| #include "components/password_manager/core/browser/bulk_leak_check_service.h" |
| #include "components/password_manager/core/browser/compromised_credentials_table.h" |
| #include "components/password_manager/core/browser/leak_detection/bulk_leak_check.h" |
| #include "components/password_manager/core/browser/leak_detection/encryption_utils.h" |
| #include "components/password_manager/core/browser/ui/compromised_credentials_manager.h" |
| #include "components/password_manager/core/browser/ui/credential_utils.h" |
| #include "components/password_manager/core/browser/ui/saved_passwords_presenter.h" |
| #include "components/password_manager/core/common/password_manager_pref_names.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/url_formatter/elide_url.h" |
| #include "components/url_formatter/url_formatter.h" |
| #include "net/base/escape.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/l10n/time_format.h" |
| #include "url/gurl.h" |
| |
| namespace extensions { |
| |
| namespace { |
| |
| using autofill::PasswordForm; |
| using password_manager::CanonicalizeUsername; |
| using password_manager::CompromiseTypeFlags; |
| using password_manager::CredentialWithPassword; |
| using password_manager::LeakCheckCredential; |
| using ui::TimeFormat; |
| |
| using CompromisedCredentialsView = |
| password_manager::CompromisedCredentialsManager::CredentialsView; |
| using SavedPasswordsView = |
| password_manager::SavedPasswordsPresenter::SavedPasswordsView; |
| using State = password_manager::BulkLeakCheckService::State; |
| |
| } // namespace |
| |
| // Key used to attach UserData to a LeakCheckCredential. |
| constexpr char kPasswordCheckDataKey[] = "password-check-data-key"; |
| |
| // Class remembering the state required to update the progress of an ongoing |
| // Password Check. |
| class PasswordCheckProgress : public base::RefCounted<PasswordCheckProgress> { |
| public: |
| base::WeakPtr<PasswordCheckProgress> GetWeakPtr() { |
| return weak_ptr_factory_.GetWeakPtr(); |
| } |
| |
| size_t remaining_in_queue() const { return remaining_in_queue_; } |
| size_t already_processed() const { return already_processed_; } |
| |
| // Increments the counts corresponding to |password|. Intended to be called |
| // for each credential that is passed to the bulk check. |
| void IncrementCounts(const PasswordForm& password) { |
| ++remaining_in_queue_; |
| ++counts_[password]; |
| } |
| |
| // Updates the counts after a |credential| has been processed by the bulk |
| // check. |
| void OnProcessed(const LeakCheckCredential& credential) { |
| auto it = counts_.find(credential); |
| const int num_matching = it != counts_.end() ? it->second : 0; |
| already_processed_ += num_matching; |
| remaining_in_queue_ -= num_matching; |
| } |
| |
| private: |
| friend class base::RefCounted<PasswordCheckProgress>; |
| ~PasswordCheckProgress() = default; |
| |
| // Count variables needed to correctly show the progress of the check to the |
| // user. |already_processed_| contains the number of credentials that have |
| // been checked already, while |remaining_in_queue_| remembers how many |
| // passwords still need to be checked. |
| // Since the bulk leak check tries to be as efficient as possible, it performs |
| // a deduplication step before starting to check passwords. In this step it |
| // canonicalizes each credential, and only processes the combinations that are |
| // unique. Since this number likely does not match the total number of saved |
| // passwords, we remember in |counts_| how many saved passwords a given |
| // canonicalized credential corresponds to. |
| size_t already_processed_ = 0; |
| size_t remaining_in_queue_ = 0; |
| std::map<password_manager::CanonicalizedCredential, size_t> counts_; |
| |
| base::WeakPtrFactory<PasswordCheckProgress> weak_ptr_factory_{this}; |
| }; |
| |
| namespace { |
| |
| // A class attached to each LeakCheckCredential that holds a shared handle to |
| // the PasswordCheckProgress and is able to update the progress accordingly. |
| class PasswordCheckData : public LeakCheckCredential::Data { |
| public: |
| explicit PasswordCheckData(scoped_refptr<PasswordCheckProgress> progress) |
| : progress_(std::move(progress)) {} |
| ~PasswordCheckData() override = default; |
| |
| std::unique_ptr<Data> Clone() override { |
| return std::make_unique<PasswordCheckData>(progress_); |
| } |
| |
| private: |
| scoped_refptr<PasswordCheckProgress> progress_; |
| }; |
| |
| api::passwords_private::PasswordCheckState ConvertPasswordCheckState( |
| State state) { |
| switch (state) { |
| case State::kIdle: |
| return api::passwords_private::PASSWORD_CHECK_STATE_IDLE; |
| case State::kRunning: |
| return api::passwords_private::PASSWORD_CHECK_STATE_RUNNING; |
| case State::kCanceled: |
| return api::passwords_private::PASSWORD_CHECK_STATE_CANCELED; |
| case State::kSignedOut: |
| return api::passwords_private::PASSWORD_CHECK_STATE_SIGNED_OUT; |
| case State::kNetworkError: |
| return api::passwords_private::PASSWORD_CHECK_STATE_OFFLINE; |
| case State::kQuotaLimit: |
| return api::passwords_private::PASSWORD_CHECK_STATE_QUOTA_LIMIT; |
| case State::kTokenRequestFailure: |
| case State::kHashingFailure: |
| case State::kServiceError: |
| return api::passwords_private::PASSWORD_CHECK_STATE_OTHER_ERROR; |
| } |
| |
| NOTREACHED(); |
| return api::passwords_private::PASSWORD_CHECK_STATE_NONE; |
| } |
| |
| std::string FormatElapsedTime(base::Time time) { |
| const base::TimeDelta elapsed_time = base::Time::Now() - time; |
| if (elapsed_time < base::TimeDelta::FromMinutes(1)) |
| return l10n_util::GetStringUTF8(IDS_SETTINGS_PASSWORDS_JUST_NOW); |
| |
| return base::UTF16ToUTF8(TimeFormat::SimpleWithMonthAndYear( |
| TimeFormat::FORMAT_ELAPSED, TimeFormat::LENGTH_LONG, elapsed_time, true)); |
| } |
| |
| // Helper struct that bundles a CredentialWithPassword with a corresponding |
| // passwords_private::CompromiseType. |
| struct CompromisedCredentialAndType { |
| CredentialWithPassword credential; |
| api::passwords_private::CompromiseType type; |
| }; |
| |
| // Orders |compromised_credentials| in such a way that phished credentials |
| // precede leaked credentials, and that credentials of the same compromise type |
| // are ordered by recency. |
| std::vector<CompromisedCredentialAndType> OrderCompromisedCredentials( |
| std::vector<CredentialWithPassword> compromised_credentials) { |
| // Move all credentials into a single list, associating with the |
| // corresponding CompromiseType. |
| std::vector<CompromisedCredentialAndType> results; |
| results.reserve(compromised_credentials.size()); |
| for (auto& credential : compromised_credentials) { |
| auto type = static_cast<api::passwords_private::CompromiseType>( |
| credential.compromise_type); |
| results.push_back({std::move(credential), type}); |
| } |
| // Reordering phished credential to the beginning. |
| auto last_phished = std::partition( |
| results.begin(), results.end(), [](const auto& credential) { |
| return credential.type != |
| api::passwords_private::COMPROMISE_TYPE_LEAKED; |
| }); |
| |
| // By construction the phished credentials precede the leaked credentials in |
| // |results|. Now sort both groups by their creation date so that most recent |
| // compromises appear first in both lists. |
| auto create_time_cmp = [](const auto& lhs, const auto& rhs) { |
| return lhs.credential.create_time > rhs.credential.create_time; |
| }; |
| std::sort(results.begin(), last_phished, create_time_cmp); |
| std::sort(last_phished, results.end(), create_time_cmp); |
| return results; |
| } |
| |
| } // namespace |
| |
| PasswordCheckDelegate::PasswordCheckDelegate(Profile* profile) |
| : profile_(profile), |
| password_store_(PasswordStoreFactory::GetForProfile( |
| profile, |
| ServiceAccessType::EXPLICIT_ACCESS)), |
| saved_passwords_presenter_(password_store_), |
| compromised_credentials_manager_(password_store_, |
| &saved_passwords_presenter_), |
| bulk_leak_check_service_adapter_( |
| &saved_passwords_presenter_, |
| BulkLeakCheckServiceFactory::GetForProfile(profile_), |
| profile_->GetPrefs()) { |
| observed_saved_passwords_presenter_.Add(&saved_passwords_presenter_); |
| observed_compromised_credentials_manager_.Add( |
| &compromised_credentials_manager_); |
| observed_bulk_leak_check_service_.Add( |
| BulkLeakCheckServiceFactory::GetForProfile(profile_)); |
| |
| // Instructs the presenter and provider to initialize and built their caches. |
| // This will soon after invoke OnCompromisedCredentialsChanged(). Calls to |
| // GetCompromisedCredentials() that might happen until then will return an |
| // empty list. |
| saved_passwords_presenter_.Init(); |
| compromised_credentials_manager_.Init(); |
| } |
| |
| PasswordCheckDelegate::~PasswordCheckDelegate() = default; |
| |
| std::vector<api::passwords_private::CompromisedCredential> |
| PasswordCheckDelegate::GetCompromisedCredentials() { |
| std::vector<CompromisedCredentialAndType> |
| ordered_compromised_credential_and_types = OrderCompromisedCredentials( |
| compromised_credentials_manager_.GetCompromisedCredentials()); |
| |
| std::vector<api::passwords_private::CompromisedCredential> |
| compromised_credentials; |
| compromised_credentials.reserve( |
| ordered_compromised_credential_and_types.size()); |
| for (const auto& credential_and_type : |
| ordered_compromised_credential_and_types) { |
| const auto& credential = credential_and_type.credential; |
| api::passwords_private::CompromisedCredential api_credential; |
| auto facet = password_manager::FacetURI::FromPotentiallyInvalidSpec( |
| credential.signon_realm); |
| if (facet.IsValidAndroidFacetURI()) { |
| api_credential.is_android_credential = true; |
| // |formatted_orgin|, |detailed_origin| and |change_password_url| need |
| // special handling for Android. Here we use affiliation information |
| // instead of the signon_realm. |
| const PasswordForm& android_form = |
| compromised_credentials_manager_.GetSavedPasswordsFor(credential)[0]; |
| if (!android_form.app_display_name.empty()) { |
| api_credential.formatted_origin = android_form.app_display_name; |
| api_credential.detailed_origin = android_form.app_display_name; |
| api_credential.change_password_url = |
| std::make_unique<std::string>(android_form.affiliated_web_realm); |
| } else { |
| // In case no affiliation information could be obtained show the |
| // formatted package name to the user. An empty change_password_url will |
| // be handled by the frontend, by not including a link in this case. |
| api_credential.formatted_origin = l10n_util::GetStringFUTF8( |
| IDS_SETTINGS_PASSWORDS_ANDROID_APP, |
| base::UTF8ToUTF16(facet.android_package_name())); |
| api_credential.detailed_origin = facet.android_package_name(); |
| } |
| } else { |
| api_credential.is_android_credential = false; |
| api_credential.formatted_origin = |
| base::UTF16ToUTF8(url_formatter::FormatUrl( |
| GURL(credential.signon_realm), |
| url_formatter::kFormatUrlOmitDefaults | |
| url_formatter::kFormatUrlOmitHTTPS | |
| url_formatter::kFormatUrlOmitTrivialSubdomains | |
| url_formatter::kFormatUrlTrimAfterHost, |
| net::UnescapeRule::SPACES, nullptr, nullptr, nullptr)); |
| api_credential.detailed_origin = |
| base::UTF16ToUTF8(url_formatter::FormatUrlForSecurityDisplay( |
| GURL(credential.signon_realm))); |
| api_credential.change_password_url = |
| std::make_unique<std::string>(credential.signon_realm); |
| } |
| |
| api_credential.id = |
| compromised_credential_id_generator_.GenerateId(credential); |
| api_credential.signon_realm = credential.signon_realm; |
| api_credential.username = base::UTF16ToUTF8(credential.username); |
| api_credential.compromise_time = |
| credential.create_time.ToJsTimeIgnoringNull(); |
| api_credential.compromise_type = credential_and_type.type; |
| api_credential.elapsed_time_since_compromise = |
| FormatElapsedTime(credential.create_time); |
| compromised_credentials.push_back(std::move(api_credential)); |
| } |
| |
| return compromised_credentials; |
| } |
| |
| base::Optional<api::passwords_private::CompromisedCredential> |
| PasswordCheckDelegate::GetPlaintextCompromisedPassword( |
| api::passwords_private::CompromisedCredential credential) const { |
| const CredentialWithPassword* compromised_credential = |
| FindMatchingCompromisedCredential(credential); |
| if (!compromised_credential) |
| return base::nullopt; |
| |
| credential.password = std::make_unique<std::string>( |
| base::UTF16ToUTF8(compromised_credential->password)); |
| return credential; |
| } |
| |
| bool PasswordCheckDelegate::ChangeCompromisedCredential( |
| const api::passwords_private::CompromisedCredential& credential, |
| base::StringPiece new_password) { |
| // Try to obtain the original CredentialWithPassword. Return false if fails. |
| const CredentialWithPassword* compromised_credential = |
| FindMatchingCompromisedCredential(credential); |
| if (!compromised_credential) |
| return false; |
| |
| return compromised_credentials_manager_.UpdateCompromisedCredentials( |
| *compromised_credential, new_password); |
| } |
| |
| bool PasswordCheckDelegate::RemoveCompromisedCredential( |
| const api::passwords_private::CompromisedCredential& credential) { |
| // Try to obtain the original CredentialWithPassword. Return false if fails. |
| const CredentialWithPassword* compromised_credential = |
| FindMatchingCompromisedCredential(credential); |
| if (!compromised_credential) |
| return false; |
| |
| return compromised_credentials_manager_.RemoveCompromisedCredential( |
| *compromised_credential); |
| } |
| |
| void PasswordCheckDelegate::StartPasswordCheck( |
| StartPasswordCheckCallback callback) { |
| // If the delegate isn't initialized yet, enqueue the callback and return |
| // early. |
| if (!is_initialized_) { |
| start_check_callbacks_.push_back(std::move(callback)); |
| return; |
| } |
| |
| // Also return early if the check is already running. |
| if (bulk_leak_check_service_adapter_.GetBulkLeakCheckState() == |
| State::kRunning) { |
| std::move(callback).Run(State::kRunning); |
| return; |
| } |
| |
| auto progress = base::MakeRefCounted<PasswordCheckProgress>(); |
| for (const auto& password : saved_passwords_presenter_.GetSavedPasswords()) |
| progress->IncrementCounts(password); |
| |
| password_check_progress_ = progress->GetWeakPtr(); |
| PasswordCheckData data(std::move(progress)); |
| is_check_running_ = bulk_leak_check_service_adapter_.StartBulkLeakCheck( |
| kPasswordCheckDataKey, &data); |
| DCHECK(is_check_running_); |
| std::move(callback).Run( |
| bulk_leak_check_service_adapter_.GetBulkLeakCheckState()); |
| } |
| |
| void PasswordCheckDelegate::StopPasswordCheck() { |
| if (!is_initialized_) { |
| for (auto&& callback : std::exchange(start_check_callbacks_, {})) |
| std::move(callback).Run(State::kIdle); |
| return; |
| } |
| |
| bulk_leak_check_service_adapter_.StopBulkLeakCheck(); |
| } |
| |
| api::passwords_private::PasswordCheckStatus |
| PasswordCheckDelegate::GetPasswordCheckStatus() const { |
| api::passwords_private::PasswordCheckStatus result; |
| |
| // Obtain the timestamp of the last completed check. This is 0.0 in case the |
| // check never completely ran before. |
| const double last_check_completed = profile_->GetPrefs()->GetDouble( |
| password_manager::prefs::kLastTimePasswordCheckCompleted); |
| if (last_check_completed) { |
| result.elapsed_time_since_last_check = std::make_unique<std::string>( |
| FormatElapsedTime(base::Time::FromDoubleT(last_check_completed))); |
| } |
| |
| State state = bulk_leak_check_service_adapter_.GetBulkLeakCheckState(); |
| SavedPasswordsView saved_passwords = |
| saved_passwords_presenter_.GetSavedPasswords(); |
| |
| // Handle the currently running case first, only then consider errors. |
| if (state == State::kRunning) { |
| result.state = api::passwords_private::PASSWORD_CHECK_STATE_RUNNING; |
| |
| if (password_check_progress_) { |
| result.already_processed = |
| std::make_unique<int>(password_check_progress_->already_processed()); |
| result.remaining_in_queue = |
| std::make_unique<int>(password_check_progress_->remaining_in_queue()); |
| } else { |
| result.already_processed = std::make_unique<int>(0); |
| result.remaining_in_queue = std::make_unique<int>(0); |
| } |
| |
| return result; |
| } |
| |
| if (saved_passwords.empty()) { |
| result.state = api::passwords_private::PASSWORD_CHECK_STATE_NO_PASSWORDS; |
| return result; |
| } |
| |
| result.state = ConvertPasswordCheckState(state); |
| return result; |
| } |
| |
| void PasswordCheckDelegate::OnSavedPasswordsChanged(SavedPasswordsView) { |
| // Getting the first notification about a change in saved passwords implies |
| // that the delegate is initialized, and start check callbacks can be invoked, |
| // if any. |
| if (!std::exchange(is_initialized_, true)) { |
| for (auto&& callback : std::exchange(start_check_callbacks_, {})) |
| StartPasswordCheck(std::move(callback)); |
| } |
| |
| // A change in the saved passwords might result in leaving or entering the |
| // NO_PASSWORDS state, thus we need to trigger a notification. |
| NotifyPasswordCheckStatusChanged(); |
| } |
| |
| void PasswordCheckDelegate::OnCompromisedCredentialsChanged( |
| CompromisedCredentialsView credentials) { |
| if (auto* event_router = |
| PasswordsPrivateEventRouterFactory::GetForProfile(profile_)) { |
| event_router->OnCompromisedCredentialsChanged(GetCompromisedCredentials()); |
| } |
| } |
| |
| void PasswordCheckDelegate::OnStateChanged(State state) { |
| if (state == State::kIdle && std::exchange(is_check_running_, false)) { |
| // When the service transitions from running into idle it has finished a |
| // check. |
| profile_->GetPrefs()->SetDouble( |
| password_manager::prefs::kLastTimePasswordCheckCompleted, |
| base::Time::Now().ToDoubleT()); |
| |
| // In case the check run to completion delay the last Check Status update by |
| // a second. This avoids flickering of the UI if the full check ran from |
| // start to finish almost immediately. |
| base::SequencedTaskRunnerHandle::Get()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(&PasswordCheckDelegate::NotifyPasswordCheckStatusChanged, |
| weak_ptr_factory_.GetWeakPtr()), |
| base::TimeDelta::FromSeconds(1)); |
| return; |
| } |
| |
| // NotifyPasswordCheckStatusChanged() invokes GetPasswordCheckStatus() |
| // obtaining the relevant information. Thus there is no need to forward the |
| // arguments passed to OnStateChanged(). |
| NotifyPasswordCheckStatusChanged(); |
| } |
| |
| void PasswordCheckDelegate::OnCredentialDone( |
| const LeakCheckCredential& credential, |
| password_manager::IsLeaked is_leaked) { |
| if (is_leaked) { |
| compromised_credentials_manager_.SaveCompromisedCredential(credential); |
| } |
| |
| // Update the progress in case there is one. |
| if (password_check_progress_) |
| password_check_progress_->OnProcessed(credential); |
| |
| // While the check is still running trigger an update of the check status, |
| // considering that the progress has changed. |
| if (bulk_leak_check_service_adapter_.GetBulkLeakCheckState() == |
| State::kRunning) { |
| NotifyPasswordCheckStatusChanged(); |
| } |
| } |
| |
| const CredentialWithPassword* |
| PasswordCheckDelegate::FindMatchingCompromisedCredential( |
| const api::passwords_private::CompromisedCredential& credential) const { |
| const CredentialWithPassword* compromised_credential = |
| compromised_credential_id_generator_.TryGetKey(credential.id); |
| if (!compromised_credential) |
| return nullptr; |
| |
| if (credential.signon_realm != compromised_credential->signon_realm || |
| credential.username != |
| base::UTF16ToUTF8(compromised_credential->username) || |
| (credential.password && |
| *credential.password != |
| base::UTF16ToUTF8(compromised_credential->password))) { |
| return nullptr; |
| } |
| |
| return compromised_credential; |
| } |
| |
| void PasswordCheckDelegate::NotifyPasswordCheckStatusChanged() { |
| if (auto* event_router = |
| PasswordsPrivateEventRouterFactory::GetForProfile(profile_)) { |
| event_router->OnPasswordCheckStatusChanged(GetPasswordCheckStatus()); |
| } |
| } |
| |
| } // namespace extensions |