chromium/chrome/browser/nearby_sharing/certificates/nearby_share_certificate_storage_impl_unittest.cc

// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <algorithm>
#include <string>

#include "base/base64url.h"
#include "base/functional/bind.h"
#include "base/json/values_util.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/task/sequenced_task_runner.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "chrome/browser/nearby_sharing/certificates/constants.h"
#include "chrome/browser/nearby_sharing/certificates/nearby_share_certificate_storage_impl.h"
#include "chrome/browser/nearby_sharing/certificates/test_util.h"
#include "chrome/browser/nearby_sharing/common/nearby_share_prefs.h"
#include "chromeos/ash/services/nearby/public/mojom/nearby_share_settings.mojom.h"
#include "components/leveldb_proto/testing/fake_db.h"
#include "components/prefs/pref_registry.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/testing_pref_service.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace {

// NOTE: Make sure secret ID alphabetical ordering does not match the 1,2,3,..
// ordering to test sorting expiration times.
const char kSecretId1[] = "b_secretid1";
const char kSecretKey1[] = "secretkey1";
const char kPublicKey1[] = "publickey1";
const int64_t kStartSeconds1 = 0;
const int32_t kStartNanos1 = 10;
const int64_t kEndSeconds1 = 100;
const int32_t kEndNanos1 = 30;
const bool kForSelectedContacts1 = false;
const char kMetadataEncryptionKey1[] = "metadataencryptionkey1";
const char kEncryptedMetadataBytes1[] = "encryptedmetadatabytes1";
const char kMetadataEncryptionKeyTag1[] = "metadataencryptionkeytag1";
const char kSecretId2[] = "c_secretid2";
const char kSecretKey2[] = "secretkey2";
const char kPublicKey2[] = "publickey2";
const int64_t kStartSeconds2 = 0;
const int32_t kStartNanos2 = 20;
const int64_t kEndSeconds2 = 200;
const int32_t kEndNanos2 = 30;
const bool kForSelectedContacts2 = false;
const char kMetadataEncryptionKey2[] = "metadataencryptionkey2";
const char kEncryptedMetadataBytes2[] = "encryptedmetadatabytes2";
const char kMetadataEncryptionKeyTag2[] = "metadataencryptionkeytag2";
const char kSecretId3[] = "a_secretid3";
const char kSecretKey3[] = "secretkey3";
const char kPublicKey3[] = "publickey3";
const int64_t kStartSeconds3 = 0;
const int32_t kStartNanos3 = 30;
const int64_t kEndSeconds3 = 300;
const int32_t kEndNanos3 = 30;
const bool kForSelectedContacts3 = false;
const char kMetadataEncryptionKey3[] = "metadataencryptionkey3";
const char kEncryptedMetadataBytes3[] = "encryptedmetadatabytes3";
const char kMetadataEncryptionKeyTag3[] = "metadataencryptionkeytag3";
const char kSecretId4[] = "d_secretid4";
const char kSecretKey4[] = "secretkey4";
const char kPublicKey4[] = "publickey4";
const int64_t kStartSeconds4 = 0;
const int32_t kStartNanos4 = 10;
const int64_t kEndSeconds4 = 100;
const int32_t kEndNanos4 = 30;
const bool kForSelectedContacts4 = false;
const char kMetadataEncryptionKey4[] = "metadataencryptionkey4";
const char kEncryptedMetadataBytes4[] = "encryptedmetadatabytes4";
const char kMetadataEncryptionKeyTag4[] = "metadataencryptionkeytag4";

std::string EncodeString(const std::string& unencoded_string) {
  std::string encoded_string;
  base::Base64UrlEncode(unencoded_string,
                        base::Base64UrlEncodePolicy::INCLUDE_PADDING,
                        &encoded_string);
  return encoded_string;
}

