blob: fffe7965e06ad977ac7427c5159110a8f9946477 [file] [log] [blame]
// 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