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

#include "base/functional/bind.h"
#include "base/memory/ptr_util.h"
#include "base/strings/string_util.h"
#include "chromeos/ash/components/multidevice/secure_message_delegate_impl.h"
#include "chromeos/ash/services/device_sync/cryptauth_enrollment_constants.h"
#include "chromeos/ash/services/device_sync/proto/cryptauth_common.pb.h"
#include "crypto/hkdf.h"

namespace ash {

namespace device_sync {

namespace {

bool IsValidSymmetricKeyType(const cryptauthv2::KeyType& type) {
  return type == cryptauthv2::KeyType::RAW128 ||
         type == cryptauthv2::KeyType::RAW256;
}

bool IsValidAsymmetricKeyType(const cryptauthv2::KeyType& type) {
  return type == cryptauthv2::KeyType::P256;
}

// If we need to create any symmetric keys, we first need to generate the
// client's ephemeral Diffie-Hellman key-pair and derive the handshake secret
// from the server's public key and the client's private key. This secret is
// used to derive new symmetric keys using HKDF.
bool IsClientEphemeralDhKeyNeeded(
    const base::flat_map<CryptAuthKeyBundle::Name,
                         CryptAuthKeyCreator::CreateKeyData>& keys_to_create) {
  for (const auto& key_to_create : keys_to_create) {
    if (IsValidSymmetricKeyType(key_to_create.second.type))
      return true;
  }

  return false;
}

size_t NumBytesForSymmetricKeyType(cryptauthv2::KeyType key_type) {
  switch (key_type) {
    case cryptauthv2::KeyType::RAW128:
      return 16u;
    case cryptauthv2::KeyType::RAW256:
      return 32u;
    default:
      NOTREACHED_IN_MIGRATION();
      return 0u;
  }
}

}  // namespace

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

// static
std::unique_ptr<CryptAuthKeyCreator>
CryptAuthKeyCreatorImpl::Factory::Create() {
  if (test_factory_)
    return test_factory_->CreateInstance();

  return base::WrapUnique(new CryptAuthKeyCreatorImpl());
}

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

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

CryptAuthKeyCreatorImpl::CryptAuthKeyCreatorImpl()
    : secure_message_delegate_(
          multidevice::SecureMessageDelegateImpl::Factory::Create()) {}

CryptAuthKeyCreatorImpl::~CryptAuthKeyCreatorImpl() = default;

void CryptAuthKeyCreatorImpl::CreateKeys(
    const base::flat_map<CryptAuthKeyBundle::Name, CreateKeyData>&
        keys_to_create,
    const std::optional<CryptAuthKey>& server_ephemeral_dh,
    CreateKeysCallback create_keys_callback) {
  DCHECK(!keys_to_create.empty());

  // Fail if CreateKeys() has already been called.
  DCHECK(num_keys_to_create_ == 0 && new_keys_.empty() &&
         !create_keys_callback_);

  num_keys_to_create_ = keys_to_create.size();
  keys_to_create_ = keys_to_create;
  create_keys_callback_ = std::move(create_keys_callback);

  if (IsClientEphemeralDhKeyNeeded(keys_to_create_)) {
    DCHECK(server_ephemeral_dh && server_ephemeral_dh->IsAsymmetricKey());
    secure_message_delegate_->GenerateKeyPair(
        base::BindOnce(&CryptAuthKeyCreatorImpl::OnClientDiffieHellmanGenerated,
                       base::Unretained(this), *server_ephemeral_dh));
    return;
  }

  StartKeyCreation(std::nullopt /* dh_handshake_secret */);
}

void CryptAuthKeyCreatorImpl::OnClientDiffieHellmanGenerated(
    const CryptAuthKey& server_ephemeral_dh,
    const std::string& public_key,
    const std::string& private_key) {
  // If the client ephemeral key-pair generation failed, we cannot generate the
  // Diffie-Hellman handshake secret and, consequently, cannot generate
  // symmetric keys. Start the key creation process with a null
  // |dh_handshake_secret|; the symmetric key creation code will handle the
  // errors.
  if (public_key.empty() || private_key.empty()) {
    StartKeyCreation(std::nullopt /* dh_handshake_secret */);
    return;
  }

  client_ephemeral_dh_ =
      CryptAuthKey(public_key, private_key, CryptAuthKey::Status::kActive,
                   cryptauthv2::KeyType::P256);

  secure_message_delegate_->DeriveKey(
      client_ephemeral_dh_->private_key(), server_ephemeral_dh.public_key(),
      base::BindOnce(
          &CryptAuthKeyCreatorImpl::OnDiffieHellmanHandshakeSecretDerived,
          base::Unretained(this)));
}

void CryptAuthKeyCreatorImpl::OnDiffieHellmanHandshakeSecretDerived(
    const std::string& symmetric_key) {
  // If the Diffie-Hellman handshake secret could not be derived, then we cannot
  // generate symmetric keys. Start the key creation process with a null
  // |dh_handshake_secret|; the symmetric key creation code will handle the
  // errors.
  if (symmetric_key.empty()) {
    StartKeyCreation(std::nullopt /* dh_handshake_secret */);
    return;
  }

  StartKeyCreation(CryptAuthKey(symmetric_key, CryptAuthKey::Status::kActive,
                                cryptauthv2::KeyType::RAW256));
}

void CryptAuthKeyCreatorImpl::StartKeyCreation(
    const std::optional<CryptAuthKey>& dh_handshake_secret) {
  for (const auto& key_to_create : keys_to_create_) {
    const CryptAuthKeyBundle::Name& bundle_name = key_to_create.first;
    const CreateKeyData& key_data = key_to_create.second;

    // If the key to create is symmetric, derive a symmetric key from the
    // Diffie-Hellman handshake secrect using HKDF. The CryptAuth v2
    // Enrollment protocol specifies that the salt should be "CryptAuth
    // Enrollment" and the info should be the key handle. This process is
    // synchronous, unlike SecureMessageDelegate calls.
    if (IsValidSymmetricKeyType(key_data.type)) {
      // Without a Diffie-Hellman secret, no symmetric keys can be created.
      if (!dh_handshake_secret) {
        OnSymmetricKeyDerived(bundle_name, std::string() /* symmetric_key */);
        continue;
      }

      std::string derived_symmetric_key_material = crypto::HkdfSha256(
          dh_handshake_secret->symmetric_key(),
          kCryptAuthSymmetricKeyDerivationSalt,
          CryptAuthKeyBundle::KeyBundleNameEnumToString(bundle_name),
          NumBytesForSymmetricKeyType(key_data.type));

      OnSymmetricKeyDerived(bundle_name, derived_symmetric_key_material);

      continue;
    }

    DCHECK(IsValidAsymmetricKeyType(key_data.type));

    // If the key material was explicitly set in CreateKeyData, bypass the
    // standard key creation.
    if (key_data.public_key) {
      DCHECK(key_data.private_key);
      OnAsymmetricKeyPairGenerated(bundle_name, *key_data.public_key,
                                   *key_data.private_key);
      continue;
    }

    secure_message_delegate_->GenerateKeyPair(
        base::BindOnce(&CryptAuthKeyCreatorImpl::OnAsymmetricKeyPairGenerated,
                       base::Unretained(this), key_to_create.first));
  }
}

void CryptAuthKeyCreatorImpl::OnAsymmetricKeyPairGenerated(
    CryptAuthKeyBundle::Name bundle_name,
    const std::string& public_key,
    const std::string& private_key) {
  DCHECK(num_keys_to_create_ > 0);
  if (public_key.empty() || private_key.empty()) {
    // Use null CryptAuthKey if key generation failed.
    new_keys_.insert_or_assign(bundle_name, std::nullopt);
  } else {
    const CryptAuthKeyCreator::CreateKeyData& create_key_data =
        keys_to_create_.find(bundle_name)->second;

    new_keys_.insert_or_assign(
        bundle_name,
        CryptAuthKey(public_key, private_key, create_key_data.status,
                     create_key_data.type, create_key_data.handle));
  }

  --num_keys_to_create_;
  if (num_keys_to_create_ == 0)
    std::move(create_keys_callback_).Run(new_keys_, client_ephemeral_dh_);
}

void CryptAuthKeyCreatorImpl::OnSymmetricKeyDerived(
    CryptAuthKeyBundle::Name bundle_name,
    const std::string& symmetric_key) {
  DCHECK(num_keys_to_create_ > 0);
  if (symmetric_key.empty()) {
    // Use null CryptAuthKey if key generation failed.
    new_keys_.insert_or_assign(bundle_name, std::nullopt);
  } else {
    const CryptAuthKeyCreator::CreateKeyData& create_key_data =
        keys_to_create_.find(bundle_name)->second;

    new_keys_.insert_or_assign(
        bundle_name,
        CryptAuthKey(symmetric_key, create_key_data.status,
                     create_key_data.type, create_key_data.handle));
  }

  --num_keys_to_create_;
  if (num_keys_to_create_ == 0)
    std::move(create_keys_callback_).Run(new_keys_, client_ephemeral_dh_);
}

}  // namespace device_sync

}  // namespace ash