nearby::sharing::proto::PublicCertificate CreatePublicCertificate(
    const std::string& secret_id,
    const std::string& secret_key,
    const std::string& public_key,
    int64_t start_seconds,
    int32_t start_nanos,
    int64_t end_seconds,
    int32_t end_nanos,
    bool for_selected_contacts,
    const std::string& metadata_encryption_key,
    const std::string& encrypted_metadata_bytes,
    const std::string& metadata_encryption_key_tag) {
  nearby::sharing::proto::PublicCertificate cert;
  cert.set_secret_id(secret_id);
  cert.set_secret_key(secret_key);
  cert.set_public_key(public_key);
  cert.mutable_start_time()->set_seconds(start_seconds);
  cert.mutable_start_time()->set_nanos(start_nanos);
  cert.mutable_end_time()->set_seconds(end_seconds);
  cert.mutable_end_time()->set_nanos(end_nanos);
  cert.set_for_selected_contacts(for_selected_contacts);
  cert.set_metadata_encryption_key(metadata_encryption_key);
  cert.set_encrypted_metadata_bytes(encrypted_metadata_bytes);
  cert.set_metadata_encryption_key_tag(metadata_encryption_key_tag);
  return cert;
}

std::vector<NearbySharePrivateCertificate> CreatePrivateCertificates(
    size_t n,
    nearby_share::mojom::Visibility visibility) {
  std::vector<NearbySharePrivateCertificate> certs;
  certs.reserve(n);
  for (size_t i = 0; i < n; ++i) {
    certs.emplace_back(visibility, base::Time::Now(),
                       GetNearbyShareTestMetadata());
  }
  return certs;
}

base::Time TimestampToTime(nearby::sharing::proto::Timestamp timestamp) {
  return base::Time::UnixEpoch() + base::Seconds(timestamp.seconds()) +
         base::Nanoseconds(timestamp.nanos());
}

}  // namespace

class NearbyShareCertificateStorageImplTest : public ::testing::Test {
 public:
  NearbyShareCertificateStorageImplTest() = default;
  ~NearbyShareCertificateStorageImplTest() override = default;
  NearbyShareCertificateStorageImplTest(
      NearbyShareCertificateStorageImplTest&) = delete;
  NearbyShareCertificateStorageImplTest& operator=(
      NearbyShareCertificateStorageImplTest&) = delete;

  void SetUp() override {
    auto db = std::make_unique<
        leveldb_proto::test::FakeDB<nearby::sharing::proto::PublicCertificate>>(
        &db_entries_);
    db_ = db.get();

    pref_service_ = std::make_unique<TestingPrefServiceSimple>();
    pref_service_->registry()->RegisterDictionaryPref(
        prefs::kNearbySharingPublicCertificateExpirationDictPrefName);
    pref_service_->registry()->RegisterListPref(
        prefs::kNearbySharingPrivateCertificateListPrefName);

    // Add public certificates to database before construction. Needed
    // to ensure test coverage of FetchPublicCertificateExpirations.
    PrepopulatePublicCertificates();

    cert_store_ = std::make_unique<NearbyShareCertificateStorageImpl>(
        pref_service_.get(), std::move(db));
  }

  void PrepopulatePublicCertificates() {
    std::vector<nearby::sharing::proto::PublicCertificate> pub_certs;
    pub_certs.emplace_back(CreatePublicCertificate(
        kSecretId1, kSecretKey1, kPublicKey1, kStartSeconds1, kStartNanos1,
        kEndSeconds1, kEndNanos1, kForSelectedContacts1,
        kMetadataEncryptionKey1, kEncryptedMetadataBytes1,
        kMetadataEncryptionKeyTag1));
    pub_certs.emplace_back(CreatePublicCertificate(
        kSecretId2, kSecretKey2, kPublicKey2, kStartSeconds2, kStartNanos2,
        kEndSeconds2, kEndNanos2, kForSelectedContacts2,
        kMetadataEncryptionKey2, kEncryptedMetadataBytes2,
        kMetadataEncryptionKeyTag2));
    pub_certs.emplace_back(CreatePublicCertificate(
        kSecretId3, kSecretKey3, kPublicKey3, kStartSeconds3, kStartNanos3,
        kEndSeconds3, kEndNanos3, kForSelectedContacts3,
        kMetadataEncryptionKey3, kEncryptedMetadataBytes3,
        kMetadataEncryptionKeyTag3));

    base::Value::Dict expiration_dict;
    db_entries_.clear();
    for (auto& cert : pub_certs) {
      expiration_dict.Set(EncodeString(cert.secret_id()),
                          base::TimeToValue(TimestampToTime(cert.end_time())));
      db_entries_.emplace(cert.secret_id(), std::move(cert));
    }
    pref_service_->SetDict(
        prefs::kNearbySharingPublicCertificateExpirationDictPrefName,
        std::move(expiration_dict));
  }

