chromium/chromeos/ash/services/device_sync/cryptauth_group_private_key_sharer_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.

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include "chromeos/ash/services/device_sync/cryptauth_group_private_key_sharer_impl.h"

#include <utility>

#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/cryptauth_client.h"
#include "chromeos/ash/services/device_sync/cryptauth_ecies_encryptor_impl.h"
#include "chromeos/ash/services/device_sync/cryptauth_key.h"
#include "chromeos/ash/services/device_sync/cryptauth_task_metrics_logger.h"
#include "crypto/sha2.h"

namespace ash {

namespace device_sync {

namespace {

// 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 kWaitingForGroupPrivateKeyEncryptionTimeout =
    kMaxAsyncExecutionTime;
constexpr base::TimeDelta kWaitingForShareGroupPrivateKeyResponseTimeout =
    kMaxAsyncExecutionTime;

CryptAuthDeviceSyncResult::ResultCode
ShareGroupPrivateKeyNetworkRequestErrorToResultCode(NetworkRequestError error) {
  switch (error) {
    case NetworkRequestError::kOffline:
      return CryptAuthDeviceSyncResult::ResultCode::
          kErrorShareGroupPrivateKeyApiCallOffline;
    case NetworkRequestError::kEndpointNotFound:
      return CryptAuthDeviceSyncResult::ResultCode::
          kErrorShareGroupPrivateKeyApiCallEndpointNotFound;
    case NetworkRequestError::kAuthenticationError:
      return CryptAuthDeviceSyncResult::ResultCode::
          kErrorShareGroupPrivateKeyApiCallAuthenticationError;
    case NetworkRequestError::kBadRequest:
      return CryptAuthDeviceSyncResult::ResultCode::
          kErrorShareGroupPrivateKeyApiCallBadRequest;
    case NetworkRequestError::kResponseMalformed:
      return CryptAuthDeviceSyncResult::ResultCode::
          kErrorShareGroupPrivateKeyApiCallResponseMalformed;
    case NetworkRequestError::kInternalServerError:
      return CryptAuthDeviceSyncResult::ResultCode::
          kErrorShareGroupPrivateKeyApiCallInternalServerError;
    case NetworkRequestError::kUnknown:
      return CryptAuthDeviceSyncResult::ResultCode::
          kErrorShareGroupPrivateKeyApiCallUnknownError;
  }
}

// The first 8 bytes of the SHA-256 hash of |str|, converted into a 64-bit
// signed integer in little-endian order. This format is chosen to be consistent
// with the CryptAuth backend implementation.
int64_t CalculateInt64Sha256Hash(const std::string& str) {
  uint8_t hash_bytes[sizeof(int64_t)];
  crypto::SHA256HashString(str, hash_bytes, sizeof(hash_bytes));

  int64_t hash_int64 = 0;
  for (size_t i = 0; i < 8u; ++i)
    hash_int64 |= static_cast<int64_t>(hash_bytes[i]) << (i * 8);

  return hash_int64;
}

void RecordGroupPrivateKeyEncryptionMetrics(
    const base::TimeDelta& execution_time,
    CryptAuthAsyncTaskResult result) {
  LogAsyncExecutionTimeMetric(
      "CryptAuth.DeviceSyncV2.GroupPrivateKeySharer.ExecutionTime."
      "GroupPrivateKeyEncryption",
      execution_time);
  LogCryptAuthAsyncTaskSuccessMetric(
      "CryptAuth.DeviceSyncV2.GroupPrivateKeySharer.AsyncTaskResult."
      "GroupPrivateKeyEncryption",
      result);
}

void RecordShareGroupPrivateKeyMetrics(const base::TimeDelta& execution_time,
                                       CryptAuthApiCallResult result) {
  LogAsyncExecutionTimeMetric(
      "CryptAuth.DeviceSyncV2.GroupPrivateKeySharer.ExecutionTime."
      "ShareGroupPrivateKey",
      execution_time);
  LogCryptAuthApiCallSuccessMetric(
      "CryptAuth.DeviceSyncV2.GroupPrivateKeySharer.ApiCallResult."
      "ShareGroupPrivateKey",
      result);
}

}  // namespace

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

// static
std::unique_ptr<CryptAuthGroupPrivateKeySharer>
CryptAuthGroupPrivateKeySharerImpl::Factory::Create(
    CryptAuthClientFactory* client_factory,
    std::unique_ptr<base::OneShotTimer> timer) {
  if (test_factory_)
    return test_factory_->CreateInstance(client_factory, std::move(timer));

  return base::WrapUnique(
      new CryptAuthGroupPrivateKeySharerImpl(client_factory, std::move(timer)));
}

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

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

CryptAuthGroupPrivateKeySharerImpl::CryptAuthGroupPrivateKeySharerImpl(
    CryptAuthClientFactory* client_factory,
    std::unique_ptr<base::OneShotTimer> timer)
    : client_factory_(client_factory), timer_(std::move(timer)) {
  DCHECK(client_factory);
}

CryptAuthGroupPrivateKeySharerImpl::~CryptAuthGroupPrivateKeySharerImpl() =
    default;

// static
std::optional<base::TimeDelta>
CryptAuthGroupPrivateKeySharerImpl::GetTimeoutForState(State state) {
  switch (state) {
    case State::kWaitingForGroupPrivateKeyEncryption:
      return kWaitingForGroupPrivateKeyEncryptionTimeout;
    case State::kWaitingForShareGroupPrivateKeyResponse:
      return kWaitingForShareGroupPrivateKeyResponseTimeout;
    default:
      // Signifies that there should not be a timeout.
      return std::nullopt;
  }
}

// static
std::optional<CryptAuthDeviceSyncResult::ResultCode>
CryptAuthGroupPrivateKeySharerImpl::ResultCodeErrorFromTimeoutDuringState(
    State state) {
  switch (state) {
    case State::kWaitingForGroupPrivateKeyEncryption:
      return CryptAuthDeviceSyncResult::ResultCode::
          kErrorTimeoutWaitingForGroupPrivateKeyEncryption;
    case State::kWaitingForShareGroupPrivateKeyResponse:
      return CryptAuthDeviceSyncResult::ResultCode::
          kErrorTimeoutWaitingForShareGroupPrivateKeyResponse;
    default:
      return std::nullopt;
  }
}

void CryptAuthGroupPrivateKeySharerImpl::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(&CryptAuthGroupPrivateKeySharerImpl::OnTimeout,
                               base::Unretained(this)));
}

