chromium/components/os_crypt/async/browser/dpapi_key_provider_unittest.cc

// 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 "components/os_crypt/async/browser/dpapi_key_provider.h"

#include <optional>
#include <utility>
#include <vector>

#include "base/logging.h"
#include "base/no_destructor.h"
#include "base/rand_util.h"
#include "base/run_loop.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/task_environment.h"
#include "components/os_crypt/async/browser/os_crypt_async.h"
#include "components/os_crypt/async/common/algorithm.mojom.h"
#include "components/os_crypt/async/common/encryptor.h"
#include "components/os_crypt/sync/os_crypt.h"
#include "components/prefs/testing_pref_service.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace os_crypt_async {

// This class tests that DPAPIKeyProvider is forwards and backwards
// compatible with OSCrypt.
class DPAPIKeyProviderTestBase : public ::testing::Test {
 protected:
  void TearDown() override {
    histograms_.ExpectBucketCount("OSCrypt.DPAPIProvider.Status",
                                  expected_histogram_, 1);
  }

  Encryptor GetInstanceSync(
      OSCryptAsync& factory,
      Encryptor::Option option = Encryptor::Option::kNone) {
    base::RunLoop run_loop;
    std::optional<Encryptor> encryptor;
    auto sub =
        factory.GetInstance(base::BindLambdaForTesting(
                                [&](Encryptor encryptor_param, bool success) {
                                  EXPECT_TRUE(success);
                                  encryptor.emplace(std::move(encryptor_param));
                                  run_loop.Quit();
                                }),
                            option);
    run_loop.Run();
    return std::move(*encryptor);
  }

  Encryptor GetInstanceWithDPAPI() {
    std::vector<std::pair<size_t, std::unique_ptr<KeyProvider>>> providers;
    providers.emplace_back(std::make_pair(
        /*precedence=*/10u, std::make_unique<DPAPIKeyProvider>(&prefs_)));
    OSCryptAsync factory(std::move(providers));
    return GetInstanceSync(factory);
  }

  TestingPrefServiceSimple prefs_;
  DPAPIKeyProvider::KeyStatus expected_histogram_ =
      DPAPIKeyProvider::KeyStatus::kSuccess;

 private:
  base::HistogramTester histograms_;
  base::test::TaskEnvironment task_environment_;
};

class DPAPIKeyProviderTest : public DPAPIKeyProviderTestBase {
 protected:
  void SetUp() override {
    OSCrypt::RegisterLocalPrefs(prefs_.registry());
    OSCrypt::Init(&prefs_);
  }

  void TearDown() override {
    OSCrypt::ResetStateForTesting();
    DPAPIKeyProviderTestBase::TearDown();
  }
};

TEST_F(DPAPIKeyProviderTest, Basic) {
  Encryptor encryptor = GetInstanceWithDPAPI();

  ASSERT_TRUE(encryptor.IsEncryptionAvailable());
  ASSERT_TRUE(encryptor.IsDecryptionAvailable());

  std::string plaintext = "secrets";
  std::string ciphertext;
  ASSERT_TRUE(encryptor.EncryptString(plaintext, &ciphertext));

  std::string decrypted;
  EXPECT_TRUE(encryptor.DecryptString(ciphertext, &decrypted));
  EXPECT_EQ(plaintext, decrypted);
}

TEST_F(DPAPIKeyProviderTest, DecryptOld) {
  Encryptor encryptor = GetInstanceWithDPAPI();

  std::string plaintext = "secrets";
  std::string ciphertext;
  ASSERT_TRUE(OSCrypt::EncryptString(plaintext, &ciphertext));

  std::string decrypted;
  EXPECT_TRUE(encryptor.DecryptString(ciphertext, &decrypted));
  EXPECT_EQ(plaintext, decrypted);
}

TEST_F(DPAPIKeyProviderTest, EncryptForOld) {
  Encryptor encryptor = GetInstanceWithDPAPI();

  std::string plaintext = "secrets";
  std::string ciphertext;
  ASSERT_TRUE(encryptor.EncryptString(plaintext, &ciphertext));

  std::string decrypted;
  EXPECT_TRUE(OSCrypt::DecryptString(ciphertext, &decrypted));
  EXPECT_EQ(plaintext, decrypted);
}

// Very small Key Provider that provides a random key.
class RandomKeyProvider : public KeyProvider {
 private:
  void GetKey(KeyCallback callback) final {
    std::vector<uint8_t> key(Encryptor::Key::kAES256GCMKeySize);
    base::RandBytes(key);
    std::move(callback).Run("_",
                            Encryptor::Key(key, mojom::Algorithm::kAES256GCM));
  }

  bool UseForEncryption() final { return true; }
  bool IsCompatibleWithOsCryptSync() final { return false; }
};

