// 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 "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 "base/values.h"
#include "components/metrics/structured/histogram_util.h"
#include "components/metrics/structured/structured_events.h"
#include "components/prefs/json_pref_store.h"
#include "crypto/hmac.h"
#include "crypto/sha2.h"

namespace metrics {
namespace structured {
namespace internal {
namespace {

// The expected size of a key, in bytes.
constexpr size_t kKeySize = 32;

// The default maximum number of days before rotating keys.
constexpr size_t kDefaultRotationPeriod = 90;

// Generates a key, which is the string representation of
// base::UnguessableToken, and is of size |kKeySize| bytes.
std::string GenerateKey() {
  const std::string key = base::UnguessableToken::Create().ToString();
  DCHECK_EQ(key.size(), kKeySize);
  return key;
}

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"});
}

}  // namespace

KeyData::KeyData(JsonPrefStore* key_store) : key_store_(key_store) {
  DCHECK(key_store_);
  ValidateKeys();
}

KeyData::~KeyData() = default;

base::Optional<std::string> KeyData::ValidateAndGetKey(
    const uint64_t project_name_hash) {
  DCHECK(key_store_);
  const int now = (base::Time::Now() - base::Time::UnixEpoch()).InDays();
  bool key_is_valid = true;

  // If the key for |project_name_hash| doesn't exist, initialize new key data.
  // Set the last rotation to a uniformly selected day between today and
  // |kDefaultRotationPeriod| days ago, to uniformly distribute users amongst
  // rotation cohorts.
  if (!key_store_->GetValue(KeyPath(project_name_hash), nullptr)) {
    const int rotation_seed = base::RandInt(0, kDefaultRotationPeriod - 1);
    SetRotationPeriod(project_name_hash, kDefaultRotationPeriod);
    SetLastRotation(project_name_hash, now - rotation_seed);
    SetKey(project_name_hash, GenerateKey());

    LogKeyValidation(KeyValidationState::kCreated);
    key_is_valid = false;
  }

  // If the key for |project_name_hash| is outdated, generate a new key and
  // write it to the |keys| pref store along with updated rotation data. Update
  // the last rotation such that the user stays in the same cohort.
  const int rotation_period = GetRotationPeriod(project_name_hash);
  const int last_rotation = GetLastRotation(project_name_hash);
  if (now - last_rotation > rotation_period) {
    const int new_last_rotation = now - (now - last_rotation) % rotation_period;
    SetLastRotation(project_name_hash, new_last_rotation);
    SetKey(project_name_hash, GenerateKey());

    LogKeyValidation(KeyValidationState::kRotated);
    key_is_valid = false;
  }

  if (key_is_valid) {
    LogKeyValidation(KeyValidationState::kValid);
  }

  const base::Value* key_json;
  if (!(key_store_->GetValue(KeyPath(project_name_hash), &key_json) &&
        key_json->is_string())) {
    LogInternalError(StructuredMetricsError::kMissingKey);
    return base::nullopt;
  }

  const std::string key = key_json->GetString();
  if (key.size() != kKeySize) {
    LogInternalError(StructuredMetricsError::kWrongKeyLength);
    return base::nullopt;
  }

  return key;
}

void KeyData::ValidateKeys() {
  for (const uint64_t project_name_hash :
       metrics::structured::events::kProjectNameHashes) {
    ValidateAndGetKey(project_name_hash);
  }
}

void KeyData::SetLastRotation(const uint64_t event, const int last_rotation) {
  return key_store_->SetValue(LastRotationPath(event),
                              std::make_unique<base::Value>(last_rotation),
                              WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS);
}

void KeyData::SetRotationPeriod(const uint64_t event,
                                const int rotation_period) {
  return key_store_->SetValue(RotationPeriodPath(event),
                              std::make_unique<base::Value>(rotation_period),
                              WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS);
}

void KeyData::SetKey(const uint64_t project_name_hash, const std::string& key) {
  return key_store_->SetValue(KeyPath(project_name_hash),
                              std::make_unique<base::Value>(key),
                              WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS);
}

int KeyData::GetLastRotation(const uint64_t event) {
  const base::Value* value;
  if (!(key_store_->GetValue(LastRotationPath(event), &value) &&
        value->is_int())) {
    LogInternalError(StructuredMetricsError::kMissingLastRotation);
    NOTREACHED();
    return 0u;
  }
  return value->GetInt();
}

int KeyData::GetRotationPeriod(const uint64_t event) {
  const base::Value* value;
  if (!(key_store_->GetValue(RotationPeriodPath(event), &value) &&
        value->is_int())) {
    LogInternalError(StructuredMetricsError::kMissingRotationPeriod);
    NOTREACHED();
    return 0u;
  }
  return value->GetInt();
}

uint64_t KeyData::UserEventId(const uint64_t project_name_hash) {
  // Retrieve the key for |event|.
  const base::Optional<std::string> key = ValidateAndGetKey(project_name_hash);
  if (!key) {
    NOTREACHED();
    return 0u;
  }

  // Compute and return the hash.
  uint64_t hash;
  crypto::SHA256HashString(key.value(), &hash, sizeof(uint64_t));
  return hash;
}

uint64_t KeyData::HashForEventMetric(const uint64_t project_name_hash,
                                     const uint64_t metric,
                                     const std::string& value) {
  // Retrieve the key for |project_name_hash|.
  const base::Optional<std::string> key = ValidateAndGetKey(project_name_hash);
  if (!key) {
    NOTREACHED();
    return 0u;
  }

  // Initialize the HMAC.
  crypto::HMAC hmac(crypto::HMAC::HashAlgorithm::SHA256);
  CHECK(hmac.Init(key.value()));

  // Compute and return the digest.
  const std::string salted_value = base::StrCat({HashToHex(metric), value});
  uint64_t digest;
  CHECK(hmac.Sign(salted_value, reinterpret_cast<uint8_t*>(&digest),
                  sizeof(digest)));
  return digest;
}

}  // namespace internal
}  // namespace structured
}  // namespace metrics