void CryptAuthGroupPrivateKeySharerImpl::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::kWaitingForGroupPrivateKeyEncryption:
      RecordGroupPrivateKeyEncryptionMetrics(
          execution_time, CryptAuthAsyncTaskResult::kTimeout);
      break;
    case State::kWaitingForShareGroupPrivateKeyResponse:
      RecordShareGroupPrivateKeyMetrics(execution_time,
                                        CryptAuthApiCallResult::kTimeout);
      break;
    default:
      NOTREACHED_IN_MIGRATION();
  }

  FinishAttempt(*error_code);
}

void CryptAuthGroupPrivateKeySharerImpl::OnAttemptStarted(
    const cryptauthv2::RequestContext& request_context,
    const CryptAuthKey& group_key,
    const IdToEncryptingKeyMap& id_to_encrypting_key_map) {
  DCHECK_EQ(State::kNotStarted, state_);
  DCHECK(!group_key.private_key().empty());

  CryptAuthEciesEncryptor::IdToInputMap group_private_keys_to_encrypt;
  for (const auto& id_encrypting_key_pair : id_to_encrypting_key_map) {
    const std::string& id = id_encrypting_key_pair.first;
    const std::string& encrypting_key = id_encrypting_key_pair.second;

    // If the encrypting key is empty, the group private key cannot be
    // encrypted. Skip this ID and attempt to encrypt the group private key for
    // as many IDs as possible.
    bool is_encrypting_key_empty = encrypting_key.empty();
    base::UmaHistogramBoolean(
        "CryptAuth.DeviceSyncV2.GroupPrivateKeySharer.IsEncryptingKeyEmpty",
        is_encrypting_key_empty);
    if (is_encrypting_key_empty) {
      PA_LOG(ERROR) << "Cannot encrypt group private key for device with ID "
                    << id << ". Encrypting key is empty.";
      did_non_fatal_error_occur_ = true;
      continue;
    }

    group_private_keys_to_encrypt[id] = CryptAuthEciesEncryptor::PayloadAndKey(
        group_key.private_key(), encrypting_key);
  }

  // All encrypting keys are empty; encryption not possible.
  if (group_private_keys_to_encrypt.empty()) {
    FinishAttempt(
        CryptAuthDeviceSyncResult::ResultCode::kErrorEncryptingGroupPrivateKey);
    return;
  }

  SetState(State::kWaitingForGroupPrivateKeyEncryption);

  encryptor_ = CryptAuthEciesEncryptorImpl::Factory::Create();
  encryptor_->BatchEncrypt(
      group_private_keys_to_encrypt,
      base::BindOnce(
          &CryptAuthGroupPrivateKeySharerImpl::OnGroupPrivateKeysEncrypted,
          base::Unretained(this), request_context, group_key));
}

