chromium/chromeos/ash/services/device_sync/cryptauth_v2_enrollment_manager_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_v2_enrollment_manager_impl.h"

#include "base/functional/bind.h"
#include "base/hash/hash.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/string_number_conversions.h"
#include "base/time/clock.h"
#include "base/values.h"
#include "chromeos/ash/components/multidevice/logging/logging.h"
#include "chromeos/ash/services/device_sync/cryptauth_enrollment_constants.h"
#include "chromeos/ash/services/device_sync/cryptauth_key_registry.h"
#include "chromeos/ash/services/device_sync/cryptauth_task_metrics_logger.h"
#include "chromeos/ash/services/device_sync/cryptauth_v2_enroller_impl.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 {

namespace device_sync {

namespace {

// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
enum class UserKeyPairState {
  // No v1 key; no v2 key. (Not enrolled)
  kNoV1KeyNoV2Key = 0,
  // v1 key exists; no v2 key. (Only v1 enrolled)
  kYesV1KeyNoV2Key = 1,
  // No v1 key; v2 key exists. (Only v2 enrolled)
  kNoV1KeyYesV2Key = 2,
  // v1 and v2 keys exist and agree.
  kYesV1KeyYesV2KeyAgree = 3,
  // v1 and v2 keys exist and disagree. (Enrolled with v2, rolled back to v1,
  // enrolled with v1, rolled forward to v2)
  kYesV1KeyYesV2KeyDisagree = 4,
  kMaxValue = kYesV1KeyYesV2KeyDisagree
};

UserKeyPairState GetUserKeyPairState(const std::string& public_key_v1,
                                     const std::string& private_key_v1,
                                     const CryptAuthKey* key_v2) {
  bool v1_key_exists = !public_key_v1.empty() && !private_key_v1.empty();

  if (v1_key_exists && key_v2) {
    if (public_key_v1 == key_v2->public_key() &&
        private_key_v1 == key_v2->private_key()) {
      return UserKeyPairState::kYesV1KeyYesV2KeyAgree;
    } else {
      return UserKeyPairState::kYesV1KeyYesV2KeyDisagree;
    }
  } else if (v1_key_exists && !key_v2) {
    return UserKeyPairState::kYesV1KeyNoV2Key;
  } else if (!v1_key_exists && key_v2) {
    return UserKeyPairState::kNoV1KeyYesV2Key;
  } else {
    return UserKeyPairState::kNoV1KeyNoV2Key;
  }
}

cryptauthv2::ClientMetadata::InvocationReason ConvertInvocationReasonV1ToV2(
    cryptauth::InvocationReason invocation_reason_v1) {
  switch (invocation_reason_v1) {
    case cryptauth::InvocationReason::INVOCATION_REASON_UNKNOWN:
      return cryptauthv2::ClientMetadata::INVOCATION_REASON_UNSPECIFIED;
    case cryptauth::InvocationReason::INVOCATION_REASON_INITIALIZATION:
      return cryptauthv2::ClientMetadata::INITIALIZATION;
    case cryptauth::InvocationReason::INVOCATION_REASON_PERIODIC:
      return cryptauthv2::ClientMetadata::PERIODIC;
    case cryptauth::InvocationReason::INVOCATION_REASON_SLOW_PERIODIC:
      return cryptauthv2::ClientMetadata::SLOW_PERIODIC;
    case cryptauth::InvocationReason::INVOCATION_REASON_FAST_PERIODIC:
      return cryptauthv2::ClientMetadata::FAST_PERIODIC;
    case cryptauth::InvocationReason::INVOCATION_REASON_EXPIRATION:
      return cryptauthv2::ClientMetadata::EXPIRATION;
    case cryptauth::InvocationReason::INVOCATION_REASON_FAILURE_RECOVERY:
      return cryptauthv2::ClientMetadata::FAILURE_RECOVERY;
    case cryptauth::InvocationReason::INVOCATION_REASON_NEW_ACCOUNT:
      return cryptauthv2::ClientMetadata::NEW_ACCOUNT;
    case cryptauth::InvocationReason::INVOCATION_REASON_CHANGED_ACCOUNT:
      return cryptauthv2::ClientMetadata::CHANGED_ACCOUNT;
    case cryptauth::InvocationReason::INVOCATION_REASON_FEATURE_TOGGLED:
      return cryptauthv2::ClientMetadata::FEATURE_TOGGLED;
    case cryptauth::InvocationReason::INVOCATION_REASON_SERVER_INITIATED:
      return cryptauthv2::ClientMetadata::SERVER_INITIATED;
    case cryptauth::InvocationReason::INVOCATION_REASON_ADDRESS_CHANGE:
      return cryptauthv2::ClientMetadata::ADDRESS_CHANGE;
    case cryptauth::InvocationReason::INVOCATION_REASON_SOFTWARE_UPDATE:
      return cryptauthv2::ClientMetadata::SOFTWARE_UPDATE;
    case cryptauth::InvocationReason::INVOCATION_REASON_MANUAL:
      return cryptauthv2::ClientMetadata::MANUAL;
    default:
      PA_LOG(WARNING) << "Unknown v1 invocation reason: "
                      << invocation_reason_v1;
      return cryptauthv2::ClientMetadata::INVOCATION_REASON_UNSPECIFIED;
  }
}

void RecordEnrollmentResult(const CryptAuthEnrollmentResult& result) {
  base::UmaHistogramBoolean("CryptAuth.EnrollmentV2.Result.Success",
                            result.IsSuccess());
  base::UmaHistogramEnumeration("CryptAuth.EnrollmentV2.Result.ResultCode",
                                result.result_code());
}

}  // namespace

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

// static
std::unique_ptr<CryptAuthEnrollmentManager>
CryptAuthV2EnrollmentManagerImpl::Factory::Create(
    const cryptauthv2::ClientAppMetadata& client_app_metadata,
    CryptAuthKeyRegistry* key_registry,
    CryptAuthClientFactory* client_factory,
    CryptAuthGCMManager* gcm_manager,
    CryptAuthScheduler* scheduler,
    PrefService* pref_service,
    base::Clock* clock) {
  if (test_factory_) {
    return test_factory_->CreateInstance(client_app_metadata, key_registry,
                                         client_factory, gcm_manager, scheduler,
                                         pref_service, clock);
  }

  return base::WrapUnique(new CryptAuthV2EnrollmentManagerImpl(
      client_app_metadata, key_registry, client_factory, gcm_manager, scheduler,
      pref_service, clock));
}

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

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

// static
void CryptAuthV2EnrollmentManagerImpl::RegisterPrefs(
    PrefRegistrySimple* registry) {
  registry->RegisterStringPref(
      prefs::kCryptAuthLastEnrolledClientAppMetadataHash, std::string());

  // TODO(nohle): Remove when v1 Enrollment is deprecated.
  registry->RegisterStringPref(prefs::kCryptAuthEnrollmentUserPublicKey,
                               std::string());
  registry->RegisterStringPref(prefs::kCryptAuthEnrollmentUserPrivateKey,
                               std::string());
}

CryptAuthV2EnrollmentManagerImpl::CryptAuthV2EnrollmentManagerImpl(
    const cryptauthv2::ClientAppMetadata& client_app_metadata,
    CryptAuthKeyRegistry* key_registry,
    CryptAuthClientFactory* client_factory,
    CryptAuthGCMManager* gcm_manager,
    CryptAuthScheduler* scheduler,
    PrefService* pref_service,
    base::Clock* clock)
    : client_app_metadata_(client_app_metadata),
      key_registry_(key_registry),
      client_factory_(client_factory),
      gcm_manager_(gcm_manager),
      scheduler_(scheduler),
      pref_service_(pref_service),
      clock_(clock) {
  // TODO(nohle): Remove when v1 Enrollment is deprecated.
  AddV1UserKeyPairToRegistryIfNecessary();

  gcm_manager_->AddObserver(this);
}

CryptAuthV2EnrollmentManagerImpl::~CryptAuthV2EnrollmentManagerImpl() {
  gcm_manager_->RemoveObserver(this);
}

void CryptAuthV2EnrollmentManagerImpl::Start() {
  scheduler_->StartEnrollmentScheduling(
      scheduler_weak_ptr_factory_.GetWeakPtr());

  std::string last_enrolled_client_app_metadata_hash = pref_service_->GetString(
      prefs::kCryptAuthLastEnrolledClientAppMetadataHash);
  if (!last_enrolled_client_app_metadata_hash.empty() &&
      GetClientAppMetadataHash() != last_enrolled_client_app_metadata_hash) {
    // Re-enroll if the ClientAppMetadata has changed since the last successful
    // enrollment. NOTE: Do not force an enrollment if the ClientAppMetadata
    // hash has never been set.
    ForceEnrollmentNow(
        cryptauth::InvocationReason::INVOCATION_REASON_SOFTWARE_UPDATE,
        std::nullopt /* session_id */);
  } else if (initial_v1_and_v2_user_key_pairs_disagree_) {
    // If the v1 and v2 user key pairs initially disagreed, force a
    // re-enrollment with the v1 user key pair that replaced the v2 user key
    // pair.
    ForceEnrollmentNow(
        cryptauth::InvocationReason::INVOCATION_REASON_INITIALIZATION,
        std::nullopt /* session_id */);
  } else if (scheduler_->GetLastSuccessfulEnrollmentTime() &&
             (GetUserPublicKey().empty() || GetUserPrivateKey().empty())) {
    // It is possible, though unlikely, that |scheduler_| has previously
    // enrolled successfully but |key_registry_| no longer holds the enrolled
    // keys, for example, if keys are deleted from the key registry or if the
    // persisted key registry pref cannot be parsed due to an encoding change.
    // In this case, force a re-enrollment.
    ForceEnrollmentNow(
        cryptauth::InvocationReason::INVOCATION_REASON_FAILURE_RECOVERY,
        std::nullopt /* session_id */);
  }
}

void CryptAuthV2EnrollmentManagerImpl::ForceEnrollmentNow(
    cryptauth::InvocationReason invocation_reason,
    const std::optional<std::string>& session_id) {
  scheduler_->RequestEnrollment(
      ConvertInvocationReasonV1ToV2(invocation_reason), session_id);
}

bool CryptAuthV2EnrollmentManagerImpl::IsEnrollmentValid() const {
  std::optional<base::Time> last_successful_enrollment_time =
      scheduler_->GetLastSuccessfulEnrollmentTime();

  if (!last_successful_enrollment_time)
    return false;

  if (GetUserPublicKey().empty() || GetUserPrivateKey().empty())
    return false;

  return (clock_->Now() - *last_successful_enrollment_time) <
         scheduler_->GetRefreshPeriod();
}

base::Time CryptAuthV2EnrollmentManagerImpl::GetLastEnrollmentTime() const {
  std::optional<base::Time> last_successful_enrollment_time =
      scheduler_->GetLastSuccessfulEnrollmentTime();

  if (!last_successful_enrollment_time)
    return base::Time();

  return *last_successful_enrollment_time;
}

base::TimeDelta CryptAuthV2EnrollmentManagerImpl::GetTimeToNextAttempt() const {
  return scheduler_->GetTimeToNextEnrollmentRequest().value_or(
      base::TimeDelta::Max());
}

bool CryptAuthV2EnrollmentManagerImpl::IsEnrollmentInProgress() const {
  return scheduler_->IsWaitingForEnrollmentResult();
}

bool CryptAuthV2EnrollmentManagerImpl::IsRecoveringFromFailure() const {
  return scheduler_->GetNumConsecutiveEnrollmentFailures() > 0;
}

std::string CryptAuthV2EnrollmentManagerImpl::GetUserPublicKey() const {
  const CryptAuthKey* user_key_pair =
      key_registry_->GetActiveKey(CryptAuthKeyBundle::Name::kUserKeyPair);

  // If a v1 key exists, it should have been added to the v2 registry already by
  // AddV1UserKeyPairToRegistryIfNecessary().
  DCHECK(
      GetV1UserPublicKey().empty() ||
      (user_key_pair && user_key_pair->public_key() == GetV1UserPublicKey()));

  if (!user_key_pair)
    return std::string();

  return user_key_pair->public_key();
}

std::string CryptAuthV2EnrollmentManagerImpl::GetUserPrivateKey() const {
  const CryptAuthKey* user_key_pair =
      key_registry_->GetActiveKey(CryptAuthKeyBundle::Name::kUserKeyPair);
  std::string private_key_v1 = GetV1UserPrivateKey();

  // If a v1 key exists, it should have been added to the v2 registry already by
  // AddV1UserKeyPairToRegistryIfNecessary().
  DCHECK(
      GetV1UserPrivateKey().empty() ||
      (user_key_pair && user_key_pair->private_key() == GetV1UserPrivateKey()));

  if (!user_key_pair)
    return std::string();

  return user_key_pair->private_key();
}

void CryptAuthV2EnrollmentManagerImpl::OnEnrollmentRequested(
    const cryptauthv2::ClientMetadata& client_metadata,
    const std::optional<cryptauthv2::PolicyReference>&
        client_directive_policy_reference) {
  NotifyEnrollmentStarted();

  current_client_metadata_ = client_metadata;
  client_directive_policy_reference_ = client_directive_policy_reference;

  base::UmaHistogramExactLinear(
      "CryptAuth.EnrollmentV2.InvocationReason",
      current_client_metadata_->invocation_reason(),
      cryptauthv2::ClientMetadata::InvocationReason_ARRAYSIZE);

  Enroll();
}

void CryptAuthV2EnrollmentManagerImpl::OnReenrollMessage(
    const std::optional<std::string>& session_id,
    const std::optional<CryptAuthFeatureType>& feature_type) {
  ForceEnrollmentNow(cryptauth::INVOCATION_REASON_SERVER_INITIATED, session_id);
}

void CryptAuthV2EnrollmentManagerImpl::Enroll() {
  DCHECK(current_client_metadata_);

  PA_LOG(VERBOSE) << "Starting CryptAuth v2 Enrollment attempt.";
  enroller_ =
      CryptAuthV2EnrollerImpl::Factory::Create(key_registry_, client_factory_);
  enroller_->Enroll(
      *current_client_metadata_, client_app_metadata_,
      client_directive_policy_reference_,
      base::BindOnce(&CryptAuthV2EnrollmentManagerImpl::OnEnrollmentFinished,
                     callback_weak_ptr_factory_.GetWeakPtr()));
}

void CryptAuthV2EnrollmentManagerImpl::OnEnrollmentFinished(
    const CryptAuthEnrollmentResult& enrollment_result) {
  // Once an enrollment attempt finishes, no other callbacks should be
  // invoked. This is particularly relevant for timeout failures.
  callback_weak_ptr_factory_.InvalidateWeakPtrs();

  // The enrollment result might be owned by the enroller, so we copy the result
  // here before destroying the enroller.
  CryptAuthEnrollmentResult enrollment_result_copy = enrollment_result;
  enroller_.reset();

  if (enrollment_result_copy.IsSuccess()) {
    PA_LOG(INFO) << "Enrollment attempt with invocation reason "
                 << current_client_metadata_->invocation_reason()
                 << " succeeded with result code "
                 << enrollment_result_copy.result_code();
    pref_service_->SetString(prefs::kCryptAuthLastEnrolledClientAppMetadataHash,
                             GetClientAppMetadataHash());
  } else {
    PA_LOG(WARNING) << "Enrollment attempt with invocation reason "
                    << current_client_metadata_->invocation_reason()
                    << " failed with result code "
                    << enrollment_result_copy.result_code();
  }

  current_client_metadata_.reset();

  RecordEnrollmentResult(enrollment_result_copy);

  scheduler_->HandleEnrollmentResult(enrollment_result_copy);

  PA_LOG(INFO) << "Time until next enrollment attempt: "
               << GetTimeToNextAttempt();

  if (!enrollment_result_copy.IsSuccess()) {
    PA_LOG(INFO) << "Number of consecutive Enrollment failures: "
                 << scheduler_->GetNumConsecutiveEnrollmentFailures();
  }

  NotifyEnrollmentFinished(enrollment_result_copy.IsSuccess());
}

std::string CryptAuthV2EnrollmentManagerImpl::GetClientAppMetadataHash() const {
  // NOTE: SerializeAsString() is not guaranteed to be stable; it could change
  // if the protobuf serialization algorithm changes or if the field
  // serialization is inherently nondeterministic. However, because we only have
  // MessageLite protocol buffers in Chrome, MessageDifferencer is not
  // available. So, we either need to compare field-by-field (maintenance heavy)
  // or compare the serializations. We choose the latter and risk a spurious
  // re-enrollment if the serialization algorithm changes. We assume the
  // ClientAppMetadata fields are serialized deterministically; unit tests will
  // fail if they are not.
  return base::NumberToString(
      base::PersistentHash(client_app_metadata_.SerializeAsString()));
}

std::string CryptAuthV2EnrollmentManagerImpl::GetV1UserPublicKey() const {
  std::optional<std::string> public_key = util::DecodeFromValueString(
      &pref_service_->GetValue(prefs::kCryptAuthEnrollmentUserPublicKey));
  if (!public_key) {
    PA_LOG(ERROR) << "Invalid public key stored in user prefs.";
    return std::string();
  }

  return *public_key;
}

std::string CryptAuthV2EnrollmentManagerImpl::GetV1UserPrivateKey() const {
  std::optional<std::string> private_key = util::DecodeFromValueString(
      &pref_service_->GetValue(prefs::kCryptAuthEnrollmentUserPrivateKey));
  if (!private_key) {
    PA_LOG(ERROR) << "Invalid private key stored in user prefs.";
    return std::string();
  }

  return *private_key;
}

void CryptAuthV2EnrollmentManagerImpl::AddV1UserKeyPairToRegistryIfNecessary() {
  std::string public_key_v1 = GetV1UserPublicKey();
  std::string private_key_v1 = GetV1UserPrivateKey();
  const CryptAuthKey* key_v2 =
      key_registry_->GetActiveKey(CryptAuthKeyBundle::Name::kUserKeyPair);
  UserKeyPairState user_key_pair_state =
      GetUserKeyPairState(public_key_v1, private_key_v1, key_v2);

  base::UmaHistogramEnumeration("CryptAuth.EnrollmentV2.UserKeyPairState",
                                user_key_pair_state);

  initial_v1_and_v2_user_key_pairs_disagree_ =
      user_key_pair_state == UserKeyPairState::kYesV1KeyYesV2KeyDisagree;

  switch (user_key_pair_state) {
    case (UserKeyPairState::kNoV1KeyNoV2Key):
      [[fallthrough]];
    case (UserKeyPairState::kNoV1KeyYesV2Key):
      [[fallthrough]];
    case (UserKeyPairState::kYesV1KeyYesV2KeyAgree):
      return;
    case (UserKeyPairState::kYesV1KeyNoV2Key):
      [[fallthrough]];
    case (UserKeyPairState::kYesV1KeyYesV2KeyDisagree):
      key_registry_->AddKey(CryptAuthKeyBundle::Name::kUserKeyPair,
                            CryptAuthKey(public_key_v1, private_key_v1,
                                         CryptAuthKey::Status::kActive,
                                         cryptauthv2::KeyType::P256,
                                         kCryptAuthFixedUserKeyPairHandle));
  };
}

}  // namespace device_sync

}  // namespace ash