// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chromeos/ash/services/device_sync/remote_device_provider_impl.h"
#include "ash/constants/ash_features.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_functions.h"
#include "base/ranges/algorithm.h"
#include "chromeos/ash/components/multidevice/logging/logging.h"
#include "chromeos/ash/components/multidevice/secure_message_delegate_impl.h"
#include "chromeos/ash/services/device_sync/remote_device_loader.h"
#include "chromeos/ash/services/device_sync/remote_device_v2_loader_impl.h"
namespace ash::device_sync {
namespace {
const int kMaxNumberOfDevicesToLog = 50;
// Only used while v1 and v2 DeviceSync are running in parallel.
void LogRemoteDeviceCountMetrics(
const multidevice::RemoteDeviceList& v1_devices,
const multidevice::RemoteDeviceList& v2_devices,
size_t num_v2_devices_with_decrypted_public_key,
size_t num_v1_devices_replaced_by_v2_devices) {
// At a minimum, the local device should always be returned from a successful
// v1 or v2 DeviceSync. Only log metrics if v1 and v2 devices are available,
// in other words, if a v1 *and* v2 DeviceSync has previously occurred.
if (v1_devices.empty() || v2_devices.empty())
return;
base::UmaHistogramExactLinear(
"CryptAuth.DeviceSyncV2.RemoteDeviceProvider.NumV1Devices",
v1_devices.size(), kMaxNumberOfDevicesToLog);
base::UmaHistogramExactLinear(
"CryptAuth.DeviceSyncV2.RemoteDeviceProvider.NumV2Devices",
v2_devices.size(), kMaxNumberOfDevicesToLog);
// Note: By CryptAuth server design, v2 devices should always be a subset of
// v1 devices. Race conditions might occur when a user adds a new device
// because v1 and v2 devices are retrieved in different RPC calls; however,
// this is only meant to be a rough estimate.
base::UmaHistogramPercentageObsoleteDoNotUse(
"CryptAuth.DeviceSyncV2.RemoteDeviceProvider.RatioOfV2ToV1Devices",
(v2_devices.size() * 100) / v1_devices.size());
base::UmaHistogramPercentageObsoleteDoNotUse(
"CryptAuth.DeviceSyncV2.RemoteDeviceProvider."
"PercentageOfV2DevicesWithDecryptedPublicKey",
(num_v2_devices_with_decrypted_public_key * 100) / v2_devices.size());
base::UmaHistogramPercentageObsoleteDoNotUse(
"CryptAuth.DeviceSyncV2.RemoteDeviceProvider."
"PercentageOfV1DevicesReplacedByV2Devices",
(num_v1_devices_replaced_by_v2_devices * 100) / v1_devices.size());
}
} // namespace
// static
RemoteDeviceProviderImpl::Factory*
RemoteDeviceProviderImpl::Factory::factory_instance_ = nullptr;
// static
std::unique_ptr<RemoteDeviceProvider> RemoteDeviceProviderImpl::Factory::Create(
CryptAuthDeviceManager* v1_device_manager,
CryptAuthV2DeviceManager* v2_device_manager,
const std::string& user_email,
const std::string& user_private_key) {
if (factory_instance_) {
return factory_instance_->CreateInstance(
v1_device_manager, v2_device_manager, user_email, user_private_key);
}
return base::WrapUnique(new RemoteDeviceProviderImpl(
v1_device_manager, v2_device_manager, user_email, user_private_key));
}
// static
void RemoteDeviceProviderImpl::Factory::SetFactoryForTesting(Factory* factory) {
factory_instance_ = factory;
}
RemoteDeviceProviderImpl::Factory::~Factory() = default;
RemoteDeviceProviderImpl::RemoteDeviceProviderImpl(
CryptAuthDeviceManager* v1_device_manager,
CryptAuthV2DeviceManager* v2_device_manager,
const std::string& user_email,
const std::string& user_private_key)
: v1_device_manager_(v1_device_manager),
v2_device_manager_(v2_device_manager),
user_email_(user_email),
user_private_key_(user_private_key) {
if (features::ShouldUseV1DeviceSync()) {
DCHECK(v1_device_manager_);
v1_device_manager_->AddObserver(this);
LoadV1RemoteDevices();
}
if (features::ShouldUseV2DeviceSync()) {
DCHECK(v2_device_manager_);
v2_device_manager_->AddObserver(this);
LoadV2RemoteDevices();
}
}
RemoteDeviceProviderImpl::~RemoteDeviceProviderImpl() {
if (v1_device_manager_)
v1_device_manager_->RemoveObserver(this);
if (v2_device_manager_)
v2_device_manager_->RemoveObserver(this);
}
void RemoteDeviceProviderImpl::OnSyncFinished(
CryptAuthDeviceManager::SyncResult sync_result,
CryptAuthDeviceManager::DeviceChangeResult device_change_result) {
DCHECK(features::ShouldUseV1DeviceSync());
if (sync_result == CryptAuthDeviceManager::SyncResult::SUCCESS &&
device_change_result ==
CryptAuthDeviceManager::DeviceChangeResult::CHANGED) {
LoadV1RemoteDevices();
}
}
void RemoteDeviceProviderImpl::OnDeviceSyncFinished(
const CryptAuthDeviceSyncResult& device_sync_result) {
DCHECK(features::ShouldUseV2DeviceSync());
if (device_sync_result.IsSuccess() &&
device_sync_result.did_device_registry_change()) {
LoadV2RemoteDevices();
}
}
void RemoteDeviceProviderImpl::LoadV1RemoteDevices() {
remote_device_v1_loader_ = RemoteDeviceLoader::Factory::Create(
v1_device_manager_->GetSyncedDevices(), user_email_, user_private_key_,
multidevice::SecureMessageDelegateImpl::Factory::Create());
remote_device_v1_loader_->Load(
base::BindOnce(&RemoteDeviceProviderImpl::OnV1RemoteDevicesLoaded,
weak_ptr_factory_.GetWeakPtr()));
}
void RemoteDeviceProviderImpl::LoadV2RemoteDevices() {
remote_device_v2_loader_ = RemoteDeviceV2LoaderImpl::Factory::Create();
remote_device_v2_loader_->Load(
v2_device_manager_->GetSyncedDevices(), user_email_, user_private_key_,
base::BindOnce(&RemoteDeviceProviderImpl::OnV2RemoteDevicesLoaded,
weak_ptr_factory_.GetWeakPtr()));
}
void RemoteDeviceProviderImpl::OnV1RemoteDevicesLoaded(
const multidevice::RemoteDeviceList& synced_v1_remote_devices) {
// If we are only using v1 DeviceSync, the complete list of RemoteDevices
// is |synced_v1_remote_devices|.
if (!features::ShouldUseV2DeviceSync()) {
synced_remote_devices_ = synced_v1_remote_devices;
remote_device_v1_loader_.reset();
// Notify observers of change. Note that there is no need to check if
// |synced_remote_devices_| has changed here because the fetch is only
// started if the change result passed to OnSyncFinished() is CHANGED.
RemoteDeviceProvider::NotifyObserversDeviceListChanged();
return;
}
synced_v1_remote_devices_to_be_merged_ = synced_v1_remote_devices;
remote_device_v1_loader_.reset();
MergeV1andV2SyncedDevices();
}
void RemoteDeviceProviderImpl::OnV2RemoteDevicesLoaded(
const multidevice::RemoteDeviceList& synced_v2_remote_devices) {
// If we are only using v2 DeviceSync, the complete list of RemoteDevices
// is |synced_v2_remote_devices|.
if (!features::ShouldUseV1DeviceSync()) {
synced_remote_devices_ = synced_v2_remote_devices;
remote_device_v2_loader_.reset();
// Notify observers of change. Note that there is no need to check if
// |synced_remote_devices_| has changed here because the fetch is only
// started if the DeviceSync result passed to OnDeviceSyncFinished()
// indicates that the device registry changed.
RemoteDeviceProvider::NotifyObserversDeviceListChanged();
return;
}
synced_v2_remote_devices_to_be_merged_ = synced_v2_remote_devices;
remote_device_v2_loader_.reset();
MergeV1andV2SyncedDevices();
}
void RemoteDeviceProviderImpl::MergeV1andV2SyncedDevices() {
DCHECK(features::ShouldUseV1DeviceSync());
DCHECK(features::ShouldUseV2DeviceSync());
multidevice::RemoteDeviceList previous_synced_remote_devices =
synced_remote_devices_;
synced_remote_devices_ = synced_v1_remote_devices_to_be_merged_;
size_t num_v2_devices_with_decrypted_public_key = 0;
size_t num_v1_devices_replaced_by_v2_devices = 0;
for (const auto& v2_device : synced_v2_remote_devices_to_be_merged_) {
// Ignore v2 devices without a decrypted public key.
if (v2_device.public_key.empty())
continue;
++num_v2_devices_with_decrypted_public_key;
std::string v2_public_key = v2_device.public_key;
auto it = base::ranges::find(synced_remote_devices_, v2_public_key,
&multidevice::RemoteDevice::public_key);
// If a v1 device has the same public key as the v2 device, replace the
// v1 device with the v2 device; otherwise, append the v2 device to the
// synced-device list.
if (it != synced_remote_devices_.end()) {
*it = v2_device;
++num_v1_devices_replaced_by_v2_devices;
} else {
synced_remote_devices_.push_back(v2_device);
}
}
LogRemoteDeviceCountMetrics(synced_v1_remote_devices_to_be_merged_,
synced_v2_remote_devices_to_be_merged_,
num_v2_devices_with_decrypted_public_key,
num_v1_devices_replaced_by_v2_devices);
// We need to explicitly check for changes to the synced-device list. It
// is possible that the v1 and/or v2 device lists changed but the merged
// list didn't change, for example, if a new v2 device appears in the
// device registry but it doesn't have a decrypted public key.
if (synced_remote_devices_ != previous_synced_remote_devices)
RemoteDeviceProvider::NotifyObserversDeviceListChanged();
}
const multidevice::RemoteDeviceList&
RemoteDeviceProviderImpl::GetSyncedDevices() const {
return synced_remote_devices_;
}
} // namespace ash::device_sync