chromium/device/fido/enclave/icloud_recovery_key_mac_unittest.mm

// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "device/fido/enclave/icloud_recovery_key_mac.h"

#import <CoreFoundation/CoreFoundation.h>
#import <Foundation/Foundation.h>
#import <Security/Security.h>

#include <memory>
#include <vector>

#include "base/apple/scoped_cftyperef.h"
#include "base/functional/callback.h"
#include "base/run_loop.h"
#include "base/test/bind.h"
#include "base/test/task_environment.h"
#include "components/trusted_vault/securebox.h"
#include "crypto/apple_keychain_v2.h"
#include "crypto/fake_apple_keychain_v2.h"
#include "crypto/scoped_fake_apple_keychain_v2.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace device::enclave {

namespace {

constexpr char kKeychainAccessGroup[] = "keychain-access-group";
constexpr uint8_t kHeader[]{'h', 'e', 'a', 'd', 'e', 'r'};
constexpr uint8_t kPlaintext[]{'h', 'e', 'l', 'l', 'o'};

class ICloudRecoveryKeyTest : public testing::Test {
 public:
  std::unique_ptr<ICloudRecoveryKey> CreateKey() {
    std::unique_ptr<ICloudRecoveryKey> new_key;
    base::RunLoop run_loop;
    ICloudRecoveryKey::Create(
        base::BindLambdaForTesting([&](std::unique_ptr<ICloudRecoveryKey> ret) {
          new_key = std::move(ret);
          run_loop.Quit();
        }),
        kKeychainAccessGroup);
    run_loop.Run();
    return new_key;
  }

 protected:
  crypto::ScopedFakeAppleKeychainV2 fake_keychain_{kKeychainAccessGroup};
  base::test::TaskEnvironment task_environment_;
};

TEST_F(ICloudRecoveryKeyTest, EndToEnd) {
  std::unique_ptr<ICloudRecoveryKey> key = CreateKey();
  std::optional<std::vector<uint8_t>> encrypted =
      key->key()->public_key().Encrypt(base::span<uint8_t>(), kHeader,
                                       kPlaintext);
  ASSERT_TRUE(encrypted);

  std::unique_ptr<ICloudRecoveryKey> retrieved;
  base::RunLoop run_loop;
  ICloudRecoveryKey::Retrieve(
      base::BindLambdaForTesting(
          [&](std::vector<std::unique_ptr<ICloudRecoveryKey>> ret) {
            ASSERT_EQ(ret.size(), 1u);
            retrieved = std::move(ret.at(0));
            run_loop.Quit();
          }),
      kKeychainAccessGroup);
  run_loop.Run();

  std::optional<std::vector<uint8_t>> decrypted =
      retrieved->key()->private_key().Decrypt(base::span<uint8_t>(), kHeader,
                                              *encrypted);
  ASSERT_TRUE(decrypted);
  EXPECT_EQ(base::span<const uint8_t>(*decrypted),
            base::span<const uint8_t>(kPlaintext));
}

TEST_F(ICloudRecoveryKeyTest, CreateAndRetrieve) {
  std::unique_ptr<ICloudRecoveryKey> key1 = CreateKey();
  ASSERT_TRUE(key1);
  ASSERT_TRUE(key1->key());

  std::unique_ptr<ICloudRecoveryKey> key2 = CreateKey();
  ASSERT_TRUE(key2);
  ASSERT_TRUE(key2->key());

  std::optional<std::vector<std::unique_ptr<ICloudRecoveryKey>>> keys;
  base::RunLoop run_loop;
  ICloudRecoveryKey::Retrieve(
      base::BindLambdaForTesting(
          [&](std::vector<std::unique_ptr<ICloudRecoveryKey>> ret) {
            keys = std::move(ret);
            run_loop.Quit();
          }),
      kKeychainAccessGroup);
  run_loop.Run();

  ASSERT_TRUE(keys);
  ASSERT_EQ(keys->size(), 2u);

  auto key1it = std::ranges::find_if(
      *keys, [&](const std::unique_ptr<ICloudRecoveryKey>& key) {
        return key->id() == key1->id();
      });
  EXPECT_NE(key1it, keys->end());

  auto key2it = std::ranges::find_if(
      *keys, [&](const std::unique_ptr<ICloudRecoveryKey>& key) {
        return key->id() == key2->id();
      });
  EXPECT_NE(key2it, keys->end());
}

TEST_F(ICloudRecoveryKeyTest, CreateKeychainError) {
  // Force a keychain error by setting the wrong access group.
  std::unique_ptr<ICloudRecoveryKey> keys;
  base::RunLoop run_loop;
  ICloudRecoveryKey::Create(
      base::BindLambdaForTesting([&](std::unique_ptr<ICloudRecoveryKey> ret) {
        keys = std::move(ret);
        run_loop.Quit();
      }),
      "wrong keychain group");
  run_loop.Run();
  EXPECT_FALSE(keys);
}

TEST_F(ICloudRecoveryKeyTest, RetrieveKeychainError) {
  // Force a keychain error by setting the wrong access group.
  std::optional<std::vector<std::unique_ptr<ICloudRecoveryKey>>> keys;
  base::RunLoop run_loop;
  ICloudRecoveryKey::Retrieve(
      base::BindLambdaForTesting(
          [&](std::vector<std::unique_ptr<ICloudRecoveryKey>> ret) {
            keys = std::move(ret);
            run_loop.Quit();
          }),
      "wrong keychain group");
  run_loop.Run();
  EXPECT_TRUE(keys->empty());
}

TEST_F(ICloudRecoveryKeyTest, RetrieveEmpty) {
  std::optional<std::vector<std::unique_ptr<ICloudRecoveryKey>>> keys;
  base::RunLoop run_loop;
  ICloudRecoveryKey::Retrieve(
      base::BindLambdaForTesting(
          [&](std::vector<std::unique_ptr<ICloudRecoveryKey>> ret) {
            keys = std::move(ret);
            run_loop.Quit();
          }),
      kKeychainAccessGroup);
  run_loop.Run();
  EXPECT_TRUE(keys->empty());
}

TEST_F(ICloudRecoveryKeyTest, RetrieveCorrupted) {
  std::unique_ptr<ICloudRecoveryKey> key1 = CreateKey();
  ASSERT_TRUE(key1);
  ASSERT_TRUE(key1->key());

  base::apple::ScopedCFTypeRef<CFDataRef> corrupted_key(
      CFDataCreate(kCFAllocatorDefault, nullptr, 0));
  CFDictionarySetValue(const_cast<CFMutableDictionaryRef>(
                           fake_keychain_.keychain()->items().at(0).get()),
                       kSecValueData, corrupted_key.get());

  std::optional<std::vector<std::unique_ptr<ICloudRecoveryKey>>> keys;
  base::RunLoop run_loop;
  ICloudRecoveryKey::Retrieve(
      base::BindLambdaForTesting(
          [&](std::vector<std::unique_ptr<ICloudRecoveryKey>> ret) {
            keys = std::move(ret);
            run_loop.Quit();
          }),
      kKeychainAccessGroup);
  run_loop.Run();
  EXPECT_TRUE(keys->empty());
}

}  // namespace

}  // namespace device::enclave