chromium/chromeos/ash/services/device_sync/cryptauth_key_creator_impl_unittest.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 <memory>
#include <optional>
#include <string>
#include <utility>

#include "base/containers/flat_map.h"
#include "base/functional/bind.h"
#include "chromeos/ash/components/multidevice/fake_secure_message_delegate.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/cryptauth_key.h"
#include "chromeos/ash/services/device_sync/cryptauth_key_bundle.h"
#include "chromeos/ash/services/device_sync/cryptauth_key_creator.h"
#include "chromeos/ash/services/device_sync/proto/cryptauth_common.pb.h"
#include "crypto/hkdf.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace ash {

namespace device_sync {

namespace {

const char kFakeServerEphemeralDhPublicKeyMaterial[] = "server_ephemeral_dh";
const char kFakeClientEphemeralDhPublicKeyMaterial[] = "client_ephemeral_dh";
const char kFakeAsymmetricKeyHandle[] = "asymmetric_key_handle";
const char kFakePublicKeyMaterial[] = "public_key";
const char kFakeSymmetricKeyHandle[] = "symmetric_key_handle";
const char kFakeProvidedPublicKeyMaterial[] = "provided_public_key";
const char kFakeProvidedPrivateKeyMaterial[] = "provided_private_key";

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

class DeviceSyncCryptAuthKeyCreatorImplTest : public testing::Test {
 public:
  DeviceSyncCryptAuthKeyCreatorImplTest(
      const DeviceSyncCryptAuthKeyCreatorImplTest&) = delete;
  DeviceSyncCryptAuthKeyCreatorImplTest& operator=(
      const DeviceSyncCryptAuthKeyCreatorImplTest&) = delete;

 protected:
  DeviceSyncCryptAuthKeyCreatorImplTest()
      : fake_secure_message_delegate_factory_(
            std::make_unique<multidevice::FakeSecureMessageDelegateFactory>()),
        fake_server_ephemeral_dh_(CryptAuthKey(
            kFakeServerEphemeralDhPublicKeyMaterial,
            fake_secure_message_delegate()->GetPrivateKeyForPublicKey(
                kFakeServerEphemeralDhPublicKeyMaterial),
            CryptAuthKey::Status::kActive,
            cryptauthv2::KeyType::P256)) {}

  ~DeviceSyncCryptAuthKeyCreatorImplTest() override = default;

  void SetUp() override {
    multidevice::SecureMessageDelegateImpl::Factory::SetFactoryForTesting(
        fake_secure_message_delegate_factory_.get());

    key_creator_ = CryptAuthKeyCreatorImpl::Factory::Create();
  }

  void TearDown() override {
    multidevice::SecureMessageDelegateImpl::Factory::SetFactoryForTesting(
        nullptr);
  }

  void CallCreateKeys(
      const base::flat_map<CryptAuthKeyBundle::Name,
                           CryptAuthKeyCreator::CreateKeyData>& keys_to_create,
      const std::optional<CryptAuthKey>& server_ephemeral_dh) {
    key_creator_->CreateKeys(
        keys_to_create, server_ephemeral_dh,
        base::BindOnce(&DeviceSyncCryptAuthKeyCreatorImplTest::OnKeysCreated,
                       base::Unretained(this)));
  }

  void VerifyKeyCreation(
      const base::flat_map<CryptAuthKeyBundle::Name,
                           std::optional<CryptAuthKey>>& expected_new_keys,
      const std::optional<CryptAuthKey>& expected_client_ephemeral_dh) {
    EXPECT_TRUE(new_keys_);
    EXPECT_TRUE(client_ephemeral_dh_);
    EXPECT_EQ(expected_new_keys, *new_keys_);
    EXPECT_EQ(expected_client_ephemeral_dh, *client_ephemeral_dh_);
  }

  std::string DeriveSecret(const CryptAuthKey& server_ephemeral_dh,
                           const CryptAuthKey& client_ephemeral_dh) {
    std::string derived_key;
    fake_secure_message_delegate()->DeriveKey(
        client_ephemeral_dh.private_key(), server_ephemeral_dh.public_key(),
        base::BindOnce([](std::string* derived_key,
                          const std::string& key) { *derived_key = key; },
                       &derived_key));
    return derived_key;
  }

