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

#include <utility>

#include "ash/constants/ash_features.h"
#include "base/containers/contains.h"
#include "base/containers/flat_set.h"
#include "base/functional/bind.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_functions.h"
#include "chromeos/ash/components/multidevice/logging/logging.h"
#include "chromeos/ash/services/device_sync/async_execution_time_metrics_logger.h"
#include "chromeos/ash/services/device_sync/attestation_certificates_syncer.h"
#include "chromeos/ash/services/device_sync/cryptauth_client.h"
#include "chromeos/ash/services/device_sync/cryptauth_ecies_encryptor_impl.h"
#include "chromeos/ash/services/device_sync/cryptauth_feature_status_getter_impl.h"
#include "chromeos/ash/services/device_sync/cryptauth_group_private_key_sharer_impl.h"
#include "chromeos/ash/services/device_sync/cryptauth_key_registry.h"
#include "chromeos/ash/services/device_sync/cryptauth_metadata_syncer_impl.h"
#include "chromeos/ash/services/device_sync/cryptauth_task_metrics_logger.h"
#include "chromeos/ash/services/device_sync/proto/cryptauth_client_app_metadata.pb.h"
#include "chromeos/ash/services/device_sync/proto/cryptauth_common.pb.h"
#include "chromeos/ash/services/device_sync/synced_bluetooth_address_tracker.h"
#include "chromeos/ash/services/device_sync/value_string_encoding.h"