  void CaptureBoolCallback(bool* dest, bool src) { *dest = src; }

  void PublicCertificateCallback(
      std::vector<nearby::sharing::proto::PublicCertificate>*
          public_certificates,
      base::OnceClosure complete,
      bool success,
      std::unique_ptr<std::vector<nearby::sharing::proto::PublicCertificate>>
          result) {
    if (success && result) {
      public_certificates->swap(*result);
    }
    std::move(complete).Run();
  }

 protected:
  std::unique_ptr<TestingPrefServiceSimple> pref_service_;
  std::map<std::string, nearby::sharing::proto::PublicCertificate> db_entries_;
  raw_ptr<
      leveldb_proto::test::FakeDB<nearby::sharing::proto::PublicCertificate>,
      DanglingUntriaged>
      db_;
  std::unique_ptr<NearbyShareCertificateStorage> cert_store_;
  std::vector<nearby::sharing::proto::PublicCertificate> public_certificates_;
};

TEST_F(NearbyShareCertificateStorageImplTest, DeferredCallbackQueue) {
  base::test::SingleThreadTaskEnvironment task_environment;
  base::RunLoop run_loop;

  std::vector<nearby::sharing::proto::PublicCertificate> public_certificates;

  cert_store_->GetPublicCertificates(base::BindOnce(
      &NearbyShareCertificateStorageImplTest::PublicCertificateCallback,
      base::Unretained(this), &public_certificates, run_loop.QuitClosure()));

  db_->InitStatusCallback(leveldb_proto::Enums::InitStatus::kOK);

  // These callbacks have to be posted to ensure that they run after the
  // deferred callbacks posted during initialization.
  base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE,
      base::BindOnce(
          &leveldb_proto::test::FakeDB<
              nearby::sharing::proto::PublicCertificate>::LoadCallback,
          base::Unretained(db_), true));

  run_loop.Run();

  EXPECT_TRUE(public_certificates_.empty());
}

TEST_F(NearbyShareCertificateStorageImplTest, GetPublicCertificates) {
  db_->InitStatusCallback(leveldb_proto::Enums::InitStatus::kOK);

  std::vector<nearby::sharing::proto::PublicCertificate> public_certificates;
  cert_store_->GetPublicCertificates(base::BindOnce(
      &NearbyShareCertificateStorageImplTest::PublicCertificateCallback,
      base::Unretained(this), &public_certificates, base::BindOnce([] {})));
  db_->LoadCallback(true);

  ASSERT_EQ(3u, public_certificates.size());
  for (nearby::sharing::proto::PublicCertificate& cert : public_certificates) {
    std::string expected_serialized, actual_serialized;
    ASSERT_TRUE(cert.SerializeToString(&expected_serialized));
    ASSERT_TRUE(db_entries_.find(cert.secret_id())
                    ->second.SerializeToString(&actual_serialized));
    ASSERT_EQ(expected_serialized, actual_serialized);
  }
}