  CryptAuthKey DeriveSymmetricKey(
      const CryptAuthKeyBundle::Name& bundle_name,
      const CryptAuthKeyCreator::CreateKeyData& key_to_create,
      const CryptAuthKey& client_ephemeral_dh) {
    std::string expected_handshake_secret =
        DeriveSecret(fake_server_ephemeral_dh_, client_ephemeral_dh);

    std::string expected_symmetric_key_material = crypto::HkdfSha256(
        expected_handshake_secret, kCryptAuthSymmetricKeyDerivationSalt,
        CryptAuthKeyBundle::KeyBundleNameEnumToString(bundle_name),
        NumBytesForSymmetricKeyType(key_to_create.type));

    return CryptAuthKey(expected_symmetric_key_material, key_to_create.status,
                        key_to_create.type, key_to_create.handle);
  }

  multidevice::FakeSecureMessageDelegate* fake_secure_message_delegate() {
    return fake_secure_message_delegate_factory_->instance();
  }

  const CryptAuthKey& fake_server_ephemeral_dh() {
    return fake_server_ephemeral_dh_;
  }

 private:
  void OnKeysCreated(
      const base::flat_map<CryptAuthKeyBundle::Name,
                           std::optional<CryptAuthKey>>& new_keys,
      const std::optional<CryptAuthKey>& client_ephemeral_dh) {
    new_keys_ = new_keys;
    client_ephemeral_dh_ = client_ephemeral_dh;
  }

  std::unique_ptr<multidevice::FakeSecureMessageDelegateFactory>
      fake_secure_message_delegate_factory_;
  std::unique_ptr<CryptAuthKeyCreator> key_creator_;

  CryptAuthKey fake_server_ephemeral_dh_;

