blob: 20afe81d1cc198691dec71e1c7eed625cbd920c5 [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// 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/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_mock_clock_override.h"
#include "base/test/task_environment.h"
#include "base/values.h"
#include "components/metrics/structured/event_base.h"
#include "components/metrics/structured/histogram_util.h"
#include "components/metrics/structured/recorder.h"
#include "components/metrics/structured/structured_events.h"
#include "components/prefs/json_pref_store.h"
#include "components/prefs/persistent_pref_store.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace metrics {
namespace structured {
namespace internal {
namespace {
// 32 byte long test key, matching the size of a real key.
constexpr char kKey[] = "abcdefghijklmnopqrstuvwxyzabcdef";
// These event and metric names are used for testing.
// - event: TestEventOne
// - metric: TestValueOne
// - metric: TestValueTwo
// - event: TestEventTwo
// - metric: TestValueOne
// The name hash of "TestEventOne".
constexpr uint64_t kEventOneHash = UINT64_C(15619026293081468407);
// The name hash of "TestEventTwo".
constexpr uint64_t kEventTwoHash = UINT64_C(15791833939776536363);
// The name hash of "TestProject".
constexpr uint64_t kProjectHash = UINT64_C(17426425568333718899);
// 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),
// "abcdefghijklmnopqrstuvwxyzabcdef")
constexpr char kValueOne[] = "value one";
constexpr char kValueTwo[] = "value two";
constexpr char kValueOneHash[] = "805B8790DC69B773";
constexpr char kValueTwoHash[] = "87CEF12FB15E0B3A";
std::string HashToHex(const uint64_t hash) {
return base::HexEncode(&hash, sizeof(uint64_t));
}
std::string KeyPath(const uint64_t event) {
return base::StrCat({"keys.", base::NumberToString(event), ".key"});
}
std::string LastRotationPath(const uint64_t event) {
return base::StrCat({"keys.", base::NumberToString(event), ".last_rotation"});
}
std::string RotationPeriodPath(const uint64_t event) {
return base::StrCat(
{"keys.", base::NumberToString(event), ".rotation_period"});
}
// Returns the total number of events registered in structured.xml. This is used
// to determine how many keys we expect to load or rotate on initialization.
int NumberOfEvents() {
return sizeof(metrics::structured::events::kProjectNameHashes) /
sizeof(uint64_t);
}
} // namespace
class KeyDataTest : public testing::Test {
protected:
void SetUp() override { ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); }
void StandardSetup() {
MakeKeyStore();
MakeKeyData();
CommitKeyStore();
}
void ResetState() {
key_data_.reset();
key_store_.reset();
base::DeleteFile(GetKeyStorePath());
ASSERT_FALSE(base::PathExists(GetKeyStorePath()));
}
void MakeKeyStore() {
key_store_ = new JsonPrefStore(GetKeyStorePath());
key_store_->ReadPrefs();
}
void MakeKeyData() { key_data_ = std::make_unique<KeyData>(GetKeyStore()); }
void CommitKeyStore() {
key_store_->CommitPendingWrite();
Wait();
ASSERT_TRUE(base::PathExists(GetKeyStorePath()));
}
JsonPrefStore* GetKeyStore() { return key_store_.get(); }
base::FilePath GetKeyStorePath() {
return temp_dir_.GetPath().Append("keys.json");
}
std::string GetString(const std::string& path) {
const base::Value* value;
GetKeyStore()->GetValue(path, &value);
return value->GetString();
}
int GetInt(const std::string& path) {
const base::Value* value;
GetKeyStore()->GetValue(path, &value);
return value->GetInt();
}
void SetString(const std::string& path, const std::string& value) {
key_store_->SetValue(path, std::make_unique<base::Value>(value),
WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS);
CommitKeyStore();
}
void SetInt(const std::string& path, const int value) {
key_store_->SetValue(path, std::make_unique<base::Value>(value),
WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS);
CommitKeyStore();
}
void SetKeyData(const uint64_t event,
const std::string& key,
const int last_rotation,
const int rotation_period) {
SetString(KeyPath(event), key);
SetInt(LastRotationPath(event), last_rotation);
SetInt(RotationPeriodPath(event), rotation_period);
}
void Wait() { task_environment_.RunUntilIdle(); }
void ExpectNoErrors() {
histogram_tester_.ExpectTotalCount("UMA.StructuredMetrics.InternalError",
0);
}
base::test::TaskEnvironment task_environment_{
base::test::TaskEnvironment::MainThreadType::UI,
base::test::TaskEnvironment::ThreadPoolExecutionMode::QUEUED};
base::ScopedTempDir temp_dir_;
base::ScopedMockClockOverride time_;
base::HistogramTester histogram_tester_;
scoped_refptr<JsonPrefStore> key_store_;
std::unique_ptr<KeyData> key_data_;
};
// If there is no key store file present, check that new keys are generated for
// each event, and those keys are of the right length and different from each
// other.
TEST_F(KeyDataTest, GeneratesKeysForEvents) {
StandardSetup();
histogram_tester_.ExpectUniqueSample(
"UMA.StructuredMetrics.KeyValidationState", KeyValidationState::kCreated,
NumberOfEvents());
const std::string key_one = GetString(KeyPath(kEventOneHash));
const std::string key_two = GetString(KeyPath(kProjectHash));
EXPECT_EQ(key_one.size(), 32ul);
EXPECT_EQ(key_two.size(), 32ul);
EXPECT_NE(key_one, key_two);
}
// 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 = 0; i < 10; ++i) {
ResetState();
StandardSetup();
keys.insert(GetString(KeyPath(kEventOneHash)));
histogram_tester_.ExpectUniqueSample(
"UMA.StructuredMetrics.KeyValidationState",
KeyValidationState::kCreated, NumberOfEvents() * (i + 1));
}
EXPECT_EQ(keys.size(), 10ul);
}
// If there is an existing key store file, check that its keys are not replaced.
TEST_F(KeyDataTest, ReuseExistingKeys) {
StandardSetup();
histogram_tester_.ExpectBucketCount(
"UMA.StructuredMetrics.KeyValidationState", KeyValidationState::kCreated,
NumberOfEvents());
const std::string key_one = GetString(KeyPath(kEventOneHash));
CommitKeyStore();
key_data_.reset();
key_store_.reset();
StandardSetup();
histogram_tester_.ExpectBucketCount(
"UMA.StructuredMetrics.KeyValidationState", KeyValidationState::kValid,
NumberOfEvents());
const std::string key_two = GetString(KeyPath(kEventOneHash));
EXPECT_EQ(key_one, key_two);
}
// Check that different events have different hashes for the same metric and
// value.
TEST_F(KeyDataTest, DifferentEventsDifferentHashes) {
StandardSetup();
// Even though
EXPECT_NE(
key_data_->HashForEventMetric(kEventOneHash, kMetricOneHash, "value"),
key_data_->HashForEventMetric(kEventTwoHash, kMetricOneHash, "value"));
ExpectNoErrors();
}
// Check that an event has different hashes for different values of the same
// metric.
TEST_F(KeyDataTest, DifferentMetricsDifferentHashes) {
StandardSetup();
EXPECT_NE(
key_data_->HashForEventMetric(kEventOneHash, kMetricOneHash, "first"),
key_data_->HashForEventMetric(kEventOneHash, kMetricOneHash, "second"));
ExpectNoErrors();
}
// Check that an event has different hashes for different metrics with the same
// value.
TEST_F(KeyDataTest, DifferentValuesDifferentHashes) {
StandardSetup();
EXPECT_NE(
key_data_->HashForEventMetric(kEventOneHash, kMetricOneHash, "value"),
key_data_->HashForEventMetric(kEventOneHash, kMetricTwoHash, "value"));
ExpectNoErrors();
}
// Ensure that KeyData::UserId is the expected value of SHA256(key).
TEST_F(KeyDataTest, CheckUserIDs) {
MakeKeyStore();
SetKeyData(kEventOneHash, kKey, 0, 90);
CommitKeyStore();
MakeKeyData();
EXPECT_EQ(HashToHex(key_data_->UserEventId(kEventOneHash)), kUserId);
EXPECT_NE(HashToHex(key_data_->UserEventId(kEventTwoHash)), kUserId);
ExpectNoErrors();
}
// Ensure that KeyData::Hash returns expected values for a known key and value.
TEST_F(KeyDataTest, CheckHashes) {
MakeKeyStore();
SetString(KeyPath(kEventOneHash), kKey);
SetKeyData(kEventOneHash, kKey, 0, 90);
CommitKeyStore();
MakeKeyData();
EXPECT_EQ(HashToHex(key_data_->HashForEventMetric(kEventOneHash,
kMetricOneHash, kValueOne)),
kValueOneHash);
EXPECT_EQ(HashToHex(key_data_->HashForEventMetric(kEventOneHash,
kMetricTwoHash, kValueTwo)),
kValueTwoHash);
ExpectNoErrors();
}
// Check that keys for a event are correctly rotated after the default 90 day
// rotation period.
TEST_F(KeyDataTest, KeysRotated) {
// This test intentionally doesn't test the key validation metric. Events can
// have custom rotation periods, so this test could rotate some arbitrary set
// of keys that we don't know ahead of time, which would require too much test
// logic.
StandardSetup();
const uint64_t first_id = key_data_->UserEventId(kEventOneHash);
const int start_day = (base::Time::Now() - base::Time::UnixEpoch()).InDays();
// TestEventOne has a default rotation period of 90 days.
EXPECT_EQ(GetInt(RotationPeriodPath(kEventOneHash)), 90);
// Set the last rotation to today for testing.
SetInt(LastRotationPath(kEventOneHash), start_day);
{
// Advancing by 50 days, the key should not be rotated.
key_data_.reset();
time_.Advance(base::TimeDelta::FromDays(50));
StandardSetup();
EXPECT_EQ(key_data_->UserEventId(kEventOneHash), first_id);
EXPECT_EQ(GetInt(LastRotationPath(kEventOneHash)), start_day);
}
{
// Advancing by another 50 days, the key should be rotated and the last
// rotation day should be incremented by 90.
key_data_.reset();
time_.Advance(base::TimeDelta::FromDays(50));
StandardSetup();
EXPECT_NE(key_data_->UserEventId(kEventOneHash), first_id);
EXPECT_EQ(GetInt(LastRotationPath(kEventOneHash)), start_day + 90);
}
{
// Advancing by 453 days, the last rotation day should now 6 periods of 90
// days ahead.
key_data_.reset();
time_.Advance(base::TimeDelta::FromDays(453));
StandardSetup();
EXPECT_EQ(GetInt(LastRotationPath(kEventOneHash)), start_day + 6 * 90);
}
}
} // namespace internal
} // namespace structured
} // namespace metrics