TEST_F(NearbyShareCertificateStorageImplTest, AddPublicCertificates) {
  db_->InitStatusCallback(leveldb_proto::Enums::InitStatus::kOK);

  std::vector<nearby::sharing::proto::PublicCertificate> new_certs = {
      CreatePublicCertificate(kSecretId3, kSecretKey2, kPublicKey2,
                              kStartSeconds2, kStartNanos2, kEndSeconds2,
                              kEndNanos2, kForSelectedContacts2,
                              kMetadataEncryptionKey2, kEncryptedMetadataBytes2,
                              kMetadataEncryptionKeyTag2),
      CreatePublicCertificate(kSecretId4, kSecretKey4, kPublicKey4,
                              kStartSeconds4, kStartNanos4, kEndSeconds4,
                              kEndNanos4, kForSelectedContacts4,
                              kMetadataEncryptionKey4, kEncryptedMetadataBytes4,
                              kMetadataEncryptionKeyTag4),
  };

  bool succeeded = false;
  cert_store_->AddPublicCertificates(
      new_certs,
      base::BindOnce(
          &NearbyShareCertificateStorageImplTest::CaptureBoolCallback,
          base::Unretained(this), &succeeded));
  db_->UpdateCallback(true);

  ASSERT_TRUE(succeeded);
  ASSERT_EQ(4u, db_entries_.size());
  ASSERT_EQ(1u, db_entries_.count(kSecretId3));
  ASSERT_EQ(1u, db_entries_.count(kSecretId4));
  auto& cert = db_entries_.find(kSecretId3)->second;
  EXPECT_EQ(kSecretKey2, cert.secret_key());
  EXPECT_EQ(kPublicKey2, cert.public_key());
  EXPECT_EQ(kStartSeconds2, cert.start_time().seconds());
  EXPECT_EQ(kStartNanos2, cert.start_time().nanos());
  EXPECT_EQ(kEndSeconds2, cert.end_time().seconds());
  EXPECT_EQ(kEndNanos2, cert.end_time().nanos());
  EXPECT_EQ(kForSelectedContacts2, cert.for_selected_contacts());
  EXPECT_EQ(kMetadataEncryptionKey2, cert.metadata_encryption_key());
  EXPECT_EQ(kEncryptedMetadataBytes2, cert.encrypted_metadata_bytes());
  EXPECT_EQ(kMetadataEncryptionKeyTag2, cert.metadata_encryption_key_tag());
  cert = db_entries_.find(kSecretId4)->second;
  EXPECT_EQ(kSecretKey4, cert.secret_key());
  EXPECT_EQ(kPublicKey4, cert.public_key());
  EXPECT_EQ(kStartSeconds4, cert.start_time().seconds());
  EXPECT_EQ(kStartNanos4, cert.start_time().nanos());
  EXPECT_EQ(kEndSeconds4, cert.end_time().seconds());
  EXPECT_EQ(kEndNanos4, cert.end_time().nanos());
  EXPECT_EQ(kForSelectedContacts4, cert.for_selected_contacts());
  EXPECT_EQ(kMetadataEncryptionKey4, cert.metadata_encryption_key());
  EXPECT_EQ(kEncryptedMetadataBytes4, cert.encrypted_metadata_bytes());
  EXPECT_EQ(kMetadataEncryptionKeyTag4, cert.metadata_encryption_key_tag());
}

TEST_F(NearbyShareCertificateStorageImplTest,
       RemoveExpiredPrivateCertificates) {
  db_->InitStatusCallback(leveldb_proto::Enums::InitStatus::kOK);

  std::vector<NearbySharePrivateCertificate> certs = CreatePrivateCertificates(
      3, nearby_share::mojom::Visibility::kAllContacts);
  cert_store_->ReplacePrivateCertificates(certs);

  std::vector<base::Time> expiration_times;
  for (const NearbySharePrivateCertificate& cert : certs) {
    expiration_times.push_back(cert.not_after());
  }
  std::sort(expiration_times.begin(), expiration_times.end());

  // Set current time to exceed the expiration times of the first two
  // certificates.
  base::Time now = expiration_times[1];

  cert_store_->RemoveExpiredPrivateCertificates(now);

  certs = *cert_store_->GetPrivateCertificates();
  ASSERT_EQ(1u, certs.size());
  for (const NearbySharePrivateCertificate& cert : certs) {
    EXPECT_LE(now, cert.not_after());
  }
}