  // A null value indicates that OnKeysCreated() was not called.
  std::optional<
      base::flat_map<CryptAuthKeyBundle::Name, std::optional<CryptAuthKey>>>
      new_keys_;
  std::optional<std::optional<CryptAuthKey>> client_ephemeral_dh_;
};

TEST_F(DeviceSyncCryptAuthKeyCreatorImplTest, AsymmetricKeyCreation) {
  base::flat_map<CryptAuthKeyBundle::Name, CryptAuthKeyCreator::CreateKeyData>
      keys_to_create = {
          {CryptAuthKeyBundle::Name::kUserKeyPair,
           CryptAuthKeyCreator::CreateKeyData(CryptAuthKey::Status::kActive,
                                              cryptauthv2::KeyType::P256,
                                              kFakeAsymmetricKeyHandle)}};

  CryptAuthKey expected_asymmetric_key(
      kFakePublicKeyMaterial,
      fake_secure_message_delegate()->GetPrivateKeyForPublicKey(
          kFakePublicKeyMaterial),
      CryptAuthKey::Status::kActive, cryptauthv2::KeyType::P256,
      kFakeAsymmetricKeyHandle);

  fake_secure_message_delegate()->set_next_public_key(kFakePublicKeyMaterial);

  CallCreateKeys(keys_to_create, std::nullopt /* fake_server_ephemeral_dh */);
  VerifyKeyCreation(
      {{CryptAuthKeyBundle::Name::kUserKeyPair,
        std::make_optional(expected_asymmetric_key)}} /* expected_new_keys */,
      std::nullopt /* expected_client_ephemeral_dh */);
}

TEST_F(DeviceSyncCryptAuthKeyCreatorImplTest, SymmetricKeyCreation) {
  CryptAuthKeyCreator::CreateKeyData symmetric_key_to_create(
      CryptAuthKey::Status::kActive, cryptauthv2::KeyType::RAW256,
      kFakeSymmetricKeyHandle);

  base::flat_map<CryptAuthKeyBundle::Name, CryptAuthKeyCreator::CreateKeyData>
      keys_to_create = {{CryptAuthKeyBundle::Name::kLegacyAuthzenKey,
                         symmetric_key_to_create}};

  CryptAuthKey expected_client_ephemeral_dh(
      kFakeClientEphemeralDhPublicKeyMaterial,
      fake_secure_message_delegate()->GetPrivateKeyForPublicKey(
          kFakeClientEphemeralDhPublicKeyMaterial),
      CryptAuthKey::Status::kActive, cryptauthv2::KeyType::P256);

  CryptAuthKey expected_symmetric_key =
      DeriveSymmetricKey(CryptAuthKeyBundle::Name::kLegacyAuthzenKey,
                         symmetric_key_to_create, expected_client_ephemeral_dh);

  fake_secure_message_delegate()->set_next_public_key(
      kFakeClientEphemeralDhPublicKeyMaterial);

  CallCreateKeys(keys_to_create, fake_server_ephemeral_dh());
  VerifyKeyCreation(
      {{CryptAuthKeyBundle::Name::kLegacyAuthzenKey,
        std::make_optional(expected_symmetric_key)}} /* expected_new_keys */,
      expected_client_ephemeral_dh);
}

TEST_F(DeviceSyncCryptAuthKeyCreatorImplTest,
       MultipleKeyCreation_KeyMaterialProvidedForAsymmetricKey) {
  CryptAuthKeyCreator::CreateKeyData symmetric_key_to_create(
      CryptAuthKey::Status::kActive, cryptauthv2::KeyType::RAW256,
      kFakeSymmetricKeyHandle);

  base::flat_map<CryptAuthKeyBundle::Name, CryptAuthKeyCreator::CreateKeyData>
      keys_to_create = {
          {CryptAuthKeyBundle::Name::kUserKeyPair,
           CryptAuthKeyCreator::CreateKeyData(
               CryptAuthKey::Status::kActive, cryptauthv2::KeyType::P256,
               kFakeAsymmetricKeyHandle, kFakeProvidedPublicKeyMaterial,
               kFakeProvidedPrivateKeyMaterial)},
          {CryptAuthKeyBundle::Name::kLegacyAuthzenKey,
           symmetric_key_to_create}};

  CryptAuthKey expected_client_ephemeral_dh(
      kFakeClientEphemeralDhPublicKeyMaterial,
      fake_secure_message_delegate()->GetPrivateKeyForPublicKey(
          kFakeClientEphemeralDhPublicKeyMaterial),
      CryptAuthKey::Status::kActive, cryptauthv2::KeyType::P256);

  CryptAuthKey expected_asymmetric_key(
      kFakeProvidedPublicKeyMaterial, kFakeProvidedPrivateKeyMaterial,
      CryptAuthKey::Status::kActive, cryptauthv2::KeyType::P256,
      kFakeAsymmetricKeyHandle);

  CryptAuthKey expected_symmetric_key =
      DeriveSymmetricKey(CryptAuthKeyBundle::Name::kLegacyAuthzenKey,
                         symmetric_key_to_create, expected_client_ephemeral_dh);

  // There is no need to generate an asymmetric key for kUserKeyPair since we
  // passed in the key material to CreateKeyData.
  fake_secure_message_delegate()->set_next_public_key(
      kFakeClientEphemeralDhPublicKeyMaterial);

  CallCreateKeys(keys_to_create, fake_server_ephemeral_dh());
  VerifyKeyCreation(
      {{CryptAuthKeyBundle::Name::kUserKeyPair,
        std::make_optional(expected_asymmetric_key)},
       {CryptAuthKeyBundle::Name::kLegacyAuthzenKey,
        std::make_optional(expected_symmetric_key)}} /* expected_new_keys */,
      expected_client_ephemeral_dh);
}

TEST_F(DeviceSyncCryptAuthKeyCreatorImplTest, AsymmetricKeyCreation_Failure) {
  base::flat_map<CryptAuthKeyBundle::Name, CryptAuthKeyCreator::CreateKeyData>
      keys_to_create = {
          {CryptAuthKeyBundle::Name::kUserKeyPair,
           CryptAuthKeyCreator::CreateKeyData(CryptAuthKey::Status::kActive,
                                              cryptauthv2::KeyType::P256,
                                              kFakeAsymmetricKeyHandle)}};

  // An empty key string returned by SecureMessage is considered a failure.
  fake_secure_message_delegate()->set_next_public_key(std::string());

  CallCreateKeys(keys_to_create, std::nullopt /* fake_server_ephemeral_dh */);
  VerifyKeyCreation({{CryptAuthKeyBundle::Name::kUserKeyPair,
                      std::nullopt}} /* expected_new_keys */,
                    std::nullopt /* expected_client_ephemeral_dh */);
}

TEST_F(DeviceSyncCryptAuthKeyCreatorImplTest,
       SymmetricKeyCreation_FailToCreateClientEphemeralKeyPair) {
  CryptAuthKeyCreator::CreateKeyData symmetric_key_to_create(
      CryptAuthKey::Status::kActive, cryptauthv2::KeyType::RAW256,
      kFakeSymmetricKeyHandle);

  base::flat_map<CryptAuthKeyBundle::Name, CryptAuthKeyCreator::CreateKeyData>
      keys_to_create = {{CryptAuthKeyBundle::Name::kLegacyAuthzenKey,
                         symmetric_key_to_create}};

  // Fail to create the client's ephemeral Diffie-Hellman key. An empty key
  // string returned by SecureMessage is considered a failure.
  fake_secure_message_delegate()->set_next_public_key(std::string());

  CallCreateKeys(keys_to_create, fake_server_ephemeral_dh());
  VerifyKeyCreation({{CryptAuthKeyBundle::Name::kLegacyAuthzenKey,
                      std::nullopt}} /* expected_new_keys */,
                    std::nullopt /* expected_client_ephemeral_dh */);
}

}  // namespace device_sync

}  // namespace ash