blob: ee69c9c4d72876cd965b9084eda6eee0590348e4 [file] [log] [blame]
// 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/enterprise/obfuscation/core/utils.h"
#include "base/containers/span_reader.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/rand_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace enterprise_obfuscation {
namespace {
// Helper function to divide data in chunks of random sizes.
void ObfuscateTestDataInChunks(base::span<const uint8_t> test_data,
std::vector<uint8_t>& obfuscated_content) {
std::array<uint8_t, kKeySize> derived_key;
std::vector<uint8_t> nonce_prefix;
auto header = CreateHeader(&derived_key, &nonce_prefix);
ASSERT_TRUE(header.has_value());
obfuscated_content.insert(obfuscated_content.end(), header.value().begin(),
header.value().end());
base::SpanReader reader(test_data);
uint32_t counter = 0;
while (reader.remaining() > 0) {
// Generate a random chunk size between 1 and remaining data size, capped at
// `kMaxChunkSize`.
size_t chunk_size =
base::RandInt(1, std::min(reader.remaining(), kMaxChunkSize));
// Read in the next chunk.
auto current_chunk = reader.Read(chunk_size);
ASSERT_TRUE(current_chunk.has_value());
auto obfuscated_result =
ObfuscateDataChunk(*current_chunk, derived_key, nonce_prefix, counter++,
reader.remaining() == 0);
ASSERT_TRUE(obfuscated_result.has_value());
obfuscated_content.insert(obfuscated_content.end(),
obfuscated_result.value().begin(),
obfuscated_result.value().end());
}
}
// Helper function to count the number of files in a directory.
int CountFilesInDirectory(const base::FilePath& dir_path) {
base::FileEnumerator enum_files(dir_path, false, base::FileEnumerator::FILES);
int count = 0;
while (!enum_files.Next().empty()) {
count++;
}
return count;
}
} // namespace
class ObfuscationUtilsTest
: public ::testing::TestWithParam<std::tuple<bool, size_t>> {
protected:
ObfuscationUtilsTest() {
feature_list_.InitWithFeatureState(kEnterpriseFileObfuscation,
file_obfuscation_feature_enabled());
}
void SetUp() override { ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); }
base::FilePath test_file_path() const {
return temp_dir_.GetPath().AppendASCII("test_file.txt");
}
size_t test_data_size() const { return std::get<1>(GetParam()); }
bool file_obfuscation_feature_enabled() const {
return std::get<0>(GetParam());
}
private:
base::test::ScopedFeatureList feature_list_;
base::ScopedTempDir temp_dir_;
};
TEST_P(ObfuscationUtilsTest, ObfuscateAndDeobfuscateSingleDataChunk) {
// Obfuscate the data chunk.
std::vector<uint8_t> test_data = base::RandBytesAsVector(test_data_size());
std::array<uint8_t, kKeySize> derived_key;
std::vector<uint8_t> nonce_prefix;
auto header = CreateHeader(&derived_key, &nonce_prefix);
constexpr uint32_t kInitialChunkCounter = 0;
auto obfuscated_chunk =
ObfuscateDataChunk(base::as_byte_span(test_data), derived_key,
nonce_prefix, kInitialChunkCounter, true);
if (!file_obfuscation_feature_enabled()) {
ASSERT_EQ(obfuscated_chunk.error(), Error::kDisabled);
ASSERT_EQ(header.error(), Error::kDisabled);
return;
}
ASSERT_TRUE(header.has_value());
ASSERT_TRUE(obfuscated_chunk.has_value());
ASSERT_NE(obfuscated_chunk.value(), test_data);
// Deobfuscate the data chunk.
auto header_data = GetHeaderData(header.value());
ASSERT_TRUE(header_data.has_value());
auto chunk_size = GetObfuscatedChunkSize(obfuscated_chunk.value());
if (test_data.size() > kMaxChunkSize) {
ASSERT_EQ(chunk_size.error(), Error::kDeobfuscationFailed);
return;
}
ASSERT_TRUE(chunk_size.has_value());
EXPECT_EQ(chunk_size.value(), test_data.size() + kAuthTagSize);
auto deobfuscated_chunk = DeobfuscateDataChunk(
base::span(obfuscated_chunk.value())
.subspan(kChunkSizePrefixSize, chunk_size.value()),
header_data.value().derived_key, header_data.value().nonce_prefix,
kInitialChunkCounter, true);
ASSERT_TRUE(deobfuscated_chunk.has_value());
EXPECT_EQ(deobfuscated_chunk.value(), test_data);
// Deobfuscation should fail when we modify the ciphertext.
obfuscated_chunk.value()[kChunkSizePrefixSize] ^= 1;
deobfuscated_chunk = DeobfuscateDataChunk(
base::span(obfuscated_chunk.value())
.subspan(kChunkSizePrefixSize, chunk_size.value()),
header_data.value().derived_key, header_data.value().nonce_prefix,
kInitialChunkCounter, true);
ASSERT_EQ(deobfuscated_chunk.error(), Error::kDeobfuscationFailed);
}
TEST_P(ObfuscationUtilsTest, DeobfuscateFileInPlace) {
base::HistogramTester histogram_tester;
std::vector<uint8_t> test_data = base::RandBytesAsVector(test_data_size());
ASSERT_TRUE(base::WriteFile(test_file_path(), test_data));
int64_t original_size = base::GetFileSize(test_file_path()).value_or(0);
auto result = DeobfuscateFileInPlace(test_file_path());
if (!file_obfuscation_feature_enabled()) {
ASSERT_EQ(result.error(), Error::kDisabled);
histogram_tester.ExpectUniqueSample(kObfuscationResultHistogram,
Error::kDisabled, 1);
return;
}
// Deobfuscating an unobfuscated file should fail.
Error unobfuscated_error = original_size == 0 ? Error::kFileOperationError
: Error::kDeobfuscationFailed;
ASSERT_EQ(result.error(), unobfuscated_error);
histogram_tester.ExpectUniqueSample(kObfuscationResultHistogram,
unobfuscated_error, 1);
// Only the original test file should remain.
EXPECT_EQ(CountFilesInDirectory(test_file_path().DirName()), 1);
std::vector<uint8_t> obfuscated_content;
ObfuscateTestDataInChunks(test_data, obfuscated_content);
ASSERT_TRUE(base::WriteFile(test_file_path(), obfuscated_content));
ASSERT_TRUE(DeobfuscateFileInPlace(test_file_path()).has_value());
histogram_tester.ExpectBucketCount(kObfuscationResultHistogram,
Error::kSuccess, 1);
auto deobfuscated_content = base::ReadFileToBytes(test_file_path());
ASSERT_TRUE(deobfuscated_content.has_value());
EXPECT_EQ(deobfuscated_content.value(), test_data);
// Get deobfuscated file size which should match original.
std::optional<int64_t> deobfuscated_size =
base::GetFileSize(test_file_path());
ASSERT_TRUE(deobfuscated_size.has_value());
EXPECT_EQ(deobfuscated_size.value(), original_size);
// Deobfuscating to an invalid path should fail.
base::FilePath invalid_path(
test_file_path().InsertBeforeExtensionASCII("_invalid"));
ASSERT_EQ(DeobfuscateFileInPlace(invalid_path).error(),
Error::kFileOperationError);
// Only the original test file should remain.
EXPECT_EQ(CountFilesInDirectory(test_file_path().DirName()), 1);
int expected_file_op_count = (original_size == 0) ? 2 : 1;
histogram_tester.ExpectBucketCount(kObfuscationResultHistogram,
Error::kFileOperationError,
expected_file_op_count);
histogram_tester.ExpectTotalCount(kObfuscationResultHistogram, 3);
}
TEST_P(ObfuscationUtilsTest, ObfuscateAndDeobfuscateVariableChunks) {
if (!file_obfuscation_feature_enabled()) {
GTEST_SKIP() << "File obfuscation feature is disabled.";
}
// Create test data.
std::vector<uint8_t> test_data = base::RandBytesAsVector(test_data_size());
// Obfuscate data in chunks of random sizes.
std::vector<uint8_t> obfuscated_content;
ObfuscateTestDataInChunks(test_data, obfuscated_content);
// Deobfuscate chunk by chunk.
std::vector<uint8_t> deobfuscated_content;
size_t offset = kHeaderSize;
uint32_t counter = 0;
std::vector<uint8_t> header(obfuscated_content.begin(),
obfuscated_content.begin() + kHeaderSize);
auto header_data = GetHeaderData(header);
ASSERT_TRUE(header_data.has_value());
while (offset < obfuscated_content.size()) {
// Read chunk size
auto chunk_size = GetObfuscatedChunkSize(
base::span(obfuscated_content).subspan(offset, kChunkSizePrefixSize));
ASSERT_TRUE(chunk_size.has_value());
offset += kChunkSizePrefixSize;
// Deobfuscate chunk
auto deobfuscated_chunk = DeobfuscateDataChunk(
base::span(obfuscated_content).subspan(offset, chunk_size.value()),
header_data.value().derived_key, header_data.value().nonce_prefix,
counter++, (offset + chunk_size.value() >= obfuscated_content.size()));
ASSERT_TRUE(deobfuscated_chunk.has_value());
std::move(deobfuscated_chunk.value().begin(),
deobfuscated_chunk.value().end(),
std::back_inserter(deobfuscated_content));
offset += chunk_size.value();
}
// Compare deobfuscated content with original test data
EXPECT_EQ(deobfuscated_content, test_data);
}
INSTANTIATE_TEST_SUITE_P(
ObfuscationUtilsFeatureTest,
ObfuscationUtilsTest,
::testing::Combine(
::testing::Bool(), // File obfuscator feature enabled/disabled
::testing::Values(0,
10,
kMaxChunkSize + 1024,
kMaxChunkSize * 2 + 1024)));
} // namespace enterprise_obfuscation