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

// Copyright 2015 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_gcm_manager_impl.h"

#include "base/functional/bind.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "chromeos/ash/components/multidevice/logging/logging.h"
#include "chromeos/ash/services/device_sync/cryptauth_feature_type.h"
#include "chromeos/ash/services/device_sync/cryptauth_key_bundle.h"
#include "chromeos/ash/services/device_sync/pref_names.h"
#include "chromeos/ash/services/device_sync/proto/cryptauth_common.pb.h"
#include "chromeos/ash/services/device_sync/public/cpp/gcm_constants.h"
#include "components/gcm_driver/gcm_driver.h"
#include "components/gcm_driver/instance_id/instance_id_driver.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. If entries are added, kMaxValue should
// be updated.
enum class TargetServiceForMetrics {
  kUnknown = 0,
  kEnrollment = 1,
  kDeviceSync = 2,
  // Used for UMA logs.
  kMaxValue = kDeviceSync
};

// The 'registrationTickleType' key-value pair is present in GCM push
// messages. The values correspond to a server-side enum.
const char kRegistrationTickleTypeKey[] = "registrationTickleType";
const char kRegistrationTickleTypeForceEnrollment[] = "1";
const char kRegistrationTickleTypeUpdateEnrollment[] = "2";
const char kRegistrationTickleTypeDevicesSync[] = "3";

// Used in GCM messages sent by CryptAuth v2 DeviceSync. The value corresponding
// to this key specifies the service to notify, 1 for Enrollment and 2 for
// DeviceSync, as enumerated in cryptauthv2::TargetService.
const char kTargetServiceKey[] = "S";

// Only used in GCM messages sent by CryptAuth v2 DeviceSync. The session_id
// field of ClientAppMetadata should be set to the value corresponding to this
// key.
const char kSessionIdKey[] = "I";

// Used in GCM messages triggered by a BatchNofityGroupDevices request. The
// value corresponding to this key is the base64url-encoded, SHA-256 8-byte hash
// of the feature_type field forwarded from the BatchNotifyGroupDevicesRequest.
// CryptAuth chooses this hashing scheme to accommodate the limited bandwidth of
// GCM messages.
const char kFeatureTypeHashKey[] = "F";

// Only used in GCM messages sent by CryptAuth v2 DeviceSync. The value
// corresponding to this key specifies the relevant DeviceSync group. Currently,
// the value should always be "DeviceSync:BetterTogether".
const char kDeviceSyncGroupNameKey[] = "K";

// Determine the target service based on the keys "registrationTickleType" and
// "S". In practice, one and only one of these keys should exist in a GCM
// message. Return null if neither is set to a valid value. If both are set for
// some reason, arbitrarily prefer a valid "S" value.
std::optional<cryptauthv2::TargetService> TargetServiceFromMessage(
    const gcm::IncomingMessage& message) {
  std::optional<cryptauthv2::TargetService>
      target_from_registration_tickle_type;
  std::optional<cryptauthv2::TargetService> target_from_target_service;

  auto it = message.data.find(kRegistrationTickleTypeKey);
  if (it != message.data.end()) {
    TargetServiceForMetrics target_service_for_metrics;
    if (it->second == kRegistrationTickleTypeForceEnrollment ||
        it->second == kRegistrationTickleTypeUpdateEnrollment) {
      target_service_for_metrics = TargetServiceForMetrics::kEnrollment;
      target_from_registration_tickle_type =
          cryptauthv2::TargetService::ENROLLMENT;
    } else if (it->second == kRegistrationTickleTypeDevicesSync) {
      target_service_for_metrics = TargetServiceForMetrics::kDeviceSync;
      target_from_registration_tickle_type =
          cryptauthv2::TargetService::DEVICE_SYNC;
    } else {
      target_service_for_metrics = TargetServiceForMetrics::kUnknown;
      PA_LOG(WARNING) << "Unknown tickle type in GCM message: " << it->second;
    }
    base::UmaHistogramEnumeration(
        "CryptAuth.Gcm.Message.TargetService.FromRegistrationTickleType",
        target_service_for_metrics);
  }

  it = message.data.find(kTargetServiceKey);
  if (it != message.data.end()) {
    TargetServiceForMetrics target_service_for_metrics;
    if (it->second ==
        base::NumberToString(cryptauthv2::TargetService::ENROLLMENT)) {
      target_service_for_metrics = TargetServiceForMetrics::kEnrollment;
      target_from_target_service = cryptauthv2::TargetService::ENROLLMENT;
    } else if (it->second ==
               base::NumberToString(cryptauthv2::TargetService::DEVICE_SYNC)) {
      target_service_for_metrics = TargetServiceForMetrics::kDeviceSync;
      target_from_target_service = cryptauthv2::TargetService::DEVICE_SYNC;
    } else {
      target_service_for_metrics = TargetServiceForMetrics::kUnknown;
      PA_LOG(WARNING) << "Invalid TargetService in GCM message: " << it->second;
    }
    base::UmaHistogramEnumeration(
        "CryptAuth.Gcm.Message.TargetService.FromTargetServiceValue",
        target_service_for_metrics);
  }

  bool are_tickle_type_and_target_service_both_specified =
      target_from_registration_tickle_type && target_from_target_service;
  base::UmaHistogramBoolean(
      "CryptAuth.Gcm.Message.TargetService."
      "AreTickleTypeAndTargetServiceBothSpecified",
      are_tickle_type_and_target_service_both_specified);
  if (are_tickle_type_and_target_service_both_specified) {
    PA_LOG(WARNING) << "Registration tickle type, "
                    << *target_from_registration_tickle_type
                    << ", and target service, " << *target_from_target_service
                    << ", are both set in the same GCM message";
  }

  // If the target service is specified via both the CryptAuth v1 registration
  // tickle type field and the v2 target service field, prefer the v2 value.
  // That said, we do not expect both to be used in the same GCM message.
  return target_from_target_service ? target_from_target_service
                                    : target_from_registration_tickle_type;
}

