chromium/chromeos/ash/services/device_sync/cryptauth_scheduler_impl.cc

// Copyright 2019 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/cryptauth_scheduler_impl.h"

#include <algorithm>
#include <utility>

#include "base/memory/ptr_util.h"
#include "chromeos/ash/components/multidevice/logging/logging.h"
#include "chromeos/ash/components/network/network_state.h"
#include "chromeos/ash/components/network/network_state_handler.h"
#include "chromeos/ash/services/device_sync/pref_names.h"
#include "chromeos/ash/services/device_sync/proto/cryptauth_logging.h"
#include "chromeos/ash/services/device_sync/value_string_encoding.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"

namespace ash::device_sync {

namespace {

constexpr base::TimeDelta kZeroTimeDelta = base::Seconds(0);

// The default period between successful enrollments in days. Superseded by the
// ClientDirective's checkin_delay_millis sent by CryptAuth.
constexpr base::TimeDelta kDefaultRefreshPeriod = base::Days(30);

// The default period, in hours, between Enrollment/DeviceSync attempts if the
// previous Enrollment/DeviceSync attempt failed. Superseded by the
// ClientDirective's retry_period_millis sent by CryptAuth.
constexpr base::TimeDelta kDefaultRetryPeriod = base::Hours(12);

// The time to wait before an "immediate" retry attempt after a failed
// Enrollment/DeviceSync attempt. Note: Some request types are throttled by
// CryptAuth if more than one is sent within a five-minute window.
constexpr base::TimeDelta kImmediateRetryDelay = base::Minutes(5);

// The default number of "immediate" retries after a failed
// Enrollment/DeviceSync attempt. Superseded by the ClientDirective's
// retry_attempts sent by CryptAuth.
const int kDefaultMaxImmediateRetries = 3;

const char kNoClientDirective[] = "[No ClientDirective]";

const char kNoClientMetadata[] = "[No ClientMetadata]";

bool IsClientDirectiveValid(
    const cryptauthv2::ClientDirective& client_directive) {
  return client_directive.checkin_delay_millis() > 0 &&
         client_directive.retry_period_millis() > 0 &&
         client_directive.retry_attempts() >= 0;
}

// Fills a ClientDirective with our chosen default parameters. This
// ClientDirective is used until a ClientDirective is received from CryptAuth.
cryptauthv2::ClientDirective CreateDefaultClientDirective() {
  cryptauthv2::ClientDirective client_directive;
  client_directive.set_checkin_delay_millis(
      kDefaultRefreshPeriod.InMilliseconds());
  client_directive.set_retry_period_millis(
      kDefaultRetryPeriod.InMilliseconds());
  client_directive.set_retry_attempts(kDefaultMaxImmediateRetries);

  return client_directive;
}

cryptauthv2::ClientDirective BuildClientDirective(PrefService* pref_service) {
  DCHECK(pref_service);
  const base::Value& encoded_client_directive =
      pref_service->GetValue(prefs::kCryptAuthSchedulerClientDirective);
  if (encoded_client_directive.GetString() == kNoClientDirective)
    return CreateDefaultClientDirective();

  std::optional<cryptauthv2::ClientDirective> client_directive_from_pref =
      util::DecodeProtoMessageFromValueString<cryptauthv2::ClientDirective>(
          &encoded_client_directive);

  return client_directive_from_pref.value_or(CreateDefaultClientDirective());
}

cryptauthv2::ClientMetadata BuildClientMetadata(
    size_t retry_count,
    const cryptauthv2::ClientMetadata::InvocationReason& invocation_reason,
    const std::optional<std::string>& session_id) {
  cryptauthv2::ClientMetadata client_metadata;
  client_metadata.set_retry_count(retry_count);
  client_metadata.set_invocation_reason(invocation_reason);
  if (session_id)
    client_metadata.set_session_id(*session_id);

  return client_metadata;
}

}  // namespace

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

// static
std::unique_ptr<CryptAuthScheduler> CryptAuthSchedulerImpl::Factory::Create(
    PrefService* pref_service,
    NetworkStateHandler* network_state_handler,
    base::Clock* clock,
    std::unique_ptr<base::OneShotTimer> enrollment_timer,
    std::unique_ptr<base::OneShotTimer> device_sync_timer) {
  if (test_factory_) {
    return test_factory_->CreateInstance(pref_service, network_state_handler,
                                         clock, std::move(enrollment_timer),
                                         std::move(device_sync_timer));
  }

  return base::WrapUnique(new CryptAuthSchedulerImpl(
      pref_service, network_state_handler, clock, std::move(enrollment_timer),
      std::move(device_sync_timer)));
}

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

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

// static
void CryptAuthSchedulerImpl::RegisterPrefs(PrefRegistrySimple* registry) {
  registry->RegisterStringPref(prefs::kCryptAuthSchedulerClientDirective,
                               kNoClientDirective);
  registry->RegisterStringPref(
      prefs::kCryptAuthSchedulerNextEnrollmentRequestClientMetadata,
      kNoClientMetadata);
  registry->RegisterStringPref(
      prefs::kCryptAuthSchedulerNextDeviceSyncRequestClientMetadata,
      kNoClientMetadata);
  registry->RegisterTimePref(
      prefs::kCryptAuthSchedulerLastEnrollmentAttemptTime, base::Time());
  registry->RegisterTimePref(
      prefs::kCryptAuthSchedulerLastDeviceSyncAttemptTime, base::Time());
  registry->RegisterTimePref(
      prefs::kCryptAuthSchedulerLastSuccessfulEnrollmentTime, base::Time());
  registry->RegisterTimePref(
      prefs::kCryptAuthSchedulerLastSuccessfulDeviceSyncTime, base::Time());
}

CryptAuthSchedulerImpl::CryptAuthSchedulerImpl(
    PrefService* pref_service,
    NetworkStateHandler* network_state_handler,
    base::Clock* clock,
    std::unique_ptr<base::OneShotTimer> enrollment_timer,
    std::unique_ptr<base::OneShotTimer> device_sync_timer)
    : pref_service_(pref_service),
      network_state_handler_(network_state_handler),
      clock_(clock),
      client_directive_(BuildClientDirective(pref_service)) {
  DCHECK(pref_service_);
  DCHECK(network_state_handler_);
  DCHECK(clock_);
  DCHECK(IsClientDirectiveValid(client_directive_));

  request_timers_[RequestType::kEnrollment] = std::move(enrollment_timer);
  request_timers_[RequestType::kDeviceSync] = std::move(device_sync_timer);

  // Queue up the most recently scheduled requests if applicable.
  InitializePendingRequest(RequestType::kEnrollment);
  InitializePendingRequest(RequestType::kDeviceSync);
}

CryptAuthSchedulerImpl::~CryptAuthSchedulerImpl() = default;

// static
std::string CryptAuthSchedulerImpl::GetLastAttemptTimePrefName(
    RequestType request_type) {
  switch (request_type) {
    case RequestType::kEnrollment:
      return prefs::kCryptAuthSchedulerLastEnrollmentAttemptTime;
    case RequestType::kDeviceSync:
      return prefs::kCryptAuthSchedulerLastDeviceSyncAttemptTime;
  }
}

// static
std::string CryptAuthSchedulerImpl::GetLastSuccessTimePrefName(
    RequestType request_type) {
  switch (request_type) {
    case RequestType::kEnrollment:
      return prefs::kCryptAuthSchedulerLastSuccessfulEnrollmentTime;
    case RequestType::kDeviceSync:
      return prefs::kCryptAuthSchedulerLastSuccessfulDeviceSyncTime;
  }
}

// static
std::string CryptAuthSchedulerImpl::GetPendingRequestPrefName(
    RequestType request_type) {
  switch (request_type) {
    case RequestType::kEnrollment:
      return prefs::kCryptAuthSchedulerNextEnrollmentRequestClientMetadata;
    case RequestType::kDeviceSync:
      return prefs::kCryptAuthSchedulerNextDeviceSyncRequestClientMetadata;
  }
}

void CryptAuthSchedulerImpl::OnEnrollmentSchedulingStarted() {
  OnSchedulingStarted(RequestType::kEnrollment);
}

void CryptAuthSchedulerImpl::OnDeviceSyncSchedulingStarted() {
  OnSchedulingStarted(RequestType::kDeviceSync);
}

void CryptAuthSchedulerImpl::RequestEnrollment(
    const cryptauthv2::ClientMetadata::InvocationReason& invocation_reason,
    const std::optional<std::string>& session_id) {
  MakeRequest(RequestType::kEnrollment, invocation_reason, session_id);
}

void CryptAuthSchedulerImpl::RequestDeviceSync(
    const cryptauthv2::ClientMetadata::InvocationReason& invocation_reason,
    const std::optional<std::string>& session_id) {
  MakeRequest(RequestType::kDeviceSync, invocation_reason, session_id);
}

void CryptAuthSchedulerImpl::HandleEnrollmentResult(
    const CryptAuthEnrollmentResult& enrollment_result) {
  HandleResult(RequestType::kEnrollment, enrollment_result.IsSuccess(),
               enrollment_result.client_directive());
}

void CryptAuthSchedulerImpl::HandleDeviceSyncResult(
    const CryptAuthDeviceSyncResult& device_sync_result) {
  // Note: "Success" for DeviceSync means no errors, not even non-fatal errors.
  HandleResult(RequestType::kDeviceSync, device_sync_result.IsSuccess(),
               device_sync_result.client_directive());
}

std::optional<base::Time>
CryptAuthSchedulerImpl::GetLastSuccessfulEnrollmentTime() const {
  return GetLastSuccessTime(RequestType::kEnrollment);
}

std::optional<base::Time>
CryptAuthSchedulerImpl::GetLastSuccessfulDeviceSyncTime() const {
  return GetLastSuccessTime(RequestType::kDeviceSync);
}

base::TimeDelta CryptAuthSchedulerImpl::GetRefreshPeriod() const {
  return base::Milliseconds(client_directive_.checkin_delay_millis());
}
std::optional<base::TimeDelta>
CryptAuthSchedulerImpl::GetTimeToNextEnrollmentRequest() const {
  return GetTimeToNextRequest(RequestType::kEnrollment);
}

std::optional<base::TimeDelta>
CryptAuthSchedulerImpl::GetTimeToNextDeviceSyncRequest() const {
  return GetTimeToNextRequest(RequestType::kDeviceSync);
}

bool CryptAuthSchedulerImpl::IsWaitingForEnrollmentResult() const {
  return IsWaitingForResult(RequestType::kEnrollment);
}

bool CryptAuthSchedulerImpl::IsWaitingForDeviceSyncResult() const {
  return IsWaitingForResult(RequestType::kDeviceSync);
}

size_t CryptAuthSchedulerImpl::GetNumConsecutiveEnrollmentFailures() const {
  return GetNumConsecutiveFailures(RequestType::kEnrollment);
}

size_t CryptAuthSchedulerImpl::GetNumConsecutiveDeviceSyncFailures() const {
  return GetNumConsecutiveFailures(RequestType::kDeviceSync);
}

void CryptAuthSchedulerImpl::DefaultNetworkChanged(
    const NetworkState* network) {
  // The updated default network may not be online.
  if (!DoesMachineHaveNetworkConnectivity())
    return;

  // Now that the device has connectivity, reschedule requests.
  ScheduleNextRequest(RequestType::kEnrollment);
  ScheduleNextRequest(RequestType::kDeviceSync);
}

void CryptAuthSchedulerImpl::OnShuttingDown() {
  DCHECK(network_state_handler_);
  network_state_handler_observer_.Reset();
  network_state_handler_ = nullptr;
}

void CryptAuthSchedulerImpl::OnSchedulingStarted(RequestType request_type) {
  if (!network_state_handler_observer_.IsObserving()) {
    DCHECK(network_state_handler_);
    network_state_handler_observer_.Observe(network_state_handler_.get());
  }

  ScheduleNextRequest(request_type);
}

void CryptAuthSchedulerImpl::MakeRequest(
    RequestType request_type,
    const cryptauthv2::ClientMetadata::InvocationReason& invocation_reason,
    const std::optional<std::string>& session_id) {
  request_timers_[request_type]->Stop();

  pending_requests_[request_type] =
      BuildClientMetadata(0 /* retry_count */, invocation_reason, session_id);

  ScheduleNextRequest(request_type);
}

void CryptAuthSchedulerImpl::HandleResult(
    RequestType request_type,
    bool success,
    const std::optional<cryptauthv2::ClientDirective>& new_client_directive) {
  DCHECK(current_requests_[request_type]);
  DCHECK(!request_timers_[request_type]->IsRunning());

  base::Time now = clock_->Now();

  pref_service_->SetTime(GetLastAttemptTimePrefName(request_type), now);

  if (new_client_directive && IsClientDirectiveValid(*new_client_directive)) {
    client_directive_ = *new_client_directive;
    PA_LOG(VERBOSE) << "New client directive:\n" << client_directive_;
    pref_service_->Set(
        prefs::kCryptAuthSchedulerClientDirective,
        util::EncodeProtoMessageAsValueString(&client_directive_));
  }

  // If successful, process InvokeNext field of ClientDirective. If unsuccessful
  // and a more immediate request isn't pending, queue up the failure recovery
  // attempt.
  if (success) {
    pref_service_->SetTime(GetLastSuccessTimePrefName(request_type), now);

    HandleInvokeNext(client_directive_.invoke_next(),
                     current_requests_[request_type]->session_id());
  } else if (!pending_requests_[request_type]) {
    current_requests_[request_type]->set_retry_count(
        current_requests_[request_type]->retry_count() + 1);
    pending_requests_[request_type] = current_requests_[request_type];
  }

  current_requests_[request_type].reset();

  // Because the ClientDirective might have changed, we update both timers.
  ScheduleNextRequest(RequestType::kEnrollment);
  ScheduleNextRequest(RequestType::kDeviceSync);
}

void CryptAuthSchedulerImpl::HandleInvokeNext(
    const ::google::protobuf::RepeatedPtrField<cryptauthv2::InvokeNext>&
        invoke_next_list,
    const std::string& session_id) {
  for (const cryptauthv2::InvokeNext& invoke_next : invoke_next_list) {
    if (invoke_next.service() == cryptauthv2::ENROLLMENT) {
      PA_LOG(VERBOSE) << "Enrollment requested by InvokeNext";
      RequestEnrollment(cryptauthv2::ClientMetadata::SERVER_INITIATED,
                        session_id);
    } else if (invoke_next.service() == cryptauthv2::DEVICE_SYNC) {
      PA_LOG(VERBOSE) << "DeviceSync requested by InvokeNext";
      RequestDeviceSync(cryptauthv2::ClientMetadata::SERVER_INITIATED,
                        session_id);
    } else {
      PA_LOG(WARNING) << "Unknown InvokeNext TargetService "
                      << invoke_next.service();
    }
  }
}

std::optional<base::Time> CryptAuthSchedulerImpl::GetLastSuccessTime(
    RequestType request_type) const {
  base::Time time =
      pref_service_->GetTime(GetLastSuccessTimePrefName(request_type));
  if (time.is_null())
    return std::nullopt;

  return time;
}

std::optional<base::TimeDelta> CryptAuthSchedulerImpl::GetTimeToNextRequest(
    RequestType request_type) const {
  // Request already in progress.
  if (IsWaitingForResult(request_type))
    return kZeroTimeDelta;

  // No pending request.
  const auto it = pending_requests_.find(request_type);
  if (it == pending_requests_.end() || !it->second)
    return std::nullopt;

  int64_t retry_count = it->second->retry_count();
  cryptauthv2::ClientMetadata::InvocationReason invocation_reason =
      it->second->invocation_reason();

  // If we are not recovering from failure, attempt all but periodic requests
  // immediately.
  if (retry_count == 0) {
    if (invocation_reason != cryptauthv2::ClientMetadata::PERIODIC)
      return kZeroTimeDelta;

    std::optional<base::Time> last_success_time =
        GetLastSuccessTime(request_type);
    DCHECK(last_success_time);

    base::TimeDelta time_since_last_success =
        clock_->Now() - *last_success_time;
    return std::max(kZeroTimeDelta,
                    GetRefreshPeriod() - time_since_last_success);
  }

  base::TimeDelta time_since_last_attempt =
      clock_->Now() -
      pref_service_->GetTime(GetLastAttemptTimePrefName(request_type));

  // Recover from failure using immediate retry.
  DCHECK(retry_count > 0);
  if (retry_count < client_directive_.retry_attempts()) {
    return std::max(kZeroTimeDelta,
                    kImmediateRetryDelay - time_since_last_attempt);
  }

  // Recover from failure after expending allotted immediate retries.
  return std::max(kZeroTimeDelta,
                  base::Milliseconds(client_directive_.retry_period_millis()) -
                      time_since_last_attempt);
}

bool CryptAuthSchedulerImpl::IsWaitingForResult(
    RequestType request_type) const {
  const auto it = current_requests_.find(request_type);
  return (it != current_requests_.end() && it->second);
}

size_t CryptAuthSchedulerImpl::GetNumConsecutiveFailures(
    RequestType request_type) const {
  const auto current_request_it = current_requests_.find(request_type);
  if (current_request_it != current_requests_.end() &&
      current_request_it->second) {
    return current_request_it->second->retry_count();
  }

  const auto pending_request_it = pending_requests_.find(request_type);
  if (pending_request_it != pending_requests_.end() &&
      pending_request_it->second) {
    return pending_request_it->second->retry_count();
  }

  return 0;
}

bool CryptAuthSchedulerImpl::DoesMachineHaveNetworkConnectivity() const {
  if (!network_state_handler_)
    return false;

  // TODO(khorimoto): IsConnectedState() can still return true if connected to
  // a captive portal; use the "online" boolean once we fetch data via the
  // networking Mojo API. See https://crbug.com/862420.
  const NetworkState* default_network =
      network_state_handler_->DefaultNetwork();
  return default_network && default_network->IsConnectedState();
}

void CryptAuthSchedulerImpl::InitializePendingRequest(
    RequestType request_type) {
  // Queue up the persisted scheduled request if applicable.
  const base::Value& client_metadata_from_pref =
      pref_service_->GetValue(GetPendingRequestPrefName(request_type));
  if (client_metadata_from_pref.GetString() != kNoClientMetadata) {
    pending_requests_[request_type] =
        util::DecodeProtoMessageFromValueString<cryptauthv2::ClientMetadata>(
            &client_metadata_from_pref);
  }

  // If we are recovering from a failure, reset the failure count to 1 in the
  // hopes that the restart solved the issue. This will allow for immediate
  // retries again if permitted by the ClientDirective.
  if (pending_requests_[request_type] &&
      pending_requests_[request_type]->retry_count() > 0) {
    pending_requests_[request_type]->set_retry_count(1);
  }
}

void CryptAuthSchedulerImpl::ScheduleNextRequest(RequestType request_type) {
  // Wait for the current attempt to finish before determining the next request
  // in case we need to recover from a failure.
  if (IsWaitingForResult(request_type))
    return;

  // For Enrollment only, if no request has already been explicitly made,
  // schedule a periodic attempt.
  if (request_type == RequestType::kEnrollment &&
      !pending_requests_[request_type]) {
    pending_requests_[request_type] =
        BuildClientMetadata(0 /* retry_count */,
                            GetLastSuccessTime(request_type)
                                ? cryptauthv2::ClientMetadata::PERIODIC
                                : cryptauthv2::ClientMetadata::INITIALIZATION,
                            std::nullopt /* session_id */);
  }

  // Schedule a first-time DeviceSync if one has never successfully completed.
  // However, unlike Enrollment, there are no periodic DeviceSyncs.
  if (request_type == RequestType::kDeviceSync &&
      !pending_requests_[request_type] && !GetLastSuccessTime(request_type)) {
    pending_requests_[request_type] = BuildClientMetadata(
        0 /* retry_count */, cryptauthv2::ClientMetadata::INITIALIZATION,
        std::nullopt /* session_id */);
  }

  if (!pending_requests_[request_type]) {
    // By this point, only DeviceSync can have no requests pending because it
    // does not schedule periodic syncs.
    DCHECK_EQ(RequestType::kDeviceSync, request_type);
    pref_service_->SetString(GetPendingRequestPrefName(request_type),
                             kNoClientMetadata);
    return;
  }

  // Persist the pending request even if scheduling hasn't started yet.
  pref_service_->Set(GetPendingRequestPrefName(request_type),
                     util::EncodeProtoMessageAsValueString(
                         &pending_requests_[request_type].value()));

  bool has_scheduling_started = (request_type == RequestType::kEnrollment &&
                                 HasEnrollmentSchedulingStarted()) ||
                                (request_type == RequestType::kDeviceSync &&
                                 HasDeviceSyncSchedulingStarted());
  if (!has_scheduling_started)
    return;

  std::optional<base::TimeDelta> delay = GetTimeToNextRequest(request_type);
  DCHECK(delay);
  request_timers_[request_type]->Start(
      FROM_HERE, *delay,
      base::BindOnce(&CryptAuthSchedulerImpl::OnTimerFired,
                     base::Unretained(this), request_type));
}

void CryptAuthSchedulerImpl::OnTimerFired(RequestType request_type) {
  DCHECK(!current_requests_[request_type]);
  DCHECK(pending_requests_[request_type]);

  if (!DoesMachineHaveNetworkConnectivity()) {
    std::string type_string =
        request_type == RequestType::kEnrollment ? "Enrollment" : "DeviceSync";
    PA_LOG(INFO) << type_string
                 << " triggered while the device is offline. Waiting "
                 << "for online connectivity before making request.";
    return;
  }

  current_requests_[request_type] = pending_requests_[request_type];
  pending_requests_[request_type].reset();

  switch (request_type) {
    case RequestType::kEnrollment: {
      std::optional<cryptauthv2::PolicyReference> policy_reference =
          std::nullopt;
      if (client_directive_.has_policy_reference())
        policy_reference = client_directive_.policy_reference();

      NotifyEnrollmentRequested(*current_requests_[request_type],
                                policy_reference);
      return;
    }
    case RequestType::kDeviceSync:
      NotifyDeviceSyncRequested(*current_requests_[request_type]);
      return;
  }
}

}  // namespace ash::device_sync