blob: 60a01effa91928df563643545c207060824fad2d [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/password_manager/core/browser/password_form_manager.h"
#include <stddef.h>
#include <algorithm>
#include <map>
#include <set>
#include <utility>
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "build/build_config.h"
#include "components/autofill/core/browser/autofill_manager.h"
#include "components/autofill/core/browser/form_structure.h"
#include "components/autofill/core/browser/proto/server.pb.h"
#include "components/autofill/core/browser/validation.h"
#include "components/autofill/core/common/password_form.h"
#include "components/password_manager/core/browser/affiliation_utils.h"
#include "components/password_manager/core/browser/browser_save_password_progress_logger.h"
#include "components/password_manager/core/browser/credentials_filter.h"
#include "components/password_manager/core/browser/log_manager.h"
#include "components/password_manager/core/browser/password_manager.h"
#include "components/password_manager/core/browser/password_manager_client.h"
#include "components/password_manager/core/browser/password_manager_driver.h"
#include "components/password_manager/core/browser/password_manager_metrics_util.h"
#include "components/password_manager/core/browser/password_manager_util.h"
#include "components/password_manager/core/browser/password_store.h"
#include "components/password_manager/core/browser/statistics_table.h"
#include "google_apis/gaia/gaia_auth_util.h"
using autofill::FormStructure;
using autofill::PasswordForm;
using autofill::PasswordFormMap;
using base::Time;
// Shorten the name to spare line breaks. The code provides enough context
// already.
typedef autofill::SavePasswordProgressLogger Logger;
namespace password_manager {
namespace {
PasswordForm CopyAndModifySSLValidity(const PasswordForm& orig,
bool ssl_valid) {
PasswordForm result(orig);
result.ssl_valid = ssl_valid;
return result;
}
// Returns true if user-typed username and password field values match with one
// of the password form within |credentials| map; otherwise false.
bool DoesUsenameAndPasswordMatchCredentials(
const base::string16& typed_username,
const base::string16& typed_password,
const autofill::PasswordFormMap& credentials) {
for (const auto& match : credentials) {
if (match.second->username_value == typed_username &&
match.second->password_value == typed_password)
return true;
}
return false;
}
std::vector<std::string> SplitPathToSegments(const std::string& path) {
return base::SplitString(path, "/", base::TRIM_WHITESPACE,
base::SPLIT_WANT_ALL);
}
// Return false iff the strings are neither empty nor equal.
bool AreStringsEqualOrEmpty(const base::string16& s1,
const base::string16& s2) {
return s1.empty() || s2.empty() || s1 == s2;
}
bool DoesStringContainOnlyDigits(const base::string16& s) {
for (auto c : s) {
if (!base::IsAsciiDigit(c))
return false;
}
return true;
}
// Heuristics to determine that a string is very unlikely to be a username.
bool IsProbablyNotUsername(const base::string16& s) {
return !s.empty() && DoesStringContainOnlyDigits(s) && s.size() < 3;
}
} // namespace
PasswordFormManager::PasswordFormManager(
PasswordManager* password_manager,
PasswordManagerClient* client,
const base::WeakPtr<PasswordManagerDriver>& driver,
const PasswordForm& observed_form,
bool ssl_valid)
: observed_form_(CopyAndModifySSLValidity(observed_form, ssl_valid)),
provisionally_saved_form_(nullptr),
other_possible_username_action_(
PasswordFormManager::IGNORE_OTHER_POSSIBLE_USERNAMES),
form_path_segments_(
observed_form_.origin.is_valid()
? SplitPathToSegments(observed_form_.origin.path())
: std::vector<std::string>()),
is_new_login_(true),
has_generated_password_(false),
is_manual_generation_(false),
password_overridden_(false),
retry_password_form_password_update_(false),
generation_available_(false),
password_manager_(password_manager),
preferred_match_(nullptr),
is_ignorable_change_password_form_(false),
is_possible_change_password_form_without_username_(
observed_form.IsPossibleChangePasswordFormWithoutUsername()),
state_(PRE_MATCHING_PHASE),
client_(client),
manager_action_(kManagerActionNone),
user_action_(kUserActionNone),
submit_result_(kSubmitResultNotSubmitted),
form_type_(kFormTypeUnspecified),
need_to_refetch_(false) {
DCHECK_EQ(observed_form.scheme == PasswordForm::SCHEME_HTML,
driver != nullptr);
if (driver)
drivers_.push_back(driver);
}
PasswordFormManager::~PasswordFormManager() {
UMA_HISTOGRAM_ENUMERATION(
"PasswordManager.ActionsTakenV3", GetActionsTaken(), kMaxNumActionsTaken);
if (submit_result_ == kSubmitResultNotSubmitted) {
if (has_generated_password_)
metrics_util::LogPasswordGenerationSubmissionEvent(
metrics_util::PASSWORD_NOT_SUBMITTED);
else if (generation_available_)
metrics_util::LogPasswordGenerationAvailableSubmissionEvent(
metrics_util::PASSWORD_NOT_SUBMITTED);
}
if (form_type_ != kFormTypeUnspecified) {
UMA_HISTOGRAM_ENUMERATION("PasswordManager.SubmittedFormType", form_type_,
kFormTypeMax);
}
}
int PasswordFormManager::GetActionsTaken() const {
return user_action_ +
kUserActionMax *
(manager_action_ + kManagerActionMax * submit_result_);
}
// static
base::string16 PasswordFormManager::PasswordToSave(const PasswordForm& form) {
if (form.new_password_element.empty() || form.new_password_value.empty())
return form.password_value;
return form.new_password_value;
}
// TODO(timsteele): use a hash of some sort in the future?
PasswordFormManager::MatchResultMask PasswordFormManager::DoesManage(
const PasswordForm& form) const {
// Non-HTML form case.
if (observed_form_.scheme != PasswordForm::SCHEME_HTML ||
form.scheme != PasswordForm::SCHEME_HTML) {
const bool forms_match = observed_form_.signon_realm == form.signon_realm &&
observed_form_.scheme == form.scheme;
return forms_match ? RESULT_COMPLETE_MATCH : RESULT_NO_MATCH;
}
// HTML form case.
MatchResultMask result = RESULT_NO_MATCH;
// Easiest case of matching origins.
bool origins_match = form.origin == observed_form_.origin;
// If this is a replay of the same form in the case a user entered an invalid
// password, the origin of the new form may equal the action of the "first"
// form instead.
origins_match = origins_match || (form.origin == observed_form_.action);
// Otherwise, if action hosts are the same, the old URL scheme is HTTP while
// the new one is HTTPS, and the new path equals to or extends the old path,
// we also consider the actions a match. This is to accommodate cases where
// the original login form is on an HTTP page, but a failed login attempt
// redirects to HTTPS (as in http://example.org -> https://example.org/auth).
if (!origins_match && !observed_form_.origin.SchemeIsCryptographic() &&
form.origin.SchemeIsCryptographic()) {
const std::string& old_path = observed_form_.origin.path();
const std::string& new_path = form.origin.path();
origins_match =
observed_form_.origin.host() == form.origin.host() &&
observed_form_.origin.port() == form.origin.port() &&
base::StartsWith(new_path, old_path, base::CompareCase::SENSITIVE);
}
if (!origins_match)
return result;
result |= RESULT_ORIGINS_MATCH;
// Autofill predictions can overwrite our default username selection so
// if this form was parsed with autofill predictions then allow the username
// element to be different.
if ((form.was_parsed_using_autofill_predictions ||
form.username_element == observed_form_.username_element) &&
form.password_element == observed_form_.password_element) {
result |= RESULT_HTML_ATTRIBUTES_MATCH;
}
// Note: although saved password forms might actually have an empty action
// URL if they were imported (see bug 1107719), the |form| we see here comes
// never from the password store, and should have an exactly matching action.
if (form.action == observed_form_.action)
result |= RESULT_ACTION_MATCH;
return result;
}
bool PasswordFormManager::IsBlacklisted() const {
DCHECK_EQ(state_, POST_MATCHING_PHASE);
return !blacklisted_matches_.empty();
}
void PasswordFormManager::PermanentlyBlacklist() {
DCHECK_EQ(state_, POST_MATCHING_PHASE);
DCHECK(!client_->IsOffTheRecord());
// Configure the form about to be saved for blacklist status.
blacklisted_matches_.push_back(
new autofill::PasswordForm(pending_credentials_));
blacklisted_matches_.back()->preferred = false;
blacklisted_matches_.back()->blacklisted_by_user = true;
blacklisted_matches_.back()->username_value.clear();
blacklisted_matches_.back()->password_value.clear();
blacklisted_matches_.back()->other_possible_usernames.clear();
blacklisted_matches_.back()->date_created = Time::Now();
PasswordStore* password_store = client_->GetPasswordStore();
DCHECK(password_store);
password_store->AddLogin(*blacklisted_matches_.back());
}
bool PasswordFormManager::IsNewLogin() const {
DCHECK_EQ(state_, POST_MATCHING_PHASE);
return is_new_login_;
}
bool PasswordFormManager::IsPendingCredentialsPublicSuffixMatch() const {
return pending_credentials_.is_public_suffix_match;
}
void PasswordFormManager::ProvisionallySave(
const PasswordForm& credentials,
OtherPossibleUsernamesAction action) {
DCHECK(state_ == MATCHING_PHASE || state_ == POST_MATCHING_PHASE) << state_;
DCHECK_NE(RESULT_NO_MATCH, DoesManage(credentials));
scoped_ptr<autofill::PasswordForm> mutable_provisionally_saved_form(
new PasswordForm(credentials));
if (credentials.IsPossibleChangePasswordForm() &&
!credentials.username_value.empty() &&
IsProbablyNotUsername(credentials.username_value)) {
mutable_provisionally_saved_form->username_value.clear();
mutable_provisionally_saved_form->username_element.clear();
is_possible_change_password_form_without_username_ = true;
}
provisionally_saved_form_ = std::move(mutable_provisionally_saved_form);
other_possible_username_action_ = action;
if (HasCompletedMatching())
CreatePendingCredentials();
}
void PasswordFormManager::Save() {
DCHECK_EQ(state_, POST_MATCHING_PHASE);
DCHECK(!client_->IsOffTheRecord());
if (IsNewLogin()) {
SaveAsNewLogin();
DeleteEmptyUsernameCredentials();
} else {
UpdateLogin();
}
// This is not in UpdateLogin() to catch PSL matched credentials.
if (pending_credentials_.times_used != 0 &&
pending_credentials_.type == PasswordForm::TYPE_GENERATED) {
metrics_util::LogPasswordGenerationSubmissionEvent(
metrics_util::PASSWORD_USED);
}
password_manager_->UpdateFormManagers();
}
void PasswordFormManager::Update(
const autofill::PasswordForm& credentials_to_update) {
if (observed_form_.IsPossibleChangePasswordForm()) {
FormStructure form_structure(credentials_to_update.form_data);
UploadChangePasswordForm(autofill::NEW_PASSWORD,
form_structure.FormSignature());
}
base::string16 password_to_save = pending_credentials_.password_value;
pending_credentials_ = credentials_to_update;
pending_credentials_.password_value = password_to_save;
pending_credentials_.preferred = true;
is_new_login_ = false;
UpdateLogin();
}
void PasswordFormManager::FetchDataFromPasswordStore() {
if (state_ == MATCHING_PHASE) {
// There is currently a password store query in progress, need to re-fetch
// store results later.
need_to_refetch_ = true;
return;
}
scoped_ptr<BrowserSavePasswordProgressLogger> logger;
if (password_manager_util::IsLoggingActive(client_)) {
logger.reset(
new BrowserSavePasswordProgressLogger(client_->GetLogManager()));
logger->LogMessage(Logger::STRING_FETCH_LOGINS_METHOD);
logger->LogNumber(Logger::STRING_FORM_MANAGER_STATE, state_);
}
provisionally_saved_form_.reset();
state_ = MATCHING_PHASE;
PasswordStore* password_store = client_->GetPasswordStore();
if (!password_store) {
if (logger)
logger->LogMessage(Logger::STRING_NO_STORE);
NOTREACHED();
return;
}
password_store->GetLogins(observed_form_, this);
// The statistics isn't needed on mobile, only on desktop. Let's save some
// processor cycles.
#if !defined(OS_IOS) && !defined(OS_ANDROID)
// The statistics is needed for the "Save password?" bubble.
password_store->GetSiteStats(observed_form_.origin.GetOrigin(), this);
#endif
}
bool PasswordFormManager::HasCompletedMatching() const {
return state_ == POST_MATCHING_PHASE;
}
void PasswordFormManager::SetSubmittedForm(const autofill::PasswordForm& form) {
bool is_change_password_form =
!form.new_password_value.empty() && !form.password_value.empty();
is_ignorable_change_password_form_ =
is_change_password_form && !form.username_marked_by_site &&
!DoesUsenameAndPasswordMatchCredentials(
form.username_value, form.password_value, best_matches_) &&
!client_->IsUpdatePasswordUIEnabled();
bool is_signup_form =
!form.new_password_value.empty() && form.password_value.empty();
bool no_username = form.username_element.empty();
if (form.layout == PasswordForm::Layout::LAYOUT_LOGIN_AND_SIGNUP) {
form_type_ = kFormTypeLoginAndSignup;
} else if (is_ignorable_change_password_form_) {
if (no_username)
form_type_ = kFormTypeChangePasswordNoUsername;
else
form_type_ = kFormTypeChangePasswordDisabled;
} else if (is_change_password_form) {
form_type_ = kFormTypeChangePasswordEnabled;
} else if (is_signup_form) {
if (no_username)
form_type_ = kFormTypeSignupNoUsername;
else
form_type_ = kFormTypeSignup;
} else if (no_username) {
form_type_ = kFormTypeLoginNoUsername;
} else {
form_type_ = kFormTypeLogin;
}
}
void PasswordFormManager::OnRequestDone(
ScopedVector<PasswordForm> logins_result) {
preferred_match_ = nullptr;
best_matches_.clear();
blacklisted_matches_.clear();
const size_t logins_result_size = logins_result.size();
scoped_ptr<BrowserSavePasswordProgressLogger> logger;
if (password_manager_util::IsLoggingActive(client_)) {
logger.reset(
new BrowserSavePasswordProgressLogger(client_->GetLogManager()));
logger->LogMessage(Logger::STRING_ON_REQUEST_DONE_METHOD);
}
// Remove credentials which need to be ignored from |logins_result|.
if (!observed_form_.ssl_valid) {
logins_result.erase(
std::partition(logins_result.begin(), logins_result.end(),
[](PasswordForm* form) { return !form->ssl_valid; }),
logins_result.end());
}
logins_result =
client_->GetStoreResultFilter()->FilterResults(std::move(logins_result));
// Deal with blacklisted forms.
auto begin_blacklisted = std::partition(
logins_result.begin(), logins_result.end(),
[](PasswordForm* form) { return !form->blacklisted_by_user; });
for (auto it = begin_blacklisted; it != logins_result.end(); ++it) {
if (IsBlacklistMatch(**it)) {
blacklisted_matches_.push_back(*it);
*it = nullptr;
}
}
logins_result.erase(begin_blacklisted, logins_result.end());
if (logins_result.empty())
return;
// Now compute scores for the remaining credentials in |login_result|.
std::vector<uint32_t> credential_scores;
credential_scores.reserve(logins_result.size());
uint32_t best_score = 0;
std::map<base::string16, uint32_t> best_scores;
for (const PasswordForm* login : logins_result) {
uint32_t current_score = ScoreResult(*login);
best_score = std::max(best_score, current_score);
best_scores[login->username_value] =
std::max(best_scores[login->username_value], current_score);
credential_scores.push_back(current_score);
}
// Fill |best_matches_| with the best-scoring credentials for each username.
for (size_t i = 0; i < logins_result.size(); ++i) {
// Take ownership of the PasswordForm from the ScopedVector.
scoped_ptr<PasswordForm> login(logins_result[i]);
logins_result[i] = nullptr;
DCHECK(!login->blacklisted_by_user);
const base::string16& username = login->username_value;
if (credential_scores[i] < best_scores[username]) {
not_best_matches_.push_back(std::move(login));
continue;
}
if (!preferred_match_ && credential_scores[i] == best_score)
preferred_match_ = login.get();
// If there is another best-score match for the same username then leave it
// and add the current form to |not_best_matches_|.
auto best_match_username = best_matches_.find(username);
if (best_match_username == best_matches_.end()) {
best_matches_.insert(std::make_pair(username, std::move(login)));
} else {
not_best_matches_.push_back(std::move(login));
}
}
UMA_HISTOGRAM_COUNTS("PasswordManager.NumPasswordsNotShown",
logins_result_size - best_matches_.size());
}
void PasswordFormManager::ProcessFrame(
const base::WeakPtr<PasswordManagerDriver>& driver) {
DCHECK_EQ(PasswordForm::SCHEME_HTML, observed_form_.scheme);
if (state_ == POST_MATCHING_PHASE)
ProcessFrameInternal(driver);
for (auto const& old_driver : drivers_) {
// |drivers_| is not a set because WeakPtr has no good candidate for a key
// (the address may change to null). So let's weed out duplicates in O(N).
if (old_driver.get() == driver.get())
return;
}
drivers_.push_back(driver);
}
void PasswordFormManager::ProcessFrameInternal(
const base::WeakPtr<PasswordManagerDriver>& driver) {
DCHECK_EQ(PasswordForm::SCHEME_HTML, observed_form_.scheme);
if (!driver || manager_action_ == kManagerActionBlacklisted)
return;
// Allow generation for any non-blacklisted form.
driver->AllowPasswordGenerationForForm(observed_form_);
if (best_matches_.empty())
return;
// Proceed to autofill.
// Note that we provide the choices but don't actually prefill a value if:
// (1) we are in Incognito mode, (2) the ACTION paths don't match,
// (3) if it matched using public suffix domain matching, or
// (4) the form is change password form.
// However, 2 and 3 should not apply to Android-based credentials found
// via affiliation-based matching (we want to autofill them).
// TODO(engedy): Clean this up. See: https://crbug.com/476519.
bool wait_for_username =
client_->IsOffTheRecord() ||
(!IsValidAndroidFacetURI(preferred_match_->signon_realm) &&
(observed_form_.action.GetWithEmptyPath() !=
preferred_match_->action.GetWithEmptyPath() ||
preferred_match_->is_public_suffix_match ||
observed_form_.IsPossibleChangePasswordForm()));
if (wait_for_username)
manager_action_ = kManagerActionNone;
else
manager_action_ = kManagerActionAutofilled;
password_manager_->Autofill(driver.get(), observed_form_, best_matches_,
*preferred_match_, wait_for_username);
}
void PasswordFormManager::ProcessLoginPrompt() {
DCHECK_NE(PasswordForm::SCHEME_HTML, observed_form_.scheme);
if (!preferred_match_)
return;
manager_action_ = kManagerActionAutofilled;
password_manager_->AutofillHttpAuth(best_matches_, *preferred_match_);
}
void PasswordFormManager::OnGetPasswordStoreResults(
ScopedVector<PasswordForm> results) {
DCHECK_EQ(state_, MATCHING_PHASE);
if (need_to_refetch_) {
// The received results are no longer up-to-date, need to re-request.
state_ = PRE_MATCHING_PHASE;
FetchDataFromPasswordStore();
need_to_refetch_ = false;
return;
}
scoped_ptr<BrowserSavePasswordProgressLogger> logger;
if (password_manager_util::IsLoggingActive(client_)) {
logger.reset(
new BrowserSavePasswordProgressLogger(client_->GetLogManager()));
logger->LogMessage(Logger::STRING_ON_GET_STORE_RESULTS_METHOD);
logger->LogNumber(Logger::STRING_NUMBER_RESULTS, results.size());
}
if (!results.empty())
OnRequestDone(std::move(results));
state_ = POST_MATCHING_PHASE;
// If password store was slow and provisionally saved form is already here
// then create pending credentials (see http://crbug.com/470322).
if (provisionally_saved_form_)
CreatePendingCredentials();
if (manager_action_ != kManagerActionBlacklisted) {
for (auto const& driver : drivers_)
ProcessFrameInternal(driver);
if (observed_form_.scheme != PasswordForm::SCHEME_HTML)
ProcessLoginPrompt();
}
}
void PasswordFormManager::OnGetSiteStatistics(
scoped_ptr<std::vector<scoped_ptr<InteractionsStats>>> stats) {
// On Windows the password request may be resolved after the statistics due to
// importing from IE.
DCHECK(state_ == MATCHING_PHASE || state_ == POST_MATCHING_PHASE) << state_;
interactions_stats_.swap(*stats);
}
void PasswordFormManager::SaveAsNewLogin() {
DCHECK_EQ(state_, POST_MATCHING_PHASE);
DCHECK(IsNewLogin());
// The new_form is being used to sign in, so it is preferred.
DCHECK(pending_credentials_.preferred);
DCHECK(!pending_credentials_.blacklisted_by_user);
// new_form contains the same basic data as observed_form_ (because its the
// same form), but with the newly added credentials.
DCHECK(!client_->IsOffTheRecord());
PasswordStore* password_store = client_->GetPasswordStore();
if (!password_store) {
NOTREACHED();
return;
}
// Upload credentials the first time they are saved. This data is used
// by password generation to help determine account creation sites.
// Credentials that have been previously used (e.g. PSL matches) are checked
// to see if they are valid account creation forms.
if (has_generated_password_) {
UploadGeneratedVote();
} else if (pending_credentials_.times_used == 0) {
if (!observed_form_.IsPossibleChangePasswordFormWithoutUsername())
UploadPasswordForm(pending_credentials_.form_data, base::string16(),
autofill::PASSWORD, std::string());
} else {
if (!observed_form_.IsPossibleChangePasswordFormWithoutUsername())
SendAutofillVotes(observed_form_, &pending_credentials_);
}
pending_credentials_.date_created = Time::Now();
SanitizePossibleUsernames(&pending_credentials_);
password_store->AddLogin(pending_credentials_);
UpdatePreferredLoginState(password_store);
}
void PasswordFormManager::SanitizePossibleUsernames(PasswordForm* form) {
// Remove any possible usernames that could be credit cards or SSN for privacy
// reasons. Also remove duplicates, both in other_possible_usernames and
// between other_possible_usernames and username_value.
std::set<base::string16> set;
for (std::vector<base::string16>::const_iterator it =
form->other_possible_usernames.begin();
it != form->other_possible_usernames.end(); ++it) {
if (!autofill::IsValidCreditCardNumber(*it) && !autofill::IsSSN(*it))
set.insert(*it);
}
set.erase(form->username_value);
std::vector<base::string16> temp(set.begin(), set.end());
form->other_possible_usernames.swap(temp);
}
void PasswordFormManager::UpdatePreferredLoginState(
PasswordStore* password_store) {
DCHECK(password_store);
PasswordFormMap::const_iterator iter;
for (iter = best_matches_.begin(); iter != best_matches_.end(); iter++) {
if (iter->second->username_value != pending_credentials_.username_value &&
iter->second->preferred && !iter->second->is_public_suffix_match) {
// This wasn't the selected login but it used to be preferred.
iter->second->preferred = false;
if (user_action_ == kUserActionNone)
user_action_ = kUserActionChoose;
password_store->UpdateLogin(*iter->second);
}
}
}
void PasswordFormManager::UpdateLogin() {
DCHECK_EQ(state_, POST_MATCHING_PHASE);
DCHECK(preferred_match_);
// If we're doing an Update, we either autofilled correctly and need to
// update the stats, or the user typed in a new password for autofilled
// username, or the user selected one of the non-preferred matches,
// thus requiring a swap of preferred bits.
DCHECK(!IsNewLogin() && pending_credentials_.preferred);
DCHECK(!client_->IsOffTheRecord());
PasswordStore* password_store = client_->GetPasswordStore();
if (!password_store) {
NOTREACHED();
return;
}
UpdateMetadataForUsage(pending_credentials_);
client_->GetStoreResultFilter()->ReportFormUsed(pending_credentials_);
// Check to see if this form is a candidate for password generation.
// Do not send votes on change password forms, since they were already sent in
// Update() method.
if (has_generated_password_) {
UploadGeneratedVote();
} else if (!observed_form_.IsPossibleChangePasswordForm()) {
SendAutofillVotes(observed_form_, &pending_credentials_);
}
UpdatePreferredLoginState(password_store);
bool password_was_updated = false;
// Update the new preferred login.
if (!selected_username_.empty()) {
// Username has changed. We set this selected username as the real
// username. Given that |username_value| is part of the Sync and
// PasswordStore primary key, the old primary key must be supplied.
PasswordForm old_primary_key(pending_credentials_);
pending_credentials_.username_value = selected_username_;
password_store->UpdateLoginWithPrimaryKey(pending_credentials_,
old_primary_key);
} else if (observed_form_.new_password_element.empty() &&
!IsValidAndroidFacetURI(pending_credentials_.signon_realm) &&
(pending_credentials_.password_element.empty() ||
pending_credentials_.username_element.empty() ||
pending_credentials_.submit_element.empty())) {
// If |observed_form_| was a sign-up or change password form, there is no
// point in trying to update element names: they are likely going to be
// different than those on a login form.
// Otherwise, given that |password_element| and |username_element| are part
// of Sync and PasswordStore primary key, the old primary key must be
// supplied to UpdateLogin().
PasswordForm old_primary_key(pending_credentials_);
pending_credentials_.password_element = observed_form_.password_element;
pending_credentials_.username_element = observed_form_.username_element;
pending_credentials_.submit_element = observed_form_.submit_element;
password_store->UpdateLoginWithPrimaryKey(pending_credentials_,
old_primary_key);
password_was_updated = true;
} else {
password_store->UpdateLogin(pending_credentials_);
password_was_updated = true;
}
// If this was password update then update all non-best matches entries with
// the same username and the same old password.
if (password_was_updated) {
PasswordFormMap::const_iterator updated_password_it =
best_matches_.find(pending_credentials_.username_value);
DCHECK(best_matches_.end() != updated_password_it);
const base::string16& old_password =
updated_password_it->second->password_value;
for (size_t i = 0; i < not_best_matches_.size(); ++i) {
if (not_best_matches_[i]->username_value ==
pending_credentials_.username_value &&
not_best_matches_[i]->password_value == old_password) {
not_best_matches_[i]->password_value =
pending_credentials_.password_value;
password_store->UpdateLogin(*not_best_matches_[i]);
}
}
}
}
void PasswordFormManager::UpdateMetadataForUsage(
const PasswordForm& credential) {
++pending_credentials_.times_used;
// Remove alternate usernames. At this point we assume that we have found
// the right username.
pending_credentials_.other_possible_usernames.clear();
}
bool PasswordFormManager::UpdatePendingCredentialsIfOtherPossibleUsername(
const base::string16& username) {
for (PasswordFormMap::const_iterator it = best_matches_.begin();
it != best_matches_.end(); ++it) {
for (size_t i = 0; i < it->second->other_possible_usernames.size(); ++i) {
if (it->second->other_possible_usernames[i] == username) {
pending_credentials_ = *it->second;
return true;
}
}
}
return false;
}
void PasswordFormManager::SendAutofillVotes(
const PasswordForm& observed,
PasswordForm* pending) {
if (pending->form_data.fields.empty())
return;
FormStructure pending_structure(pending->form_data);
FormStructure observed_structure(observed.form_data);
// Ignore |pending_structure| if its FormData has no fields. This is to
// weed out those credentials that were saved before FormData was added
// to PasswordForm. Even without this check, these FormStructure's won't
// be uploaded, but it makes it hard to see if we are encountering
// unexpected errors.
if (pending_structure.FormSignature() != observed_structure.FormSignature()) {
// Only upload if this is the first time the password has been used.
// Otherwise the credentials have been used on the same field before so
// they aren't from an account creation form.
// Also bypass uploading if the username was edited. Offering generation
// in cases where we currently save the wrong username isn't great.
// TODO(gcasto): Determine if generation should be offered in this case.
if (pending->times_used == 1 && selected_username_.empty()) {
if (UploadPasswordForm(pending->form_data, pending->username_element,
autofill::ACCOUNT_CREATION_PASSWORD,
observed_structure.FormSignature())) {
pending->generation_upload_status =
autofill::PasswordForm::POSITIVE_SIGNAL_SENT;
}
}
} else if (pending->generation_upload_status ==
autofill::PasswordForm::POSITIVE_SIGNAL_SENT) {
// A signal was sent that this was an account creation form, but the
// credential is now being used on the same form again. This cancels out
// the previous vote.
if (UploadPasswordForm(pending->form_data, base::string16(),
autofill::NOT_ACCOUNT_CREATION_PASSWORD,
std::string())) {
pending->generation_upload_status =
autofill::PasswordForm::NEGATIVE_SIGNAL_SENT;
}
}
}
bool PasswordFormManager::UploadPasswordForm(
const autofill::FormData& form_data,
const base::string16& username_field,
const autofill::ServerFieldType& password_type,
const std::string& login_form_signature) {
DCHECK(password_type == autofill::PASSWORD ||
password_type == autofill::ACCOUNT_CREATION_PASSWORD ||
autofill::NOT_ACCOUNT_CREATION_PASSWORD);
DCHECK(!has_generated_password_);
autofill::AutofillManager* autofill_manager =
client_->GetAutofillManagerForMainFrame();
if (!autofill_manager || !autofill_manager->download_manager())
return false;
FormStructure form_structure(form_data);
if (!autofill_manager->ShouldUploadForm(form_structure) ||
!form_structure.ShouldBeCrowdsourced()) {
UMA_HISTOGRAM_BOOLEAN("PasswordGeneration.UploadStarted", false);
return false;
}
// Find the first password field to label. If the provided username field name
// is not empty, then also find the first field with that name to label.
// We don't try to label anything else.
bool found_password_field = false;
bool should_find_username_field = !username_field.empty();
for (size_t i = 0; i < form_structure.field_count(); ++i) {
autofill::AutofillField* field = form_structure.field(i);
autofill::ServerFieldType type = autofill::UNKNOWN_TYPE;
if (!found_password_field && field->form_control_type == "password") {
type = password_type;
found_password_field = true;
} else if (should_find_username_field && field->name == username_field) {
type = autofill::USERNAME;
should_find_username_field = false;
}
autofill::ServerFieldTypeSet types;
types.insert(type);
field->set_possible_types(types);
}
DCHECK(found_password_field);
DCHECK(!should_find_username_field);
autofill::ServerFieldTypeSet available_field_types;
available_field_types.insert(password_type);
available_field_types.insert(autofill::USERNAME);
// Force uploading as these events are relatively rare and we want to make
// sure to receive them.
form_structure.set_upload_required(UPLOAD_REQUIRED);
bool success = autofill_manager->download_manager()->StartUploadRequest(
form_structure, false /* was_autofilled */, available_field_types,
login_form_signature, true /* observed_submission */);
UMA_HISTOGRAM_BOOLEAN("PasswordGeneration.UploadStarted", success);
return success;
}
bool PasswordFormManager::UploadChangePasswordForm(
const autofill::ServerFieldType& password_type,
const std::string& login_form_signature) {
DCHECK(!has_generated_password_);
DCHECK(password_type == autofill::NEW_PASSWORD ||
password_type == autofill::PROBABLY_NEW_PASSWORD ||
autofill::NOT_NEW_PASSWORD);
if (!provisionally_saved_form_ ||
provisionally_saved_form_->new_password_element.empty()) {
// |new_password_element| is empty for non change password forms, for
// example when the password was overriden.
return false;
}
autofill::AutofillManager* autofill_manager =
client_->GetAutofillManagerForMainFrame();
if (!autofill_manager || !autofill_manager->download_manager())
return false;
// Create a map from field names to field types.
std::map<base::string16, autofill::ServerFieldType> field_types;
if (!pending_credentials_.username_element.empty())
field_types[provisionally_saved_form_->username_element] =
autofill::USERNAME;
if (!pending_credentials_.password_element.empty())
field_types[provisionally_saved_form_->password_element] =
autofill::PASSWORD;
field_types[provisionally_saved_form_->new_password_element] = password_type;
// Find all password fields after |new_password_element| and set their type to
// |password_type|. They are considered to be confirmation fields.
const autofill::FormData& form_data = observed_form_.form_data;
bool is_new_password_field_found = false;
for (size_t i = 0; i < form_data.fields.size(); ++i) {
const autofill::FormFieldData& field = form_data.fields[i];
if (field.form_control_type != "password")
continue;
if (is_new_password_field_found) {
field_types[field.name] = password_type;
// We don't care about password fields after a confirmation field.
break;
} else if (field.name == provisionally_saved_form_->new_password_element) {
is_new_password_field_found = true;
}
}
DCHECK(is_new_password_field_found);
// Create FormStructure with field type information for uploading a vote.
FormStructure form_structure(form_data);
if (!autofill_manager->ShouldUploadForm(form_structure) ||
!form_structure.ShouldBeCrowdsourced())
return false;
autofill::ServerFieldTypeSet available_field_types;
for (size_t i = 0; i < form_structure.field_count(); ++i) {
autofill::AutofillField* field = form_structure.field(i);
autofill::ServerFieldType type = autofill::UNKNOWN_TYPE;
auto iter = field_types.find(field->name);
if (iter != field_types.end()) {
type = iter->second;
available_field_types.insert(type);
}
autofill::ServerFieldTypeSet types;
types.insert(type);
field->set_possible_types(types);
}
// Force uploading as these events are relatively rare and we want to make
// sure to receive them. It also makes testing easier if these requests
// always pass.
form_structure.set_upload_required(UPLOAD_REQUIRED);
return autofill_manager->download_manager()->StartUploadRequest(
form_structure, false /* was_autofilled */, available_field_types,
login_form_signature, true /* observed_submission */);
}
bool PasswordFormManager::UploadGeneratedVote() {
DCHECK(has_generated_password_);
if (generation_element_.empty())
return false;
// Create FormStructure with field type information for uploading a vote.
FormStructure form_structure(observed_form_.form_data);
autofill::AutofillManager* autofill_manager =
client_->GetAutofillManagerForMainFrame();
if (!autofill_manager->ShouldUploadForm(form_structure) ||
!form_structure.ShouldBeCrowdsourced())
return false;
autofill::ServerFieldTypeSet available_field_types;
available_field_types.insert(autofill::UNKNOWN_TYPE);
available_field_types.insert(autofill::PASSWORD);
autofill::AutofillUploadContents::Field::PasswordGenerationType type =
autofill::AutofillUploadContents::Field::NO_GENERATION;
if (is_manual_generation_) {
type = observed_form_.IsPossibleChangePasswordForm()
? autofill::AutofillUploadContents::Field::
MANUALLY_TRIGGERED_GENERATION_ON_CHANGE_PASSWORD_FORM
: autofill::AutofillUploadContents::Field::
MANUALLY_TRIGGERED_GENERATION_ON_SIGN_UP_FORM;
} else {
type = observed_form_.IsPossibleChangePasswordForm()
? autofill::AutofillUploadContents::Field::
AUTOMATICALLY_TRIGGERED_GENERATION_ON_CHANGE_PASSWORD_FORM
: autofill::AutofillUploadContents::Field::
AUTOMATICALLY_TRIGGERED_GENERATION_ON_SIGN_UP_FORM;
}
bool generation_field_found = false;
for (size_t i = 0; i < form_structure.field_count(); ++i) {
autofill::AutofillField* field = form_structure.field(i);
if (field->name == generation_element_) {
field->set_generation_type(type);
autofill::ServerFieldTypeSet types;
types.insert(autofill::PASSWORD);
field->set_possible_types(types);
generation_field_found = true;
break;
}
}
if (!generation_field_found)
return false;
return autofill_manager->download_manager()->StartUploadRequest(
form_structure, false /* was_autofilled */, available_field_types,
std::string(), true /* observed_submission */);
}
void PasswordFormManager::CreatePendingCredentials() {
DCHECK(provisionally_saved_form_);
base::string16 password_to_save(PasswordToSave(*provisionally_saved_form_));
// Make sure the important fields stay the same as the initially observed or
// autofilled ones, as they may have changed if the user experienced a login
// failure.
// Look for these credentials in the list containing auto-fill entries.
PasswordForm* saved_form =
FindBestSavedMatch(provisionally_saved_form_.get());
if (saved_form != nullptr) {
// The user signed in with a login we autofilled.
pending_credentials_ = *saved_form;
password_overridden_ =
pending_credentials_.password_value != password_to_save;
if (IsPendingCredentialsPublicSuffixMatch()) {
// If the autofilled credentials were a PSL match or credentials stored
// from Android apps store a copy with the current origin and signon
// realm. This ensures that on the next visit, a precise match is found.
is_new_login_ = true;
user_action_ = password_overridden_ ? kUserActionOverridePassword
: kUserActionChoosePslMatch;
// Since this credential will not overwrite a previously saved credential,
// username_value can be updated now.
if (!selected_username_.empty())
pending_credentials_.username_value = selected_username_;
// Update credential to reflect that it has been used for submission.
// If this isn't updated, then password generation uploads are off for
// sites where PSL matching is required to fill the login form, as two
// PASSWORD votes are uploaded per saved password instead of one.
//
// TODO(gcasto): It would be nice if other state were shared such that if
// say a password was updated on one match it would update on all related
// passwords. This is a much larger change.
UpdateMetadataForUsage(pending_credentials_);
// Update |pending_credentials_| in order to be able correctly save it.
pending_credentials_.origin = provisionally_saved_form_->origin;
pending_credentials_.signon_realm =
provisionally_saved_form_->signon_realm;
// Normally, the copy of the PSL matched credentials, adapted for the
// current domain, is saved automatically without asking the user, because
// the copy likely represents the same account, i.e., the one for which
// the user already agreed to store a password.
//
// However, if the user changes the suggested password, it might indicate
// that the autofilled credentials and |provisionally_saved_form_|
// actually correspond to two different accounts (see
// http://crbug.com/385619). In that case the user should be asked again
// before saving the password. This is ensured by setting
// |password_overriden_| on |pending_credentials_| to false and setting
// |origin| and |signon_realm| to correct values.
//
// There is still the edge case when the autofilled credentials represent
// the same account as |provisionally_saved_form_| but the stored password
// was out of date. In that case, the user just had to manually enter the
// new password, which is now in |provisionally_saved_form_|. The best
// thing would be to save automatically, and also update the original
// credentials. However, we have no way to tell if this is the case.
// This will likely happen infrequently, and the inconvenience put on the
// user by asking them is not significant, so we are fine with asking
// here again.
if (password_overridden_) {
pending_credentials_.is_public_suffix_match = false;
password_overridden_ = false;
}
} else { // Not a PSL match.
is_new_login_ = false;
if (password_overridden_)
user_action_ = kUserActionOverridePassword;
}
} else if (other_possible_username_action_ ==
ALLOW_OTHER_POSSIBLE_USERNAMES &&
UpdatePendingCredentialsIfOtherPossibleUsername(
provisionally_saved_form_->username_value)) {
// |pending_credentials_| is now set. Note we don't update
// |pending_credentials_.username_value| to |credentials.username_value|
// yet because we need to keep the original username to modify the stored
// credential.
selected_username_ = provisionally_saved_form_->username_value;
is_new_login_ = false;
} else if (client_->IsUpdatePasswordUIEnabled() && !best_matches_.empty() &&
provisionally_saved_form_->type !=
autofill::PasswordForm::TYPE_API &&
(provisionally_saved_form_
->IsPossibleChangePasswordFormWithoutUsername() ||
provisionally_saved_form_->username_element.empty())) {
PasswordForm* best_update_match = FindBestMatchForUpdatePassword(
provisionally_saved_form_->password_value);
retry_password_form_password_update_ =
provisionally_saved_form_->username_element.empty() &&
provisionally_saved_form_->new_password_element.empty();
is_new_login_ = false;
if (best_update_match) {
pending_credentials_ = *best_update_match;
} else if (has_generated_password_) {
// If a password was generated and we didn't find match we have to save it
// in separate entry since we have to store it but we don't know where.
CreatePendingCredentialsForNewCredentials();
is_new_login_ = true;
} else {
// We don't care about |pending_credentials_| if we didn't find the best
// match, since the user will select the correct one.
pending_credentials_.origin = provisionally_saved_form_->origin;
}
} else {
CreatePendingCredentialsForNewCredentials();
}
if (!IsValidAndroidFacetURI(pending_credentials_.signon_realm)) {
pending_credentials_.action = provisionally_saved_form_->action;
// If the user selected credentials we autofilled from a PasswordForm
// that contained no action URL (IE6/7 imported passwords, for example),
// bless it with the action URL from the observed form. See bug 1107719.
if (pending_credentials_.action.is_empty())
pending_credentials_.action = observed_form_.action;
}
pending_credentials_.password_value = password_to_save;
pending_credentials_.preferred = provisionally_saved_form_->preferred;
if (user_action_ == kUserActionOverridePassword &&
pending_credentials_.type == PasswordForm::TYPE_GENERATED &&
!has_generated_password_) {
metrics_util::LogPasswordGenerationSubmissionEvent(
metrics_util::PASSWORD_OVERRIDDEN);
pending_credentials_.type = PasswordForm::TYPE_MANUAL;
}
if (has_generated_password_)
pending_credentials_.type = PasswordForm::TYPE_GENERATED;
// In case of change password forms we need to leave
// |provisionally_saved_form_| in order to be able to determine which field is
// password and which is a new password during sending a vote in other cases
// we can reset |provisionally_saved_form_|.
if (!client_->IsUpdatePasswordUIEnabled() ||
!provisionally_saved_form_->IsPossibleChangePasswordForm())
provisionally_saved_form_.reset();
}
uint32_t PasswordFormManager::ScoreResult(const PasswordForm& candidate) const {
DCHECK_EQ(state_, MATCHING_PHASE);
DCHECK(!candidate.blacklisted_by_user);
// For scoring of candidate login data:
// The most important element that should match is the signon_realm followed
// by the origin, the action, the password name, the submit button name, and
// finally the username input field name.
// If public suffix origin match was not used, it gives an addition of
// 128 (1 << 7).
// Exact origin match gives an addition of 64 (1 << 6) + # of matching url
// dirs.
// Partial match gives an addition of 32 (1 << 5) + # matching url dirs
// That way, a partial match cannot trump an exact match even if
// the partial one matches all other attributes (action, elements) (and
// regardless of the matching depth in the URL path).
// When comparing path segments, only consider at most 63 of them, so that the
// potential gain from shared path prefix is not more than from an exact
// origin match.
const size_t kSegmentCountCap = 63;
const size_t capped_form_path_segment_count =
std::min(form_path_segments_.size(), kSegmentCountCap);
uint32_t score = 0u;
if (!candidate.is_public_suffix_match) {
score += 1u << 8;
}
if (candidate.preferred)
score += 1u << 7;
if (candidate.origin == observed_form_.origin) {
// This check is here for the most common case which
// is we have a single match in the db for the given host,
// so we don't generally need to walk the entire URL path (the else
// clause).
score += (1u << 6) + static_cast<uint32_t>(capped_form_path_segment_count);
} else {
// Walk the origin URL paths one directory at a time to see how
// deep the two match.
std::vector<std::string> candidate_path_segments =
SplitPathToSegments(candidate.origin.path());
size_t depth = 0u;
const size_t max_dirs = std::min(capped_form_path_segment_count,
candidate_path_segments.size());
while ((depth < max_dirs) &&
(form_path_segments_[depth] == candidate_path_segments[depth])) {
depth++;
score++;
}
// do we have a partial match?
score += (depth > 0u) ? 1u << 5 : 0u;
}
if (observed_form_.scheme == PasswordForm::SCHEME_HTML) {
if (candidate.action == observed_form_.action)
score += 1u << 3;
if (candidate.password_element == observed_form_.password_element)
score += 1u << 2;
if (candidate.submit_element == observed_form_.submit_element)
score += 1u << 1;
if (candidate.username_element == observed_form_.username_element)
score += 1u << 0;
}
return score;
}
bool PasswordFormManager::IsBlacklistMatch(
const autofill::PasswordForm& blacklisted_form) const {
DCHECK(blacklisted_form.blacklisted_by_user);
if (blacklisted_form.is_public_suffix_match)
return false;
if (blacklisted_form.origin.GetOrigin() != observed_form_.origin.GetOrigin())
return false;
if (observed_form_.scheme == PasswordForm::SCHEME_HTML) {
if (!AreStringsEqualOrEmpty(blacklisted_form.submit_element,
observed_form_.submit_element))
return false;
if (!AreStringsEqualOrEmpty(blacklisted_form.password_element,
observed_form_.password_element))
return false;
if (!AreStringsEqualOrEmpty(blacklisted_form.username_element,
observed_form_.username_element))
return false;
}
return true;
}
void PasswordFormManager::DeleteEmptyUsernameCredentials() {
if (best_matches_.empty() || pending_credentials_.username_value.empty())
return;
PasswordStore* password_store = client_->GetPasswordStore();
if (!password_store) {
NOTREACHED();
return;
}
for (auto iter = best_matches_.begin(); iter != best_matches_.end(); ++iter) {
PasswordForm* form = iter->second.get();
if (!form->is_public_suffix_match && form->username_value.empty() &&
form->password_value == pending_credentials_.password_value)
password_store->RemoveLogin(*form);
}
}
PasswordForm* PasswordFormManager::FindBestMatchForUpdatePassword(
const base::string16& password) const {
if (best_matches_.size() == 1 && !has_generated_password_) {
// In case when the user has only one credential and the current password is
// not generated, consider it the same as is being saved.
return best_matches_.begin()->second.get();
}
if (password.empty())
return nullptr;
for (auto it = best_matches_.begin(); it != best_matches_.end(); ++it) {
if (it->second->password_value == password)
return it->second.get();
}
return nullptr;
}
PasswordForm* PasswordFormManager::FindBestSavedMatch(
const PasswordForm* form) const {
PasswordFormMap::const_iterator it = best_matches_.find(form->username_value);
if (it != best_matches_.end())
return it->second.get();
if (form->type == autofill::PasswordForm::TYPE_API)
// Match Credential API forms only by username.
return nullptr;
if (!form->username_element.empty() || !form->new_password_element.empty())
return nullptr;
for (const auto& stored_match : best_matches_) {
if (stored_match.second->password_value == form->password_value)
return stored_match.second.get();
}
return nullptr;
}
void PasswordFormManager::CreatePendingCredentialsForNewCredentials() {
// User typed in a new, unknown username.
user_action_ = kUserActionOverrideUsernameAndPassword;
pending_credentials_ = observed_form_;
if (provisionally_saved_form_->was_parsed_using_autofill_predictions)
pending_credentials_.username_element =
provisionally_saved_form_->username_element;
pending_credentials_.username_value =
provisionally_saved_form_->username_value;
pending_credentials_.other_possible_usernames =
provisionally_saved_form_->other_possible_usernames;
// The password value will be filled in later, remove any garbage for now.
pending_credentials_.password_value.clear();
pending_credentials_.new_password_value.clear();
// If this was a sign-up or change password form, the names of the elements
// are likely different than those on a login form, so do not bother saving
// them. We will fill them with meaningful values in UpdateLogin() when the
// user goes onto a real login form for the first time.
if (!provisionally_saved_form_->new_password_element.empty()) {
pending_credentials_.password_element.clear();
}
}
void PasswordFormManager::OnNopeUpdateClicked() {
UploadChangePasswordForm(autofill::NOT_NEW_PASSWORD, std::string());
}
void PasswordFormManager::OnNoInteractionOnUpdate() {
UploadChangePasswordForm(autofill::PROBABLY_NEW_PASSWORD, std::string());
}
void PasswordFormManager::LogSubmitPassed() {
if (submit_result_ != kSubmitResultFailed) {
if (has_generated_password_) {
metrics_util::LogPasswordGenerationSubmissionEvent(
metrics_util::PASSWORD_SUBMITTED);
} else if (generation_available_) {
metrics_util::LogPasswordGenerationAvailableSubmissionEvent(
metrics_util::PASSWORD_SUBMITTED);
}
}
submit_result_ = kSubmitResultPassed;
}
void PasswordFormManager::LogSubmitFailed() {
if (has_generated_password_) {
metrics_util::LogPasswordGenerationSubmissionEvent(
metrics_util::GENERATED_PASSWORD_FORCE_SAVED);
} else if (generation_available_) {
metrics_util::LogPasswordGenerationAvailableSubmissionEvent(
metrics_util::PASSWORD_SUBMISSION_FAILED);
}
submit_result_ = kSubmitResultFailed;
}
void PasswordFormManager::WipeStoreCopyIfOutdated() {
DCHECK_NE(PRE_MATCHING_PHASE, state_);
UMA_HISTOGRAM_BOOLEAN("PasswordManager.StoreReadyWhenWiping",
HasCompletedMatching());
PasswordStore* password_store = client_->GetPasswordStore();
if (!password_store)
return;
for (PasswordFormMap::const_iterator stored_credential =
best_matches_.begin();
stored_credential != best_matches_.end();
/*no increment here*/) {
// Beware erase() below, keep the cycle iterator valid.
PasswordFormMap::const_iterator credential_to_delete = stored_credential;
++stored_credential;
if (pending_credentials_.password_value ==
credential_to_delete->second->password_value) {
continue;
}
if (!gaia::AreEmailsSame(
base::UTF16ToUTF8(pending_credentials_.username_value),
base::UTF16ToUTF8(credential_to_delete->first))) {
continue;
}
password_store->RemoveLogin(*credential_to_delete->second);
if (credential_to_delete->second.get() == preferred_match_)
preferred_match_ = nullptr;
best_matches_.erase(credential_to_delete);
}
}
} // namespace password_manager