// Returns null if |key| doesn't exist in the |message.data| map.
std::optional<std::string> StringValueFromMessage(
    const std::string& key,
    const gcm::IncomingMessage& message) {
  auto it = message.data.find(key);
  if (it == message.data.end())
    return std::nullopt;

  return it->second;
}

// Returns null if |message| does not contain the feature type key-value pair or
// if the value does not correspond to one of the CryptAuthFeatureType enums.
std::optional<CryptAuthFeatureType> FeatureTypeFromMessage(
    const gcm::IncomingMessage& message) {
  std::optional<std::string> feature_type_hash =
      StringValueFromMessage(kFeatureTypeHashKey, message);

  if (!feature_type_hash)
    return std::nullopt;

  std::optional<CryptAuthFeatureType> feature_type =
      CryptAuthFeatureTypeFromGcmHash(*feature_type_hash);
  base::UmaHistogramBoolean("CryptAuth.Gcm.Message.IsKnownFeatureType",
                            feature_type.has_value());
  if (feature_type) {
    base::UmaHistogramEnumeration("CryptAuth.Gcm.Message.FeatureType",
                                  *feature_type);
  } else {
    PA_LOG(WARNING) << "GCM message contains unknown feature type hash: "
                    << *feature_type_hash;
  }

  return feature_type;
}

// If the DeviceSync group name is provided in the GCM message, verify that the
// value agrees with the name of the corresponding enrolled key. On Chrome OS,
// the only relevant DeviceSync group name is "DeviceSync:BetterTogether".
bool IsDeviceSyncGroupNameValid(const gcm::IncomingMessage& message) {
  std::optional<std::string> group_name =
      StringValueFromMessage(kDeviceSyncGroupNameKey, message);
  if (!group_name)
    return true;

  bool is_device_sync_group_name_valid =
      *group_name == CryptAuthKeyBundle::KeyBundleNameEnumToString(
                         CryptAuthKeyBundle::Name::kDeviceSyncBetterTogether);
  base::UmaHistogramBoolean("CryptAuth.Gcm.Message.IsDeviceSyncGroupNameValid",
                            is_device_sync_group_name_valid);

  return is_device_sync_group_name_valid;
}

void RecordGCMRegistrationMetrics(instance_id::InstanceID::Result result,
                                  base::TimeDelta execution_time) {
  base::UmaHistogramCustomTimes(
      "CryptAuth.Gcm.Registration.AttemptTimeWithRetries", execution_time,
      base::Seconds(1) /* min */, base::Minutes(10) /* max */,
      100 /* buckets */);

  base::UmaHistogramEnumeration("CryptAuth.Gcm.Registration.Result2", result);
}

}  // namespace

// static
CryptAuthGCMManagerImpl::Factory*
    CryptAuthGCMManagerImpl::Factory::factory_instance_ = nullptr;

// static
std::unique_ptr<CryptAuthGCMManager> CryptAuthGCMManagerImpl::Factory::Create(
    instance_id::InstanceIDDriver* instance_id_driver,
    PrefService* pref_service) {
  if (factory_instance_)
    return factory_instance_->CreateInstance(instance_id_driver, pref_service);

  return base::WrapUnique(
      new CryptAuthGCMManagerImpl(instance_id_driver, pref_service));
}

// static
void CryptAuthGCMManagerImpl::Factory::SetFactoryForTesting(Factory* factory) {
  factory_instance_ = factory;
}

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

CryptAuthGCMManagerImpl::CryptAuthGCMManagerImpl(
    instance_id::InstanceIDDriver* instance_id_driver,
    PrefService* pref_service)
    : instance_id_driver_(instance_id_driver),
      pref_service_(pref_service),
      registration_in_progress_(false) {}

CryptAuthGCMManagerImpl::~CryptAuthGCMManagerImpl() {
  if (IsListening())
    instance_id_driver_->GetInstanceID(kCryptAuthGcmAppId)
        ->gcm_driver()
        ->RemoveAppHandler(kCryptAuthGcmAppId);
}

