| // 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/sync/password_sync_bridge.h" |
| |
| #include <unordered_set> |
| |
| #include "base/auto_reset.h" |
| #include "base/logging.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "components/autofill/core/common/password_form.h" |
| #include "components/password_manager/core/browser/password_store_sync.h" |
| #include "components/sync/model/metadata_batch.h" |
| #include "components/sync/model/metadata_change_list.h" |
| #include "components/sync/model/model_type_change_processor.h" |
| #include "components/sync/model/mutable_data_batch.h" |
| #include "components/sync/model_impl/in_memory_metadata_change_list.h" |
| #include "components/sync/model_impl/sync_metadata_store_change_list.h" |
| #include "net/base/escape.h" |
| #include "url/gurl.h" |
| |
| namespace password_manager { |
| |
| namespace { |
| |
| sync_pb::PasswordSpecifics SpecificsFromPassword( |
| const autofill::PasswordForm& password_form) { |
| sync_pb::PasswordSpecifics specifics; |
| sync_pb::PasswordSpecificsData* password_data = |
| specifics.mutable_client_only_encrypted_data(); |
| password_data->set_scheme(password_form.scheme); |
| password_data->set_signon_realm(password_form.signon_realm); |
| password_data->set_origin(password_form.origin.spec()); |
| password_data->set_action(password_form.action.spec()); |
| password_data->set_username_element( |
| base::UTF16ToUTF8(password_form.username_element)); |
| password_data->set_password_element( |
| base::UTF16ToUTF8(password_form.password_element)); |
| password_data->set_username_value( |
| base::UTF16ToUTF8(password_form.username_value)); |
| password_data->set_password_value( |
| base::UTF16ToUTF8(password_form.password_value)); |
| password_data->set_preferred(password_form.preferred); |
| password_data->set_date_created( |
| password_form.date_created.ToDeltaSinceWindowsEpoch().InMicroseconds()); |
| password_data->set_blacklisted(password_form.blacklisted_by_user); |
| password_data->set_type(password_form.type); |
| password_data->set_times_used(password_form.times_used); |
| password_data->set_display_name( |
| base::UTF16ToUTF8(password_form.display_name)); |
| password_data->set_avatar_url(password_form.icon_url.spec()); |
| password_data->set_federation_url( |
| password_form.federation_origin.opaque() |
| ? std::string() |
| : password_form.federation_origin.Serialize()); |
| return specifics; |
| } |
| |
| autofill::PasswordForm PasswordFromEntityChange( |
| const syncer::EntityChange& entity_change, |
| base::Time sync_time) { |
| DCHECK(entity_change.data().specifics.has_password()); |
| const sync_pb::PasswordSpecificsData& password_data = |
| entity_change.data().specifics.password().client_only_encrypted_data(); |
| |
| autofill::PasswordForm password; |
| password.scheme = |
| static_cast<autofill::PasswordForm::Scheme>(password_data.scheme()); |
| password.signon_realm = password_data.signon_realm(); |
| password.origin = GURL(password_data.origin()); |
| password.action = GURL(password_data.action()); |
| password.username_element = |
| base::UTF8ToUTF16(password_data.username_element()); |
| password.password_element = |
| base::UTF8ToUTF16(password_data.password_element()); |
| password.username_value = base::UTF8ToUTF16(password_data.username_value()); |
| password.password_value = base::UTF8ToUTF16(password_data.password_value()); |
| password.preferred = password_data.preferred(); |
| password.date_created = base::Time::FromDeltaSinceWindowsEpoch( |
| // Use FromDeltaSinceWindowsEpoch because create_time_us has |
| // always used the Windows epoch. |
| base::TimeDelta::FromMicroseconds(password_data.date_created())); |
| password.blacklisted_by_user = password_data.blacklisted(); |
| password.type = |
| static_cast<autofill::PasswordForm::Type>(password_data.type()); |
| password.times_used = password_data.times_used(); |
| password.display_name = base::UTF8ToUTF16(password_data.display_name()); |
| password.icon_url = GURL(password_data.avatar_url()); |
| password.federation_origin = |
| url::Origin::Create(GURL(password_data.federation_url())); |
| password.date_synced = sync_time; |
| |
| return password; |
| } |
| |
| std::unique_ptr<syncer::EntityData> CreateEntityData( |
| const autofill::PasswordForm& form) { |
| auto entity_data = std::make_unique<syncer::EntityData>(); |
| *entity_data->specifics.mutable_password() = SpecificsFromPassword(form); |
| entity_data->non_unique_name = form.signon_realm; |
| return entity_data; |
| } |
| |
| int ParsePrimaryKey(const std::string& storage_key) { |
| int primary_key = 0; |
| bool success = base::StringToInt(storage_key, &primary_key); |
| DCHECK(success) |
| << "Invalid storage key. Failed to convert the storage key to " |
| "an integer"; |
| return primary_key; |
| } |
| |
| // A simple class for scoping a password store sync transaction. This does not |
| // support rollback since the password store sync doesn't either. |
| class ScopedStoreTransaction { |
| public: |
| explicit ScopedStoreTransaction(PasswordStoreSync* store) : store_(store) { |
| store_->BeginTransaction(); |
| } |
| ~ScopedStoreTransaction() { store_->CommitTransaction(); } |
| |
| private: |
| PasswordStoreSync* store_; |
| |
| DISALLOW_COPY_AND_ASSIGN(ScopedStoreTransaction); |
| }; |
| |
| } // namespace |
| |
| PasswordSyncBridge::PasswordSyncBridge( |
| std::unique_ptr<syncer::ModelTypeChangeProcessor> change_processor, |
| PasswordStoreSync* password_store_sync) |
| : ModelTypeSyncBridge(std::move(change_processor)), |
| password_store_sync_(password_store_sync) { |
| DCHECK(password_store_sync_); |
| // TODO(crbug.com/902349): Read the actual metadata from the PasswordStoreSync |
| // be introducing a new API to read them. |
| this->change_processor()->ModelReadyToSync( |
| std::make_unique<syncer::MetadataBatch>()); |
| } |
| |
| PasswordSyncBridge::~PasswordSyncBridge() = default; |
| |
| void PasswordSyncBridge::ActOnPasswordStoreChanges( |
| const PasswordStoreChangeList& local_changes) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| // It's the responsibility of the callers to call this method within the same |
| // transaction as the data changes to fulfill atomic writes of data and |
| // metadata constraint. |
| |
| // TODO(mamir):ActOnPasswordStoreChanges() DCHECK we are inside a |
| // transaction!; |
| |
| if (!change_processor()->IsTrackingMetadata()) { |
| return; // Sync processor not yet ready, don't sync. |
| } |
| |
| // ActOnPasswordStoreChanges() can be called from ApplySyncChanges(). Do |
| // nothing in this case. |
| if (is_processing_remote_sync_changes_) { |
| return; |
| } |
| |
| syncer::SyncMetadataStoreChangeList metadata_change_list( |
| password_store_sync_->GetMetadataStore(), syncer::PASSWORDS); |
| |
| for (const PasswordStoreChange& change : local_changes) { |
| const std::string storage_key = base::NumberToString(change.primary_key()); |
| switch (change.type()) { |
| case PasswordStoreChange::ADD: |
| case PasswordStoreChange::UPDATE: { |
| change_processor()->Put(storage_key, CreateEntityData(change.form()), |
| &metadata_change_list); |
| break; |
| } |
| case PasswordStoreChange::REMOVE: { |
| change_processor()->Delete(storage_key, &metadata_change_list); |
| break; |
| } |
| } |
| } |
| } |
| |
| std::unique_ptr<syncer::MetadataChangeList> |
| PasswordSyncBridge::CreateMetadataChangeList() { |
| return std::make_unique<syncer::InMemoryMetadataChangeList>(); |
| } |
| |
| base::Optional<syncer::ModelError> PasswordSyncBridge::MergeSyncData( |
| std::unique_ptr<syncer::MetadataChangeList> metadata_change_list, |
| syncer::EntityChangeList entity_data) { |
| base::AutoReset<bool> processing_changes(&is_processing_remote_sync_changes_, |
| true); |
| // Read all local passwords. |
| PrimaryKeyToFormMap key_to_local_form_map; |
| if (!password_store_sync_->ReadAllLogins(&key_to_local_form_map)) { |
| return syncer::ModelError(FROM_HERE, |
| "Failed to load entries from password store."); |
| } |
| |
| // Collect the client tags of remote passwords. Note that |entity_data| only |
| // contains client tag *hashes*. |
| std::unordered_set<std::string> client_tags_of_remote_passwords; |
| for (const syncer::EntityChange& entity_change : entity_data) { |
| client_tags_of_remote_passwords.insert(GetClientTag(entity_change.data())); |
| } |
| |
| // This is used to keep track of all the changes applied to the password |
| // store to notify other observers of the password store. |
| PasswordStoreChangeList password_store_changes; |
| base::Optional<syncer::ModelError> error; |
| { |
| ScopedStoreTransaction transaction(password_store_sync_); |
| // For any local password that doesn't exist in the remote passwords, issue |
| // a change_processor()->Put(). Password comparison is done by comparing the |
| // client tags. In addition, collect the client tags of local passwords. |
| std::unordered_set<std::string> client_tags_of_local_passwords; |
| for (const auto& pair : key_to_local_form_map) { |
| std::unique_ptr<syncer::EntityData> entity_data = |
| CreateEntityData(/*password_form=*/*pair.second); |
| const std::string client_tag_of_local_password = |
| GetClientTag(*entity_data); |
| client_tags_of_local_passwords.insert(client_tag_of_local_password); |
| if (client_tags_of_remote_passwords.count(client_tag_of_local_password) == |
| 0) { |
| change_processor()->Put( |
| /*storage_key=*/base::NumberToString(pair.first), |
| std::move(entity_data), metadata_change_list.get()); |
| } |
| } |
| |
| // For any remote password that doesn't exist in the local passwords, issue |
| // a password_store_sync_->AddLoginSync() and for those that exist in the |
| // local passwords, issue a password_store_sync_->UpdateLoginSync(). |
| // Password comparison is done by comparing the client tags. In both cases, |
| // invoke the change_processor()->UpdateStorageKey(). |
| const base::Time time_now = base::Time::Now(); |
| for (const syncer::EntityChange& entity_change : entity_data) { |
| const std::string client_tag_of_remote_password = |
| GetClientTag(entity_change.data()); |
| PasswordStoreChangeList changes; |
| if (client_tags_of_local_passwords.count(client_tag_of_remote_password) == |
| 0) { |
| changes = password_store_sync_->AddLoginSync( |
| PasswordFromEntityChange(entity_change, /*sync_time=*/time_now)); |
| DCHECK_LE(1U, changes.size()); |
| } else { |
| changes = password_store_sync_->UpdateLoginSync( |
| PasswordFromEntityChange(entity_change, /*sync_time=*/time_now)); |
| DCHECK_LE(1U, changes.size()); |
| } |
| if (changes.empty()) { |
| return syncer::ModelError( |
| FROM_HERE, "Failed to add/update an entry in the password store."); |
| } |
| change_processor()->UpdateStorageKey( |
| entity_change.data(), |
| /*storage_key=*/ |
| base::NumberToString(changes[0].primary_key()), |
| metadata_change_list.get()); |
| password_store_changes.push_back(changes[0]); |
| } |
| |
| // Persist the metadata changes. |
| // TODO(mamir): add some test coverage for the metadata persistence. |
| syncer::SyncMetadataStoreChangeList sync_metadata_store_change_list( |
| password_store_sync_->GetMetadataStore(), syncer::PASSWORDS); |
| // |metadata_change_list| must have been created via |
| // CreateMetadataChangeList() so downcasting is safe. |
| static_cast<syncer::InMemoryMetadataChangeList*>(metadata_change_list.get()) |
| ->TransferChangesTo(&sync_metadata_store_change_list); |
| error = sync_metadata_store_change_list.TakeError(); |
| } // End of scoped transaction. |
| |
| if (!password_store_changes.empty()) { |
| // It could be the case that there are no remote passwords. In such case, |
| // there would be no changes to the password store other than the sync |
| // metadata changes, and no need to notify observers since they aren't |
| // interested in changes to sync metadata. |
| password_store_sync_->NotifyLoginsChanged(password_store_changes); |
| } |
| |
| return base::nullopt; |
| } |
| |
| base::Optional<syncer::ModelError> PasswordSyncBridge::ApplySyncChanges( |
| std::unique_ptr<syncer::MetadataChangeList> metadata_change_list, |
| syncer::EntityChangeList entity_changes) { |
| base::AutoReset<bool> processing_changes(&is_processing_remote_sync_changes_, |
| true); |
| |
| const base::Time time_now = base::Time::Now(); |
| |
| // This is used to keep track of all the changes applied to the password store |
| // to notify other observers of the password store. |
| PasswordStoreChangeList password_store_changes; |
| base::Optional<syncer::ModelError> error; |
| { |
| ScopedStoreTransaction transaction(password_store_sync_); |
| |
| for (const syncer::EntityChange& entity_change : entity_changes) { |
| PasswordStoreChangeList changes; |
| switch (entity_change.type()) { |
| case syncer::EntityChange::ACTION_ADD: |
| changes = password_store_sync_->AddLoginSync( |
| PasswordFromEntityChange(entity_change, /*sync_time=*/time_now)); |
| // If the addition has been successful, inform the processor about the |
| // assigned storage key. AddLoginSync() might return multiple changes |
| // and the last one should be the one representing the actual addition |
| // in the DB. |
| if (changes.empty()) { |
| return syncer::ModelError( |
| FROM_HERE, "Failed to add an entry to the password store."); |
| } |
| DCHECK_EQ(1U, changes.size()); |
| DCHECK_EQ(PasswordStoreChange::ADD, changes[0].type()); |
| change_processor()->UpdateStorageKey( |
| entity_change.data(), |
| /*storage_key=*/ |
| base::NumberToString(changes[0].primary_key()), |
| metadata_change_list.get()); |
| break; |
| case syncer::EntityChange::ACTION_UPDATE: |
| changes = password_store_sync_->UpdateLoginSync( |
| PasswordFromEntityChange(entity_change, /*sync_time=*/time_now)); |
| if (changes.empty()) { |
| return syncer::ModelError( |
| FROM_HERE, "Failed to update an entry in the password store."); |
| } |
| DCHECK_EQ(1U, changes.size()); |
| DCHECK(changes[0].primary_key() == |
| ParsePrimaryKey(entity_change.storage_key())); |
| break; |
| case syncer::EntityChange::ACTION_DELETE: { |
| int primary_key = ParsePrimaryKey(entity_change.storage_key()); |
| changes = |
| password_store_sync_->RemoveLoginByPrimaryKeySync(primary_key); |
| if (changes.empty()) { |
| return syncer::ModelError( |
| FROM_HERE, |
| "Failed to delete an entry from the password store."); |
| } |
| DCHECK_EQ(1U, changes.size()); |
| DCHECK_EQ(changes[0].primary_key(), primary_key); |
| break; |
| } |
| } |
| password_store_changes.push_back(changes[0]); |
| } |
| |
| // Persist the metadata changes. |
| // TODO(mamir): add some test coverage for the metadata persistence. |
| syncer::SyncMetadataStoreChangeList sync_metadata_store_change_list( |
| password_store_sync_->GetMetadataStore(), syncer::PASSWORDS); |
| // |metadata_change_list| must have been created via |
| // CreateMetadataChangeList() so downcasting is safe. |
| static_cast<syncer::InMemoryMetadataChangeList*>(metadata_change_list.get()) |
| ->TransferChangesTo(&sync_metadata_store_change_list); |
| error = sync_metadata_store_change_list.TakeError(); |
| } // End of scoped transaction. |
| |
| if (!password_store_changes.empty()) { |
| // It could be the case that there are no password store changes, and all |
| // changes are only metadata changes. In such case, no need to notify |
| // observers since they aren't interested in changes to sync metadata. |
| password_store_sync_->NotifyLoginsChanged(password_store_changes); |
| } |
| return error; |
| } |
| |
| void PasswordSyncBridge::GetData(StorageKeyList storage_keys, |
| DataCallback callback) { |
| // This method is called only when there are uncommitted changes on startup. |
| // There are more efficient implementations, but since this method is rarely |
| // called, simplicity is preferred over efficiency. |
| PrimaryKeyToFormMap key_to_form_map; |
| if (!password_store_sync_->ReadAllLogins(&key_to_form_map)) { |
| change_processor()->ReportError( |
| {FROM_HERE, "Failed to load entries from the password store."}); |
| return; |
| } |
| |
| auto batch = std::make_unique<syncer::MutableDataBatch>(); |
| for (const std::string& storage_key : storage_keys) { |
| int primary_key = ParsePrimaryKey(storage_key); |
| if (key_to_form_map.count(primary_key) != 0) { |
| batch->Put(storage_key, CreateEntityData(*key_to_form_map[primary_key])); |
| } |
| } |
| std::move(callback).Run(std::move(batch)); |
| } |
| |
| void PasswordSyncBridge::GetAllDataForDebugging(DataCallback callback) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| PrimaryKeyToFormMap key_to_form_map; |
| if (!password_store_sync_->ReadAllLogins(&key_to_form_map)) { |
| change_processor()->ReportError( |
| {FROM_HERE, "Failed to load entries from the password store."}); |
| return; |
| } |
| |
| auto batch = std::make_unique<syncer::MutableDataBatch>(); |
| for (const auto& pair : key_to_form_map) { |
| autofill::PasswordForm form = *pair.second; |
| form.password_value = base::UTF8ToUTF16("hidden"); |
| batch->Put(base::NumberToString(pair.first), CreateEntityData(form)); |
| } |
| std::move(callback).Run(std::move(batch)); |
| } |
| |
| std::string PasswordSyncBridge::GetClientTag( |
| const syncer::EntityData& entity_data) { |
| DCHECK(entity_data.specifics.has_password()) |
| << "EntityData does not have password specifics."; |
| |
| const sync_pb::PasswordSpecificsData& password_data = |
| entity_data.specifics.password().client_only_encrypted_data(); |
| |
| return (net::EscapePath(GURL(password_data.origin()).spec()) + "|" + |
| net::EscapePath(password_data.username_element()) + "|" + |
| net::EscapePath(password_data.username_value()) + "|" + |
| net::EscapePath(password_data.password_element()) + "|" + |
| net::EscapePath(password_data.signon_realm())); |
| } |
| |
| std::string PasswordSyncBridge::GetStorageKey( |
| const syncer::EntityData& entity_data) { |
| NOTREACHED() << "PasswordSyncBridge does not support GetStorageKey."; |
| return std::string(); |
| } |
| |
| bool PasswordSyncBridge::SupportsGetStorageKey() const { |
| return false; |
| } |
| |
| syncer::ModelTypeSyncBridge::StopSyncResponse |
| PasswordSyncBridge::ApplyStopSyncChanges( |
| std::unique_ptr<syncer::MetadataChangeList> delete_metadata_change_list) { |
| // TODO(crbug.com/902349): Implement disable-sync case by a more robust |
| // implementation, via a dedicated method in PasswordStoreSync. |
| return ModelTypeSyncBridge::ApplyStopSyncChanges( |
| std::move(delete_metadata_change_list)); |
| } |
| |
| } // namespace password_manager |