TEST_F(DPAPIKeyProviderTest, EncryptWithOptions) {
  std::vector<std::pair<size_t, std::unique_ptr<KeyProvider>>> providers;
  providers.emplace_back(std::make_pair(
      /*precedence=*/10u, std::make_unique<DPAPIKeyProvider>(&prefs_)));
  // Random Key Provider will take precedence here.
  providers.emplace_back(std::make_pair(/*precedence=*/15u,
                                        std::make_unique<RandomKeyProvider>()));

  OSCryptAsync factory(std::move(providers));
  Encryptor encryptor = GetInstanceSync(factory);
  std::optional<std::vector<uint8_t>> ciphertext;
  {
    // This should use RandomKeyProvider.
    ciphertext = encryptor.EncryptString("secrets");
    ASSERT_TRUE(ciphertext);
    EXPECT_EQ(ciphertext->at(0), '_');
    std::string plaintext;
    // Fail, as it's encrypted with the '_' key provider.
    EXPECT_FALSE(OSCrypt::DecryptString(
        std::string(ciphertext->begin(), ciphertext->end()), &plaintext));

    // Encryptor should be able to decrypt.
    const auto decrypted = encryptor.DecryptData(*ciphertext);
    EXPECT_TRUE(decrypted);
    EXPECT_EQ(*decrypted, "secrets");
  }
  {
    // Now, obtain a second encryptor but with the kEncryptSyncCompat option.
    Encryptor encryptor_with_option =
        GetInstanceSync(factory, Encryptor::Option::kEncryptSyncCompat);
    // This should now encrypt with DPAPI key provider, compatible with OSCrypt
    // sync, but still contain both keys.
    const auto second_ciphertext =
        encryptor_with_option.EncryptString("moresecrets");
    ASSERT_TRUE(second_ciphertext);
    std::string plaintext;

    // First, test a decrypt using OSCrypt sync works.
    ASSERT_TRUE(OSCrypt::DecryptString(
        std::string(second_ciphertext->begin(), second_ciphertext->end()),
        &plaintext));
    EXPECT_EQ(plaintext, "moresecrets");

    // Now test both encryptors can decrypt both sets of ciphertext, regardless
    // of the option.
    {
      // First Encryptor with first ciphertext.
      const auto decrypted = encryptor.DecryptData(*ciphertext);
      ASSERT_TRUE(decrypted);
      EXPECT_EQ(*decrypted, "secrets");
    }
    {
      // First Encryptor with second ciphertext.
      const auto decrypted = encryptor.DecryptData(*second_ciphertext);
      ASSERT_TRUE(decrypted);
      EXPECT_EQ(*decrypted, "moresecrets");
    }
    {
      // Second encryptor (with option) with first ciphertext.
      const auto decrypted = encryptor_with_option.DecryptData(*ciphertext);
      ASSERT_TRUE(decrypted);
      EXPECT_EQ(*decrypted, "secrets");
    }
    {
      // Second encryptor (with option) with second ciphertext.
      const auto decrypted =
          encryptor_with_option.DecryptData(*second_ciphertext);
      ASSERT_TRUE(decrypted);
      EXPECT_EQ(*decrypted, "moresecrets");
    }
  }
}
// Only test a few scenarios here, just to verify the error histogram is always
// logged.
TEST_F(DPAPIKeyProviderTest, OSCryptNotInit) {
  prefs_.ClearPref("os_crypt.encrypted_key");
  Encryptor encryptor = GetInstanceWithDPAPI();
  // Encryption is available because OSCrypt sync already initialized before the
  // test fixture invalidated the key for the DPAPI Key Provider, and so while
  // the encryptor has no keyring, it delegates successfully to OSCrypt sync.
  EXPECT_TRUE(encryptor.IsEncryptionAvailable());
  EXPECT_TRUE(encryptor.IsDecryptionAvailable());
  expected_histogram_ = DPAPIKeyProvider::KeyStatus::kKeyNotFound;
}

TEST_F(DPAPIKeyProviderTest, OSCryptBadKeyHeader) {
  prefs_.SetString("os_crypt.encrypted_key", "badkeybadkey");
  Encryptor encryptor = GetInstanceWithDPAPI();
  expected_histogram_ = DPAPIKeyProvider::KeyStatus::kInvalidKeyHeader;
}

TEST_F(DPAPIKeyProviderTestBase, NoOSCrypt) {
  Encryptor encryptor = GetInstanceWithDPAPI();
  // Compare with DPAPIKeyProviderTest.OSCryptNotInit above: Encryption is not
  // available for this test because OSCrypt was never initialized and so the
  // encryptor has no key, and OSCrypt::IsEncryptionAvailable is also returning
  // false.
  EXPECT_FALSE(encryptor.IsEncryptionAvailable());
  EXPECT_FALSE(encryptor.IsDecryptionAvailable());
  expected_histogram_ = DPAPIKeyProvider::KeyStatus::kKeyNotFound;
}

}  // namespace os_crypt_async