blob: 6337d6ae020479e36ed225f2d2d6e136d19f492a [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/os_crypt/app_bound_encryption_win.h"
#include <optional>
#include <string>
#include "base/command_line.h"
#include "base/containers/span.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/metrics/histogram_base.h"
#include "base/metrics/histogram_samples.h"
#include "base/metrics/statistics_recorder.h"
#include "base/path_service.h"
#include "base/process/launch.h"
#include "base/process/process_info.h"
#include "base/strings/strcat.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "base/threading/thread_restrictions.h"
#include "base/types/expected.h"
#include "build/branding_buildflags.h"
#include "build/build_config.h"
#include "chrome/browser/browser_features.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/os_crypt/app_bound_encryption_provider_win.h"
#include "chrome/browser/os_crypt/test_support.h"
#include "chrome/browser/policy/chrome_browser_policy_connector.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/common/chrome_paths_internal.h"
#include "chrome/elevation_service/elevator.h"
#include "chrome/install_static/test/scoped_install_details.h"
#include "chrome/installer/util/install_service_work_item.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/windows_services/service_program/test_support/scoped_log_grabber.h"
#include "components/os_crypt/async/browser/os_crypt_async.h"
#include "components/os_crypt/sync/os_crypt.h"
#include "components/policy/core/common/mock_configuration_policy_provider.h"
#include "components/policy/core/common/policy_map.h"
#include "components/policy/policy_constants.h"
#include "components/prefs/mock_pref_change_callback.h"
#include "components/prefs/pref_store.h"
#include "components/prefs/testing_pref_service.h"
#include "content/public/test/browser_test.h"
#include "testing/gmock/include/gmock/gmock.h"
using testing::_;
namespace os_crypt {
namespace {
void WaitForHistogram(const std::string& histogram_name) {
// Continue if histogram was already recorded.
if (base::StatisticsRecorder::FindHistogram(histogram_name)) {
return;
}
// Else, wait until the histogram is recorded.
base::RunLoop run_loop;
auto histogram_observer =
std::make_unique<base::StatisticsRecorder::ScopedHistogramSampleObserver>(
histogram_name, run_loop.QuitClosure());
run_loop.Run();
}
os_crypt_async::Encryptor GetInstanceSync(
os_crypt_async::OSCryptAsync& factory,
os_crypt_async::Encryptor::Option option =
os_crypt_async::Encryptor::Option::kNone) {
base::test::TestFuture<os_crypt_async::Encryptor> future;
factory.GetInstance(future.GetCallback(), option);
return future.Take();
}
} // namespace
class AppBoundEncryptionWinTest : public InProcessBrowserTest {
public:
AppBoundEncryptionWinTest()
: scoped_install_details_(std::make_unique<FakeInstallDetails>()) {}
protected:
void SetUp() override {
if (base::GetCurrentProcessIntegrityLevel() != base::HIGH_INTEGRITY) {
GTEST_SKIP() << "Elevation is required for this test.";
}
#if defined(ARCH_CPU_32_BITS)
// Flaky on 32-bit win-rel-ready bot. See crbug.com/430106357.
GTEST_SKIP() << "Temporarily disabled on 32-bit. See crbug.com/430106357.";
#else
if (should_install_service_) {
maybe_uninstall_service_ = InstallService(log_grabber_);
EXPECT_TRUE(maybe_uninstall_service_.has_value());
}
// Browser tests use a custom user data dir, which would normally result in
// App-Bound encryption being disabled with
// `SupportLevel::kNotUsingDefaultUserDataDir`, so this call forces the
// non-standard testing data dir to be considered a default one, except if
// a test is explicitly requesting to use a non-standard one see
// `AppBoundEncryptionWinTestWithUserDataDir`.
chrome::SetUsingDefaultUserDataDirectoryForTesting(
set_default_user_data_dir_);
InProcessBrowserTest::SetUp();
#endif // defined(ARCH_CPU_32_BITS)
}
void TearDown() override { maybe_uninstall_service_.reset(); }
// Used by multi-stage tests to persist data between each part of the test.
void StoreData(base::span<const uint8_t> data) {
base::ScopedAllowBlockingForTesting allow_blocking;
const auto data_path =
browser()->profile()->GetPath().Append(FILE_PATH_LITERAL("TestData"));
ASSERT_FALSE(base::PathExists(data_path));
EXPECT_TRUE(base::WriteFile(data_path, data));
}
std::optional<std::vector<uint8_t>> RetrieveData() {
base::ScopedAllowBlockingForTesting allow_blocking;
return base::ReadFileToBytes(
browser()->profile()->GetPath().Append(FILE_PATH_LITERAL("TestData")));
}
static bool IsPreTest() {
const std::string_view test_name(
::testing::UnitTest::GetInstance()->current_test_info()->name());
return test_name.find("PRE_") != std::string_view::npos;
}
base::HistogramTester histogram_tester_;
std::optional<base::ScopedClosureRunner> maybe_uninstall_service_;
ScopedLogGrabber log_grabber_;
bool set_default_user_data_dir_ = true;
bool should_install_service_ = true;
private:
install_static::ScopedInstallDetails scoped_install_details_;
};
// Test App-Bound is supported for tests.
IN_PROC_BROWSER_TEST_F(AppBoundEncryptionWinTest, Supported) {
EXPECT_EQ(SupportLevel::kSupported, GetAppBoundEncryptionSupportLevel(
g_browser_process->local_state()));
}
// Test the basic interface to Encrypt and Decrypt data.
IN_PROC_BROWSER_TEST_F(AppBoundEncryptionWinTest, EncryptDecrypt) {
ASSERT_TRUE(install_static::IsSystemInstall());
const std::string plaintext("plaintext");
std::string ciphertext;
DWORD last_error;
HRESULT hr =
EncryptAppBoundString(ProtectionLevel::PROTECTION_PATH_VALIDATION,
plaintext, ciphertext, last_error);
ASSERT_HRESULT_SUCCEEDED(hr);
std::string returned_plaintext;
std::optional<std::string> maybe_new_ciphertext;
hr = DecryptAppBoundString(ciphertext, returned_plaintext,
ProtectionLevel::PROTECTION_PATH_VALIDATION,
maybe_new_ciphertext, last_error);
EXPECT_FALSE(maybe_new_ciphertext);
ASSERT_HRESULT_SUCCEEDED(hr);
EXPECT_EQ(plaintext, returned_plaintext);
}
// Test the basic interface to Encrypt and Decrypt data.
IN_PROC_BROWSER_TEST_F(AppBoundEncryptionWinTest, EncryptDecryptWithFlags) {
ASSERT_TRUE(install_static::IsSystemInstall());
const std::string plaintext("plaintext");
std::string ciphertext;
{
DWORD last_error;
elevation_service::EncryptFlags flags{.use_latest_key = false};
HRESULT hr =
EncryptAppBoundString(ProtectionLevel::PROTECTION_PATH_VALIDATION,
plaintext, ciphertext, last_error, &flags);
ASSERT_HRESULT_SUCCEEDED(hr);
EXPECT_EQ(last_error, DWORD{ERROR_SUCCESS});
}
{
std::string returned_plaintext;
std::optional<std::string> maybe_new_ciphertext;
DWORD last_error;
HRESULT hr =
DecryptAppBoundString(ciphertext, returned_plaintext,
ProtectionLevel::PROTECTION_PATH_VALIDATION,
maybe_new_ciphertext, last_error);
ASSERT_HRESULT_SUCCEEDED(hr);
EXPECT_FALSE(maybe_new_ciphertext);
EXPECT_EQ(last_error, DWORD{ERROR_SUCCESS});
EXPECT_EQ(plaintext, returned_plaintext);
}
// Encrypt with new encryption key. There's no real way to know the new key is
// being used because it's a blob of encrypted data.
{
DWORD last_error;
std::string new_ciphertext;
elevation_service::EncryptFlags flags{.use_latest_key = true};
HRESULT hr =
EncryptAppBoundString(ProtectionLevel::PROTECTION_PATH_VALIDATION,
plaintext, new_ciphertext, last_error, &flags);
ASSERT_HRESULT_SUCCEEDED(hr);
EXPECT_EQ(last_error, DWORD{ERROR_SUCCESS});
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
// Ciphertext is bigger for latest key version. Longer ciphertext, more
// security! This new encryption is only available in Chrome branded builds
// so this (fuzzy) check can only happen there.
EXPECT_GT(new_ciphertext.size(), ciphertext.size());
#endif // BUILDFLAG(GOOGLE_CHROME_BRANDING)
// Verify decrypt still works for data encrypted with the latest key.
std::string returned_plaintext;
std::optional<std::string> maybe_new_ciphertext;
hr = DecryptAppBoundString(ciphertext, returned_plaintext,
ProtectionLevel::PROTECTION_PATH_VALIDATION,
maybe_new_ciphertext, last_error);
ASSERT_HRESULT_SUCCEEDED(hr);
EXPECT_FALSE(maybe_new_ciphertext);
EXPECT_EQ(last_error, DWORD{ERROR_SUCCESS});
EXPECT_EQ(plaintext, returned_plaintext);
}
}
// Test that invalid data is handled correctly.
IN_PROC_BROWSER_TEST_F(AppBoundEncryptionWinTest, EncryptDecryptInvalid) {
ASSERT_TRUE(install_static::IsSystemInstall());
std::string ciphertext("invalidciphertext");
std::string returned_plaintext;
DWORD last_error = 0;
std::optional<std::string> maybe_new_ciphertext;
const HRESULT hr =
DecryptAppBoundString(ciphertext, returned_plaintext,
ProtectionLevel::PROTECTION_PATH_VALIDATION,
maybe_new_ciphertext, last_error);
EXPECT_FALSE(maybe_new_ciphertext);
EXPECT_EQ(elevation_service::Elevator::kErrorCouldNotDecryptWithSystemContext,
hr);
}
// These tests verify that the metrics are recorded correctly. The first load of
// browser in the PRE_ test stores the "Test Key" with App-Bound encryption and
// the second stage of the test verifies it can be retrieved successfully.
IN_PROC_BROWSER_TEST_F(AppBoundEncryptionWinTest, PRE_MetricsTest) {
histogram_tester_.ExpectUniqueSample(
"OSCrypt.AppBoundEncryption.SupportLevel", SupportLevel::kSupported, 1);
// These histograms are recorded on a background worker thread, so the test
// needs to wait until this task completes and the histograms are recorded.
WaitForHistogram("OSCrypt.AppBoundProvider.Encrypt.ResultCode");
histogram_tester_.ExpectBucketCount(
"OSCrypt.AppBoundProvider.Encrypt.ResultCode", S_OK, 1);
}
IN_PROC_BROWSER_TEST_F(AppBoundEncryptionWinTest, MetricsTest) {
ASSERT_TRUE(install_static::IsSystemInstall());
// These histograms are recorded on a background worker thread, so the test
// needs to wait until this task completes and the histograms are recorded.
WaitForHistogram("OSCrypt.AppBoundProvider.Decrypt.ResultCode");
histogram_tester_.ExpectBucketCount(
"OSCrypt.AppBoundProvider.Decrypt.ResultCode", S_OK, 1);
}
// Run this test manually to force uninstall the service using
// --gtest_filter=AppBoundEncryptionWinTest.MANUAL_Uninstall --run-manual.
IN_PROC_BROWSER_TEST_F(AppBoundEncryptionWinTest, MANUAL_Uninstall) {}
using AppBoundEncryptionWinTestNoService = InProcessBrowserTest;
IN_PROC_BROWSER_TEST_F(AppBoundEncryptionWinTestNoService, NoService) {
const std::string plaintext("plaintext");
std::string ciphertext;
DWORD last_error;
HRESULT hr =
EncryptAppBoundString(ProtectionLevel::PROTECTION_PATH_VALIDATION,
plaintext, ciphertext, last_error);
EXPECT_EQ(REGDB_E_CLASSNOTREG, hr);
EXPECT_EQ(DWORD{ERROR_GEN_FAILURE}, last_error);
}
// This policy test is here and not in chrome/browser/policy/test as it requires
// a fake system install to correctly show as kSupported, and this testing class
// already has the scaffolding in place to achieve this.
class AppBoundEncryptionWinTestWithPolicyBase
: public AppBoundEncryptionWinTest {
protected:
void MaybeEnablePolicy(std::optional<bool> policy_state) {
policy_provider_.SetDefaultReturns(
/*is_initialization_complete_return=*/true,
/*is_first_policy_load_complete_return=*/true);
policy::PolicyMap values;
if (policy_state.has_value()) {
values.Set(policy::key::kApplicationBoundEncryptionEnabled,
policy::POLICY_LEVEL_MANDATORY, policy::POLICY_SCOPE_MACHINE,
policy::POLICY_SOURCE_CLOUD, base::Value(*policy_state),
nullptr);
}
policy_provider_.UpdateChromePolicy(values);
policy::BrowserPolicyConnector::SetPolicyProviderForTesting(
&policy_provider_);
}
testing::NiceMock<policy::MockConfigurationPolicyProvider> policy_provider_;
};
class AppBoundEncryptionWinTestWithPolicy
: public AppBoundEncryptionWinTestWithPolicyBase,
public ::testing::WithParamInterface<
/*policy::key::kApplicationBoundEncryptionEnabled=*/std::optional<
bool>> {
private:
void SetUp() override {
MaybeEnablePolicy(GetParam());
AppBoundEncryptionWinTestWithPolicyBase::SetUp();
}
};
IN_PROC_BROWSER_TEST_P(AppBoundEncryptionWinTestWithPolicy,
TestPolicySupported) {
const auto support_level =
GetAppBoundEncryptionSupportLevel(g_browser_process->local_state());
if (!GetParam().has_value()) {
EXPECT_EQ(support_level, SupportLevel::kSupported);
return;
}
EXPECT_EQ(support_level, *GetParam() ? SupportLevel::kSupported
: SupportLevel::kDisabledByPolicy);
}
class AppBoundEncryptionWinDecryptionNotAvailableTest
: public AppBoundEncryptionWinTest {
void SetUp() override {
// Install the service only for the pre-test part.
should_install_service_ = IsPreTest();
AppBoundEncryptionWinTest::SetUp();
}
};
IN_PROC_BROWSER_TEST_F(AppBoundEncryptionWinDecryptionNotAvailableTest,
PRE_DecryptionTemporaryFailure) {
EXPECT_EQ(GetAppBoundEncryptionSupportLevel(g_browser_process->local_state()),
SupportLevel::kSupported);
auto encryptor = GetInstanceSync(*g_browser_process->os_crypt_async());
const auto app_bound_data = encryptor.EncryptString("app-bound secret");
ASSERT_TRUE(app_bound_data);
ASSERT_GT(app_bound_data->size(), 3u);
// kAppBoundDataPrefix for App-Bound.
constexpr uint8_t kV20Header[] = {'v', '2', '0'};
EXPECT_THAT(base::span(*app_bound_data).first<3>(),
::testing::ElementsAreArray(kV20Header));
ASSERT_NO_FATAL_FAILURE(StoreData(*app_bound_data));
}
IN_PROC_BROWSER_TEST_F(AppBoundEncryptionWinDecryptionNotAvailableTest,
DecryptionTemporaryFailure) {
// Supported, but the service is not present. Decryptions should fail.
EXPECT_EQ(GetAppBoundEncryptionSupportLevel(g_browser_process->local_state()),
SupportLevel::kSupported);
auto encryptor = GetInstanceSync(*g_browser_process->os_crypt_async());
{
const auto previous_data = RetrieveData();
ASSERT_TRUE(previous_data);
os_crypt_async::Encryptor::DecryptFlags flags;
const auto plaintext = encryptor.DecryptData(*previous_data, &flags);
EXPECT_FALSE(plaintext);
// Decryption is temporarily unavailable, as the service is not present.
EXPECT_TRUE(flags.temporarily_unavailable);
}
{
os_crypt_async::Encryptor::DecryptFlags flags;
auto plaintext =
encryptor.DecryptData(base::as_byte_span("invalid_data"), &flags);
EXPECT_FALSE(plaintext);
// Invalid data is permanently unavailable.
EXPECT_FALSE(flags.temporarily_unavailable);
}
}
class AppBoundEncryptionWinTestWithVariablePolicy
: public AppBoundEncryptionWinTestWithPolicyBase {
private:
void SetUp() override {
if (!IsPreTest()) {
// Disable App-Bound in the second part of the test.
MaybeEnablePolicy(false);
}
AppBoundEncryptionWinTestWithPolicyBase::SetUp();
}
};
IN_PROC_BROWSER_TEST_F(AppBoundEncryptionWinTestWithVariablePolicy,
PRE_EncryptionDisabled) {
EXPECT_EQ(GetAppBoundEncryptionSupportLevel(g_browser_process->local_state()),
SupportLevel::kSupported);
auto encryptor = GetInstanceSync(*g_browser_process->os_crypt_async());
const auto app_bound_data = encryptor.EncryptString("app-bound secret");
ASSERT_TRUE(app_bound_data);
ASSERT_GT(app_bound_data->size(), 3u);
// kAppBoundDataPrefix for App-Bound.
constexpr uint8_t kV20Header[] = {'v', '2', '0'};
EXPECT_THAT(base::span(*app_bound_data).first<3>(),
::testing::ElementsAreArray(kV20Header));
ASSERT_NO_FATAL_FAILURE(StoreData(*app_bound_data));
}
IN_PROC_BROWSER_TEST_F(AppBoundEncryptionWinTestWithVariablePolicy,
EncryptionDisabled) {
EXPECT_EQ(GetAppBoundEncryptionSupportLevel(g_browser_process->local_state()),
SupportLevel::kDisabledByPolicy);
auto encryptor = GetInstanceSync(*g_browser_process->os_crypt_async());
const auto data = encryptor.EncryptString("secret");
ASSERT_TRUE(data);
ASSERT_GT(data->size(), 3u);
// kEncryptionVersionPrefix for DPAPI i.e. not App-Bound.
constexpr uint8_t kV10Header[] = {'v', '1', '0'};
EXPECT_THAT(base::span(*data).first<3>(),
::testing::ElementsAreArray(kV10Header));
// Also decrypt the data that was previously encrypted in the PRE test, and
// verify it decrypts even if App-Bound is disabled by policy. This is also
// tested elsewhere.
const auto previous_data = RetrieveData();
ASSERT_TRUE(previous_data);
os_crypt_async::Encryptor::DecryptFlags flags;
const auto plaintext = encryptor.DecryptData(*previous_data, &flags);
ASSERT_TRUE(plaintext);
// App-Bound is now disabled, so App-Bound encrypted data should be
// re-encrypted in order to ensure it's encrypted with the DPAPI key provider.
EXPECT_TRUE(flags.should_reencrypt);
EXPECT_EQ(*plaintext, "app-bound secret");
}
INSTANTIATE_TEST_SUITE_P(
Enabled,
AppBoundEncryptionWinTestWithPolicy,
::testing::Values(
/*policy::key::kApplicationBoundEncryptionEnabled=*/true));
INSTANTIATE_TEST_SUITE_P(
Disabled,
AppBoundEncryptionWinTestWithPolicy,
::testing::Values(
/*policy::key::kApplicationBoundEncryptionEnabled=*/false));
INSTANTIATE_TEST_SUITE_P(
NotSet,
AppBoundEncryptionWinTestWithPolicy,
::testing::Values(
/*policy::key::kApplicationBoundEncryptionEnabled=*/std::nullopt));
class AppBoundEncryptionWinReencryptTest
: public AppBoundEncryptionWinTest,
public ::testing::WithParamInterface<
std::tuple</*fake_reencrypt*/ bool, /*enable_feature*/ bool>> {
public:
AppBoundEncryptionWinReencryptTest() {
feature_list_.InitWithFeatureState(features::kAppBoundDataReencrypt,
std::get<1>(GetParam()));
}
protected:
// Re-encrypt should only happen if both the feature is enabled, and the
// service is faking the re-encryption signal.
static bool ExpectReencrypt() {
return std::get<0>(GetParam()) && std::get<1>(GetParam());
}
void SetUp() override {
if (base::GetCurrentProcessIntegrityLevel() != base::HIGH_INTEGRITY) {
GTEST_SKIP() << "Elevation is required for this test.";
}
maybe_uninstall_service_ =
InstallService(log_grabber_, std::get<0>(GetParam()));
EXPECT_TRUE(maybe_uninstall_service_.has_value());
// Service already installed, do not try installing again.
should_install_service_ = false;
AppBoundEncryptionWinTest::SetUp();
}
private:
base::test::ScopedFeatureList feature_list_;
};
// Test the basic interface to Encrypt and Decrypt data.
IN_PROC_BROWSER_TEST_P(AppBoundEncryptionWinReencryptTest, EncryptDecrypt) {
ASSERT_TRUE(install_static::IsSystemInstall());
const std::string plaintext("plaintext");
std::string ciphertext;
DWORD last_error;
base::HistogramTester histograms;
elevation_service::EncryptFlags flags{.use_latest_key = true};
HRESULT hr =
EncryptAppBoundString(ProtectionLevel::PROTECTION_PATH_VALIDATION,
plaintext, ciphertext, last_error, &flags);
ASSERT_HRESULT_SUCCEEDED(hr);
std::string returned_plaintext;
std::optional<std::string> maybe_new_ciphertext;
hr = DecryptAppBoundString(ciphertext, returned_plaintext,
ProtectionLevel::PROTECTION_PATH_VALIDATION,
maybe_new_ciphertext, last_error);
ASSERT_HRESULT_SUCCEEDED(hr);
EXPECT_EQ(plaintext, returned_plaintext);
if (ExpectReencrypt()) {
ASSERT_TRUE(maybe_new_ciphertext);
std::optional<std::string> even_newer_ciphertext;
// Verify that the new replacement ciphertext returned can still be
// decrypted.
hr = DecryptAppBoundString(*maybe_new_ciphertext, returned_plaintext,
ProtectionLevel::PROTECTION_PATH_VALIDATION,
even_newer_ciphertext, last_error);
ASSERT_HRESULT_SUCCEEDED(hr);
EXPECT_EQ(plaintext, returned_plaintext);
} else {
ASSERT_FALSE(maybe_new_ciphertext);
}
}
// This could be a unit test, but it needs the service installed to work, so
// makes sense for it to be here alongside the other app-bound encryption tests.
IN_PROC_BROWSER_TEST_P(AppBoundEncryptionWinReencryptTest, KeyProviderTest) {
const char* kPrefName = "os_crypt.app_bound_encrypted_key";
ASSERT_TRUE(install_static::IsSystemInstall());
TestingPrefServiceSimple prefs;
MockPrefChangeCallback observer(&prefs);
PrefChangeRegistrar registrar;
registrar.Init(&prefs);
registrar.Add(kPrefName, observer.GetCallback());
// The first time the GetKey is called, the provider should generate a random
// key, encrypt it with app-bound, then persist the encrypted key to store.
EXPECT_CALL(observer, OnPreferenceChanged(_)).Times(1);
os_crypt_async::AppBoundEncryptionProviderWin::RegisterLocalPrefs(
prefs.registry());
// `Key` has no public constructor and is move-only so use a std::optional as
// a handy container.
std::optional<os_crypt_async::Encryptor::Key> encryption_key;
std::string encrypted_key;
{
os_crypt_async::AppBoundEncryptionProviderWin provider(&prefs);
base::test::TestFuture<
const std::string&,
base::expected<os_crypt_async::Encryptor::Key,
os_crypt_async::KeyProvider::KeyError>>
future;
provider.GetKey(future.GetCallback());
auto [tag, key] = future.Take();
EXPECT_EQ(tag, "v20");
ASSERT_TRUE(key.has_value());
encryption_key.emplace(std::move(*key));
encrypted_key = prefs.GetString(kPrefName);
EXPECT_FALSE(encrypted_key.empty());
}
::testing::Mock::VerifyAndClearExpectations(&observer);
// The second time the GetKey is called, the provider should retrieve the key
// from store then perform a decryption via app-bound. If re-encryption is
// specified then a re-encryption call is made and a second write should
// happen to the store with the new encrypted key.
EXPECT_CALL(observer, OnPreferenceChanged(_))
.Times(ExpectReencrypt() ? 1 : 0);
{
os_crypt_async::AppBoundEncryptionProviderWin provider(&prefs);
base::test::TestFuture<
const std::string&,
base::expected<os_crypt_async::Encryptor::Key,
os_crypt_async::KeyProvider::KeyError>>
future;
provider.GetKey(future.GetCallback());
const auto& [_, key] = future.Get();
ASSERT_TRUE(key.has_value());
// The key returned should be the same as it's been decrypted from the
// store, regardless of whether it's been re-encrypted or not.
EXPECT_EQ(*key, *encryption_key);
if (ExpectReencrypt()) {
// Re-encryption should always change the encrypted value, because the
// underlying encryption schemes use random IVs, nonces or salts.
EXPECT_NE(prefs.GetString(kPrefName), encrypted_key);
// Verify the encrypted key pref (base64, with the header "APPB") is long
// enough to be a valid encrypted key, and not just empty or truncated. A
// truncated key will be 'QVBQQg==' which is base64 for 'APPB'.
EXPECT_GT(prefs.GetString(kPrefName).length(), 10u);
} else {
EXPECT_EQ(prefs.GetString(kPrefName), encrypted_key);
}
}
}
INSTANTIATE_TEST_SUITE_P(
,
AppBoundEncryptionWinReencryptTest,
::testing::Combine(::testing::Bool(), ::testing::Bool()),
[](const auto& info) {
return base::StrCat(
{std::get<0>(info.param) ? "FakeReencrypt" : "NoFakeReencrypt",
std::get<1>(info.param) ? "FeatureOn" : "FeatureOff"});
});
// These tests do not function correctly in component builds because they rely
// on being able to run a standalone executable child process in various
// different directories, and a component build has too many dynamic DLL
// dependencies to conveniently move around the file system hermetically.
#if !defined(COMPONENT_BUILD)
class AppBoundEncryptionWinTestMultiProcess : public AppBoundEncryptionWinTest {
protected:
enum class Operation {
kEncrypt,
kDecrypt,
};
void SetUp() override {
ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
AppBoundEncryptionWinTest::SetUp();
}
void EncryptOrDecryptInTestProcess(
base::FilePath::StringViewType filename,
std::optional<base::FilePath::StringViewType> sub_dir,
const std::string& input_data,
std::string& output_data,
Operation op,
HRESULT& result) {
base::ScopedAllowBlockingForTesting allow_blocking;
const auto input_file_path = temp_dir_.GetPath().Append(L"input-file");
const auto output_file_path = temp_dir_.GetPath().Append(L"output-file");
ASSERT_TRUE(base::WriteFile(input_file_path, input_data));
// The binary must run from 'testdir' this is because otherwise the scoped
// temp dir ends with a `scoped_dir` path which conflicts with a production
// environment that path validation has to correctly cater for.
auto executable_file_dir = temp_dir_.GetPath().Append(L"testdir");
if (sub_dir) {
executable_file_dir = executable_file_dir.Append(*sub_dir);
}
base::CreateDirectory(executable_file_dir);
const auto executable_file_path = executable_file_dir.Append(filename);
std::ignore = base::DeleteFile(executable_file_path);
const auto orig_exe = base::PathService::CheckedGet(base::DIR_EXE)
.Append(FILE_PATH_LITERAL("app_binary.exe"));
ASSERT_TRUE(base::CopyFile(orig_exe, executable_file_path));
base::CommandLine cmd(executable_file_path);
cmd.AppendSwitchPath(switches::kAppBoundTestInputFilename, input_file_path);
cmd.AppendSwitchPath(switches::kAppBoundTestOutputFilename,
output_file_path);
switch (op) {
case Operation::kEncrypt:
cmd.AppendSwitch(switches::kAppBoundTestModeEncrypt);
break;
case Operation::kDecrypt:
cmd.AppendSwitch(switches::kAppBoundTestModeDecrypt);
break;
}
base::LaunchOptions options;
options.start_hidden = true;
options.wait = true;
auto process = base::LaunchProcess(cmd, options);
int exit_code;
EXPECT_TRUE(process.WaitForExit(&exit_code));
result = static_cast<HRESULT>(exit_code);
if (SUCCEEDED(result)) {
EXPECT_TRUE(base::ReadFileToString(output_file_path, &output_data));
}
// This ensures the process has really terminated before this function
// returns, as base::Process destructor does not do this by default.
process.Terminate(0, /*wait=*/true);
}
private:
base::ScopedTempDir temp_dir_;
};
IN_PROC_BROWSER_TEST_F(AppBoundEncryptionWinTestMultiProcess,
EncryptDecryptProcess) {
const std::string kSecret("secret");
{
std::string ciphertext;
HRESULT result;
ASSERT_NO_FATAL_FAILURE(EncryptOrDecryptInTestProcess(
L"app1.exe", {}, kSecret, ciphertext, Operation::kEncrypt, result));
EXPECT_EQ(S_OK, result);
std::string plaintext;
ASSERT_NO_FATAL_FAILURE(EncryptOrDecryptInTestProcess(
L"app1.exe", {}, ciphertext, plaintext, Operation::kDecrypt, result));
EXPECT_EQ(S_OK, result);
EXPECT_EQ(kSecret, plaintext);
ASSERT_NO_FATAL_FAILURE(EncryptOrDecryptInTestProcess(
L"app2.exe", {}, ciphertext, plaintext, Operation::kDecrypt, result));
EXPECT_EQ(S_OK, result);
EXPECT_EQ(kSecret, plaintext);
ASSERT_NO_FATAL_FAILURE(
EncryptOrDecryptInTestProcess(L"app1.exe", L"Application", ciphertext,
plaintext, Operation::kDecrypt, result));
EXPECT_EQ(S_OK, result);
EXPECT_EQ(kSecret, plaintext);
ASSERT_NO_FATAL_FAILURE(
EncryptOrDecryptInTestProcess(L"app1.exe", L"Temp", ciphertext,
plaintext, Operation::kDecrypt, result));
EXPECT_EQ(S_OK, result);
EXPECT_EQ(kSecret, plaintext);
ASSERT_NO_FATAL_FAILURE(
EncryptOrDecryptInTestProcess(L"app1.exe", L"Bad", ciphertext,
plaintext, Operation::kDecrypt, result));
EXPECT_EQ(elevation_service::Elevator::kValidationDidNotPass, result);
}
{
// Explicitly test the most frequent chrome-specific cases.
std::string ciphertext;
HRESULT result;
ASSERT_NO_FATAL_FAILURE(
EncryptOrDecryptInTestProcess(L"chrome.exe", L"Application", kSecret,
ciphertext, Operation::kEncrypt, result));
EXPECT_EQ(S_OK, result);
std::string plaintext;
ASSERT_NO_FATAL_FAILURE(EncryptOrDecryptInTestProcess(
L"new_chrome.exe", L"Application", ciphertext, plaintext,
Operation::kDecrypt, result));
EXPECT_EQ(S_OK, result);
EXPECT_EQ(kSecret, plaintext);
ASSERT_NO_FATAL_FAILURE(
EncryptOrDecryptInTestProcess(L"old_chrome.exe", L"Temp", ciphertext,
plaintext, Operation::kDecrypt, result));
EXPECT_EQ(S_OK, result);
EXPECT_EQ(kSecret, plaintext);
}
}
#endif // !defined(COMPONENT_BUILD)
struct AppBoundTestCase {
// Test case name.
std::string name;
// If true, then the temporary dir used by tests for --user-data-dir is
// considered a 'default user data dir' by the PRE part of the test.
bool allow_non_standard_udd_in_pre;
// If true, then the temporary dir used by tests for --user-data-dir is
// considered a 'default user data dir' by the main part of the test.
bool allow_non_standard_udd_in_main;
// Whether or not the data will start with the 'v20' app-bound header,
// indicating that it's secured with App-Bound encryption.
bool expect_encrypt_with_app_bound;
// Whether or not the Decrypt operation succeeds. If false, then the
// `temporarily_unavailable` is also expected to be true.
bool expect_decrypt_works;
// If decrypt works, whether or not OSCrypt indicates that the data should be
// re-encrypted.
bool should_reencrypt = false;
// Support level for the PRE part of the test.
SupportLevel expected_support_level_in_pre;
// Support level for the main part of the test.
SupportLevel expected_support_level_in_main;
};
class AppBoundEncryptionWinTestWithUserDataDir
: public AppBoundEncryptionWinTest,
public testing::WithParamInterface<AppBoundTestCase> {
public:
void SetUp() override {
set_default_user_data_dir_ =
IsPreTest() ? GetParam().allow_non_standard_udd_in_pre
: GetParam().allow_non_standard_udd_in_main;
AppBoundEncryptionWinTest::SetUp();
}
};
IN_PROC_BROWSER_TEST_P(AppBoundEncryptionWinTestWithUserDataDir,
PRE_EncryptionDecryption) {
EXPECT_EQ(GetAppBoundEncryptionSupportLevel(g_browser_process->local_state()),
GetParam().expected_support_level_in_pre);
auto encryptor = GetInstanceSync(*g_browser_process->os_crypt_async());
const auto encrypted_data = encryptor.EncryptString("super secret");
ASSERT_TRUE(encrypted_data);
if (GetParam().expect_encrypt_with_app_bound) {
ASSERT_GT(encrypted_data->size(), 3u);
// kAppBoundDataPrefix for App-Bound.
constexpr uint8_t kV20Header[] = {'v', '2', '0'};
EXPECT_THAT(base::span(*encrypted_data).first<3>(),
::testing::ElementsAreArray(kV20Header));
}
ASSERT_NO_FATAL_FAILURE(StoreData(*encrypted_data));
}
IN_PROC_BROWSER_TEST_P(AppBoundEncryptionWinTestWithUserDataDir,
EncryptionDecryption) {
EXPECT_EQ(GetAppBoundEncryptionSupportLevel(g_browser_process->local_state()),
GetParam().expected_support_level_in_main);
auto encryptor = GetInstanceSync(*g_browser_process->os_crypt_async());
const auto previous_data = RetrieveData();
ASSERT_TRUE(previous_data);
os_crypt_async::Encryptor::DecryptFlags flags;
const auto plaintext = encryptor.DecryptData(*previous_data, &flags);
if (GetParam().expect_decrypt_works) {
ASSERT_TRUE(plaintext);
EXPECT_EQ("super secret", *plaintext);
EXPECT_EQ(flags.should_reencrypt, GetParam().should_reencrypt);
} else {
EXPECT_FALSE(plaintext);
EXPECT_TRUE(flags.temporarily_unavailable);
}
}
INSTANTIATE_TEST_SUITE_P(
/* no prefix */,
AppBoundEncryptionWinTestWithUserDataDir,
testing::ValuesIn<AppBoundTestCase>({
{.name = "standard_udd",
.allow_non_standard_udd_in_pre = true,
.allow_non_standard_udd_in_main = true,
.expect_encrypt_with_app_bound = true,
.expect_decrypt_works = true,
.should_reencrypt = false,
.expected_support_level_in_pre = SupportLevel::kSupported,
.expected_support_level_in_main = SupportLevel::kSupported},
{.name = "non_standard_udd",
.allow_non_standard_udd_in_pre = false,
.allow_non_standard_udd_in_main = false,
.expect_encrypt_with_app_bound = false,
.expect_decrypt_works = true,
.should_reencrypt = false,
.expected_support_level_in_pre =
SupportLevel::kNotUsingDefaultUserDataDir,
.expected_support_level_in_main =
SupportLevel::kNotUsingDefaultUserDataDir},
// Switch from a standard UDD to a non-standard UDD. The encrypt that
// took place in the first stage was app-bound and should not be
// decryptable by the second stage.
{.name = "was_standard_udd_now_non_standard_udd",
.allow_non_standard_udd_in_pre = true,
.allow_non_standard_udd_in_main = false,
.expect_encrypt_with_app_bound = true,
// This is the only configuration where decryption should fail.
.expect_decrypt_works = false,
.expected_support_level_in_pre = SupportLevel::kSupported,
.expected_support_level_in_main =
SupportLevel::kNotUsingDefaultUserDataDir},
// Switch from a non-standard UDD to a standard UDD. The encrypt that
// took place in the first stage would have been using non-app-bound
// since it did not provide a key, but should still be decryptable with
// the other Key Provider(s) (e.g. DPAPI). Because DPAPI is weaker than
// App-Bound, OSCrypt indicates `should_reencrypt` as true.
{.name = "was_non_standard_udd_now_standard_udd",
.allow_non_standard_udd_in_pre = false,
.allow_non_standard_udd_in_main = true,
.expect_encrypt_with_app_bound = false,
.expect_decrypt_works = true,
.should_reencrypt = true,
.expected_support_level_in_pre =
SupportLevel::kNotUsingDefaultUserDataDir,
.expected_support_level_in_main = SupportLevel::kSupported},
}),
[](const auto& info) { return info.param.name; });
} // namespace os_crypt