TEST_F(NearbyShareCertificateStorageImplTest, RemoveExpiredPublicCertificates) {
  db_->InitStatusCallback(leveldb_proto::Enums::InitStatus::kOK);

  std::vector<base::Time> expiration_times;
  for (const auto& pair : db_entries_) {
    expiration_times.emplace_back(TimestampToTime(pair.second.end_time()));
  }
  std::sort(expiration_times.begin(), expiration_times.end());

  // The current time exceeds the expiration times of the first two certificates
  // even accounting for the expiration time tolerance applied to public
  // certificates to account for clock skew.
  base::Time now = expiration_times[1] +
                   kNearbySharePublicCertificateValidityBoundOffsetTolerance;

  bool succeeded = false;
  cert_store_->RemoveExpiredPublicCertificates(
      now, base::BindOnce(
               &NearbyShareCertificateStorageImplTest::CaptureBoolCallback,
               base::Unretained(this), &succeeded));
  db_->UpdateCallback(true);

  ASSERT_TRUE(succeeded);
  ASSERT_EQ(1u, db_entries_.size());
  for (const auto& pair : db_entries_) {
    EXPECT_LE(now - kNearbySharePublicCertificateValidityBoundOffsetTolerance,
              TimestampToTime(pair.second.end_time()));
  }
}

TEST_F(NearbyShareCertificateStorageImplTest, ReplaceGetPrivateCertificates) {
  db_->InitStatusCallback(leveldb_proto::Enums::InitStatus::kOK);

  auto certs_before = CreatePrivateCertificates(
      3, nearby_share::mojom::Visibility::kAllContacts);
  cert_store_->ReplacePrivateCertificates(certs_before);
  auto certs_after = cert_store_->GetPrivateCertificates();

  ASSERT_TRUE(certs_after.has_value());
  ASSERT_EQ(certs_before.size(), certs_after->size());
  for (size_t i = 0; i < certs_before.size(); ++i) {
    EXPECT_EQ(certs_before[i].ToDictionary(), (*certs_after)[i].ToDictionary());
  }

  certs_before = CreatePrivateCertificates(
      1, nearby_share::mojom::Visibility::kAllContacts);
  cert_store_->ReplacePrivateCertificates(certs_before);
  certs_after = cert_store_->GetPrivateCertificates();

  ASSERT_TRUE(certs_after.has_value());
  ASSERT_EQ(certs_before.size(), certs_after->size());
  for (size_t i = 0; i < certs_before.size(); ++i) {
    EXPECT_EQ(certs_before[i].ToDictionary(), (*certs_after)[i].ToDictionary());
  }
}

TEST_F(NearbyShareCertificateStorageImplTest, UpdatePrivateCertificates) {
  db_->InitStatusCallback(leveldb_proto::Enums::InitStatus::kOK);

  std::vector<NearbySharePrivateCertificate> initial_certs =
      CreatePrivateCertificates(3,
                                nearby_share::mojom::Visibility::kAllContacts);
  cert_store_->ReplacePrivateCertificates(initial_certs);

  NearbySharePrivateCertificate cert_to_update = initial_certs[1];
  EXPECT_EQ(initial_certs[1].ToDictionary(), cert_to_update.ToDictionary());
  cert_to_update.EncryptMetadataKey();
  EXPECT_NE(initial_certs[1].ToDictionary(), cert_to_update.ToDictionary());

  cert_store_->UpdatePrivateCertificate(cert_to_update);

  std::vector<NearbySharePrivateCertificate> new_certs =
      *cert_store_->GetPrivateCertificates();
  EXPECT_EQ(initial_certs.size(), new_certs.size());
  for (size_t i = 0; i < new_certs.size(); ++i) {
    NearbySharePrivateCertificate expected_cert =
        i == 1 ? cert_to_update : initial_certs[i];
    EXPECT_EQ(expected_cert.ToDictionary(), new_certs[i].ToDictionary());
  }
}

TEST_F(NearbyShareCertificateStorageImplTest,
       NextPrivateCertificateExpirationTime) {
  db_->InitStatusCallback(leveldb_proto::Enums::InitStatus::kOK);

  auto certs = CreatePrivateCertificates(
      3, nearby_share::mojom::Visibility::kAllContacts);
  cert_store_->ReplacePrivateCertificates(certs);
  std::optional<base::Time> next_expiration =
      cert_store_->NextPrivateCertificateExpirationTime();

  ASSERT_TRUE(next_expiration.has_value());
  bool found = false;
  for (auto& cert : certs) {
    EXPECT_GE(cert.not_after(), *next_expiration);
    if (cert.not_after() == *next_expiration)
      found = true;
  }
  EXPECT_TRUE(found);
}

