blob: 79709aaf54605f04684314241f4c615b1fe2a6e6 [file] [log] [blame]
// Copyright 2019 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.h"
#include <memory>
#include <string>
#include "base/containers/flat_set.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/run_loop.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/task_environment.h"
#include "base/values.h"
#include "components/metrics/structured/histogram_util.h"
#include "components/metrics/structured/recorder.h"
#include "components/metrics/structured/storage.pb.h"
#include "components/prefs/persistent_pref_store.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace metrics::structured {
namespace {
// 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 = UINT64_C(16881314472396226433);
// The name hash of "TestProjectTwo".
constexpr uint64_t kProjectTwoHash = UINT64_C(5876808001962504629);
// The name hash of "TestMetricOne".
constexpr uint64_t kMetricOneHash = UINT64_C(637929385654885975);
// The name hash of "TestMetricTwo".
constexpr uint64_t kMetricTwoHash = UINT64_C(14083999144141567134);
// 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 int kKeyRotationPeriod = 90;
std::string HashToHex(const uint64_t hash) {
return base::HexEncode(&hash, sizeof(uint64_t));
}
} // namespace
class KeyDataTest : public testing::Test {
protected:
void SetUp() override {
ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
// Move the mock date forward from day 0, because KeyData assumes that day 0
// is a bug.
task_environment_.AdvanceClock(base::Days(1000));
}
void ResetState() {
key_data_.reset();
base::DeleteFile(GetPath());
ASSERT_FALSE(base::PathExists(GetPath()));
}
base::FilePath GetPath() {
return temp_dir_.GetPath().Append(FILE_PATH_LITERAL("keys"));
}
void MakeKeyData() {
key_data_ = std::make_unique<KeyData>(GetPath(), base::Seconds(0),
base::DoNothing());
Wait();
}
void SaveKeyData() {
key_data_->WriteNowForTest();
Wait();
ASSERT_TRUE(base::PathExists(GetPath()));
}
int Today() { return (base::Time::Now() - base::Time::UnixEpoch()).InDays(); }
// Read the on-disk file and return the information about the key for
// |project_name_hash|. Fails if a key does not exist.
KeyProto GetKey(const uint64_t project_name_hash) {
std::string proto_str;
CHECK(base::ReadFileToString(GetPath(), &proto_str));
KeyDataProto proto;
CHECK(proto.ParseFromString(proto_str));
const auto it = proto.keys().find(project_name_hash);
CHECK(it != proto.keys().end());
return it->second;
}
// Write a KeyDataProto to disk with a single key described by the arguments.
void SetupKey(const uint64_t project_name_hash,
const std::string& key,
const int last_rotation,
const int 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.
ASSERT_FALSE(key_data_);
KeyDataProto proto;
KeyProto& key_proto = (*proto.mutable_keys())[project_name_hash];
key_proto.set_key(key);
key_proto.set_last_rotation(last_rotation);
key_proto.set_rotation_period(rotation_period);
ASSERT_TRUE(base::WriteFile(GetPath(), proto.SerializeAsString()));
}
void Wait() { task_environment_.RunUntilIdle(); }
void ExpectNoErrors() {
histogram_tester_.ExpectTotalCount("UMA.StructuredMetrics.InternalError",
0);
}
void ExpectKeyValidation(const int valid,
const int created,
const int rotated) {
static const std::string histogram =
"UMA.StructuredMetrics.KeyValidationState";
histogram_tester_.ExpectBucketCount(histogram, KeyValidationState::kValid,
valid);
histogram_tester_.ExpectBucketCount(histogram, KeyValidationState::kCreated,
created);
histogram_tester_.ExpectBucketCount(histogram, KeyValidationState::kRotated,
rotated);
}
base::test::TaskEnvironment task_environment_{
base::test::TaskEnvironment::MainThreadType::UI,
base::test::TaskEnvironment::ThreadPoolExecutionMode::QUEUED,
base::test::TaskEnvironment::TimeSource::MOCK_TIME};
base::ScopedTempDir temp_dir_;
base::HistogramTester histogram_tester_;
std::unique_ptr<KeyData> key_data_;
};
// 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(KeyDataTest, GeneratesKeysForProjects) {
// Make key data and use two keys, in order to generate them.
MakeKeyData();
key_data_->Id(kProjectOneHash, kKeyRotationPeriod);
key_data_->Id(kProjectTwoHash, kKeyRotationPeriod);
SaveKeyData();
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);
ExpectNoErrors();
ExpectKeyValidation(/*valid=*/0, /*created=*/2, /*rotated=*/0);
}
// When repeatedly initialized with no key store file present, ensure the keys
// generated each time are distinct.
TEST_F(KeyDataTest, GeneratesDistinctKeys) {
base::flat_set<std::string> keys;
for (int i = 1; i <= 10; ++i) {
// Reset on-disk and in-memory state, regenerate the key, and save it to
// disk.
ResetState();
MakeKeyData();
key_data_->Id(kProjectOneHash, kKeyRotationPeriod);
SaveKeyData();
keys.insert(GetKey(kProjectOneHash).key());
ExpectKeyValidation(/*valid=*/0, /*created=*/i, /*rotated=*/0);
}
ExpectNoErrors();
EXPECT_EQ(keys.size(), 10ul);
}
// If there is an existing key store file, check that its keys are not replaced.
TEST_F(KeyDataTest, ReuseExistingKeys) {
// Create a file with one key.
MakeKeyData();
const uint64_t id_one = key_data_->Id(kProjectOneHash, kKeyRotationPeriod);
SaveKeyData();
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.
key_data_.reset();
// Open the file again and check we use the same key.
MakeKeyData();
const uint64_t id_two = key_data_->Id(kProjectOneHash, kKeyRotationPeriod);
ExpectKeyValidation(/*valid=*/1, /*created=*/1, /*rotated=*/0);
SaveKeyData();
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(KeyDataTest, DifferentEventsDifferentHashes) {
MakeKeyData();
EXPECT_NE(key_data_->HmacMetric(kProjectOneHash, kMetricOneHash, "value",
kKeyRotationPeriod),
key_data_->HmacMetric(kProjectTwoHash, kMetricOneHash, "value",
kKeyRotationPeriod));
ExpectNoErrors();
}
// Check that an event has different hashes for different metrics with the same
// value.
TEST_F(KeyDataTest, DifferentMetricsDifferentHashes) {
MakeKeyData();
EXPECT_NE(key_data_->HmacMetric(kProjectOneHash, kMetricOneHash, "value",
kKeyRotationPeriod),
key_data_->HmacMetric(kProjectOneHash, kMetricTwoHash, "value",
kKeyRotationPeriod));
ExpectNoErrors();
}
// Check that an event has different hashes for different values of the same
// metric.
TEST_F(KeyDataTest, DifferentValuesDifferentHashes) {
MakeKeyData();
EXPECT_NE(key_data_->HmacMetric(kProjectOneHash, kMetricOneHash, "first",
kKeyRotationPeriod),
key_data_->HmacMetric(kProjectOneHash, kMetricOneHash, "second",
kKeyRotationPeriod));
ExpectNoErrors();
}
// Ensure that KeyData::UserId is the expected value of SHA256(key).
TEST_F(KeyDataTest, CheckUserIDs) {
SetupKey(kProjectOneHash, kKey, Today(), kKeyRotationPeriod);
MakeKeyData();
EXPECT_EQ(HashToHex(key_data_->Id(kProjectOneHash, kKeyRotationPeriod)),
kUserId);
EXPECT_NE(HashToHex(key_data_->Id(kProjectTwoHash, kKeyRotationPeriod)),
kUserId);
ExpectKeyValidation(/*valid=*/1, /*created=*/1, /*rotated=*/0);
ExpectNoErrors();
}
// Ensure that KeyData::Hash returns expected values for a known key and value.
TEST_F(KeyDataTest, CheckHashes) {
SetupKey(kProjectOneHash, kKey, Today(), kKeyRotationPeriod);
MakeKeyData();
EXPECT_EQ(HashToHex(key_data_->HmacMetric(kProjectOneHash, kMetricOneHash,
kValueOne, kKeyRotationPeriod)),
kValueOneHash);
EXPECT_EQ(HashToHex(key_data_->HmacMetric(kProjectOneHash, kMetricTwoHash,
kValueTwo, kKeyRotationPeriod)),
kValueTwoHash);
ExpectKeyValidation(/*valid=*/2, /*created=*/0, /*rotated=*/0);
ExpectNoErrors();
}
// Check that keys for a event are correctly rotated after a given rotation
// period.
TEST_F(KeyDataTest, KeysRotated) {
const int start_day = Today();
SetupKey(kProjectOneHash, kKey, start_day, kKeyRotationPeriod);
MakeKeyData();
const uint64_t first_id = key_data_->Id(kProjectOneHash, kKeyRotationPeriod);
EXPECT_EQ(key_data_->LastKeyRotation(kProjectOneHash), start_day);
ExpectKeyValidation(/*valid=*/1, /*created=*/0, /*rotated=*/0);
{
// Advancing by |kKeyRotationPeriod|-1 days, the key should not be rotated.
task_environment_.AdvanceClock(base::Days(kKeyRotationPeriod - 1));
EXPECT_EQ(key_data_->Id(kProjectOneHash, kKeyRotationPeriod), first_id);
EXPECT_EQ(key_data_->LastKeyRotation(kProjectOneHash), start_day);
SaveKeyData();
ASSERT_EQ(GetKey(kProjectOneHash).last_rotation(), start_day);
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(base::Days(kKeyRotationPeriod + 1));
EXPECT_NE(key_data_->Id(kProjectOneHash, kKeyRotationPeriod), first_id);
SaveKeyData();
int expected_last_key_rotation = start_day + 2 * kKeyRotationPeriod;
EXPECT_EQ(GetKey(kProjectOneHash).last_rotation(),
expected_last_key_rotation);
EXPECT_EQ(key_data_->LastKeyRotation(kProjectOneHash),
expected_last_key_rotation);
ExpectKeyValidation(/*valid=*/2, /*created=*/0, /*rotated=*/1);
ASSERT_EQ(GetKey(kProjectOneHash).rotation_period(), kKeyRotationPeriod);
}
{
// Advancing by |2* kKeyRotationPeriod| days, the last rotation day should
// now 4 periods of |kKeyRotationPeriod| days ahead.
task_environment_.AdvanceClock(base::Days(kKeyRotationPeriod * 2));
key_data_->Id(kProjectOneHash, kKeyRotationPeriod);
SaveKeyData();
int expected_last_key_rotation = start_day + 4 * kKeyRotationPeriod;
EXPECT_EQ(GetKey(kProjectOneHash).last_rotation(),
expected_last_key_rotation);
EXPECT_EQ(key_data_->LastKeyRotation(kProjectOneHash),
expected_last_key_rotation);
ExpectKeyValidation(/*valid=*/2, /*created=*/0, /*rotated=*/2);
}
}
// Check that keys with updated rotations are correctly rotated.
TEST_F(KeyDataTest, KeysWithUpdatedRotations) {
int first_key_rotation_period = 60;
const int start_day = Today();
SetupKey(kProjectOneHash, kKey, start_day, first_key_rotation_period);
MakeKeyData();
const uint64_t first_id =
key_data_->Id(kProjectOneHash, first_key_rotation_period);
EXPECT_EQ(key_data_->LastKeyRotation(kProjectOneHash), start_day);
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|.
int new_key_rotation_period = 50;
task_environment_.AdvanceClock(base::Days(new_key_rotation_period + 1));
const uint64_t second_id =
key_data_->Id(kProjectOneHash, new_key_rotation_period);
EXPECT_NE(first_id, second_id);
SaveKeyData();
// Key should have been rotated with new_key_rotation_period.
int expected_last_key_rotation = start_day + new_key_rotation_period;
EXPECT_EQ(GetKey(kProjectOneHash).last_rotation(),
expected_last_key_rotation);
EXPECT_EQ(key_data_->LastKeyRotation(kProjectOneHash),
expected_last_key_rotation);
ExpectKeyValidation(/*valid=*/1, /*created=*/0, /*rotated=*/1);
}
} // namespace metrics::structured