blob: 8774d8df2d4c015309752a059590fc8ce7fd323a [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// 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_suggestion_generator.h"
#include <set>
#include "base/base64.h"
#include "base/i18n/case_conversion.h"
#include "base/strings/utf_string_conversions.h"
#include "components/affiliations/core/browser/affiliation_utils.h"
#include "components/password_manager/core/browser/features/password_features.h"
#include "components/password_manager/core/browser/password_feature_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_util.h"
#include "components/password_manager/core/browser/password_ui_utils.h"
#include "components/password_manager/core/browser/ui/credential_ui_entry.h"
#include "components/password_manager/core/browser/webauthn_credentials_delegate.h"
#include "components/sync/base/features.h"
#include "ui/base/l10n/l10n_util.h"
#include "url/gurl.h"
namespace password_manager {
namespace {
using affiliations::FacetURI;
constexpr char16_t kPasswordReplacementChar = 0x2022;
// Returns |username| unless it is empty. For an empty |username| returns a
// localised string saying this username is empty. Use this for displaying the
// usernames to the user. |replaced| is set to true iff |username| is empty.
std::u16string ReplaceEmptyUsername(const std::u16string& username,
bool* replaced) {
*replaced = username.empty();
if (username.empty()) {
return l10n_util::GetStringUTF16(IDS_PASSWORD_MANAGER_EMPTY_LOGIN);
}
return username;
}
// Returns the prettified version of |signon_realm| to be displayed on the UI.
std::u16string GetHumanReadableRealm(const std::string& signon_realm) {
// For Android application realms, remove the hash component. Otherwise, make
// no changes.
FacetURI maybe_facet_uri(FacetURI::FromPotentiallyInvalidSpec(signon_realm));
if (maybe_facet_uri.IsValidAndroidFacetURI()) {
return base::UTF8ToUTF16("android://" +
maybe_facet_uri.android_package_name() + "/");
}
GURL realm(signon_realm);
if (realm.is_valid()) {
return base::UTF8ToUTF16(realm.host());
}
return base::UTF8ToUTF16(signon_realm);
}
// Returns a string representing the icon of either the account store or the
// local password store.
#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)
autofill::Suggestion::Icon CreateStoreIcon(bool for_account_store) {
return for_account_store ? autofill::Suggestion::Icon::kGoogle
: autofill::Suggestion::Icon::kNoIcon;
}
#endif
#if !BUILDFLAG(IS_ANDROID)
autofill::Suggestion CreateWebAuthnEntry(bool listed_passkeys) {
autofill::Suggestion suggestion(l10n_util::GetStringUTF16(
listed_passkeys ? IDS_PASSWORD_MANAGER_USE_DIFFERENT_PASSKEY
: IDS_PASSWORD_MANAGER_USE_PASSKEY));
suggestion.icon = autofill::Suggestion::Icon::kDevice;
suggestion.type = autofill::SuggestionType::kWebauthnSignInWithAnotherDevice;
return suggestion;
}
#endif // !BUILDFLAG(IS_ANDROID)
autofill::Suggestion CreateGenerationEntry() {
autofill::Suggestion suggestion(
l10n_util::GetStringUTF16(IDS_PASSWORD_MANAGER_GENERATE_PASSWORD));
// The UI code will pick up an icon from the resources based on the string.
suggestion.icon = autofill::Suggestion::Icon::kKey;
suggestion.type = autofill::SuggestionType::kGeneratePasswordEntry;
return suggestion;
}
// Entry for opting in to password account storage and then filling.
autofill::Suggestion CreateEntryToOptInToAccountStorageThenFill() {
bool has_passkey_sync = false;
#if !BUILDFLAG(IS_ANDROID)
has_passkey_sync =
base::FeatureList::IsEnabled(syncer::kSyncWebauthnCredentials);
#endif
autofill::Suggestion suggestion(l10n_util::GetStringUTF16(
has_passkey_sync
? IDS_PASSWORD_MANAGER_OPT_INTO_ACCOUNT_STORE_WITH_PASSKEYS
: IDS_PASSWORD_MANAGER_OPT_INTO_ACCOUNT_STORE));
suggestion.type = autofill::SuggestionType::kPasswordAccountStorageOptIn;
suggestion.icon = autofill::Suggestion::Icon::kGoogle;
return suggestion;
}
// Entry for opting in to password account storage and then generating password.
autofill::Suggestion CreateEntryToOptInToAccountStorageThenGenerate() {
autofill::Suggestion suggestion(
l10n_util::GetStringUTF16(IDS_PASSWORD_MANAGER_GENERATE_PASSWORD));
suggestion.type =
autofill::SuggestionType::kPasswordAccountStorageOptInAndGenerate;
suggestion.icon = autofill::Suggestion::Icon::kKey;
return suggestion;
}
// Entry for sigining in again which unlocks the password account storage.
autofill::Suggestion CreateEntryToReSignin() {
autofill::Suggestion suggestion(
l10n_util::GetStringUTF16(IDS_PASSWORD_MANAGER_RE_SIGNIN_ACCOUNT_STORE));
suggestion.type = autofill::SuggestionType::kPasswordAccountStorageReSignin;
suggestion.icon = autofill::Suggestion::Icon::kGoogle;
return suggestion;
}
void MaybeAppendManagePasswordsEntry(
std::vector<autofill::Suggestion>* suggestions) {
bool has_no_fillable_suggestions = base::ranges::none_of(
*suggestions,
[](autofill::SuggestionType id) {
return id == autofill::SuggestionType::kPasswordEntry ||
id == autofill::SuggestionType::kAccountStoragePasswordEntry ||
id == autofill::SuggestionType::kGeneratePasswordEntry ||
id == autofill::SuggestionType::kWebauthnCredential;
},
&autofill::Suggestion::type);
if (has_no_fillable_suggestions) {
return;
}
bool has_webauthn_credential = base::ranges::any_of(
*suggestions,
[](autofill::SuggestionType type) {
return type == autofill::SuggestionType::kWebauthnCredential;
},
&autofill::Suggestion::type);
// Add a separator before the manage option unless there are no suggestions
// yet.
if (!suggestions->empty()) {
suggestions->emplace_back(autofill::SuggestionType::kSeparator);
}
autofill::Suggestion suggestion(l10n_util::GetStringUTF16(
has_webauthn_credential
? IDS_PASSWORD_MANAGER_MANAGE_PASSWORDS_AND_PASSKEYS
: IDS_PASSWORD_MANAGER_MANAGE_PASSWORDS));
suggestion.type = autofill::SuggestionType::kAllSavedPasswordsEntry;
suggestion.icon = autofill::Suggestion::Icon::kSettings;
// The UI code will pick up an icon from the resources based on the string.
suggestion.trailing_icon = autofill::Suggestion::Icon::kGooglePasswordManager;
suggestions->emplace_back(std::move(suggestion));
}
// If |field_suggestion| matches |field_content|, creates a Suggestion out of it
// and appends to |suggestions|.
void AppendSuggestionIfMatching(
const std::u16string& field_suggestion,
const std::u16string& field_contents,
const gfx::Image& custom_icon,
const std::string& signon_realm,
bool from_account_store,
size_t password_length,
std::vector<autofill::Suggestion>* suggestions) {
std::u16string lower_suggestion = base::i18n::ToLower(field_suggestion);
std::u16string lower_contents = base::i18n::ToLower(field_contents);
if (base::StartsWith(lower_suggestion, lower_contents,
base::CompareCase::SENSITIVE)) {
bool replaced_username;
autofill::Suggestion suggestion(
ReplaceEmptyUsername(field_suggestion, &replaced_username));
suggestion.main_text.is_primary =
autofill::Suggestion::Text::IsPrimary(!replaced_username);
suggestion.labels = {
{autofill::Suggestion::Text(GetHumanReadableRealm(signon_realm))}};
suggestion.additional_label =
std::u16string(password_length, kPasswordReplacementChar);
suggestion.voice_over = l10n_util::GetStringFUTF16(
IDS_PASSWORD_MANAGER_PASSWORD_FOR_ACCOUNT, suggestion.main_text.value);
if (!suggestion.labels.empty()) {
// The domainname is only shown for passwords with a common eTLD+1
// but different subdomain.
DCHECK_EQ(suggestion.labels.size(), 1U);
DCHECK_EQ(suggestion.labels[0].size(), 1U);
*suggestion.voice_over += u", ";
*suggestion.voice_over += suggestion.labels[0][0].value;
}
suggestion.type =
from_account_store
? autofill::SuggestionType::kAccountStoragePasswordEntry
: autofill::SuggestionType::kPasswordEntry;
suggestion.custom_icon = custom_icon;
// The UI code will pick up an icon from the resources based on the string.
suggestion.icon = autofill::Suggestion::Icon::kGlobe;
#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)
if (!base::FeatureList::IsEnabled(
password_manager::features::kButterOnDesktopFollowup)) {
suggestion.trailing_icon = CreateStoreIcon(from_account_store);
}
#endif
suggestions->emplace_back(std::move(suggestion));
}
}
// This function attempts to fill |suggestions| from |fill_data| based on
// |current_username| that is the current value of the field.
void GetSuggestions(const autofill::PasswordFormFillData& fill_data,
const std::u16string& current_username,
const gfx::Image& custom_icon,
std::vector<autofill::Suggestion>* suggestions) {
AppendSuggestionIfMatching(
fill_data.preferred_login.username_value, current_username, custom_icon,
fill_data.preferred_login.realm,
fill_data.preferred_login.uses_account_store,
fill_data.preferred_login.password_value.size(), suggestions);
int prefered_match = suggestions->size();
for (const auto& login : fill_data.additional_logins) {
AppendSuggestionIfMatching(
login.username_value, current_username, custom_icon, login.realm,
login.uses_account_store, login.password_value.size(), suggestions);
}
std::sort(suggestions->begin() + prefered_match, suggestions->end(),
[](const autofill::Suggestion& a, const autofill::Suggestion& b) {
return a.main_text.value < b.main_text.value;
});
}
void AddPasswordUsernameChildSuggestion(const std::u16string& username,
autofill::Suggestion& suggestion) {
suggestion.children.emplace_back(
username, autofill::SuggestionType::kPasswordFieldByFieldFilling);
}
void AddFillPasswordChildSuggestion(autofill::Suggestion& suggestion,
const CredentialUIEntry& credential,
IsCrossDomain is_cross_origin) {
autofill::Suggestion fill_password(
l10n_util::GetStringUTF16(
IDS_PASSWORD_MANAGER_MANUAL_FALLBACK_FILL_PASSWORD_ENTRY),
autofill::SuggestionType::kFillPassword);
fill_password.payload = autofill::Suggestion::PasswordSuggestionDetails(
credential.password,
GetHumanReadableRealm(credential.GetFirstSignonRealm()),
is_cross_origin.value());
suggestion.children.emplace_back(std::move(fill_password));
}
void AddViewPasswordDetailsChildSuggestion(autofill::Suggestion& suggestion) {
autofill::Suggestion view_password_details(
l10n_util::GetStringUTF16(
IDS_PASSWORD_MANAGER_MANUAL_FALLBACK_VIEW_DETAILS_ENTRY),
autofill::SuggestionType::kViewPasswordDetails);
view_password_details.icon = autofill::Suggestion::Icon::kKey;
suggestion.children.emplace_back(std::move(view_password_details));
}
void AppendManualFallbackSuggestions(
const CredentialUIEntry& credential,
IsTriggeredOnPasswordForm on_password_form,
IsCrossDomain is_cross_origin,
std::vector<autofill::Suggestion>* suggestions) {
// A separate suggestion with the same (username, password) pair is displayed
// for every affiliated domain. For example, if the credential was saved on
// apple.com and icloud.com, there will be 2 suggestions for both of these
// websites.
for (const CredentialUIEntry::DomainInfo& domain_info :
credential.GetAffiliatedDomains()) {
const std::u16string kDisplaySingonRealm =
base::UTF8ToUTF16(domain_info.name);
autofill::Suggestion suggestion(kDisplaySingonRealm,
autofill::SuggestionType::kPasswordEntry);
bool replaced;
const std::u16string maybe_username =
ReplaceEmptyUsername(credential.username, &replaced);
suggestion.additional_label = maybe_username;
suggestion.icon = autofill::Suggestion::Icon::kGlobe;
suggestion.payload = autofill::Suggestion::PasswordSuggestionDetails(
credential.password, kDisplaySingonRealm, is_cross_origin.value());
suggestion.is_acceptable = on_password_form.value();
if (!replaced) {
AddPasswordUsernameChildSuggestion(maybe_username, suggestion);
}
AddFillPasswordChildSuggestion(suggestion, credential, is_cross_origin);
suggestion.children.emplace_back(autofill::SuggestionType::kSeparator);
AddViewPasswordDetailsChildSuggestion(suggestion);
suggestions->emplace_back(std::move(suggestion));
}
}
} // namespace
PasswordSuggestionGenerator::PasswordSuggestionGenerator(
PasswordManagerDriver* password_manager_driver,
PasswordManagerClient* password_client)
: password_manager_driver_(password_manager_driver),
password_client_{password_client} {}
std::vector<autofill::Suggestion>
PasswordSuggestionGenerator::GetSuggestionsForDomain(
base::optional_ref<const autofill::PasswordFormFillData> fill_data,
const gfx::Image& page_favicon,
const std::u16string& username_filter,
OffersGeneration offers_generation,
ShowPasswordSuggestions show_password_suggestions,
ShowWebAuthnCredentials show_webauthn_credentials) const {
std::vector<autofill::Suggestion> suggestions;
bool show_account_storage_optin =
password_client_ && password_client_->GetPasswordFeatureManager()
->ShouldShowAccountStorageOptIn();
bool show_account_storage_resignin =
password_client_ && password_client_->GetPasswordFeatureManager()
->ShouldShowAccountStorageReSignin(
password_client_->GetLastCommittedURL());
// Add WebAuthn credentials suitable for an ongoing request if available.
WebAuthnCredentialsDelegate* delegate =
password_client_->GetWebAuthnCredentialsDelegateForDriver(
password_manager_driver_);
// |uses_passkeys| is used on desktop only to offer a way to sign in with a
// passkey on another device. On Android this is always false. It also will
// not be set on iOS since |show_webauthn_credentials| is always false.
bool uses_passkeys = false;
if (show_webauthn_credentials && delegate &&
delegate->GetPasskeys().has_value()) {
#if !BUILDFLAG(IS_ANDROID)
uses_passkeys = true;
#endif
base::ranges::transform(
*delegate->GetPasskeys(), std::back_inserter(suggestions),
[&page_favicon](const auto& passkey) {
autofill::Suggestion suggestion(ToUsernameString(passkey.username()));
suggestion.icon = autofill::Suggestion::Icon::kGlobe;
suggestion.type = autofill::SuggestionType::kWebauthnCredential;
suggestion.custom_icon = page_favicon;
suggestion.payload = autofill::Suggestion::Guid(
base::Base64Encode(passkey.credential_id()));
suggestion.labels = {
{autofill::Suggestion::Text(passkey.GetAuthenticatorLabel())}};
return suggestion;
});
}
if (!fill_data.has_value() && !show_account_storage_optin &&
!show_account_storage_resignin && !uses_passkeys && suggestions.empty()) {
// Probably the credential was deleted in the mean time.
return suggestions;
}
// Add password suggestions if they exist and were requested.
if (show_password_suggestions && fill_data.has_value()) {
GetSuggestions(*fill_data, username_filter, page_favicon, &suggestions);
}
#if !BUILDFLAG(IS_ANDROID)
// Add "Sign in with another device" button.
if (uses_passkeys && delegate->OfferPasskeysFromAnotherDeviceOption()) {
bool listed_passkeys = delegate->GetPasskeys().has_value() &&
delegate->GetPasskeys()->size() > 0;
suggestions.emplace_back(CreateWebAuthnEntry(listed_passkeys));
}
#endif
// Add password generation entry, if available.
if (offers_generation) {
suggestions.emplace_back(
show_account_storage_optin
? CreateEntryToOptInToAccountStorageThenGenerate()
: CreateGenerationEntry());
}
// Add button to opt into using the account storage for passwords and then
// suggest.
if (show_account_storage_optin) {
suggestions.emplace_back(CreateEntryToOptInToAccountStorageThenFill());
}
// Add button to sign-in which unlocks the previously used account store.
if (show_account_storage_resignin) {
suggestions.emplace_back(CreateEntryToReSignin());
}
// Add "Manage all passwords" link to settings.
MaybeAppendManagePasswordsEntry(&suggestions);
return suggestions;
}
std::vector<autofill::Suggestion>
PasswordSuggestionGenerator::GetManualFallbackSuggestions(
base::span<const PasswordForm> suggested_credentials,
base::span<const CredentialUIEntry> credentials,
IsTriggeredOnPasswordForm on_password_form) const {
std::vector<autofill::Suggestion> suggestions;
const bool generate_sections =
!suggested_credentials.empty() && !credentials.empty();
if (generate_sections) {
suggestions.emplace_back(
l10n_util::GetStringUTF16(
IDS_PASSWORD_MANAGER_MANUAL_FALLBACK_SUGGESTED_PASSWORDS_SECTION_TITLE),
autofill::SuggestionType::kTitle);
}
std::set<std::string> suggested_signon_realms;
for (const auto& form : suggested_credentials) {
suggested_signon_realms.insert(form.signon_realm);
AppendManualFallbackSuggestions(CredentialUIEntry(form), on_password_form,
IsCrossDomain(false), &suggestions);
}
if (generate_sections) {
suggestions.emplace_back(
l10n_util::GetStringUTF16(
IDS_PASSWORD_MANAGER_MANUAL_FALLBACK_ALL_PASSWORDS_SECTION_TITLE),
autofill::SuggestionType::kTitle);
}
// Only the "All passwords" section should be sorted alphabetically.
const size_t relevant_section_offset = suggestions.size();
for (const CredentialUIEntry& credential : credentials) {
// Check if any credential in the "Suggested" section has the same singon
// realm as this `CredentialUIEntry`.
const bool has_suggested_realm = base::ranges::any_of(
credential.facets,
[&suggested_signon_realms](const std::string& signon_realm) {
return suggested_signon_realms.count(signon_realm);
},
&CredentialFacet::signon_realm);
AppendManualFallbackSuggestions(credential, on_password_form,
IsCrossDomain(!has_suggested_realm),
&suggestions);
}
base::ranges::sort(suggestions.begin() + relevant_section_offset,
suggestions.end(), base::ranges::less(),
[](const autofill::Suggestion& suggestion) {
return suggestion.main_text.value;
});
// Add "Manage all passwords" link to settings.
MaybeAppendManagePasswordsEntry(&suggestions);
return suggestions;
}
} // namespace password_manager