namespace ash {

namespace device_sync {

namespace {

const cryptauthv2::KeyType kGroupKeyType = cryptauthv2::KeyType::P256;

// Timeout values for asynchronous operations.
// TODO(https://crbug.com/933656): Use async execution time metrics to tune
// these timeout values. For now, set these timeouts to the max execution time
// recorded by the metrics.
constexpr base::TimeDelta kWaitingForEncryptedGroupPrivateKeyProcessingTimeout =
    kMaxAsyncExecutionTime;
constexpr base::TimeDelta kWaitingForEncryptedDeviceMetadataProcessingTimeout =
    kMaxAsyncExecutionTime;

void RecordGroupPrivateKeyDecryptionMetrics(base::TimeDelta execution_time,
                                            CryptAuthAsyncTaskResult result) {
  LogAsyncExecutionTimeMetric(
      "CryptAuth.DeviceSyncV2.DeviceSyncer.ExecutionTime."
      "GroupPrivateKeyDecryption",
      execution_time);
  LogCryptAuthAsyncTaskSuccessMetric(
      "CryptAuth.DeviceSyncV2.DeviceSyncer.AsyncTaskResult."
      "GroupPrivateKeyDecryption",
      result);
}

void RecordDeviceMetadataDecryptionMetrics(base::TimeDelta execution_time,
                                           CryptAuthAsyncTaskResult result) {
  LogAsyncExecutionTimeMetric(
      "CryptAuth.DeviceSyncV2.DeviceSyncer.ExecutionTime."
      "DeviceMetadataDecryption",
      execution_time);
  LogCryptAuthAsyncTaskSuccessMetric(
      "CryptAuth.DeviceSyncV2.DeviceSyncer.AsyncTaskResult."
      "DeviceMetadataDecryption",
      result);
}

}  // namespace

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

// static
std::unique_ptr<CryptAuthDeviceSyncer>
CryptAuthDeviceSyncerImpl::Factory::Create(
    CryptAuthDeviceRegistry* device_registry,
    CryptAuthKeyRegistry* key_registry,
    CryptAuthClientFactory* client_factory,
    SyncedBluetoothAddressTracker* synced_bluetooth_address_tracker,
    AttestationCertificatesSyncer* attestation_certificates_syncer,
    PrefService* pref_service,
    std::unique_ptr<base::OneShotTimer> timer) {
  if (test_factory_) {
    return test_factory_->CreateInstance(
        device_registry, key_registry, client_factory,
        synced_bluetooth_address_tracker, attestation_certificates_syncer,
        pref_service, std::move(timer));
  }

  return base::WrapUnique(new CryptAuthDeviceSyncerImpl(
      device_registry, key_registry, client_factory,
      synced_bluetooth_address_tracker, attestation_certificates_syncer,
      pref_service, std::move(timer)));
}

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

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

CryptAuthDeviceSyncerImpl::CryptAuthDeviceSyncerImpl(
    CryptAuthDeviceRegistry* device_registry,
    CryptAuthKeyRegistry* key_registry,
    CryptAuthClientFactory* client_factory,
    SyncedBluetoothAddressTracker* synced_bluetooth_address_tracker,
    AttestationCertificatesSyncer* attestation_certificates_syncer,
    PrefService* pref_service,
    std::unique_ptr<base::OneShotTimer> timer)
    : device_registry_(device_registry),
      key_registry_(key_registry),
      client_factory_(client_factory),
      synced_bluetooth_address_tracker_(synced_bluetooth_address_tracker),
      attestation_certificates_syncer_(attestation_certificates_syncer),
      pref_service_(pref_service),
      timer_(std::move(timer)) {
  DCHECK(device_registry);
  DCHECK(key_registry);
  DCHECK(client_factory);
}

CryptAuthDeviceSyncerImpl::~CryptAuthDeviceSyncerImpl() = default;

// static
std::optional<base::TimeDelta> CryptAuthDeviceSyncerImpl::GetTimeoutForState(
    State state) {
  switch (state) {
    case State::kWaitingForEncryptedGroupPrivateKeyProcessing:
      return kWaitingForEncryptedGroupPrivateKeyProcessingTimeout;
    case State::kWaitingForEncryptedDeviceMetadataProcessing:
      return kWaitingForEncryptedDeviceMetadataProcessingTimeout;
    default:
      // Signifies that there should not be a timeout.
      // Note: CryptAuthMetadataSyncerImpl, CryptAuthFeatureStatusGetterImpl,
      // CryptAuthGroupPrivateKeySharerImpl, and BluetoothAdapter guarantee that
      // the callbacks passed to their public methods are always invoke; in
      // other words, these implementations handle their relevant timeouts
      // internally.
      return std::nullopt;
  }
}

// static
std::optional<CryptAuthDeviceSyncResult::ResultCode>
CryptAuthDeviceSyncerImpl::ResultCodeErrorFromTimeoutDuringState(State state) {
  switch (state) {
    case State::kWaitingForEncryptedGroupPrivateKeyProcessing:
      return CryptAuthDeviceSyncResult::ResultCode::
          kErrorTimeoutWaitingForGroupPrivateKeyDecryption;
    case State::kWaitingForEncryptedDeviceMetadataProcessing:
      return CryptAuthDeviceSyncResult::ResultCode::
          kErrorTimeoutWaitingForDeviceMetadataDecryption;
    default:
      return std::nullopt;
  }
}

void CryptAuthDeviceSyncerImpl::OnAttemptStarted(
    const cryptauthv2::ClientMetadata& client_metadata,
    const cryptauthv2::ClientAppMetadata& client_app_metadata) {
  DCHECK_EQ(State::kNotStarted, state_);

  request_context_.set_group(CryptAuthKeyBundle::KeyBundleNameEnumToString(
      CryptAuthKeyBundle::Name::kDeviceSyncBetterTogether));
  request_context_.mutable_client_metadata()->CopyFrom(client_metadata);
  request_context_.set_device_id(client_app_metadata.instance_id());
  request_context_.set_device_id_token(client_app_metadata.instance_id_token());

  const CryptAuthKey* user_key_pair =
      key_registry_->GetActiveKey(CryptAuthKeyBundle::Name::kUserKeyPair);
  if (!user_key_pair) {
    FinishAttempt(
        CryptAuthDeviceSyncResult::ResultCode::kErrorMissingUserKeyPair);
    return;
  }

  local_better_together_device_metadata_.set_public_key(
      user_key_pair->public_key());
  local_better_together_device_metadata_.set_no_pii_device_name(
      client_app_metadata.device_model());

  AttemptNextStep();
}

void CryptAuthDeviceSyncerImpl::SetState(State state) {
  timer_->Stop();

  PA_LOG(INFO) << "Transitioning from " << state_ << " to " << state;
  state_ = state;
  last_state_change_timestamp_ = base::TimeTicks::Now();

  std::optional<base::TimeDelta> timeout_for_state = GetTimeoutForState(state);
  if (!timeout_for_state)
    return;

  timer_->Start(FROM_HERE, *timeout_for_state,
                base::BindOnce(&CryptAuthDeviceSyncerImpl::OnTimeout,
                               base::Unretained(this)));
}

void CryptAuthDeviceSyncerImpl::OnTimeout() {
  // If there's a timeout specified, there should be a corresponding error code.
  std::optional<CryptAuthDeviceSyncResult::ResultCode> error_code =
      ResultCodeErrorFromTimeoutDuringState(state_);
  DCHECK(error_code);

  base::TimeDelta execution_time =
      base::TimeTicks::Now() - last_state_change_timestamp_;
  switch (state_) {
    case State::kWaitingForEncryptedGroupPrivateKeyProcessing:
      RecordGroupPrivateKeyDecryptionMetrics(
          execution_time, CryptAuthAsyncTaskResult::kTimeout);
      break;
    case State::kWaitingForEncryptedDeviceMetadataProcessing:
      RecordDeviceMetadataDecryptionMetrics(execution_time,
                                            CryptAuthAsyncTaskResult::kTimeout);
      break;
    default:
      NOTREACHED_IN_MIGRATION();
  }

  FinishAttempt(*error_code);
}

void CryptAuthDeviceSyncerImpl::AttemptNextStep() {
  switch (state_) {
    case State::kNotStarted:
      GetBluetoothAddress();
      return;
    case State::kWaitingForBluetoothAddress:
      if (features::IsCryptauthAttestationSyncingEnabled()) {
        GetAttestationCertificates();
        return;
      }
      [[fallthrough]];
    case State::kWaitingForAttestationCertificates:
      SyncMetadata();
      return;
    case State::kWaitingForMetadataSync:
      GetFeatureStatuses();
      return;
    case State::kWaitingForFeatureStatuses:
      ProcessEncryptedGroupPrivateKey();
      return;
    case State::kWaitingForEncryptedGroupPrivateKeyProcessing:
      ProcessEncryptedDeviceMetadata();
      return;
    case State::kWaitingForEncryptedDeviceMetadataProcessing:
      ShareGroupPrivateKey();
      return;
    case State::kWaitingForGroupPrivateKeySharing: {
      CryptAuthDeviceSyncResult::ResultCode result_code =
          did_non_fatal_error_occur_
              ? CryptAuthDeviceSyncResult::ResultCode::
                    kFinishedWithNonFatalErrors
              : CryptAuthDeviceSyncResult::ResultCode::kSuccess;
      FinishAttempt(result_code);
      return;
    }
    case State::kFinished:
      NOTREACHED_IN_MIGRATION();
      return;
  }
}

void CryptAuthDeviceSyncerImpl::GetBluetoothAddress() {
  DCHECK_EQ(State::kNotStarted, state_);
  SetState(State::kWaitingForBluetoothAddress);
  synced_bluetooth_address_tracker_->GetBluetoothAddress(
      base::BindOnce(&CryptAuthDeviceSyncerImpl::OnBluetoothAddress,
                     weak_ptr_factory_.GetWeakPtr()));
}

void CryptAuthDeviceSyncerImpl::OnBluetoothAddress(
    const std::string& bluetooth_address) {
  DCHECK_EQ(State::kWaitingForBluetoothAddress, state_);

  if (!bluetooth_address.empty()) {
    local_better_together_device_metadata_.set_bluetooth_public_address(
        bluetooth_address);
  }

  AttemptNextStep();
}

void CryptAuthDeviceSyncerImpl::GetAttestationCertificates() {
  SetState(State::kWaitingForAttestationCertificates);
  const CryptAuthKey* user_key_pair =
      key_registry_->GetActiveKey(CryptAuthKeyBundle::Name::kUserKeyPair);
  attestation_certificates_syncer_->UpdateCerts(
      base::BindOnce(&CryptAuthDeviceSyncerImpl::OnAttestationCertificates,
                     weak_ptr_factory_.GetWeakPtr()),
      user_key_pair->public_key());
}

void CryptAuthDeviceSyncerImpl::OnAttestationCertificates(
    const std::vector<std::string>& cert_chain,
    bool valid) {
  cryptauthv2::AttestationData* attestation_data =
      local_better_together_device_metadata_.mutable_attestation_data();
  attestation_data->set_type(
      cryptauthv2::AttestationData::CROS_SOFT_BIND_CERT_CHAIN);
  for (const std::string& cert : cert_chain) {
    attestation_data->add_certificates(cert);
  }
  are_attestation_certs_valid_ = valid;
  AttemptNextStep();
}

void CryptAuthDeviceSyncerImpl::SyncMetadata() {
  SetState(State::kWaitingForMetadataSync);

  metadata_syncer_ = CryptAuthMetadataSyncerImpl::Factory::Create(
      client_factory_, pref_service_);
  metadata_syncer_->SyncMetadata(
      request_context_, local_better_together_device_metadata_,
      key_registry_->GetActiveKey(
          CryptAuthKeyBundle::Name::kDeviceSyncBetterTogetherGroupKey),
      base::BindOnce(&CryptAuthDeviceSyncerImpl::OnSyncMetadataFinished,
                     base::Unretained(this)));
}

void CryptAuthDeviceSyncerImpl::OnSyncMetadataFinished(
    const CryptAuthMetadataSyncer::IdToDeviceMetadataPacketMap&
        id_to_device_metadata_packet_map,
    std::unique_ptr<CryptAuthKey> new_group_key,
    const std::optional<cryptauthv2::EncryptedGroupPrivateKey>&
        encrypted_group_private_key,
    const std::optional<cryptauthv2::ClientDirective>& new_client_directive,
    CryptAuthDeviceSyncResult::ResultCode device_sync_result_code) {
  DCHECK_EQ(State::kWaitingForMetadataSync, state_);

  id_to_device_metadata_packet_map_ = id_to_device_metadata_packet_map;
  encrypted_group_private_key_ = encrypted_group_private_key;
  new_client_directive_ = new_client_directive;

  // If a new group key pair was created or if CryptAuth returned a new group
  // public key during the metadata sync, add the new group key to the key
  // registry.
  if (new_group_key)
    SetGroupKey(*new_group_key);

  switch (CryptAuthDeviceSyncResult::GetResultType(device_sync_result_code)) {
    case CryptAuthDeviceSyncResult::ResultType::kNonFatalError:
      did_non_fatal_error_occur_ = true;
      [[fallthrough]];
    case CryptAuthDeviceSyncResult::ResultType::kSuccess:
      // At a minimum, the local device metadata should be returned if no fatal
      // error occurred.
      DCHECK(base::Contains(id_to_device_metadata_packet_map_,
                            request_context_.device_id()));

      // A group key should be established by now if no fatal error occurred.
      DCHECK(key_registry_->GetActiveKey(
          CryptAuthKeyBundle::Name::kDeviceSyncBetterTogetherGroupKey));

      AttemptNextStep();
      return;
    case CryptAuthDeviceSyncResult::ResultType::kFatalError:
      FinishAttempt(device_sync_result_code);
      return;
  }
}

void CryptAuthDeviceSyncerImpl::SetGroupKey(const CryptAuthKey& new_group_key) {
  DCHECK_EQ(kGroupKeyType, new_group_key.type());

  const CryptAuthKey* current_group_key = key_registry_->GetActiveKey(
      CryptAuthKeyBundle::Name::kDeviceSyncBetterTogetherGroupKey);
  if (current_group_key) {
    if (*current_group_key == new_group_key)
      return;

    PA_LOG(VERBOSE) << "Deleting old DeviceSync BetterTogether group key:\n"
                    << "public = "
                    << util::EncodeAsString(current_group_key->public_key())
                    << ",\nprivate = "
                    << (current_group_key->private_key().empty()
                            ? "[empty]"
                            : "[not empty]");
    key_registry_->DeleteKey(
        CryptAuthKeyBundle::Name::kDeviceSyncBetterTogetherGroupKey,
        current_group_key->handle());
  }

  PA_LOG(VERBOSE) << "New DeviceSync BetterTogether group key:\n"
                  << "public = "
                  << util::EncodeAsString(new_group_key.public_key())
                  << ",\nprivate = "
                  << (new_group_key.private_key().empty() ? "[empty]"
                                                          : "[not empty]");

  key_registry_->AddKey(
      CryptAuthKeyBundle::Name::kDeviceSyncBetterTogetherGroupKey,
      new_group_key);
}

void CryptAuthDeviceSyncerImpl::GetFeatureStatuses() {
  SetState(State::kWaitingForFeatureStatuses);

  base::flat_set<std::string> device_ids;
  for (const auto& id_packet_pair : id_to_device_metadata_packet_map_)
    device_ids.insert(id_packet_pair.first);

  feature_status_getter_ =
      CryptAuthFeatureStatusGetterImpl::Factory::Create(client_factory_);
  feature_status_getter_->GetFeatureStatuses(
      request_context_, device_ids,
      base::BindOnce(&CryptAuthDeviceSyncerImpl::OnGetFeatureStatusesFinished,
                     base::Unretained(this)));
}

void CryptAuthDeviceSyncerImpl::OnGetFeatureStatusesFinished(
    const CryptAuthFeatureStatusGetter::IdToDeviceSoftwareFeatureInfoMap&
        id_to_device_software_feature_info_map,
    CryptAuthDeviceSyncResult::ResultCode device_sync_result_code) {
  DCHECK_EQ(State::kWaitingForFeatureStatuses, state_);

  switch (CryptAuthDeviceSyncResult::GetResultType(device_sync_result_code)) {
    case CryptAuthDeviceSyncResult::ResultType::kNonFatalError:
      did_non_fatal_error_occur_ = true;
      [[fallthrough]];
    case CryptAuthDeviceSyncResult::ResultType::kSuccess:
      // We require that the local device feature statuses are returned; the
      // local device is needed in the registry.
      if (!base::Contains(id_to_device_software_feature_info_map,
                          request_context_.device_id())) {
        FinishAttempt(CryptAuthDeviceSyncResult::ResultCode::
                          kErrorMissingLocalDeviceFeatureStatuses);
        return;
      }

      BuildNewDeviceRegistry(id_to_device_software_feature_info_map);
      AttemptNextStep();
      return;

    case CryptAuthDeviceSyncResult::ResultType::kFatalError:
      FinishAttempt(device_sync_result_code);
      return;
  }
}

void CryptAuthDeviceSyncerImpl::BuildNewDeviceRegistry(
    const CryptAuthFeatureStatusGetter::IdToDeviceSoftwareFeatureInfoMap&
        id_to_device_software_feature_info_map) {
  // Add all device information to the new registry except the new remote device
  // BetterTogether metadata that will be decrypted and added later if possible.
  // In the interim, use the existing BetterTogether metadata for the device
  // from the current registry, if available.
  new_device_registry_map_ = CryptAuthDeviceRegistry::InstanceIdToDeviceMap();
  for (const auto& id_device_software_feature_info_pair :
       id_to_device_software_feature_info_map) {
    const std::string& id = id_device_software_feature_info_pair.first;

    // The IDs in |id_to_device_software_feature_info_map| should be a subset of
    // those in |id_to_device_metadata_packet_map|.
    const auto packet_it = id_to_device_metadata_packet_map_.find(id);
    DCHECK(packet_it != id_to_device_metadata_packet_map_.end());
    const cryptauthv2::DeviceMetadataPacket& packet = packet_it->second;

    // Add BetterTogetherDeviceMetadata for the local device and all devices
    // with BetterTogetherDeviceMetadata in the existing device registry.
    std::optional<cryptauthv2::BetterTogetherDeviceMetadata> beto_metadata;
    if (id == request_context_.device_id()) {
      beto_metadata = local_better_together_device_metadata_;
    } else {
      const CryptAuthDevice* existing_device = device_registry_->GetDevice(id);
      if (existing_device)
        beto_metadata = existing_device->better_together_device_metadata;
    }

    new_device_registry_map_->try_emplace(
        id, id, packet.device_name(), packet.device_public_key(),
        id_device_software_feature_info_pair.second.last_modified_time,
        beto_metadata,
        id_device_software_feature_info_pair.second.feature_state_map);
  }
}

void CryptAuthDeviceSyncerImpl::ProcessEncryptedGroupPrivateKey() {
  SetState(State::kWaitingForEncryptedGroupPrivateKeyProcessing);

  // CryptAuth will not return the group private key in the SyncMetadata
  // response if the key has not been uploaded by another user device or
  // possibly if we already own the group private key.
  if (!encrypted_group_private_key_) {
    group_private_key_status_ =
        GroupPrivateKeyStatus::kNoEncryptedGroupPrivateKeyReceived;
    AttemptNextStep();
    return;
  }

  if (encrypted_group_private_key_->encrypted_private_key().empty()) {
    // TODO(crbug.com/41443836): Log metrics for empty private key.
    PA_LOG(ERROR) << "Group private key from CryptAuth unexpectedly empty.";
    did_non_fatal_error_occur_ = true;
    group_private_key_status_ =
        GroupPrivateKeyStatus::kEncryptedGroupPrivateKeyEmpty;
    AttemptNextStep();
    return;
  }

  const CryptAuthKey* device_sync_better_together_key =
      key_registry_->GetActiveKey(
          CryptAuthKeyBundle::Name::kDeviceSyncBetterTogether);
  if (!device_sync_better_together_key ||
      device_sync_better_together_key->private_key().empty()) {
    group_private_key_status_ =
        GroupPrivateKeyStatus::kLocalDeviceSyncBetterTogetherKeyMissing;
    FinishAttempt(CryptAuthDeviceSyncResult::ResultCode::
                      kErrorMissingLocalDeviceSyncBetterTogetherKey);
    return;
  }

  encryptor_ = CryptAuthEciesEncryptorImpl::Factory::Create();
  encryptor_->Decrypt(
      encrypted_group_private_key_->encrypted_private_key(),
      device_sync_better_together_key->private_key(),
      base::BindOnce(&CryptAuthDeviceSyncerImpl::OnGroupPrivateKeyDecrypted,
                     base::Unretained(this)));
}

void CryptAuthDeviceSyncerImpl::OnGroupPrivateKeyDecrypted(
    const std::optional<std::string>& group_private_key_from_cryptauth) {
  DCHECK_EQ(State::kWaitingForEncryptedGroupPrivateKeyProcessing, state_);

  bool success = group_private_key_from_cryptauth.has_value();
  RecordGroupPrivateKeyDecryptionMetrics(
      base::TimeTicks::Now() - last_state_change_timestamp_,
      success ? CryptAuthAsyncTaskResult::kSuccess
              : CryptAuthAsyncTaskResult::kError);

  if (!success) {
    group_private_key_status_ =
        GroupPrivateKeyStatus::kGroupPrivateKeyDecryptionFailed;
    FinishAttempt(
        CryptAuthDeviceSyncResult::ResultCode::kErrorDecryptingGroupPrivateKey);
    return;
  }
  group_private_key_status_ =
      GroupPrivateKeyStatus::kGroupPrivateKeySuccessfullyDecrypted;

  const CryptAuthKey* group_key = key_registry_->GetActiveKey(
      CryptAuthKeyBundle::Name::kDeviceSyncBetterTogetherGroupKey);
  DCHECK(group_key);

  // If there is no group private key in the key registry, add the newly
  // decrypted group private key. If a group private key already exists in the
  // key registry, verify it against the newly decrypted group private key.
  if (group_key->private_key().empty()) {
    SetGroupKey(CryptAuthKey(group_key->public_key(),
                             *group_private_key_from_cryptauth,
                             CryptAuthKey::Status::kActive, kGroupKeyType));
  } else {
    bool is_group_private_key_consistent =
        group_key->private_key() == group_private_key_from_cryptauth;
    base::UmaHistogramBoolean(
        "CryptAuth.DeviceSyncV2.DeviceSyncer.IsGroupPrivateKeyConsistent",
        is_group_private_key_consistent);
    if (!is_group_private_key_consistent) {
      PA_LOG(ERROR) << "Group private key from CryptAuth unexpectedly "
                    << "disagrees with the one in local storage. Using "
                    << "group private key from local key registry.";
      did_non_fatal_error_occur_ = true;
    }
  }

  AttemptNextStep();
}

void CryptAuthDeviceSyncerImpl::ProcessEncryptedDeviceMetadata() {
  SetState(State::kWaitingForEncryptedDeviceMetadataProcessing);

  // If we still do not have a group private key, we cannot decrypt device
  // metadata nor share the group private key. Finish the DeviceSync attempt and
  // wait for a GCM notification alerting us that the group private key is
  // available.
  const CryptAuthKey* group_key = key_registry_->GetActiveKey(
      CryptAuthKeyBundle::Name::kDeviceSyncBetterTogetherGroupKey);
  DCHECK(group_key);
  if (group_key->private_key().empty()) {
    PA_LOG(WARNING)
        << "CryptAuthDeviceSyncerImpl::" << __func__
        << ": Missing group private key needed to decrypt device metadata. "
        << "Finishing DeviceSync attempt and waiting for GCM message from "
        << "CryptAuth when the group private key becomes available.";
    better_together_metadata_status_ =
        BetterTogetherMetadataStatus::kGroupPrivateKeyMissing;
    CryptAuthDeviceSyncResult::ResultCode result_code =
        did_non_fatal_error_occur_
            ? CryptAuthDeviceSyncResult::ResultCode::kFinishedWithNonFatalErrors
            : CryptAuthDeviceSyncResult::ResultCode::kSuccess;
    FinishAttempt(result_code);
    return;
  }

  DCHECK(new_device_registry_map_);
  CryptAuthEciesEncryptor::IdToInputMap id_to_encrypted_metadata_map;
  for (const auto& id_device_pair : *new_device_registry_map_) {
    const auto it =
        id_to_device_metadata_packet_map_.find(id_device_pair.first);
    DCHECK(it != id_to_device_metadata_packet_map_.end());

    // Do not try to decrypt metadata that is not sent. This can happen if a
    // device has not uploaded metadata encrypted with the correct group public
    // key.
    if (it->second.encrypted_metadata().empty())
      continue;

    id_to_encrypted_metadata_map[id_device_pair.first] =
        CryptAuthEciesEncryptor::PayloadAndKey(it->second.encrypted_metadata(),
                                               group_key->private_key());
  }

  if (id_to_encrypted_metadata_map.empty()) {
    PA_LOG(ERROR) << "No encrypted metadata sent by CryptAuth. We expect the "
                  << "local device's encrypted metadata, at a minimum.";
    better_together_metadata_status_ =
        BetterTogetherMetadataStatus::kEncryptedMetadataEmpty;
    did_non_fatal_error_occur_ = true;
    AttemptNextStep();
    return;
  }

  encryptor_ = CryptAuthEciesEncryptorImpl::Factory::Create();
  encryptor_->BatchDecrypt(
      id_to_encrypted_metadata_map,
      base::BindOnce(&CryptAuthDeviceSyncerImpl::OnDeviceMetadataDecrypted,
                     base::Unretained(this)));
}

void CryptAuthDeviceSyncerImpl::OnDeviceMetadataDecrypted(
    const CryptAuthEciesEncryptor::IdToOutputMap&
        id_to_decrypted_metadata_map) {
  DCHECK_EQ(State::kWaitingForEncryptedDeviceMetadataProcessing, state_);

  // Record a success because the operation did not timeout. A separate metric
  // tracks individual decryption failures.
  RecordDeviceMetadataDecryptionMetrics(
      base::TimeTicks::Now() - last_state_change_timestamp_,
      CryptAuthAsyncTaskResult::kSuccess);
  better_together_metadata_status_ =
      BetterTogetherMetadataStatus::kMetadataDecrypted;

  AddDecryptedMetadataToNewDeviceRegistry(id_to_decrypted_metadata_map);

  AttemptNextStep();
}

void CryptAuthDeviceSyncerImpl::AddDecryptedMetadataToNewDeviceRegistry(
    const CryptAuthEciesEncryptor::IdToOutputMap&
        id_to_decrypted_metadata_map) {
  DCHECK(new_device_registry_map_);

  // Update the new device registry with BetterTogether device metadata.
  for (const auto& id_metadata_pair : id_to_decrypted_metadata_map) {
    bool was_metadata_decrypted = id_metadata_pair.second.has_value();
    base::UmaHistogramBoolean(
        "CryptAuth.DeviceSyncV2.DeviceSyncer.MetadataDecryptionSuccess",
        was_metadata_decrypted);
    if (!was_metadata_decrypted) {
      PA_LOG(ERROR) << "Metadata for device with Instance ID "
                    << id_metadata_pair.first
                    << " was not able to be decrypted.";
      did_non_fatal_error_occur_ = true;
      continue;
    }

    cryptauthv2::BetterTogetherDeviceMetadata decrypted_metadata;
    bool was_metadata_parsed =
        decrypted_metadata.ParseFromString(*id_metadata_pair.second);
    base::UmaHistogramBoolean(
        "CryptAuth.DeviceSyncV2.DeviceSyncer.MetadataParsingSuccess",
        was_metadata_parsed);
    if (!was_metadata_parsed) {
      PA_LOG(ERROR) << "Metadata for device with Instance ID "
                    << id_metadata_pair.first << " was not able to be parsed.";
      did_non_fatal_error_occur_ = true;
      continue;
    }

    auto it = new_device_registry_map_->find(id_metadata_pair.first);
    DCHECK(it != new_device_registry_map_->end());

    // The local device should already have its metadata set. Verify consistency
    // with data from CryptAuth.
    if (id_metadata_pair.first == request_context_.device_id()) {
      DCHECK(it->second.better_together_device_metadata);
      bool is_local_device_metadata_consistent =
          id_metadata_pair.second ==
          it->second.better_together_device_metadata->SerializeAsString();
      base::UmaHistogramBoolean(
          "CryptAuth.DeviceSyncV2.DeviceSyncer.IsLocalDeviceMetadataConsistent",
          is_local_device_metadata_consistent);
      if (!is_local_device_metadata_consistent) {
        PA_LOG(ERROR) << "Local device (Instance ID: "
                      << request_context_.device_id()
                      << ") metadata disagrees with that sent in SyncMetadata "
                      << "response.";
        did_non_fatal_error_occur_ = true;
      }
      continue;
    }

    it->second.better_together_device_metadata = decrypted_metadata;
  }
}

void CryptAuthDeviceSyncerImpl::ShareGroupPrivateKey() {
  SetState(State::kWaitingForGroupPrivateKeySharing);

  CryptAuthGroupPrivateKeySharer::IdToEncryptingKeyMap id_to_encrypting_key_map;
  for (const auto& id_packet_pair : id_to_device_metadata_packet_map_) {
    if (!id_packet_pair.second.need_group_private_key())
      continue;

    id_to_encrypting_key_map.insert_or_assign(
        id_packet_pair.first, id_packet_pair.second.device_public_key());
  }

  // No device needs the group private key.
  if (id_to_encrypting_key_map.empty()) {
    OnShareGroupPrivateKeyFinished(
        CryptAuthDeviceSyncResult::ResultCode::kSuccess);
    return;
  }

  const CryptAuthKey* group_key = key_registry_->GetActiveKey(
      CryptAuthKeyBundle::Name::kDeviceSyncBetterTogetherGroupKey);
  DCHECK(group_key);

  group_private_key_sharer_ =
      CryptAuthGroupPrivateKeySharerImpl::Factory::Create(client_factory_);
  group_private_key_sharer_->ShareGroupPrivateKey(
      request_context_, *group_key, id_to_encrypting_key_map,
      base::BindOnce(&CryptAuthDeviceSyncerImpl::OnShareGroupPrivateKeyFinished,
                     base::Unretained(this)));
}

void CryptAuthDeviceSyncerImpl::OnShareGroupPrivateKeyFinished(
    CryptAuthDeviceSyncResult::ResultCode device_sync_result_code) {
  DCHECK_EQ(State::kWaitingForGroupPrivateKeySharing, state_);

  switch (CryptAuthDeviceSyncResult::GetResultType(device_sync_result_code)) {
    case CryptAuthDeviceSyncResult::ResultType::kNonFatalError:
      did_non_fatal_error_occur_ = true;
      [[fallthrough]];
    case CryptAuthDeviceSyncResult::ResultType::kSuccess:
      AttemptNextStep();
      return;
    case CryptAuthDeviceSyncResult::ResultType::kFatalError:
      FinishAttempt(device_sync_result_code);
      return;
  }
}

void CryptAuthDeviceSyncerImpl::FinishAttempt(
    CryptAuthDeviceSyncResult::ResultCode result_code) {
  SetState(State::kFinished);

  metadata_syncer_.reset();
  feature_status_getter_.reset();
  encryptor_.reset();
  group_private_key_sharer_.reset();

  CryptAuthDeviceSyncResult::ResultType result_type =
      CryptAuthDeviceSyncResult::GetResultType(result_code);
  if (result_type == CryptAuthDeviceSyncResult::ResultType::kSuccess) {
    synced_bluetooth_address_tracker_->SetLastSyncedBluetoothAddress(
        local_better_together_device_metadata_.bluetooth_public_address());
    if (features::IsCryptauthAttestationSyncingEnabled()) {
      if (are_attestation_certs_valid_) {
        attestation_certificates_syncer_->SetLastSyncTimestamp();
      }
      are_attestation_certs_valid_ = false;
    }
  }

  bool did_device_registry_change =
      new_device_registry_map_ &&
      device_registry_->SetRegistry(*new_device_registry_map_);

  OnAttemptFinished(CryptAuthDeviceSyncResult(
      result_code, did_device_registry_change, new_client_directive_));
}

std::ostream& operator<<(std::ostream& stream,
                         const CryptAuthDeviceSyncerImpl::State& state) {
  switch (state) {
    case CryptAuthDeviceSyncerImpl::State::kNotStarted:
      stream << "[DeviceSyncer state: Not started]";
      break;
    case CryptAuthDeviceSyncerImpl::State::kWaitingForBluetoothAddress:
      stream << "[DeviceSyncer state: Waiting for Bluetooth address]";
      break;
    case CryptAuthDeviceSyncerImpl::State::kWaitingForAttestationCertificates:
      stream << "[DeviceSyncer state: Waiting for attestation certs]";
      break;
    case CryptAuthDeviceSyncerImpl::State::kWaitingForMetadataSync:
      stream << "[DeviceSyncer state: Waiting for metadata sync]";
      break;
    case CryptAuthDeviceSyncerImpl::State::kWaitingForFeatureStatuses:
      stream << "[DeviceSyncer state: Waiting for feature statuses]";
      break;
    case CryptAuthDeviceSyncerImpl::State::
        kWaitingForEncryptedGroupPrivateKeyProcessing:
      stream << "[DeviceSyncer state: Waiting for encrypted group private key "
             << "processing]";
      break;
    case CryptAuthDeviceSyncerImpl::State::
        kWaitingForEncryptedDeviceMetadataProcessing:
      stream << "[DeviceSyncer state: Waiting for encrypted device metadata "
                "processing]";
      break;
    case CryptAuthDeviceSyncerImpl::State::kWaitingForGroupPrivateKeySharing:
      stream << "[DeviceSyncer state: Waiting for group private key "
             << "to be shared]";
      break;
    case CryptAuthDeviceSyncerImpl::State::kFinished:
      stream << "[DeviceSyncer state: Finished]";
      break;
  }

  return stream;
}

}  // namespace device_sync

}  // namespace ash