blob: 71ce59d85469f4e7aa41059dfe0b365aa73dd591 [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/lib/key_data.h"
#include <memory>
#include <utility>
#include "base/check.h"
#include "base/logging.h"
#include "base/notreached.h"
#include "base/numerics/byte_conversions.h"
#include "base/rand_util.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/time/time.h"
#include "base/unguessable_token.h"
#include "components/metrics/structured/lib/histogram_util.h"
#include "components/metrics/structured/lib/key_util.h"
#include "crypto/hash.h"
#include "crypto/hmac.h"
namespace metrics::structured {
namespace {
std::string HashToHex(const uint64_t hash) {
return base::HexEncode(&hash, sizeof(uint64_t));
}
int NowInDays() {
return (base::Time::Now() - base::Time::UnixEpoch()).InDays();
}
} // namespace
KeyData::KeyData(std::unique_ptr<StorageDelegate> storage_delegate)
: storage_delegate_(std::move(storage_delegate)) {
CHECK(storage_delegate_);
}
KeyData::~KeyData() = default;
uint64_t KeyData::Id(const uint64_t project_name_hash,
base::TimeDelta key_rotation_period) {
if (!storage_delegate_->IsReady()) {
return 0u;
}
// Retrieve the key for |project_name_hash|.
EnsureKeyUpdated(project_name_hash, key_rotation_period);
const std::optional<std::string_view> key = GetKeyBytes(project_name_hash);
CHECK(key);
// Compute and return the hash.
auto hash = crypto::hash::Sha256(*key);
return base::U64FromNativeEndian(base::span<uint8_t>(hash).first<8>());
}
uint64_t KeyData::HmacMetric(const uint64_t project_name_hash,
const uint64_t metric_name_hash,
const std::string& value,
base::TimeDelta key_rotation_period) {
if (!storage_delegate_->IsReady()) {
return 0u;
}
// Retrieve the key for |project_name_hash|.
EnsureKeyUpdated(project_name_hash, key_rotation_period);
const std::optional<std::string_view> key = GetKeyBytes(project_name_hash);
CHECK(key);
// Compute and return the digest.
const std::string salted_value =
base::StrCat({HashToHex(metric_name_hash), value});
auto hmac = crypto::hmac::SignSha256(base::as_byte_span(*key),
base::as_byte_span(salted_value));
return base::U64FromNativeEndian(base::span<uint8_t>(hmac).first<8>());
}
std::optional<base::TimeDelta> KeyData::LastKeyRotation(
const uint64_t project_name_hash) const {
const KeyProto* key = storage_delegate_->GetKey(project_name_hash);
if (!key) {
return std::nullopt;
}
return base::Days(key->last_rotation());
}
std::optional<int> KeyData::GetKeyAgeInWeeks(uint64_t project_name_hash) const {
std::optional<base::TimeDelta> last_rotation =
LastKeyRotation(project_name_hash);
if (!last_rotation.has_value()) {
return std::nullopt;
}
const int now = NowInDays();
const int days_since_rotation = now - last_rotation->InDays();
return days_since_rotation / 7;
}
void KeyData::Purge() {
storage_delegate_->Purge();
}
void KeyData::EnsureKeyUpdated(const uint64_t project_name_hash,
base::TimeDelta key_rotation_period) {
CHECK(storage_delegate_->IsReady());
const int now = NowInDays();
const int key_rotation_period_days = key_rotation_period.InDays();
const KeyProto* key = storage_delegate_->GetKey(project_name_hash);
// Generate or rotate key.
if (!key || key->last_rotation() == 0) {
LogKeyValidation(KeyValidationState::kCreated);
// If the key does not exist, generate a new one. Set the last rotation to a
// uniformly selected day between today and |key_rotation_period| days
// ago, to uniformly distribute users amongst rotation cohorts.
const int rotation_seed = base::RandInt(0, key_rotation_period_days - 1);
storage_delegate_->UpsertKey(project_name_hash,
base::Days(now - rotation_seed),
key_rotation_period);
} else if (now - key->last_rotation() > key_rotation_period_days) {
LogKeyValidation(KeyValidationState::kRotated);
// If the key is outdated, generate a new one. Update the last rotation
// such that the user stays in the same cohort.
//
// Note that if the max key rotation period has changed, the new rotation
// period will be used to calculate whether the key should be rotated or
// not.
const int new_last_rotation =
now - (now - key->last_rotation()) % key_rotation_period_days;
storage_delegate_->UpsertKey(
project_name_hash, base::Days(new_last_rotation), key_rotation_period);
} else {
LogKeyValidation(KeyValidationState::kValid);
}
}
const std::optional<std::string_view> KeyData::GetKeyBytes(
const uint64_t project_name_hash) const {
// Re-fetch the key after keys are rotated.
const KeyProto* key = storage_delegate_->GetKey(project_name_hash);
if (!key) {
return std::nullopt;
}
// Return the key unless it's the wrong size, in which case return nullopt.
const std::string_view key_string = key->key();
if (key_string.size() != kKeySize) {
return std::nullopt;
}
return key_string;
}
} // namespace metrics::structured