blob: b5efa6f4c8e93497cf38154ef3a1c18def077d82 [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/metrics/structured/key_data_prefs_delegate.h"
#include <memory>
#include <string_view>
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/string_number_conversions.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "components/metrics/structured/histogram_util.h"
#include "components/metrics/structured/lib/histogram_util.h"
#include "components/metrics/structured/lib/key_data.h"
#include "components/metrics/structured/lib/key_util.h"
#include "components/metrics/structured/lib/proto/key.pb.h"
#include "components/metrics/structured/structured_metrics_validator.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "components/prefs/testing_pref_service.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace metrics::structured {
namespace {
constexpr char kTestPrefName[] = "TestPref";
// 32 byte long test key, matching the size of a real key.
constexpr char kKey[] = "abcdefghijklmnopqrstuvwxyzabcdef";
// These project, event, and metric names are used for testing.
// - project: TestProjectOne
// - event: TestEventOne
// - metric: TestMetricOne
// - metric: TestMetricTwo
// - project: TestProjectTwo
// The name hash of "TestProjectOne".
constexpr uint64_t kProjectOneHash = 16881314472396226433ull;
// The name hash of "TestProjectTwo".
constexpr uint64_t kProjectTwoHash = 5876808001962504629ull;
// The name hash of "TestMetricOne".
constexpr uint64_t kMetricOneHash = 637929385654885975ull;
// The name hash of "TestMetricTwo".
constexpr uint64_t kMetricTwoHash = 14083999144141567134ull;
// The hex-encoded frst 8 bytes of SHA256(kKey), ie. the user ID for key kKey.
constexpr char kUserId[] = "2070DF23E0D95759";
// Test values and their hashes. Hashes are the first 8 bytes of:
// HMAC_SHA256(concat(hex(kMetricNHash), kValueN), kKey)
constexpr char kValueOne[] = "value one";
constexpr char kValueTwo[] = "value two";
constexpr char kValueOneHash[] = "805B8790DC69B773";
constexpr char kValueTwoHash[] = "87CEF12FB15E0B3A";
constexpr base::TimeDelta kKeyRotationPeriod = base::Days(90);
std::string HashToHex(const uint64_t hash) {
return base::HexEncode(&hash, sizeof(uint64_t));
}
} // namespace
class KeyDataPrefsDelegateTest : public testing::Test {
public:
void SetUp() override {
prefs_.registry()->RegisterDictionaryPref(kTestPrefName);
// Move the mock date forward from day 0, because KeyDataFileDelegate
// assumes that day 0 is a bug.
task_environment_.AdvanceClock(base::Days(1000));
}
void CreateKeyData() {
auto delegate =
std::make_unique<KeyDataPrefsDelegate>(&prefs_, kTestPrefName);
delegate_ = delegate.get();
key_data_ = std::make_unique<KeyData>(std::move(delegate));
}
void ResetKeyData() {
delegate_ = nullptr;
key_data_.reset();
}
// Read the key directly from the prefs.
KeyProto GetKey(const uint64_t project_name_hash) {
auto* validators = validator::Validators::Get();
std::string_view project_name =
validators->GetProjectName(project_name_hash).value();
const base::Value::Dict& keys_dict = prefs_.GetDict(kTestPrefName);
const base::Value::Dict* value = keys_dict.FindDict(project_name);
std::optional<KeyProto> key = util::CreateKeyProtoFromValue(*value);
return std::move(key).value();
}
base::TimeDelta Today() {
return base::Time::Now() - base::Time::UnixEpoch();
}
// Write a KeyDataProto to prefs with a single key described by the
// arguments.
bool SetupKey(const uint64_t project_name_hash,
const std::string& key,
const base::TimeDelta last_rotation,
const base::TimeDelta rotation_period) {
// It's a test logic error for the key data to exist when calling SetupKey,
// because it will desync the in-memory proto from the underlying storage.
if (key_data_) {
return false;
}
KeyProto key_proto;
key_proto.set_key(key);
key_proto.set_last_rotation(last_rotation.InDays());
key_proto.set_rotation_period(rotation_period.InDays());
ScopedDictPrefUpdate pref_updater(&prefs_, kTestPrefName);
base::Value::Dict& dict = pref_updater.Get();
const validator::Validators* validators = validator::Validators::Get();
auto project_name = validators->GetProjectName(project_name_hash);
auto value = util::CreateValueFromKeyProto(key_proto);
dict.Set(*project_name, std::move(value));
return true;
}
void ExpectKeyValidation(const int valid,
const int created,
const int rotated) {
static constexpr char kHistogram[] =
"UMA.StructuredMetrics.KeyValidationState";
histogram_tester_.ExpectBucketCount(kHistogram, KeyValidationState::kValid,
valid);
histogram_tester_.ExpectBucketCount(kHistogram,
KeyValidationState::kCreated, created);
histogram_tester_.ExpectBucketCount(kHistogram,
KeyValidationState::kRotated, rotated);
}
protected:
base::test::TaskEnvironment task_environment_{
base::test::TaskEnvironment::MainThreadType::UI,
base::test::TaskEnvironment::ThreadPoolExecutionMode::QUEUED,
base::test::TaskEnvironment::TimeSource::MOCK_TIME};
TestingPrefServiceSimple prefs_;
base::HistogramTester histogram_tester_;
std::unique_ptr<KeyData> key_data_;
raw_ptr<KeyDataPrefsDelegate> delegate_;
};
// If there is no key store file present, check that new keys are generated for
// each project, and those keys are of the right length and different from each
// other.
TEST_F(KeyDataPrefsDelegateTest, GeneratesKeysForProjects) {
// Make key data and use two keys, in order to generate them.
CreateKeyData();
key_data_->Id(kProjectOneHash, kKeyRotationPeriod);
key_data_->Id(kProjectTwoHash, kKeyRotationPeriod);
const std::string key_one = GetKey(kProjectOneHash).key();
const std::string key_two = GetKey(kProjectTwoHash).key();
EXPECT_EQ(key_one.size(), 32ul);
EXPECT_EQ(key_two.size(), 32ul);
EXPECT_NE(key_one, key_two);
ExpectKeyValidation(/*valid=*/0, /*created=*/2, /*rotated=*/0);
}
// If there is an existing key store file, check that its keys are not replaced.
TEST_F(KeyDataPrefsDelegateTest, ReuseExistingKeys) {
// Create a file with one key.
CreateKeyData();
const uint64_t id_one = key_data_->Id(kProjectOneHash, kKeyRotationPeriod);
ExpectKeyValidation(/*valid=*/0, /*created=*/1, /*rotated=*/0);
const std::string key_one = GetKey(kProjectOneHash).key();
// Reset the in-memory state, leave the on-disk state intact.
ResetKeyData();
// Open the file again and check we use the same key.
CreateKeyData();
const uint64_t id_two = key_data_->Id(kProjectOneHash, kKeyRotationPeriod);
ExpectKeyValidation(/*valid=*/1, /*created=*/1, /*rotated=*/0);
const std::string key_two = GetKey(kProjectOneHash).key();
EXPECT_EQ(id_one, id_two);
EXPECT_EQ(key_one, key_two);
}
// Check that different events have different hashes for the same metric and
// value.
TEST_F(KeyDataPrefsDelegateTest, DifferentEventsDifferentHashes) {
CreateKeyData();
EXPECT_NE(key_data_->HmacMetric(kProjectOneHash, kMetricOneHash, "value",
kKeyRotationPeriod),
key_data_->HmacMetric(kProjectTwoHash, kMetricOneHash, "value",
kKeyRotationPeriod));
}
// Check that an event has different hashes for different metrics with the same
// value.
TEST_F(KeyDataPrefsDelegateTest, DifferentMetricsDifferentHashes) {
CreateKeyData();
EXPECT_NE(key_data_->HmacMetric(kProjectOneHash, kMetricOneHash, "value",
kKeyRotationPeriod),
key_data_->HmacMetric(kProjectOneHash, kMetricTwoHash, "value",
kKeyRotationPeriod));
}
// Check that an event has different hashes for different values of the same
// metric.
TEST_F(KeyDataPrefsDelegateTest, DifferentValuesDifferentHashes) {
CreateKeyData();
EXPECT_NE(key_data_->HmacMetric(kProjectOneHash, kMetricOneHash, "first",
kKeyRotationPeriod),
key_data_->HmacMetric(kProjectOneHash, kMetricOneHash, "second",
kKeyRotationPeriod));
}
// Ensure that KeyDataFileDelegate::UserId is the expected value of SHA256(key).
TEST_F(KeyDataPrefsDelegateTest, CheckUserIDs) {
ASSERT_TRUE(SetupKey(kProjectOneHash, kKey, Today(), kKeyRotationPeriod));
CreateKeyData();
EXPECT_EQ(HashToHex(key_data_->Id(kProjectOneHash, kKeyRotationPeriod)),
kUserId);
EXPECT_NE(HashToHex(key_data_->Id(kProjectTwoHash, kKeyRotationPeriod)),
kUserId);
}
// Ensure that KeyDataFileDelegate::Hash returns expected values for a known
// key / and value.
TEST_F(KeyDataPrefsDelegateTest, CheckHashes) {
ASSERT_TRUE(SetupKey(kProjectOneHash, kKey, Today(), kKeyRotationPeriod));
CreateKeyData();
EXPECT_EQ(HashToHex(key_data_->HmacMetric(kProjectOneHash, kMetricOneHash,
kValueOne, kKeyRotationPeriod)),
kValueOneHash);
EXPECT_EQ(HashToHex(key_data_->HmacMetric(kProjectOneHash, kMetricTwoHash,
kValueTwo, kKeyRotationPeriod)),
kValueTwoHash);
}
//// Check that keys for a event are correctly rotated after a given rotation
//// period.
TEST_F(KeyDataPrefsDelegateTest, KeysRotated) {
const base::TimeDelta start_day = Today();
ASSERT_TRUE(SetupKey(kProjectOneHash, kKey, start_day, kKeyRotationPeriod));
CreateKeyData();
const uint64_t first_id = key_data_->Id(kProjectOneHash, kKeyRotationPeriod);
EXPECT_EQ(key_data_->LastKeyRotation(kProjectOneHash)->InDays(),
start_day.InDays());
ExpectKeyValidation(/*valid=*/1, /*created=*/0, /*rotated=*/0);
{
// Advancing by |kKeyRotationPeriod|-1 days, the key should not be rotated.
task_environment_.AdvanceClock(kKeyRotationPeriod - base::Days(1));
EXPECT_EQ(key_data_->Id(kProjectOneHash, kKeyRotationPeriod), first_id);
EXPECT_EQ(key_data_->LastKeyRotation(kProjectOneHash)->InDays(),
start_day.InDays());
ASSERT_EQ(GetKey(kProjectOneHash).last_rotation(), start_day.InDays());
ExpectKeyValidation(/*valid=*/2, /*created=*/0, /*rotated=*/0);
}
{
// Advancing by another |key_rotation_period|+1 days, the key should be
// rotated and the last rotation day should be incremented by
// |key_rotation_period|.
task_environment_.AdvanceClock(kKeyRotationPeriod + base::Days(1));
EXPECT_NE(key_data_->Id(kProjectOneHash, kKeyRotationPeriod), first_id);
base::TimeDelta expected_last_key_rotation =
start_day + 2 * kKeyRotationPeriod;
EXPECT_EQ(GetKey(kProjectOneHash).last_rotation(),
expected_last_key_rotation.InDays());
EXPECT_EQ(key_data_->LastKeyRotation(kProjectOneHash)->InDays(),
expected_last_key_rotation.InDays());
ExpectKeyValidation(/*valid=*/2, /*created=*/0, /*rotated=*/1);
ASSERT_EQ(GetKey(kProjectOneHash).rotation_period(),
kKeyRotationPeriod.InDays());
}
{
// Advancing by |2* kKeyRotationPeriod| days, the last rotation day should
// now 4 periods of |kKeyRotationPeriod| days ahead.
task_environment_.AdvanceClock(kKeyRotationPeriod * 2);
key_data_->Id(kProjectOneHash, kKeyRotationPeriod);
base::TimeDelta expected_last_key_rotation =
start_day + 4 * kKeyRotationPeriod;
EXPECT_EQ(GetKey(kProjectOneHash).last_rotation(),
expected_last_key_rotation.InDays());
EXPECT_EQ(key_data_->LastKeyRotation(kProjectOneHash)->InDays(),
expected_last_key_rotation.InDays());
ExpectKeyValidation(/*valid=*/2, /*created=*/0, /*rotated=*/2);
}
}
//// Check that keys with updated rotations are correctly rotated.
TEST_F(KeyDataPrefsDelegateTest, KeysWithUpdatedRotations) {
base::TimeDelta first_key_rotation_period = base::Days(60);
const base::TimeDelta start_day = Today();
ASSERT_TRUE(
SetupKey(kProjectOneHash, kKey, start_day, first_key_rotation_period));
CreateKeyData();
const uint64_t first_id =
key_data_->Id(kProjectOneHash, first_key_rotation_period);
EXPECT_EQ(key_data_->LastKeyRotation(kProjectOneHash)->InDays(),
start_day.InDays());
ExpectKeyValidation(/*valid=*/1, /*created=*/0, /*rotated=*/0);
// Advance days by |new_key_rotation_period| + 1. This should fall within
// the rotation of the |new_key_rotation_period| but outside
// |first_key_rotation_period|.
const base::TimeDelta new_key_rotation_period = base::Days(50);
task_environment_.AdvanceClock(
base::Days(new_key_rotation_period.InDays() + 1));
const uint64_t second_id =
key_data_->Id(kProjectOneHash, new_key_rotation_period);
EXPECT_NE(first_id, second_id);
// Key should have been rotated with new_key_rotation_period.
base::TimeDelta expected_last_key_rotation =
start_day + new_key_rotation_period;
EXPECT_EQ(GetKey(kProjectOneHash).last_rotation(),
expected_last_key_rotation.InDays());
EXPECT_EQ(key_data_->LastKeyRotation(kProjectOneHash)->InDays(),
expected_last_key_rotation.InDays());
ExpectKeyValidation(/*valid=*/1, /*created=*/0, /*rotated=*/1);
}
TEST_F(KeyDataPrefsDelegateTest, Purge) {
const base::TimeDelta start_day = Today();
ASSERT_TRUE(SetupKey(kProjectOneHash, kKey, start_day, kKeyRotationPeriod));
CreateKeyData();
EXPECT_EQ(delegate_->proto_.keys().size(), 1ul);
key_data_->Purge();
EXPECT_EQ(delegate_->proto_.keys().size(), 0ul);
const base::Value::Dict& keys_dict = prefs_.GetDict(kTestPrefName);
EXPECT_EQ(keys_dict.size(), 0ul);
}
} // namespace metrics::structured