| // 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 "components/password_manager/core/browser/votes_uploader.h" |
| |
| #include <ctype.h> |
| #include <map> |
| |
| #include "base/metrics/histogram_macros.h" |
| #include "base/rand_util.h" |
| #include "components/autofill/core/browser/autofill_download_manager.h" |
| #include "components/autofill/core/browser/form_structure.h" |
| #include "components/autofill/core/browser/randomized_encoder.h" |
| #include "components/autofill/core/common/form_data.h" |
| #include "components/password_manager/core/browser/browser_save_password_progress_logger.h" |
| #include "components/password_manager/core/browser/password_manager_client.h" |
| #include "components/password_manager/core/browser/password_manager_util.h" |
| |
| using autofill::AutofillDownloadManager; |
| using autofill::AutofillField; |
| using autofill::AutofillUploadContents; |
| using autofill::FormData; |
| using autofill::FormStructure; |
| using autofill::PasswordForm; |
| using autofill::RandomizedEncoder; |
| using autofill::ServerFieldType; |
| using autofill::ServerFieldTypeSet; |
| using autofill::ValueElementPair; |
| |
| using Logger = autofill::SavePasswordProgressLogger; |
| |
| namespace password_manager { |
| namespace { |
| |
| // Sets autofill types of password and new password fields in |field_types|. |
| // |password_type| (the autofill type of new password field) should be equal to |
| // NEW_PASSWORD, PROBABLY_NEW_PASSWORD or NOT_NEW_PASSWORD. These values |
| // correspond to cases when the user confirmed password update, did nothing or |
| // declined to update password respectively. |
| void SetFieldLabelsOnUpdate(const ServerFieldType password_type, |
| const PasswordForm& submitted_form, |
| FieldTypeMap* field_types) { |
| DCHECK(password_type == autofill::NEW_PASSWORD || |
| password_type == autofill::PROBABLY_NEW_PASSWORD || |
| password_type == autofill::NOT_NEW_PASSWORD) |
| << password_type; |
| if (submitted_form.new_password_element.empty()) |
| return; |
| |
| (*field_types)[submitted_form.password_element] = autofill::PASSWORD; |
| (*field_types)[submitted_form.new_password_element] = password_type; |
| } |
| |
| // Sets the autofill type of the password field stored in |submitted_form| to |
| // |password_type| in |field_types| map. |
| void SetFieldLabelsOnSave(const ServerFieldType password_type, |
| const PasswordForm& form, |
| FieldTypeMap* field_types) { |
| DCHECK(password_type == autofill::PASSWORD || |
| password_type == autofill::ACCOUNT_CREATION_PASSWORD || |
| password_type == autofill::NOT_ACCOUNT_CREATION_PASSWORD) |
| << password_type; |
| |
| if (!form.new_password_element.empty()) |
| (*field_types)[form.new_password_element] = password_type; |
| else if (!form.password_element.empty()) |
| (*field_types)[form.password_element] = password_type; |
| } |
| |
| // Label username and password fields with autofill types in |form_structure| |
| // based on |field_types|, and vote types based on |vote_types|. The function |
| // also adds the types to |available_field_types|. For fields of |USERNAME| |
| // type, a vote type must exist. |
| void LabelFields(const FieldTypeMap& field_types, |
| const VoteTypeMap& vote_types, |
| FormStructure* form_structure, |
| ServerFieldTypeSet* available_field_types) { |
| for (size_t i = 0; i < form_structure->field_count(); ++i) { |
| AutofillField* field = form_structure->field(i); |
| |
| ServerFieldType type = autofill::UNKNOWN_TYPE; |
| if (!field->name.empty()) { |
| auto iter = field_types.find(field->name); |
| if (iter != field_types.end()) { |
| type = iter->second; |
| available_field_types->insert(type); |
| } |
| |
| auto vote_type_iter = vote_types.find(field->name); |
| if (vote_type_iter != vote_types.end()) |
| field->set_vote_type(vote_type_iter->second); |
| DCHECK(type != autofill::USERNAME || |
| field->vote_type() != |
| AutofillUploadContents::Field::NO_INFORMATION); |
| } |
| |
| ServerFieldTypeSet types; |
| types.insert(type); |
| field->set_possible_types(types); |
| } |
| } |
| |
| // Returns true iff |credentials| has the same password as an entry in |matches| |
| // which doesn't have a username. |
| bool IsAddingUsernameToExistingMatch( |
| const PasswordForm& credentials, |
| const std::map<base::string16, const PasswordForm*>& matches) { |
| const auto match = matches.find(base::string16()); |
| return !credentials.username_value.empty() && match != matches.end() && |
| !match->second->is_public_suffix_match && |
| match->second->password_value == credentials.password_value; |
| } |
| |
| // Helper functions for character type classification. The built-in functions |
| // depend on locale, platform and other stuff. To make the output more |
| // predictable, the function are re-implemented here. |
| bool IsNumeric(int c) { |
| return '0' <= c && c <= '9'; |
| } |
| bool IsLowercaseLetter(int c) { |
| return 'a' <= c && c <= 'z'; |
| } |
| bool IsUppercaseLetter(int c) { |
| return 'A' <= c && c <= 'Z'; |
| } |
| bool IsSpecialSymbol(int c) { |
| return ('!' <= c && c <= '/') || (':' <= c && c <= '@') || |
| ('[' <= c && c <= '`') || ('{' <= c && c <= '~'); |
| } |
| |
| } // namespace |
| |
| VotesUploader::VotesUploader(PasswordManagerClient* client, |
| bool is_possible_change_password_form) |
| : client_(client), |
| is_possible_change_password_form_(is_possible_change_password_form) {} |
| |
| VotesUploader::VotesUploader(const VotesUploader& other) = default; |
| VotesUploader::~VotesUploader() = default; |
| |
| void VotesUploader::SendVotesOnSave( |
| const FormData& observed, |
| const PasswordForm& submitted_form, |
| const std::map<base::string16, const PasswordForm*>& best_matches, |
| PasswordForm* pending_credentials) { |
| if (pending_credentials->times_used == 1 || |
| IsAddingUsernameToExistingMatch(*pending_credentials, best_matches)) |
| UploadFirstLoginVotes(best_matches, *pending_credentials, submitted_form); |
| |
| // 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 (pending_credentials->times_used == 0) { |
| UploadPasswordVote(*pending_credentials, submitted_form, autofill::PASSWORD, |
| std::string()); |
| if (has_username_correction_vote_) { |
| UploadPasswordVote(username_correction_vote_, submitted_form, |
| autofill::USERNAME, |
| FormStructure(observed).FormSignatureAsStr()); |
| } |
| } else { |
| SendVoteOnCredentialsReuse(observed, submitted_form, pending_credentials); |
| } |
| } |
| |
| void VotesUploader::SendVoteOnCredentialsReuse( |
| const FormData& observed, |
| const PasswordForm& submitted_form, |
| PasswordForm* pending) { |
| // 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->form_data.fields.empty()) |
| return; |
| |
| FormStructure pending_structure(pending->form_data); |
| FormStructure observed_structure(observed); |
| |
| if (pending_structure.form_signature() != |
| observed_structure.form_signature()) { |
| // 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. |
| if (pending->times_used == 1) { |
| if (UploadPasswordVote(*pending, submitted_form, |
| autofill::ACCOUNT_CREATION_PASSWORD, |
| observed_structure.FormSignatureAsStr())) { |
| pending->generation_upload_status = PasswordForm::POSITIVE_SIGNAL_SENT; |
| } |
| } |
| } else if (pending->generation_upload_status == |
| 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 (UploadPasswordVote(*pending, submitted_form, |
| autofill::NOT_ACCOUNT_CREATION_PASSWORD, |
| std::string())) { |
| pending->generation_upload_status = PasswordForm::NEGATIVE_SIGNAL_SENT; |
| } |
| } else if (generation_popup_was_shown_) { |
| // Even if there is no autofill vote to be sent, send the vote about the |
| // usage of the generation popup. |
| UploadPasswordVote(*pending, submitted_form, autofill::UNKNOWN_TYPE, |
| std::string()); |
| } |
| } |
| |
| bool VotesUploader::UploadPasswordVote( |
| const PasswordForm& form_to_upload, |
| const PasswordForm& submitted_form, |
| const ServerFieldType autofill_type, |
| const std::string& login_form_signature) { |
| // Check if there is any vote to be sent. |
| bool has_autofill_vote = autofill_type != autofill::UNKNOWN_TYPE; |
| bool has_password_generation_vote = generation_popup_was_shown_; |
| if (!has_autofill_vote && !has_password_generation_vote) |
| return false; |
| |
| if (form_to_upload.form_data.fields.empty()) { |
| // List of fields may be empty in tests. |
| return false; |
| } |
| |
| AutofillDownloadManager* download_manager = |
| client_->GetAutofillDownloadManager(); |
| if (!download_manager) |
| return false; |
| |
| // If this is an update, a vote about the observed form is sent. If the user |
| // re-uses credentials, a vote about the saved form is sent. If the user saves |
| // credentials, the observed and pending forms are the same. |
| FormStructure form_structure(form_to_upload.form_data); |
| form_structure.set_submission_event(submitted_form.submission_event); |
| |
| ServerFieldTypeSet available_field_types; |
| // A map from field names to field types. |
| FieldTypeMap field_types; |
| auto username_vote_type = AutofillUploadContents::Field::NO_INFORMATION; |
| if (autofill_type != autofill::USERNAME) { |
| if (has_autofill_vote) { |
| bool is_update = autofill_type == autofill::NEW_PASSWORD || |
| autofill_type == autofill::PROBABLY_NEW_PASSWORD || |
| autofill_type == autofill::NOT_NEW_PASSWORD; |
| |
| if (is_update) { |
| if (form_to_upload.new_password_element.empty()) |
| return false; |
| SetFieldLabelsOnUpdate(autofill_type, form_to_upload, &field_types); |
| } else { // Saving. |
| SetFieldLabelsOnSave(autofill_type, form_to_upload, &field_types); |
| } |
| if (autofill_type != autofill::ACCOUNT_CREATION_PASSWORD) { |
| // If |autofill_type| == autofill::ACCOUNT_CREATION_PASSWORD, Chrome |
| // will upload a vote for another form: the one that the credential was |
| // saved on. |
| field_types[submitted_form.confirmation_password_element] = |
| autofill::CONFIRMATION_PASSWORD; |
| form_structure.set_passwords_were_revealed( |
| has_passwords_revealed_vote_); |
| } |
| } |
| if (autofill_type != autofill::ACCOUNT_CREATION_PASSWORD) { |
| if (generation_popup_was_shown_) |
| AddGeneratedVote(&form_structure); |
| if (has_username_edited_vote_) { |
| field_types[form_to_upload.username_element] = autofill::USERNAME; |
| username_vote_type = AutofillUploadContents::Field::USERNAME_EDITED; |
| } |
| } else { // User reuses credentials. |
| // If the saved username value was used, then send a confirmation vote for |
| // username. |
| if (!submitted_form.username_value.empty()) { |
| DCHECK(submitted_form.username_value == form_to_upload.username_value); |
| field_types[form_to_upload.username_element] = autofill::USERNAME; |
| username_vote_type = AutofillUploadContents::Field::CREDENTIALS_REUSED; |
| } |
| } |
| if (autofill_type == autofill::PASSWORD) { |
| // The password attributes should be uploaded only on the first save. |
| DCHECK(form_to_upload.times_used == 0); |
| GeneratePasswordAttributesVote(form_to_upload.password_value, |
| &form_structure); |
| } |
| } else { // User overwrites username. |
| field_types[form_to_upload.username_element] = autofill::USERNAME; |
| field_types[form_to_upload.password_element] = |
| autofill::ACCOUNT_CREATION_PASSWORD; |
| username_vote_type = AutofillUploadContents::Field::USERNAME_OVERWRITTEN; |
| } |
| LabelFields(field_types, |
| {{form_to_upload.username_element, username_vote_type}}, |
| &form_structure, &available_field_types); |
| |
| // Force uploading as these events are relatively rare and we want to make |
| // sure to receive them. |
| form_structure.set_upload_required(UPLOAD_REQUIRED); |
| |
| if (password_manager_util::IsLoggingActive(client_)) { |
| BrowserSavePasswordProgressLogger logger(client_->GetLogManager()); |
| logger.LogFormStructure(Logger::STRING_FORM_VOTES, form_structure); |
| } |
| |
| // Annotate the form with the source language of the page. |
| form_structure.set_page_language(client_->GetPageLanguage()); |
| |
| // Attach the Randomized Encoder. |
| form_structure.set_randomized_encoder( |
| RandomizedEncoder::Create(client_->GetPrefs())); |
| |
| bool success = download_manager->StartUploadRequest( |
| form_structure, false /* was_autofilled */, available_field_types, |
| login_form_signature, true /* observed_submission */, |
| nullptr /* prefs */); |
| |
| UMA_HISTOGRAM_BOOLEAN("PasswordGeneration.UploadStarted", success); |
| return success; |
| } |
| |
| // TODO(crbug.com/840384): Share common code with UploadPasswordVote. |
| void VotesUploader::UploadFirstLoginVotes( |
| const std::map<base::string16, const PasswordForm*>& best_matches, |
| const PasswordForm& pending_credentials, |
| const PasswordForm& form_to_upload) { |
| AutofillDownloadManager* download_manager = |
| client_->GetAutofillDownloadManager(); |
| if (!download_manager) |
| return; |
| |
| if (form_to_upload.form_data.fields.empty()) { |
| // List of fields may be empty in tests. |
| return; |
| } |
| |
| FormStructure form_structure(form_to_upload.form_data); |
| form_structure.set_submission_event(form_to_upload.submission_event); |
| |
| FieldTypeMap field_types = { |
| {form_to_upload.username_element, autofill::USERNAME}}; |
| VoteTypeMap vote_types = {{form_to_upload.username_element, |
| AutofillUploadContents::Field::FIRST_USE}}; |
| if (!password_overridden_) { |
| field_types[form_to_upload.password_element] = autofill::PASSWORD; |
| vote_types[form_to_upload.password_element] = |
| AutofillUploadContents::Field::FIRST_USE; |
| } |
| |
| ServerFieldTypeSet available_field_types; |
| LabelFields(field_types, vote_types, &form_structure, &available_field_types); |
| SetKnownValueFlag(pending_credentials, best_matches, &form_structure); |
| |
| // Force uploading as these events are relatively rare and we want to make |
| // sure to receive them. |
| form_structure.set_upload_required(UPLOAD_REQUIRED); |
| |
| if (password_manager_util::IsLoggingActive(client_)) { |
| BrowserSavePasswordProgressLogger logger(client_->GetLogManager()); |
| logger.LogFormStructure(Logger::STRING_FORM_VOTES, form_structure); |
| } |
| |
| // Annotate the form with the source language of the page. |
| form_structure.set_page_language(client_->GetPageLanguage()); |
| |
| // Attach the Randomized Encoder. |
| form_structure.set_randomized_encoder( |
| RandomizedEncoder::Create(client_->GetPrefs())); |
| |
| download_manager->StartUploadRequest( |
| form_structure, false /* was_autofilled */, available_field_types, |
| std::string(), true /* observed_submission */, nullptr /* prefs */); |
| } |
| |
| void VotesUploader::AddGeneratedVote(FormStructure* form_structure) { |
| DCHECK(form_structure); |
| DCHECK(generation_popup_was_shown_); |
| |
| if (generation_element_.empty()) |
| return; |
| |
| AutofillUploadContents::Field::PasswordGenerationType type = |
| AutofillUploadContents::Field::NO_GENERATION; |
| if (has_generated_password_) { |
| UMA_HISTOGRAM_BOOLEAN("PasswordGeneration.IsTriggeredManually", |
| is_manual_generation_); |
| if (is_manual_generation_) { |
| type = is_possible_change_password_form_ |
| ? AutofillUploadContents::Field:: |
| MANUALLY_TRIGGERED_GENERATION_ON_CHANGE_PASSWORD_FORM |
| : AutofillUploadContents::Field:: |
| MANUALLY_TRIGGERED_GENERATION_ON_SIGN_UP_FORM; |
| } else { |
| type = |
| is_possible_change_password_form_ |
| ? AutofillUploadContents::Field:: |
| AUTOMATICALLY_TRIGGERED_GENERATION_ON_CHANGE_PASSWORD_FORM |
| : AutofillUploadContents::Field:: |
| AUTOMATICALLY_TRIGGERED_GENERATION_ON_SIGN_UP_FORM; |
| } |
| } else |
| type = AutofillUploadContents::Field::IGNORED_GENERATION_POPUP; |
| |
| for (size_t i = 0; i < form_structure->field_count(); ++i) { |
| AutofillField* field = form_structure->field(i); |
| if (field->name == generation_element_) { |
| field->set_generation_type(type); |
| if (has_generated_password_) { |
| field->set_generated_password_changed(generated_password_changed_); |
| UMA_HISTOGRAM_BOOLEAN("PasswordGeneration.GeneratedPasswordWasEdited", |
| generated_password_changed_); |
| } |
| break; |
| } |
| } |
| } |
| |
| void VotesUploader::SetKnownValueFlag( |
| const PasswordForm& pending_credentials, |
| const std::map<base::string16, const PasswordForm*>& best_matches, |
| FormStructure* form) { |
| DCHECK(!password_overridden_ || |
| best_matches.find(pending_credentials.username_value) != |
| best_matches.end()) |
| << "The credential is being overriden, but it does not exist in " |
| "the best matches."; |
| |
| const base::string16& known_username = pending_credentials.username_value; |
| // If we are updating a password, the known value is the old password, not |
| // the new one. |
| const base::string16& known_password = |
| password_overridden_ ? best_matches.at(known_username)->password_value |
| : pending_credentials.password_value; |
| |
| for (auto& field : *form) { |
| if (field->value.empty()) |
| continue; |
| if (known_username == field->value || known_password == field->value) { |
| field->properties_mask |= autofill::FieldPropertiesFlags::KNOWN_VALUE; |
| } |
| } |
| } |
| |
| bool VotesUploader::FindUsernameInOtherPossibleUsernames( |
| const PasswordForm& match, |
| const base::string16& username) { |
| DCHECK(!has_username_correction_vote_); |
| |
| for (const ValueElementPair& pair : match.other_possible_usernames) { |
| if (pair.first == username) { |
| username_correction_vote_ = match; |
| username_correction_vote_.username_element = pair.second; |
| has_username_correction_vote_ = true; |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| bool VotesUploader::FindCorrectedUsernameElement( |
| const std::map<base::string16, const PasswordForm*>& best_matches, |
| const std::vector<const PasswordForm*>& not_best_matches, |
| const base::string16& username, |
| const base::string16& password) { |
| if (username.empty()) |
| return false; |
| for (const auto& key_value : best_matches) { |
| const PasswordForm* match = key_value.second; |
| if ((match->password_value == password) && |
| FindUsernameInOtherPossibleUsernames(*match, username)) |
| return true; |
| } |
| for (const PasswordForm* match : not_best_matches) { |
| if ((match->password_value == password) && |
| FindUsernameInOtherPossibleUsernames(*match, username)) |
| return true; |
| } |
| return false; |
| } |
| |
| void VotesUploader::GeneratePasswordAttributesVote( |
| const base::string16& password_value, |
| FormStructure* form_structure) { |
| // Don't crowdsource password attributes for non-ascii passwords. |
| for (const auto& e : password_value) |
| if (!(IsUppercaseLetter(e) || IsLowercaseLetter(e) || IsNumeric(e) || |
| IsSpecialSymbol(e))) |
| return; |
| |
| // Select a character class attribute to upload. |
| int bucket = base::RandGenerator(9); |
| bool (*predicate)(int c) = nullptr; |
| autofill::PasswordAttribute character_class_attribute = |
| autofill::PasswordAttribute::kHasSpecialSymbol; |
| if (bucket == 0) { |
| predicate = &IsLowercaseLetter; |
| character_class_attribute = |
| autofill::PasswordAttribute::kHasLowercaseLetter; |
| } else if (bucket == 1) { |
| predicate = &IsUppercaseLetter; |
| character_class_attribute = |
| autofill::PasswordAttribute::kHasUppercaseLetter; |
| } else if (bucket == 2) { |
| predicate = &IsNumeric; |
| character_class_attribute = autofill::PasswordAttribute::kHasNumeric; |
| } else { // 3 <= bucket < 9 |
| // Upload symbols more often as 2/3rd of issues are because of missing |
| // special symbols. |
| predicate = &IsSpecialSymbol; |
| character_class_attribute = autofill::PasswordAttribute::kHasSpecialSymbol; |
| } |
| bool actual_value_for_character_class = |
| std::any_of(password_value.begin(), password_value.end(), predicate); |
| |
| // Apply the randomized response technique to noisify the actual value |
| // (https://en.wikipedia.org/wiki/Randomized_response). |
| bool randomized_value_for_character_class = |
| base::RandGenerator(2) ? actual_value_for_character_class |
| : base::RandGenerator(2); |
| form_structure->set_password_attributes_vote(std::make_pair( |
| character_class_attribute, randomized_value_for_character_class)); |
| |
| size_t actual_length = password_value.size(); |
| size_t randomized_length = actual_length <= 1 || base::RandGenerator(5) == 0 |
| ? actual_length |
| : base::RandGenerator(actual_length - 1) + 1; |
| form_structure->set_password_length_vote(randomized_length); |
| } |
| |
| } // namespace password_manager |