chromium/chromeos/ash/services/device_sync/public/cpp/device_sync_client_impl.cc

// Copyright 2018 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/public/cpp/device_sync_client_impl.h"

#include <string>
#include <utility>
#include <vector>

#include "ash/constants/ash_features.h"
#include "base/base64url.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_functions.h"
#include "base/ranges/algorithm.h"
#include "chromeos/ash/components/multidevice/expiring_remote_device_cache.h"
#include "chromeos/ash/components/multidevice/logging/logging.h"
#include "chromeos/ash/components/multidevice/remote_device.h"
#include "chromeos/ash/services/device_sync/public/mojom/device_sync.mojom.h"

namespace ash {

namespace device_sync {

namespace {

bool IsValidInstanceId(const std::string& instance_id) {
  if (instance_id.empty()) {
    PA_LOG(ERROR) << "Instance ID cannot be empty.";
    return false;
  }

  std::string decoded_iid;
  if (!base::Base64UrlDecode(instance_id,
                             base::Base64UrlDecodePolicy::IGNORE_PADDING,
                             &decoded_iid)) {
    PA_LOG(ERROR) << "Instance ID must be Base64Url encoded.";
    return false;
  }

  if (decoded_iid.size() != 8u) {
    PA_LOG(ERROR) << "Instance ID must be 8 bytes after Base64Url decoding.";
    return false;
  }

  return true;
}

}  // namespace

// static
DeviceSyncClientImpl::Factory* DeviceSyncClientImpl::Factory::test_factory_ =
    nullptr;

// static
std::unique_ptr<DeviceSyncClient> DeviceSyncClientImpl::Factory::Create() {
  if (test_factory_)
    return test_factory_->CreateInstance();

  return std::make_unique<DeviceSyncClientImpl>();
}

// static
void DeviceSyncClientImpl::Factory::SetFactoryForTesting(
    Factory* test_factory) {
  test_factory_ = test_factory;
}

DeviceSyncClientImpl::Factory::~Factory() = default;

DeviceSyncClientImpl::DeviceSyncClientImpl()
    : expiring_device_cache_(
          std::make_unique<multidevice::ExpiringRemoteDeviceCache>()) {}

DeviceSyncClientImpl::~DeviceSyncClientImpl() = default;

void DeviceSyncClientImpl::Initialize(
    scoped_refptr<base::TaskRunner> task_runner) {
  device_sync_->AddObserver(GenerateRemote(), base::OnceClosure());

  // Delay calling these until after initialization finishes.
  task_runner->PostTask(
      FROM_HERE, base::BindOnce(&DeviceSyncClientImpl::LoadLocalDeviceMetadata,
                                weak_ptr_factory_.GetWeakPtr()));
  task_runner->PostTask(FROM_HERE,
                        base::BindOnce(&DeviceSyncClientImpl::LoadSyncedDevices,
                                       weak_ptr_factory_.GetWeakPtr()));
}

mojo::Remote<mojom::DeviceSync>* DeviceSyncClientImpl::GetDeviceSyncRemote() {
  return &device_sync_;
}

void DeviceSyncClientImpl::OnEnrollmentFinished() {
  // Before notifying observers that enrollment has finished, sync down the
  // local device metadata. This ensures that observers will have access to the
  // metadata of the newly-synced local device as soon as
  // NotifyOnEnrollmentFinished() is invoked.
  LoadLocalDeviceMetadata();
}

void DeviceSyncClientImpl::OnNewDevicesSynced() {
  // Before notifying observers that new devices have synced, sync down the new
  // devices. This ensures that observers will have access to the synced devices
  // as soon as NotifyOnNewDevicesSynced() is invoked.
  LoadSyncedDevices();
}

void DeviceSyncClientImpl::ForceEnrollmentNow(
    mojom::DeviceSync::ForceEnrollmentNowCallback callback) {
  device_sync_->ForceEnrollmentNow(std::move(callback));
}

void DeviceSyncClientImpl::ForceSyncNow(
    mojom::DeviceSync::ForceSyncNowCallback callback) {
  device_sync_->ForceSyncNow(std::move(callback));
}

void DeviceSyncClientImpl::GetBetterTogetherMetadataStatus(
    mojom::DeviceSync::GetBetterTogetherMetadataStatusCallback callback) {
  device_sync_->GetBetterTogetherMetadataStatus(std::move(callback));
}

void DeviceSyncClientImpl::GetGroupPrivateKeyStatus(
    mojom::DeviceSync::GetGroupPrivateKeyStatusCallback callback) {
  device_sync_->GetGroupPrivateKeyStatus(std::move(callback));
}

multidevice::RemoteDeviceRefList DeviceSyncClientImpl::GetSyncedDevices() {
  DCHECK(is_ready());
  return expiring_device_cache_->GetNonExpiredRemoteDevices();
}

std::optional<multidevice::RemoteDeviceRef>
DeviceSyncClientImpl::GetLocalDeviceMetadata() {
  DCHECK(is_ready());
  base::UmaHistogramBoolean("CryptAuth.GetLocalDeviceMetadata.IsReady",
                            is_ready());

  // Because we expect the the client to be ready when this function is called,
  // we also expect the local device to be non-null.
  std::optional<multidevice::RemoteDeviceRef> local_device =
      expiring_device_cache_->GetRemoteDevice(local_instance_id_,
                                              local_legacy_device_id_);
  base::UmaHistogramBoolean("CryptAuth.GetLocalDeviceMetadata.Result",
                            local_device.has_value());
  if (!local_device) {
    PA_LOG(ERROR)
        << "DeviceSyncClientImpl::" << __func__
        << ": Could not retrieve local device metadata. local_instance_id="
        << local_instance_id_.value_or("[null]") << ", local_legacy_device_id="
        << local_legacy_device_id_.value_or("[null]")
        << ", is_ready=" << (is_ready() ? "yes" : "no");
  }

  return local_device;
}

void DeviceSyncClientImpl::SetSoftwareFeatureState(
    const std::string public_key,
    multidevice::SoftwareFeature software_feature,
    bool enabled,
    bool is_exclusive,
    mojom::DeviceSync::SetSoftwareFeatureStateCallback callback) {
  device_sync_->SetSoftwareFeatureState(public_key, software_feature, enabled,
                                        is_exclusive, std::move(callback));
}

void DeviceSyncClientImpl::SetFeatureStatus(
    const std::string& device_instance_id,
    multidevice::SoftwareFeature feature,
    FeatureStatusChange status_change,
    mojom::DeviceSync::SetFeatureStatusCallback callback) {
  if (!IsValidInstanceId(device_instance_id)) {
    std::move(callback).Run(mojom::NetworkRequestResult::kBadRequest);
    return;
  }

  device_sync_->SetFeatureStatus(device_instance_id, feature, status_change,
                                 std::move(callback));
}

void DeviceSyncClientImpl::FindEligibleDevices(
    multidevice::SoftwareFeature software_feature,
    FindEligibleDevicesCallback callback) {
  device_sync_->FindEligibleDevices(
      software_feature,
      base::BindOnce(&DeviceSyncClientImpl::OnFindEligibleDevicesCompleted,
                     weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void DeviceSyncClientImpl::NotifyDevices(
    const std::vector<std::string>& device_instance_ids,
    cryptauthv2::TargetService target_service,
    multidevice::SoftwareFeature feature,
    mojom::DeviceSync::NotifyDevicesCallback callback) {
  for (const std::string& iid : device_instance_ids) {
    if (!IsValidInstanceId(iid)) {
      std::move(callback).Run(mojom::NetworkRequestResult::kBadRequest);
      return;
    }
  }

  device_sync_->NotifyDevices(device_instance_ids, target_service, feature,
                              std::move(callback));
}

void DeviceSyncClientImpl::GetDevicesActivityStatus(
    mojom::DeviceSync::GetDevicesActivityStatusCallback callback) {
  device_sync_->GetDevicesActivityStatus(std::move(callback));
}

void DeviceSyncClientImpl::GetDebugInfo(
    mojom::DeviceSync::GetDebugInfoCallback callback) {
  device_sync_->GetDebugInfo(std::move(callback));
}

void DeviceSyncClientImpl::AttemptToBecomeReady() {
  if (is_ready())
    return;

  if (waiting_for_synced_devices_ || waiting_for_local_device_metadata_)
    return;

  NotifyReady();

  if (pending_notify_enrollment_finished_)
    NotifyEnrollmentFinished();

  if (pending_notify_new_synced_devices_)
    NotifyNewDevicesSynced();

  pending_notify_enrollment_finished_ = false;
  pending_notify_new_synced_devices_ = false;
}

void DeviceSyncClientImpl::LoadSyncedDevices() {
  device_sync_->GetSyncedDevices(
      base::BindOnce(&DeviceSyncClientImpl::OnGetSyncedDevicesCompleted,
                     weak_ptr_factory_.GetWeakPtr()));
}

void DeviceSyncClientImpl::LoadLocalDeviceMetadata() {
  device_sync_->GetLocalDeviceMetadata(
      base::BindOnce(&DeviceSyncClientImpl::OnGetLocalDeviceMetadataCompleted,
                     weak_ptr_factory_.GetWeakPtr()));
}

void DeviceSyncClientImpl::OnGetSyncedDevicesCompleted(
    const std::optional<std::vector<multidevice::RemoteDevice>>&
        remote_devices) {
  if (!remote_devices) {
    PA_LOG(INFO) << "Tried to fetch synced devices before service was fully "
                    "initialized; waiting for sync to complete before "
                    "continuing.";
    return;
  }

  waiting_for_synced_devices_ = false;

  if (waiting_for_local_device_metadata_)
    LoadLocalDeviceMetadata();

  expiring_device_cache_->SetRemoteDevicesAndInvalidateOldEntries(
      *remote_devices);

  // Don't yet notify observers that new devices have synced until the client
  // is ready.
  if (is_ready()) {
    PA_LOG(INFO) << "Client is ready. Notifying new devices have synced.";
    NotifyNewDevicesSynced();
  } else {
    PA_LOG(INFO)
        << "Client is NOT ready. Waiting to notify new devices have synced.";
    pending_notify_new_synced_devices_ = true;
    AttemptToBecomeReady();
  }
}

void DeviceSyncClientImpl::OnGetLocalDeviceMetadataCompleted(
    const std::optional<multidevice::RemoteDevice>& local_device_metadata) {
  if (!local_device_metadata) {
    PA_LOG(INFO) << "Tried to get local device metadata before service was "
                    "fully initialized; waiting for enrollment to complete "
                    "before continuing.";
    return;
  }

  if (features::ShouldUseV1DeviceSync()) {
    local_instance_id_ = local_device_metadata->instance_id.empty()
                             ? std::nullopt
                             : std::make_optional<std::string>(
                                   local_device_metadata->instance_id);
    local_legacy_device_id_ = local_device_metadata->GetDeviceId().empty()
                                  ? std::nullopt
                                  : std::make_optional<std::string>(
                                        local_device_metadata->GetDeviceId());
  } else {
    local_instance_id_ = local_device_metadata->instance_id.empty()
                             ? std::nullopt
                             : std::make_optional<std::string>(
                                   local_device_metadata->instance_id);
  }

  bool has_id = local_instance_id_ || local_legacy_device_id_;
  base::UmaHistogramBoolean("CryptAuth.GetLocalDeviceMetadata.HasId", has_id);
  if (!has_id) {
    PA_LOG(ERROR) << "DeviceSyncClientImpl::" << __func__
                  << ": Local device identifiers are unexpectedly empty.";
    return;
  }

  expiring_device_cache_->UpdateRemoteDevice(*local_device_metadata);

  waiting_for_local_device_metadata_ = false;

  // Don't yet notify observers that enrollment has finished until the client
  // is ready.
  if (is_ready()) {
    PA_LOG(INFO) << "Client is ready. Notifying enrollment finished.";
    NotifyEnrollmentFinished();
  } else {
    PA_LOG(INFO)
        << "Client is NOT ready. Waiting to notify enrollment finished.";
    pending_notify_enrollment_finished_ = true;
    AttemptToBecomeReady();
  }
}

void DeviceSyncClientImpl::OnFindEligibleDevicesCompleted(
    FindEligibleDevicesCallback callback,
    mojom::NetworkRequestResult result_code,
    mojom::FindEligibleDevicesResponsePtr response) {
  multidevice::RemoteDeviceRefList eligible_devices;
  multidevice::RemoteDeviceRefList ineligible_devices;

  if (result_code == mojom::NetworkRequestResult::kSuccess) {
    base::ranges::transform(
        response->eligible_devices, std::back_inserter(eligible_devices),
        [this](const auto& device) {
          return *expiring_device_cache_->GetRemoteDevice(device.instance_id,
                                                          device.GetDeviceId());
        });
    base::ranges::transform(response->ineligible_devices,
                            std::back_inserter(ineligible_devices),
                            [this](const auto& device) {
                              return *expiring_device_cache_->GetRemoteDevice(
                                  device.instance_id, device.GetDeviceId());
                            });
  }

  std::move(callback).Run(result_code, eligible_devices, ineligible_devices);
}

mojo::PendingRemote<mojom::DeviceSyncObserver>
DeviceSyncClientImpl::GenerateRemote() {
  return observer_receiver_.BindNewPipeAndPassRemote();
}

void DeviceSyncClientImpl::FlushForTesting() {
  device_sync_.FlushForTesting();
}

}  // namespace device_sync

}  // namespace ash