TEST_F(NearbyShareCertificateStorageImplTest,
       NextPublicCertificateExpirationTime) {
  db_->InitStatusCallback(leveldb_proto::Enums::InitStatus::kOK);

  std::optional<base::Time> next_expiration =
      cert_store_->NextPublicCertificateExpirationTime();

  ASSERT_TRUE(next_expiration.has_value());
  bool found = false;
  for (const auto& pair : db_entries_) {
    base::Time curr_expiration = TimestampToTime(pair.second.end_time());
    EXPECT_GE(curr_expiration, *next_expiration);
    if (curr_expiration == *next_expiration)
      found = true;
  }
  EXPECT_TRUE(found);
}

TEST_F(NearbyShareCertificateStorageImplTest, ClearPrivateCertificates) {
  db_->InitStatusCallback(leveldb_proto::Enums::InitStatus::kOK);

  std::vector<NearbySharePrivateCertificate> certs_before =
      CreatePrivateCertificates(3,
                                nearby_share::mojom::Visibility::kAllContacts);
  cert_store_->ReplacePrivateCertificates(certs_before);
  cert_store_->ClearPrivateCertificates();
  auto certs_after = cert_store_->GetPrivateCertificates();

  ASSERT_TRUE(certs_after.has_value());
  EXPECT_EQ(0u, certs_after->size());
}

TEST_F(NearbyShareCertificateStorageImplTest,
       ClearPrivateCertificatesOfVisibility) {
  db_->InitStatusCallback(leveldb_proto::Enums::InitStatus::kOK);

  std::vector<NearbySharePrivateCertificate> certs_all_contacts =
      CreatePrivateCertificates(3,
                                nearby_share::mojom::Visibility::kAllContacts);
  std::vector<NearbySharePrivateCertificate> certs_selected_contacts =
      CreatePrivateCertificates(
          3, nearby_share::mojom::Visibility::kSelectedContacts);
  std::vector<NearbySharePrivateCertificate> all_certs;
  all_certs.reserve(certs_all_contacts.size() + certs_selected_contacts.size());
  all_certs.insert(all_certs.end(), certs_all_contacts.begin(),
                   certs_all_contacts.end());
  all_certs.insert(all_certs.end(), certs_selected_contacts.begin(),
                   certs_selected_contacts.end());

  // Remove all-contacts certs then selected-contacts certs.
  {
    cert_store_->ReplacePrivateCertificates(all_certs);
    cert_store_->ClearPrivateCertificatesOfVisibility(
        nearby_share::mojom::Visibility::kAllContacts);
    auto certs_after = cert_store_->GetPrivateCertificates();
    ASSERT_TRUE(certs_after.has_value());
    ASSERT_EQ(certs_selected_contacts.size(), certs_after->size());
    for (size_t i = 0; i < certs_selected_contacts.size(); ++i) {
      EXPECT_EQ(certs_selected_contacts[i].ToDictionary(),
                (*certs_after)[i].ToDictionary());
    }

    cert_store_->ClearPrivateCertificatesOfVisibility(
        nearby_share::mojom::Visibility::kSelectedContacts);
    certs_after = cert_store_->GetPrivateCertificates();
    ASSERT_TRUE(certs_after.has_value());
    EXPECT_EQ(0u, certs_after->size());
  }

  // Remove selected-contacts certs then all-contacts certs.
  {
    cert_store_->ReplacePrivateCertificates(all_certs);
    cert_store_->ClearPrivateCertificatesOfVisibility(
        nearby_share::mojom::Visibility::kSelectedContacts);
    auto certs_after = cert_store_->GetPrivateCertificates();
    ASSERT_TRUE(certs_after.has_value());
    ASSERT_EQ(certs_all_contacts.size(), certs_after->size());
    for (size_t i = 0; i < certs_all_contacts.size(); ++i) {
      EXPECT_EQ(certs_all_contacts[i].ToDictionary(),
                (*certs_after)[i].ToDictionary());
    }

    cert_store_->ClearPrivateCertificatesOfVisibility(
        nearby_share::mojom::Visibility::kAllContacts);
    certs_after = cert_store_->GetPrivateCertificates();
    ASSERT_TRUE(certs_after.has_value());
    EXPECT_EQ(0u, certs_after->size());
  }
}