void CryptAuthGroupPrivateKeySharerImpl::OnGroupPrivateKeysEncrypted(
    const cryptauthv2::RequestContext& request_context,
    const CryptAuthKey& group_key,
    const CryptAuthEciesEncryptor::IdToOutputMap&
        id_to_encrypted_group_private_key_map) {
  DCHECK_EQ(State::kWaitingForGroupPrivateKeyEncryption, state_);

  // Record a success because the operation did not timeout. A separate metric
  // tracks individual encryption failures.
  RecordGroupPrivateKeyEncryptionMetrics(
      base::TimeTicks::Now() - last_state_change_timestamp_,
      CryptAuthAsyncTaskResult::kSuccess);

  cryptauthv2::ShareGroupPrivateKeyRequest request;
  request.mutable_context()->CopyFrom(request_context);

  for (const auto& id_encrypted_key_pair :
       id_to_encrypted_group_private_key_map) {
    // If the group private key could not be encrypted for this ID--due to an
    // invalid encrypting key, for instance--skip it. Continue to share as many
    // encrypted group private keys as possible.
    bool was_encryption_successful = id_encrypted_key_pair.second.has_value();
    base::UmaHistogramBoolean(
        "CryptAuth.DeviceSyncV2.GroupPrivateKeySharer.EncryptionSuccess",
        was_encryption_successful);
    if (!was_encryption_successful) {
      PA_LOG(ERROR) << "Group private key could not be encrypted for device "
                    << "with ID " << id_encrypted_key_pair.first;
      did_non_fatal_error_occur_ = true;
      continue;
    }

    cryptauthv2::EncryptedGroupPrivateKey* encrypted_key =
        request.add_encrypted_group_private_keys();
    encrypted_key->set_recipient_device_id(id_encrypted_key_pair.first);
    encrypted_key->set_sender_device_id(request_context.device_id());
    encrypted_key->set_encrypted_private_key(*id_encrypted_key_pair.second);

    // CryptAuth requires a SHA-256 hash of the group public key as an int64.
    encrypted_key->set_group_public_key_hash(
        CalculateInt64Sha256Hash(group_key.public_key()));
  }

  // All encryption attempts failed; nothing to share.
  if (request.encrypted_group_private_keys().empty()) {
    FinishAttempt(
        CryptAuthDeviceSyncResult::ResultCode::kErrorEncryptingGroupPrivateKey);
    return;
  }

  SetState(State::kWaitingForShareGroupPrivateKeyResponse);

  cryptauth_client_ = client_factory_->CreateInstance();
  cryptauth_client_->ShareGroupPrivateKey(
      request,
      base::BindOnce(
          &CryptAuthGroupPrivateKeySharerImpl::OnShareGroupPrivateKeySuccess,
          base::Unretained(this)),
      base::BindOnce(
          &CryptAuthGroupPrivateKeySharerImpl::OnShareGroupPrivateKeyFailure,
          base::Unretained(this)));
}

void CryptAuthGroupPrivateKeySharerImpl::OnShareGroupPrivateKeySuccess(
    const cryptauthv2::ShareGroupPrivateKeyResponse& response) {
  DCHECK_EQ(State::kWaitingForShareGroupPrivateKeyResponse, state_);

  RecordShareGroupPrivateKeyMetrics(
      base::TimeTicks::Now() - last_state_change_timestamp_,
      CryptAuthApiCallResult::kSuccess);

  CryptAuthDeviceSyncResult::ResultCode result_code =
      did_non_fatal_error_occur_
          ? CryptAuthDeviceSyncResult::ResultCode::kFinishedWithNonFatalErrors
          : CryptAuthDeviceSyncResult::ResultCode::kSuccess;
  FinishAttempt(result_code);
}

void CryptAuthGroupPrivateKeySharerImpl::OnShareGroupPrivateKeyFailure(
    NetworkRequestError error) {
  DCHECK_EQ(State::kWaitingForShareGroupPrivateKeyResponse, state_);

  RecordShareGroupPrivateKeyMetrics(
      base::TimeTicks::Now() - last_state_change_timestamp_,
      CryptAuthApiCallResultFromNetworkRequestError(error));

  FinishAttempt(ShareGroupPrivateKeyNetworkRequestErrorToResultCode(error));
}

void CryptAuthGroupPrivateKeySharerImpl::FinishAttempt(
    CryptAuthDeviceSyncResult::ResultCode result_code) {
  encryptor_.reset();
  cryptauth_client_.reset();

  SetState(State::kFinished);

  OnAttemptFinished(result_code);
}

std::ostream& operator<<(
    std::ostream& stream,
    const CryptAuthGroupPrivateKeySharerImpl::State& state) {
  switch (state) {
    case CryptAuthGroupPrivateKeySharerImpl::State::kNotStarted:
      stream << "[GroupPrivateKeySharer state: Not started]";
      break;
    case CryptAuthGroupPrivateKeySharerImpl::State::
        kWaitingForGroupPrivateKeyEncryption:
      stream << "[GroupPrivateKeySharer state: Waiting for group private key "
             << "encryption]";
      break;
    case CryptAuthGroupPrivateKeySharerImpl::State::
        kWaitingForShareGroupPrivateKeyResponse:
      stream << "[GroupPrivateKeySharer state: Waiting for "
             << "ShareGroupPrivateKey response]";
      break;
    case CryptAuthGroupPrivateKeySharerImpl::State::kFinished:
      stream << "[GroupPrivateKeySharer state: Finished]";
      break;
  }

  return stream;
}

}  // namespace device_sync

}  // namespace ash