void CryptAuthGCMManagerImpl::StartListening() {
  if (IsListening()) {
    PA_LOG(VERBOSE) << "GCM app handler already added";
    return;
  }

  instance_id_driver_->GetInstanceID(kCryptAuthGcmAppId)
      ->gcm_driver()
      ->AddAppHandler(kCryptAuthGcmAppId, this);
}

bool CryptAuthGCMManagerImpl::IsListening() {
  return instance_id_driver_->GetInstanceID(kCryptAuthGcmAppId)
             ->gcm_driver()
             ->GetAppHandler(kCryptAuthGcmAppId) == this;
}

void CryptAuthGCMManagerImpl::RegisterWithGCM() {
  if (registration_in_progress_) {
    PA_LOG(VERBOSE) << "GCM Registration is already in progress";
    return;
  }

  PA_LOG(VERBOSE) << "Beginning GCM registration...";
  registration_in_progress_ = true;
  gcm_registration_start_timestamp_ = base::TimeTicks::Now();

  instance_id_driver_->GetInstanceID(kCryptAuthGcmAppId)
      ->GetToken(
          kCryptAuthGcmSenderId, instance_id::kGCMScope,
          /*time_to_live=*/base::TimeDelta(), /*flags=*/{},
          base::BindOnce(&CryptAuthGCMManagerImpl::OnRegistrationCompleted,
                         weak_ptr_factory_.GetWeakPtr()));
}

std::string CryptAuthGCMManagerImpl::GetRegistrationId() {
  return pref_service_->GetString(prefs::kCryptAuthGCMRegistrationId);
}

void CryptAuthGCMManagerImpl::AddObserver(Observer* observer) {
  observers_.AddObserver(observer);
}

void CryptAuthGCMManagerImpl::RemoveObserver(Observer* observer) {
  observers_.RemoveObserver(observer);
}

void CryptAuthGCMManagerImpl::ShutdownHandler() {}

void CryptAuthGCMManagerImpl::OnStoreReset() {
  // We will automatically re-register to GCM and re-enroll the new registration
  // ID to Cryptauth during the next scheduled sync.
  pref_service_->ClearPref(prefs::kCryptAuthGCMRegistrationId);
}

void CryptAuthGCMManagerImpl::OnMessage(const std::string& app_id,
                                        const gcm::IncomingMessage& message) {
  std::vector<std::string> fields;
  for (const auto& kv : message.data)
    fields.push_back(std::string(kv.first) + ": " + std::string(kv.second));

  PA_LOG(VERBOSE) << "GCM message received:\n"
                  << "  sender_id: " << message.sender_id << "\n"
                  << "  collapse_key: " << message.collapse_key << "\n"
                  << "  data:\n    " << base::JoinString(fields, "\n    ");

  std::optional<cryptauthv2::TargetService> target_service =
      TargetServiceFromMessage(message);
  if (!target_service) {
    PA_LOG(ERROR) << "GCM message does not specify a valid target service.";
    return;
  }

  if (!IsDeviceSyncGroupNameValid(message)) {
    PA_LOG(ERROR) << "GCM message contains unexpected DeviceSync group name: "
                  << *StringValueFromMessage(kDeviceSyncGroupNameKey, message);
    return;
  }

  std::optional<std::string> session_id =
      StringValueFromMessage(kSessionIdKey, message);
  std::optional<CryptAuthFeatureType> feature_type =
      FeatureTypeFromMessage(message);

  if (target_service == cryptauthv2::TargetService::ENROLLMENT) {
    for (auto& observer : observers_)
      observer.OnReenrollMessage(session_id, feature_type);

    return;
  }

  DCHECK(target_service == cryptauthv2::TargetService::DEVICE_SYNC);
  for (auto& observer : observers_)
    observer.OnResyncMessage(session_id, feature_type);
}

void CryptAuthGCMManagerImpl::OnMessagesDeleted(const std::string& app_id) {}

void CryptAuthGCMManagerImpl::OnSendError(
    const std::string& app_id,
    const gcm::GCMClient::SendErrorDetails& details) {
  NOTREACHED_IN_MIGRATION();
}

void CryptAuthGCMManagerImpl::OnSendAcknowledged(
    const std::string& app_id,
    const std::string& message_id) {
  NOTREACHED_IN_MIGRATION();
}

void CryptAuthGCMManagerImpl::OnRegistrationCompleted(
    const std::string& registration_id,
    instance_id::InstanceID::Result result) {
  registration_in_progress_ = false;
  RecordGCMRegistrationMetrics(
      result, base::TimeTicks::Now() -
                  gcm_registration_start_timestamp_ /* execution_time */);

  if (result != instance_id::InstanceID::Result::SUCCESS) {
    PA_LOG(WARNING) << "GCM registration failed with result="
                    << static_cast<int>(result);
    for (auto& observer : observers_)
      observer.OnGCMRegistrationResult(false);
    return;
  }

  PA_LOG(VERBOSE) << "GCM registration success, registration_id="
                  << registration_id;
  pref_service_->SetString(prefs::kCryptAuthGCMRegistrationId, registration_id);
  for (auto& observer : observers_)
    observer.OnGCMRegistrationResult(true);
}

}  // namespace device_sync

}  // namespace ash