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

#include <utility>

#include "base/functional/bind.h"
#include "base/metrics/histogram_macros.h"
#include "chromeos/ash/components/multidevice/logging/logging.h"
#include "chromeos/ash/components/multidevice/secure_message_delegate.h"
#include "chromeos/ash/services/device_sync/cryptauth_client_impl.h"
#include "crypto/sha2.h"

namespace ash {

namespace device_sync {

namespace {

// A successful SetupEnrollment or FinishEnrollment response should contain this
// status string.
const char kResponseStatusOk[] = "ok";

// The name of the "gcmV1" protocol that the enrolling device supports.
const char kSupportedEnrollmentTypeGcmV1[] = "gcmV1";

// The version field of the GcmMetadata message.
const int kGCMMetadataVersion = 1;

// Returns true if |device_info| contains the required fields for enrollment.
bool ValidateDeviceInfo(const cryptauth::GcmDeviceInfo& device_info) {
  if (!device_info.has_long_device_id()) {
    PA_LOG(ERROR)
        << "Expected long_device_id field in cryptauth::GcmDeviceInfo.";
    return false;
  }

  if (!device_info.has_device_type()) {
    PA_LOG(ERROR) << "Expected device_type field in cryptauth::GcmDeviceInfo.";
    return false;
  }

  return true;
}

// Creates the public metadata to put in the SecureMessage that is sent to the
// server with the FinishEnrollment request.
std::string CreateEnrollmentPublicMetadata() {
  cryptauth::GcmMetadata metadata;
  metadata.set_version(kGCMMetadataVersion);
  metadata.set_type(cryptauth::MessageType::ENROLLMENT);
  return metadata.SerializeAsString();
}

void RecordEnrollmentResult(bool success) {
  UMA_HISTOGRAM_BOOLEAN("CryptAuth.Enrollment.Result", success);
}

}  // namespace

CryptAuthEnrollerImpl::CryptAuthEnrollerImpl(
    CryptAuthClientFactory* client_factory,
    std::unique_ptr<multidevice::SecureMessageDelegate> secure_message_delegate)
    : client_factory_(client_factory),
      secure_message_delegate_(std::move(secure_message_delegate)) {}

CryptAuthEnrollerImpl::~CryptAuthEnrollerImpl() {}

void CryptAuthEnrollerImpl::Enroll(
    const std::string& user_public_key,
    const std::string& user_private_key,
    const cryptauth::GcmDeviceInfo& device_info,
    cryptauth::InvocationReason invocation_reason,
    EnrollmentFinishedCallback callback) {
  if (enroll_called_) {
    PA_LOG(ERROR) << "Enroll() already called. Do not reuse.";
    std::move(callback).Run(false);
    return;
  }

  user_public_key_ = user_public_key;
  user_private_key_ = user_private_key;
  device_info_ = device_info;
  invocation_reason_ = invocation_reason;
  callback_ = std::move(callback);
  enroll_called_ = true;

  if (!ValidateDeviceInfo(device_info)) {
    std::move(callback_).Run(false);
    return;
  }

  secure_message_delegate_->GenerateKeyPair(
      base::BindOnce(&CryptAuthEnrollerImpl::OnKeyPairGenerated,
                     weak_ptr_factory_.GetWeakPtr()));
}

void CryptAuthEnrollerImpl::OnKeyPairGenerated(const std::string& public_key,
                                               const std::string& private_key) {
  PA_LOG(VERBOSE)
      << "Ephemeral key pair generated, calling SetupEnrollment API.";
  session_public_key_ = public_key;
  session_private_key_ = private_key;

  cryptauth_client_ = client_factory_->CreateInstance();
  cryptauth::SetupEnrollmentRequest request;
  request.add_types(kSupportedEnrollmentTypeGcmV1);
  request.set_invocation_reason(invocation_reason_);
  cryptauth_client_->SetupEnrollment(
      request,
      base::BindOnce(&CryptAuthEnrollerImpl::OnSetupEnrollmentSuccess,
                     weak_ptr_factory_.GetWeakPtr()),
      base::BindOnce(&CryptAuthEnrollerImpl::OnSetupEnrollmentFailure,
                     weak_ptr_factory_.GetWeakPtr()));
}

void CryptAuthEnrollerImpl::OnSetupEnrollmentSuccess(
    const cryptauth::SetupEnrollmentResponse& response) {
  if (response.status() != kResponseStatusOk) {
    PA_LOG(WARNING) << "Unexpected status for SetupEnrollment: "
                    << response.status();
    std::move(callback_).Run(false);
    return;
  }

  if (response.infos_size() == 0) {
    PA_LOG(ERROR) << "No response info returned by server for SetupEnrollment";
    std::move(callback_).Run(false);
    return;
  }

  PA_LOG(VERBOSE)
      << "SetupEnrollment request succeeded: deriving symmetric key.";
  setup_info_ = response.infos(0);
  device_info_.set_enrollment_session_id(setup_info_.enrollment_session_id());

  secure_message_delegate_->DeriveKey(
      session_private_key_, setup_info_.server_ephemeral_key(),
      base::BindOnce(&CryptAuthEnrollerImpl::OnKeyDerived,
                     weak_ptr_factory_.GetWeakPtr()));
}

void CryptAuthEnrollerImpl::OnSetupEnrollmentFailure(
    NetworkRequestError error) {
  PA_LOG(WARNING) << "SetupEnrollment API failed with error: " << error;
  std::move(callback_).Run(false);
}

void CryptAuthEnrollerImpl::OnKeyDerived(const std::string& symmetric_key) {
  PA_LOG(VERBOSE) << "Derived symmetric key, "
                  << "encrypting enrollment data for upload.";

  // Make sure we're enrolling the same public key used below to sign the
  // secure message.
  device_info_.set_user_public_key(user_public_key_);
  device_info_.set_key_handle(user_public_key_);

  // Hash the symmetric key and add it to the |device_info_| to be uploaded.
  device_info_.set_device_authzen_key_hash(
      crypto::SHA256HashString(symmetric_key));

  // The server verifies that the access token set here and in the header
  // of the FinishEnrollment() request are the same.
  device_info_.set_oauth_token(cryptauth_client_->GetAccessTokenUsed());
  PA_LOG(VERBOSE) << "Using access token: " << device_info_.oauth_token();

  symmetric_key_ = symmetric_key;
  multidevice::SecureMessageDelegate::CreateOptions options;
  options.encryption_scheme = securemessage::NONE;
  options.signature_scheme = securemessage::ECDSA_P256_SHA256;
  options.verification_key_id = user_public_key_;

  // The inner message contains the signed device information that will be
  // sent to CryptAuth.
  secure_message_delegate_->CreateSecureMessage(
      device_info_.SerializeAsString(), user_private_key_, options,
      base::BindOnce(&CryptAuthEnrollerImpl::OnInnerSecureMessageCreated,
                     weak_ptr_factory_.GetWeakPtr()));
}

void CryptAuthEnrollerImpl::OnInnerSecureMessageCreated(
    const std::string& inner_message) {
  if (inner_message.empty()) {
    PA_LOG(ERROR) << "Error creating inner message";
    std::move(callback_).Run(false);
    return;
  }

  multidevice::SecureMessageDelegate::CreateOptions options;
  options.encryption_scheme = securemessage::AES_256_CBC;
  options.signature_scheme = securemessage::HMAC_SHA256;
  options.public_metadata = CreateEnrollmentPublicMetadata();

  // The outer message encrypts and signs the inner message with the derived
  // symmetric session key.
  secure_message_delegate_->CreateSecureMessage(
      inner_message, symmetric_key_, options,
      base::BindOnce(&CryptAuthEnrollerImpl::OnOuterSecureMessageCreated,
                     weak_ptr_factory_.GetWeakPtr()));
}

void CryptAuthEnrollerImpl::OnOuterSecureMessageCreated(
    const std::string& outer_message) {
  PA_LOG(VERBOSE) << "SecureMessage created, calling FinishEnrollment API.";

  cryptauth::FinishEnrollmentRequest request;
  request.set_enrollment_session_id(setup_info_.enrollment_session_id());
  request.set_enrollment_message(outer_message);
  request.set_device_ephemeral_key(session_public_key_);
  request.set_invocation_reason(invocation_reason_);

  cryptauth_client_ = client_factory_->CreateInstance();
  cryptauth_client_->FinishEnrollment(
      request,
      base::BindOnce(&CryptAuthEnrollerImpl::OnFinishEnrollmentSuccess,
                     weak_ptr_factory_.GetWeakPtr()),
      base::BindOnce(&CryptAuthEnrollerImpl::OnFinishEnrollmentFailure,
                     weak_ptr_factory_.GetWeakPtr()));
}

void CryptAuthEnrollerImpl::OnFinishEnrollmentSuccess(
    const cryptauth::FinishEnrollmentResponse& response) {
  const bool success = response.status() == kResponseStatusOk;

  if (!success) {
    PA_LOG(WARNING) << "Unexpected status for FinishEnrollment: "
                    << response.status();
  }

  RecordEnrollmentResult(success);
  std::move(callback_).Run(success);
}

void CryptAuthEnrollerImpl::OnFinishEnrollmentFailure(
    NetworkRequestError error) {
  PA_LOG(WARNING) << "FinishEnrollment API failed with error: " << error;
  RecordEnrollmentResult(false /* success */);
  std::move(callback_).Run(false);
}

}  // namespace device_sync

}  // namespace ash