From e9ae655916201b21de96b7c3cdba80fd45122064 Mon Sep 17 00:00:00 2001 From: Francis Tsui Date: Wed, 24 Jan 2024 17:03:35 -0800 Subject: [PATCH] Add more sharing code to github PiperOrigin-RevId: 601277252 --- .github/workflows/validate.yaml | 6 +- sharing/certificates/BUILD | 124 +++ sharing/certificates/common.cc | 126 +++ sharing/certificates/common.h | 77 ++ sharing/certificates/common_test.cc | 184 ++++ sharing/certificates/constants.h | 117 +++ .../fake_nearby_share_certificate_manager.cc | 125 +++ .../fake_nearby_share_certificate_manager.h | 147 ++++ .../fake_nearby_share_certificate_storage.cc | 165 ++++ .../fake_nearby_share_certificate_storage.h | 178 ++++ .../nearby_share_certificate_manager.cc | 108 +++ .../nearby_share_certificate_manager.h | 154 ++++ .../nearby_share_certificate_manager_impl.cc | 706 +++++++++++++++ .../nearby_share_certificate_manager_impl.h | 219 +++++ ...rby_share_certificate_manager_impl_test.cc | 807 +++++++++++++++++ .../nearby_share_certificate_storage.cc | 130 +++ .../nearby_share_certificate_storage.h | 115 +++ .../nearby_share_certificate_storage_impl.cc | 602 +++++++++++++ .../nearby_share_certificate_storage_impl.h | 146 ++++ ...rby_share_certificate_storage_impl_test.cc | 810 ++++++++++++++++++ ...arby_share_decrypted_public_certificate.cc | 261 ++++++ ...earby_share_decrypted_public_certificate.h | 119 +++ ...share_decrypted_public_certificate_test.cc | 143 ++++ .../nearby_share_encrypted_metadata_key.cc | 51 ++ .../nearby_share_encrypted_metadata_key.h | 48 ++ .../nearby_share_private_certificate.cc | 420 +++++++++ .../nearby_share_private_certificate.h | 189 ++++ .../nearby_share_private_certificate_test.cc | 188 ++++ sharing/certificates/test_util.cc | 343 ++++++++ sharing/certificates/test_util.h | 84 ++ sharing/contacts/BUILD | 87 ++ .../fake_nearby_share_contact_manager.cc | 68 ++ .../fake_nearby_share_contact_manager.h | 116 +++ .../contacts/nearby_share_contact_manager.cc | 72 ++ .../contacts/nearby_share_contact_manager.h | 106 +++ .../nearby_share_contact_manager_impl.cc | 442 ++++++++++ .../nearby_share_contact_manager_impl.h | 166 ++++ .../nearby_share_contact_manager_impl_test.cc | 672 +++++++++++++++ .../contacts/nearby_share_contacts_sorter.cc | 161 ++++ .../contacts/nearby_share_contacts_sorter.h | 45 + .../nearby_share_contacts_sorter_test.cc | 251 ++++++ sharing/internal/base/BUILD | 10 - .../internal/base/utf_string_configuration.h | 120 --- .../internal/base/utf_string_conversions.cc | 510 ----------- .../internal/base/utf_string_conversions.h | 107 +-- .../base/utf_string_conversions_test.cc | 375 -------- sharing/local_device_data/BUILD | 97 +++ ..._nearby_share_local_device_data_manager.cc | 179 ++++ ...e_nearby_share_local_device_data_manager.h | 176 ++++ .../nearby_share_local_device_data_manager.cc | 69 ++ .../nearby_share_local_device_data_manager.h | 134 +++ ...by_share_local_device_data_manager_impl.cc | 414 +++++++++ ...rby_share_local_device_data_manager_impl.h | 121 +++ ...are_local_device_data_manager_impl_test.cc | 530 ++++++++++++ 54 files changed, 10809 insertions(+), 1111 deletions(-) create mode 100644 sharing/certificates/BUILD create mode 100644 sharing/certificates/common.cc create mode 100644 sharing/certificates/common.h create mode 100644 sharing/certificates/common_test.cc create mode 100644 sharing/certificates/constants.h create mode 100644 sharing/certificates/fake_nearby_share_certificate_manager.cc create mode 100644 sharing/certificates/fake_nearby_share_certificate_manager.h create mode 100644 sharing/certificates/fake_nearby_share_certificate_storage.cc create mode 100644 sharing/certificates/fake_nearby_share_certificate_storage.h create mode 100644 sharing/certificates/nearby_share_certificate_manager.cc create mode 100644 sharing/certificates/nearby_share_certificate_manager.h create mode 100644 sharing/certificates/nearby_share_certificate_manager_impl.cc create mode 100644 sharing/certificates/nearby_share_certificate_manager_impl.h create mode 100644 sharing/certificates/nearby_share_certificate_manager_impl_test.cc create mode 100644 sharing/certificates/nearby_share_certificate_storage.cc create mode 100644 sharing/certificates/nearby_share_certificate_storage.h create mode 100644 sharing/certificates/nearby_share_certificate_storage_impl.cc create mode 100644 sharing/certificates/nearby_share_certificate_storage_impl.h create mode 100644 sharing/certificates/nearby_share_certificate_storage_impl_test.cc create mode 100644 sharing/certificates/nearby_share_decrypted_public_certificate.cc create mode 100644 sharing/certificates/nearby_share_decrypted_public_certificate.h create mode 100644 sharing/certificates/nearby_share_decrypted_public_certificate_test.cc create mode 100644 sharing/certificates/nearby_share_encrypted_metadata_key.cc create mode 100644 sharing/certificates/nearby_share_encrypted_metadata_key.h create mode 100644 sharing/certificates/nearby_share_private_certificate.cc create mode 100644 sharing/certificates/nearby_share_private_certificate.h create mode 100644 sharing/certificates/nearby_share_private_certificate_test.cc create mode 100644 sharing/certificates/test_util.cc create mode 100644 sharing/certificates/test_util.h create mode 100644 sharing/contacts/BUILD create mode 100644 sharing/contacts/fake_nearby_share_contact_manager.cc create mode 100644 sharing/contacts/fake_nearby_share_contact_manager.h create mode 100644 sharing/contacts/nearby_share_contact_manager.cc create mode 100644 sharing/contacts/nearby_share_contact_manager.h create mode 100644 sharing/contacts/nearby_share_contact_manager_impl.cc create mode 100644 sharing/contacts/nearby_share_contact_manager_impl.h create mode 100644 sharing/contacts/nearby_share_contact_manager_impl_test.cc create mode 100644 sharing/contacts/nearby_share_contacts_sorter.cc create mode 100644 sharing/contacts/nearby_share_contacts_sorter.h create mode 100644 sharing/contacts/nearby_share_contacts_sorter_test.cc delete mode 100644 sharing/internal/base/utf_string_configuration.h delete mode 100644 sharing/internal/base/utf_string_conversions.cc delete mode 100644 sharing/internal/base/utf_string_conversions_test.cc create mode 100644 sharing/local_device_data/BUILD create mode 100644 sharing/local_device_data/fake_nearby_share_local_device_data_manager.cc create mode 100644 sharing/local_device_data/fake_nearby_share_local_device_data_manager.h create mode 100644 sharing/local_device_data/nearby_share_local_device_data_manager.cc create mode 100644 sharing/local_device_data/nearby_share_local_device_data_manager.h create mode 100644 sharing/local_device_data/nearby_share_local_device_data_manager_impl.cc create mode 100644 sharing/local_device_data/nearby_share_local_device_data_manager_impl.h create mode 100644 sharing/local_device_data/nearby_share_local_device_data_manager_impl_test.cc diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index 2336972a..cfc64898 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -32,11 +32,11 @@ jobs: steps: - uses: actions/checkout@v3 - name: Build Connections - run: CC=clang CXX=clang++ bazel build //connections:core --spawn_strategy=standalone + run: CC=clang CXX=clang++ bazel build --copt='-DGITHUB_BUILD' //connections:core --spawn_strategy=standalone - name: Build Presence - run: CC=clang CXX=clang++ bazel build //presence --spawn_strategy=standalone + run: CC=clang CXX=clang++ bazel build --copt='-DGITHUB_BUILD' //presence --spawn_strategy=standalone - name: Build Sharing - run: CC=clang CXX=clang++ bazel build //sharing/proto/... //sharing/internal/public:nearby_context //sharing/common:all //sharing/scheduling //sharing/fast_initiation:nearby_fast_initiation //sharing/analytics --spawn_strategy=standalone + run: CC=clang CXX=clang++ bazel build --copt='-DGITHUB_BUILD' //sharing/certificates //sharing/contacts //sharing/local_device_data //sharing/proto/... //sharing/internal/public:nearby_context //sharing/common:all //sharing/scheduling //sharing/fast_initiation:nearby_fast_initiation //sharing/analytics --spawn_strategy=standalone build-rust-linux: name: Build Rust on Linux diff --git a/sharing/certificates/BUILD b/sharing/certificates/BUILD new file mode 100644 index 00000000..89e5f54e --- /dev/null +++ b/sharing/certificates/BUILD @@ -0,0 +1,124 @@ +licenses(["notice"]) + +cc_library( + name = "certificates", + srcs = [ + "common.cc", + "nearby_share_certificate_manager.cc", + "nearby_share_certificate_manager_impl.cc", + "nearby_share_certificate_storage.cc", + "nearby_share_certificate_storage_impl.cc", + "nearby_share_decrypted_public_certificate.cc", + "nearby_share_encrypted_metadata_key.cc", + "nearby_share_private_certificate.cc", + ], + hdrs = [ + "common.h", + "constants.h", + "nearby_share_certificate_manager.h", + "nearby_share_certificate_manager_impl.h", + "nearby_share_certificate_storage.h", + "nearby_share_certificate_storage_impl.h", + "nearby_share_decrypted_public_certificate.h", + "nearby_share_encrypted_metadata_key.h", + "nearby_share_private_certificate.h", + ], + visibility = ["//visibility:public"], + deps = [ + "//internal/base", + "//internal/crypto_cros", + "//internal/platform:types", + "//internal/platform/implementation:types", + "//sharing/common", + "//sharing/contacts", + "//sharing/internal/api:platform", + "//sharing/internal/base", + "//sharing/internal/public:logging", + "//sharing/internal/public:types", + "//sharing/local_device_data", + "//sharing/proto:share_cc_proto", + "//sharing/scheduling", + "@com_google_absl//absl/container:btree", + "@com_google_absl//absl/container:flat_hash_map", + "@com_google_absl//absl/container:flat_hash_set", + "@com_google_absl//absl/functional:any_invocable", + "@com_google_absl//absl/functional:bind_front", + "@com_google_absl//absl/memory", + "@com_google_absl//absl/random", + "@com_google_absl//absl/status:statusor", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/synchronization", + "@com_google_absl//absl/time", + "@com_google_absl//absl/types:span", + "@com_google_protobuf//:protobuf_lite", + ], +) + +cc_library( + name = "test_support", + testonly = True, + srcs = [ + "fake_nearby_share_certificate_manager.cc", + "fake_nearby_share_certificate_storage.cc", + "test_util.cc", + ], + hdrs = [ + "fake_nearby_share_certificate_manager.h", + "fake_nearby_share_certificate_storage.h", + "test_util.h", + ], + visibility = ["//visibility:public"], + deps = [ + ":certificates", + "//internal/account", + "//internal/base:bluetooth_address", + "//internal/crypto_cros", + "//sharing/common", + "//sharing/contacts", + "//sharing/internal/api:platform", + "//sharing/internal/public:types", + "//sharing/local_device_data", + "//sharing/proto:share_cc_proto", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/time", + "@com_google_absl//absl/types:span", + ], +) + +cc_test( + name = "certificates_test", + srcs = [ + "common_test.cc", + "nearby_share_certificate_manager_impl_test.cc", + "nearby_share_certificate_storage_impl_test.cc", + "nearby_share_decrypted_public_certificate_test.cc", + "nearby_share_private_certificate_test.cc", + ], + deps = [ + ":certificates", + ":test_support", + "//internal/data:data_manager", + "//internal/platform/implementation:types", + "//internal/platform/implementation/g3", # fixdeps: keep + "//internal/test", + "//sharing/common", + "//sharing/contacts:test_support", + "//sharing/internal/api:mock_sharing_platform", + "//sharing/internal/public:logging", + "//sharing/internal/test:nearby_test", + "//sharing/local_device_data:test_support", + "//sharing/proto:share_cc_proto", + "//sharing/scheduling", + "//sharing/scheduling:test_support", + "@com_github_protobuf_matchers//protobuf-matchers", + "@com_google_absl//absl/container:flat_hash_map", + "@com_google_absl//absl/functional:any_invocable", + "@com_google_absl//absl/meta:type_traits", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:statusor", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/time", + "@com_google_absl//absl/types:span", + "@com_google_googletest//:gtest_main", + ], +) diff --git a/sharing/certificates/common.cc b/sharing/certificates/common.cc new file mode 100644 index 00000000..6395131a --- /dev/null +++ b/sharing/certificates/common.cc @@ -0,0 +1,126 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/certificates/common.h" + +#include +#include + +#include +#include +#include +#include +#include + +#include "absl/time/time.h" +#include "absl/types/span.h" +#include "internal/crypto_cros/encryptor.h" +#include "internal/crypto_cros/hkdf.h" +#include "internal/crypto_cros/random.h" +#include "internal/crypto_cros/symmetric_key.h" +#include "sharing/certificates/constants.h" +#include "sharing/internal/public/logging.h" + +namespace nearby { +namespace sharing { + +bool IsNearbyShareCertificateExpired(absl::Time current_time, + absl::Time not_after, + bool use_public_certificate_tolerance) { + absl::Duration tolerance = + use_public_certificate_tolerance + ? kNearbySharePublicCertificateValidityBoundOffsetTolerance + : absl::ZeroDuration(); + + return current_time >= not_after + tolerance; +} + +bool IsNearbyShareCertificateWithinValidityPeriod( + absl::Time current_time, absl::Time not_before, absl::Time not_after, + bool use_public_certificate_tolerance) { + absl::Duration tolerance = + use_public_certificate_tolerance + ? kNearbySharePublicCertificateValidityBoundOffsetTolerance + : absl::ZeroDuration(); + + return current_time >= not_before - tolerance && + !IsNearbyShareCertificateExpired(current_time, not_after, + use_public_certificate_tolerance); +} + +std::vector DeriveNearbyShareKey(absl::Span key, + size_t new_num_bytes) { + return crypto::HkdfSha256(key, + /*salt=*/absl::Span(), + /*info=*/absl::Span(), + new_num_bytes); +} + +std::vector ComputeAuthenticationTokenHash( + absl::Span authentication_token, + absl::Span secret_key) { + return crypto::HkdfSha256(authentication_token, secret_key, + /*info=*/absl::Span(), + kNearbyShareNumBytesAuthenticationTokenHash); +} + +std::vector GenerateRandomBytes(size_t num_bytes) { + std::vector bytes(num_bytes); + crypto::RandBytes(absl::Span(bytes)); + return bytes; +} + +std::unique_ptr CreateNearbyShareCtrEncryptor( + const crypto::SymmetricKey* secret_key, absl::Span salt) { + NL_DCHECK(secret_key); + NL_DCHECK_EQ(kNearbyShareNumBytesSecretKey, secret_key->key().size()); + NL_DCHECK_EQ(kNearbyShareNumBytesMetadataEncryptionKeySalt, salt.size()); + + auto encryptor = std::make_unique(); + + // For CTR mode, the iv input to Init() must be empty. Instead, the iv is + // set via SetCounter(). + if (!encryptor->Init(secret_key, crypto::Encryptor::Mode::CTR, + /*iv=*/absl::Span())) { + NL_LOG(ERROR) << "Encryptor could not be initialized."; + return nullptr; + } + + std::vector iv = + DeriveNearbyShareKey(salt, kNearbyShareNumBytesAesCtrIv); + if (!encryptor->SetCounter(iv)) { + NL_LOG(ERROR) << "Could not set encryptor counter."; + return nullptr; + } + + return encryptor; +} + +absl::Time FromJavaTime(int64_t ms_since_epoch) { + return absl::UnixEpoch() + absl::Milliseconds(ms_since_epoch); +} + +int64_t ToJavaTime(absl::Time time) { + // Preserve 0 so the invalid result doesn't depend on the platform. + if (time == absl::InfiniteFuture()) { + return std::numeric_limits::max(); + } else if (time == absl::InfinitePast()) { + return std::numeric_limits::min(); + } else { + return (time - absl::UnixEpoch()) / absl::Milliseconds(1); + } +} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/certificates/common.h b/sharing/certificates/common.h new file mode 100644 index 00000000..52ec0122 --- /dev/null +++ b/sharing/certificates/common.h @@ -0,0 +1,77 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_CERTIFICATES_COMMON_H_ +#define THIRD_PARTY_NEARBY_SHARING_CERTIFICATES_COMMON_H_ + +#include +#include + +#include +#include + +#include "absl/time/time.h" +#include "absl/types/span.h" +#include "internal/crypto_cros/encryptor.h" +#include "internal/crypto_cros/symmetric_key.h" + +namespace nearby { +namespace sharing { + +// Returns true if the |current_time| exceeds |not_after| by more than the +// public certificate clock-skew tolerance if applicable. +bool IsNearbyShareCertificateExpired(absl::Time current_time, + absl::Time not_after, + bool use_public_certificate_tolerance); + +// Returns true if the |current_time| is in the interval +// [|not_before| - tolerance, |not_after| + tolerance), where a clock-skew +// tolerance is only non-zero if |use_public_certificate_tolerance| is true. +bool IsNearbyShareCertificateWithinValidityPeriod( + absl::Time current_time, absl::Time not_before, absl::Time not_after, + bool use_public_certificate_tolerance); + +// Uses HKDF to create a hash of the |authentication_token|, using the +// |secret_key|. A trivial info parameter is used, and the output length is +// fixed to be kNearbyShareNumBytesAuthenticationTokenHash to conform with the +// GmsCore implementation. +std::vector ComputeAuthenticationTokenHash( + absl::Span authentication_token, + absl::Span secret_key); + +// Uses HKDF to generate a new key of length |new_num_bytes| from |key|. To +// conform with the GmsCore implementation, trivial salt and info are used. +std::vector DeriveNearbyShareKey(absl::Span key, + size_t new_num_bytes); + +// Generates a random byte array with size |num_bytes|. +std::vector GenerateRandomBytes(size_t num_bytes); + +// Creates a CTR Encryptor used for metadata key encryption/decryption. +std::unique_ptr CreateNearbyShareCtrEncryptor( + const crypto::SymmetricKey* secret_key, absl::Span salt); + +// Generates Time from JAVA Time +absl::Time FromJavaTime(int64_t ms_since_epoch); +int64_t ToJavaTime(absl::Time time); + +template +absl::Span as_bytes(absl::Span s) noexcept { + return {reinterpret_cast(s.data()), s.size() * sizeof(T)}; +} + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_CERTIFICATES_COMMON_H_ diff --git a/sharing/certificates/common_test.cc b/sharing/certificates/common_test.cc new file mode 100644 index 00000000..f2852f7f --- /dev/null +++ b/sharing/certificates/common_test.cc @@ -0,0 +1,184 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/certificates/common.h" + +#include + +#include +#include +#include +#include + +#include "gtest/gtest.h" +#include "absl/time/time.h" +#include "absl/types/span.h" +#include "sharing/certificates/constants.h" +#include "sharing/certificates/nearby_share_decrypted_public_certificate.h" +#include "sharing/certificates/nearby_share_private_certificate.h" +#include "sharing/certificates/test_util.h" +#include "sharing/common/nearby_share_enums.h" + +namespace nearby { +namespace sharing { +namespace { + +using ::nearby::sharing::proto::DeviceVisibility; + +TEST(NearbyShareCertificatesCommonTest, AuthenticationTokenHash) { + EXPECT_EQ( + GetNearbyShareTestPayloadHashUsingSecretKey(), + ComputeAuthenticationTokenHash( + GetNearbyShareTestPayloadToSign(), + as_bytes(absl::MakeSpan(GetNearbyShareTestSecretKey()->key())))); +} + +TEST(NearbyShareCertificatesCommonTest, ValidityPeriod_PrivateCertificate) { + NearbySharePrivateCertificate cert = GetNearbyShareTestPrivateCertificate( + DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS); + const bool use_public_certificate_tolerance = false; + + // Set time before validity period. + absl::Time now = cert.not_before() - absl::Milliseconds(1); + EXPECT_FALSE(IsNearbyShareCertificateExpired( + now, cert.not_after(), use_public_certificate_tolerance)); + EXPECT_FALSE(IsNearbyShareCertificateWithinValidityPeriod( + now, cert.not_before(), cert.not_after(), + use_public_certificate_tolerance)); + + // Set time at inclusive lower bound of validity period. + now = cert.not_before(); + EXPECT_FALSE(IsNearbyShareCertificateExpired( + now, cert.not_after(), use_public_certificate_tolerance)); + EXPECT_TRUE(IsNearbyShareCertificateWithinValidityPeriod( + now, cert.not_before(), cert.not_after(), + use_public_certificate_tolerance)); + + // Set time in the middle of the validity period. + now = cert.not_before() + (cert.not_after() - cert.not_before()) / 2; + EXPECT_FALSE(IsNearbyShareCertificateExpired( + now, cert.not_after(), use_public_certificate_tolerance)); + EXPECT_TRUE(IsNearbyShareCertificateWithinValidityPeriod( + now, cert.not_before(), cert.not_after(), + use_public_certificate_tolerance)); + + // Set time at non-inclusive upper bound of validity period. + now = cert.not_after(); + EXPECT_TRUE(IsNearbyShareCertificateExpired( + now, cert.not_after(), use_public_certificate_tolerance)); + EXPECT_FALSE(IsNearbyShareCertificateWithinValidityPeriod( + now, cert.not_before(), cert.not_after(), + use_public_certificate_tolerance)); + + // Set time after validity period. + now = cert.not_after() + absl::Milliseconds(1); + EXPECT_TRUE(IsNearbyShareCertificateExpired( + now, cert.not_after(), use_public_certificate_tolerance)); + EXPECT_FALSE(IsNearbyShareCertificateWithinValidityPeriod( + now, cert.not_before(), cert.not_after(), + use_public_certificate_tolerance)); +} + +TEST(NearbyShareCertificatesCommonTest, ValidityPeriod_PublicCertificate) { + NearbyShareDecryptedPublicCertificate cert = + *NearbyShareDecryptedPublicCertificate::DecryptPublicCertificate( + GetNearbyShareTestPublicCertificate( + DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS), + GetNearbyShareTestEncryptedMetadataKey()); + const bool use_public_certificate_tolerance = true; + + // Set time before validity period, outside of tolerance. + absl::Time now = cert.not_before() - + kNearbySharePublicCertificateValidityBoundOffsetTolerance - + absl::Milliseconds(1); + EXPECT_FALSE(IsNearbyShareCertificateExpired( + now, cert.not_after(), use_public_certificate_tolerance)); + EXPECT_FALSE(IsNearbyShareCertificateWithinValidityPeriod( + now, cert.not_before(), cert.not_after(), + use_public_certificate_tolerance)); + + // Set time before validity period, at inclusive bound with tolerance. + now = cert.not_before() - + kNearbySharePublicCertificateValidityBoundOffsetTolerance; + EXPECT_FALSE(IsNearbyShareCertificateExpired( + now, cert.not_after(), use_public_certificate_tolerance)); + EXPECT_TRUE(IsNearbyShareCertificateWithinValidityPeriod( + now, cert.not_before(), cert.not_after(), + use_public_certificate_tolerance)); + + // Set time before validity period, inside of tolerance. + now = cert.not_before() - + kNearbySharePublicCertificateValidityBoundOffsetTolerance / 2; + EXPECT_FALSE(IsNearbyShareCertificateExpired( + now, cert.not_after(), use_public_certificate_tolerance)); + EXPECT_TRUE(IsNearbyShareCertificateWithinValidityPeriod( + now, cert.not_before(), cert.not_after(), + use_public_certificate_tolerance)); + + // Set time at inclusive lower bound of validity period. + now = cert.not_before(); + EXPECT_FALSE(IsNearbyShareCertificateExpired( + now, cert.not_after(), use_public_certificate_tolerance)); + EXPECT_TRUE(IsNearbyShareCertificateWithinValidityPeriod( + now, cert.not_before(), cert.not_after(), + use_public_certificate_tolerance)); + + // Set time in the middle of the validity period. + now = cert.not_before() + (cert.not_after() - cert.not_before()) / 2; + EXPECT_FALSE(IsNearbyShareCertificateExpired( + now, cert.not_after(), use_public_certificate_tolerance)); + EXPECT_TRUE(IsNearbyShareCertificateWithinValidityPeriod( + now, cert.not_before(), cert.not_after(), + use_public_certificate_tolerance)); + + // Set time at upper bound of validity period. + now = cert.not_after(); + EXPECT_FALSE(IsNearbyShareCertificateExpired( + now, cert.not_after(), use_public_certificate_tolerance)); + EXPECT_TRUE(IsNearbyShareCertificateWithinValidityPeriod( + now, cert.not_before(), cert.not_after(), + use_public_certificate_tolerance)); + + // Set time after validity period, inside of tolerance. + now = cert.not_after() + + kNearbySharePublicCertificateValidityBoundOffsetTolerance / 2; + EXPECT_FALSE(IsNearbyShareCertificateExpired( + now, cert.not_after(), use_public_certificate_tolerance)); + EXPECT_TRUE(IsNearbyShareCertificateWithinValidityPeriod( + now, cert.not_before(), cert.not_after(), + use_public_certificate_tolerance)); + + // Set time after validity period, at non-inclusive tolerance bound. + now = cert.not_after() + + kNearbySharePublicCertificateValidityBoundOffsetTolerance; + EXPECT_TRUE(IsNearbyShareCertificateExpired( + now, cert.not_after(), use_public_certificate_tolerance)); + EXPECT_FALSE(IsNearbyShareCertificateWithinValidityPeriod( + now, cert.not_before(), cert.not_after(), + use_public_certificate_tolerance)); + + // Set time after validity period, outside of tolerance. + now = cert.not_after() + + kNearbySharePublicCertificateValidityBoundOffsetTolerance + + absl::Milliseconds(1); + EXPECT_TRUE(IsNearbyShareCertificateExpired( + now, cert.not_after(), use_public_certificate_tolerance)); + EXPECT_FALSE(IsNearbyShareCertificateWithinValidityPeriod( + now, cert.not_before(), cert.not_after(), + use_public_certificate_tolerance)); +} + +} // namespace +} // namespace sharing +} // namespace nearby diff --git a/sharing/certificates/constants.h b/sharing/certificates/constants.h new file mode 100644 index 00000000..d2b17cab --- /dev/null +++ b/sharing/certificates/constants.h @@ -0,0 +1,117 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_CERTIFICATES_CONSTANTS_H_ +#define THIRD_PARTY_NEARBY_SHARING_CERTIFICATES_CONSTANTS_H_ + +#include + +#include "absl/time/time.h" + +namespace nearby { +namespace sharing { + +// The number of days a certificate is valid. +constexpr absl::Duration kNearbyShareCertificateValidityPeriod = + absl::Hours(72); + +// The maximum offset for obfuscating a private certificate's not before/after +// timestamps when converting to a public certificate. +constexpr absl::Duration kNearbyShareMaxPrivateCertificateValidityBoundOffset = + absl::Hours(2); + +// To account for clock skew between the local device and remote devices, public +// certificates will be considered valid if the current time is within the +// bounds [not-before - tolerance, not-after + tolerance). +constexpr absl::Duration + kNearbySharePublicCertificateValidityBoundOffsetTolerance = + absl::Minutes(30); + +// The number of private certificates for a given visibility to be stored and +// rotated on the local device. +constexpr size_t kNearbyShareNumPrivateCertificates = 3; + +// The number of bytes comprising the hash of the authentication token using the +// secret key. +constexpr size_t kNearbyShareNumBytesAuthenticationTokenHash = 6; + +// Length of key in bytes required by AES-GCM encryption. +constexpr size_t kNearbyShareNumBytesAesGcmKey = 32; + +// Length of salt in bytes required by AES-GCM encryption. +constexpr size_t kNearbyShareNumBytesAesGcmIv = 12; + +// Length of salt in bytes required by AES-CTR encryption. +constexpr size_t kNearbyShareNumBytesAesCtrIv = 16; + +// The number of bytes of the AES secret key used to encrypt/decrypt the +// metadata encryption key. +constexpr size_t kNearbyShareNumBytesSecretKey = 32; + +// The number of the bytes of the AES key used to encrypt personal info +// metadata. For example, name and picture data. These bytes are broadcast in an +// advertisement to other devices, thus the smaller byte size. +constexpr size_t kNearbyShareNumBytesMetadataEncryptionKey = 14; + +// The number of bytes for the salt used for encryption of the metadata +// encryption key. These bytes are broadcast in the advertisement to other +// devices. +constexpr size_t kNearbyShareNumBytesMetadataEncryptionKeySalt = 2; + +// The number of bytes used for the hash of the metadata encryption key. +constexpr size_t kNearbyShareNumBytesMetadataEncryptionKeyTag = 32; + +// The number of bytes in a certificate's identifier. +constexpr size_t kNearbyShareNumBytesCertificateId = 32; + +// Half of the possible 2-byte salt values. +// +// Note: Static identifiers can be tracked over time by setting up persistent +// scanners at known locations (e.g. at different isles within a supermarket). +// As the scanners’ location is already known, anyone who walks past the scanner +// has their location recorded too. This can be used for heuristics (e.g. number +// of customers in a store, customers who prefer product X also prefer product +// Y, dwell time), or can be attached to an identity (e.g. rewards card when +// checking out at the cashier). By rotating our identifiers, we prevent +// inadvertently leaking location. However, even rotations can be tracked as we +// get closer to running out of salts. If tracked over a long enough time, the +// device that avoids salts that you’ve seen in the past is statistically likely +// to be the device you’re tracking. Therefore, we only use half of the +// available 2-byte salts. +constexpr size_t kNearbyShareMaxNumMetadataEncryptionKeySalts = 32768; + +// The max number of retries allowed to generate a salt. This is a sanity check +// that will never be hit. +constexpr size_t kNearbyShareMaxNumMetadataEncryptionKeySaltGenerationRetries = + 128; + +// The prefix prepended to the UKEY2 authentication token by the sender before +// signing. +constexpr char kNearbyShareSenderVerificationPrefix = 0x01; + +// The prefix prepended to the UKEY2 authentication token by the receiver before +// signing. +constexpr char kNearbyShareReceiverVerificationPrefix = 0x02; + +// The maximum number of attempts to initialize LevelDB in Certificate Storage. +constexpr size_t kNearbyShareCertificateStorageMaxNumInitializeAttempts = 3; + +// The frequency with which to download public certificates. +constexpr absl::Duration kNearbySharePublicCertificateDownloadPeriod = + absl::Hours(12); + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_CERTIFICATES_CONSTANTS_H_ diff --git a/sharing/certificates/fake_nearby_share_certificate_manager.cc b/sharing/certificates/fake_nearby_share_certificate_manager.cc new file mode 100644 index 00000000..56ceb291 --- /dev/null +++ b/sharing/certificates/fake_nearby_share_certificate_manager.cc @@ -0,0 +1,125 @@ +// Copyright 2022-2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/certificates/fake_nearby_share_certificate_manager.h" + +#include + +#include +#include +#include +#include +#include +#include + +#include "absl/strings/string_view.h" +#include "sharing/certificates/nearby_share_certificate_manager.h" +#include "sharing/certificates/nearby_share_encrypted_metadata_key.h" +#include "sharing/certificates/nearby_share_private_certificate.h" +#include "sharing/certificates/test_util.h" +#include "sharing/contacts/nearby_share_contact_manager.h" +#include "sharing/internal/api/sharing_rpc_client.h" +#include "sharing/internal/public/context.h" +#include "sharing/local_device_data/nearby_share_local_device_data_manager.h" +#include "sharing/proto/rpc_resources.pb.h" + +namespace nearby { +namespace sharing { + +using ::nearby::sharing::proto::DeviceVisibility; + +class NearbyShareClientFactory; + +FakeNearbyShareCertificateManager::Factory::Factory() = default; + +FakeNearbyShareCertificateManager::Factory::~Factory() = default; + +std::unique_ptr +FakeNearbyShareCertificateManager::Factory::CreateInstance( + nearby::Context* context, + NearbyShareLocalDeviceDataManager* local_device_data_manager, + NearbyShareContactManager* contact_manager, absl::string_view profile_path, + nearby::sharing::api::SharingRpcClientFactory* client_factory) { + auto instance = std::make_unique(); + instances_.push_back(instance.get()); + + return instance; +} + +FakeNearbyShareCertificateManager::GetDecryptedPublicCertificateCall:: + GetDecryptedPublicCertificateCall( + NearbyShareEncryptedMetadataKey encrypted_metadata_key, + CertDecryptedCallback callback) + : encrypted_metadata_key(std::move(encrypted_metadata_key)), + callback(std::move(callback)) {} + +FakeNearbyShareCertificateManager::GetDecryptedPublicCertificateCall:: + GetDecryptedPublicCertificateCall( + GetDecryptedPublicCertificateCall&& other) = default; + +FakeNearbyShareCertificateManager::GetDecryptedPublicCertificateCall& +FakeNearbyShareCertificateManager::GetDecryptedPublicCertificateCall::operator=( + GetDecryptedPublicCertificateCall&& other) = default; + +FakeNearbyShareCertificateManager::GetDecryptedPublicCertificateCall:: + ~GetDecryptedPublicCertificateCall() = default; + +FakeNearbyShareCertificateManager::FakeNearbyShareCertificateManager() + : next_salt_(GetNearbyShareTestSalt()) {} + +FakeNearbyShareCertificateManager::~FakeNearbyShareCertificateManager() = + default; + +std::vector +FakeNearbyShareCertificateManager::GetPrivateCertificatesAsPublicCertificates( + DeviceVisibility visibility) { + ++num_get_private_certificates_as_public_certificates_calls_; + return GetNearbyShareTestPublicCertificateList(visibility); +} + +void FakeNearbyShareCertificateManager::GetDecryptedPublicCertificate( + NearbyShareEncryptedMetadataKey encrypted_metadata_key, + CertDecryptedCallback callback) { + get_decrypted_public_certificate_calls_.emplace_back(encrypted_metadata_key, + std::move(callback)); +} + +void FakeNearbyShareCertificateManager::DownloadPublicCertificates() { + ++num_download_public_certificates_calls_; +} + +void FakeNearbyShareCertificateManager::ClearPublicCertificates( + std::function callback) { + ++num_clear_public_certificates_calls_; + callback(true); +} + +void FakeNearbyShareCertificateManager::OnStart() {} + +void FakeNearbyShareCertificateManager::OnStop() {} + +std::optional +FakeNearbyShareCertificateManager::GetValidPrivateCertificate( + DeviceVisibility visibility) const { + auto cert = GetNearbyShareTestPrivateCertificate(visibility); + cert.next_salts_for_testing() = std::queue>(); + cert.next_salts_for_testing().push(next_salt_); + return cert; +} + +void FakeNearbyShareCertificateManager::UpdatePrivateCertificateInStorage( + const NearbySharePrivateCertificate& private_certificate) {} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/certificates/fake_nearby_share_certificate_manager.h b/sharing/certificates/fake_nearby_share_certificate_manager.h new file mode 100644 index 00000000..1cafbd26 --- /dev/null +++ b/sharing/certificates/fake_nearby_share_certificate_manager.h @@ -0,0 +1,147 @@ +// Copyright 2022-2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_CERTIFICATES_FAKE_NEARBY_SHARE_CERTIFICATE_MANAGER_H_ +#define THIRD_PARTY_NEARBY_SHARING_CERTIFICATES_FAKE_NEARBY_SHARE_CERTIFICATE_MANAGER_H_ + +#include +#include + +#include +#include +#include +#include +#include + +#include "absl/strings/string_view.h" +#include "sharing/certificates/nearby_share_certificate_manager.h" +#include "sharing/certificates/nearby_share_certificate_manager_impl.h" +#include "sharing/certificates/nearby_share_encrypted_metadata_key.h" +#include "sharing/certificates/nearby_share_private_certificate.h" +#include "sharing/contacts/nearby_share_contact_manager.h" +#include "sharing/internal/api/sharing_rpc_client.h" +#include "sharing/internal/public/context.h" +#include "sharing/local_device_data/nearby_share_local_device_data_manager.h" +#include "sharing/proto/rpc_resources.pb.h" + +namespace nearby { +namespace sharing { + +// A fake implementation of NearbyShareCertificateManager, along with a fake +// factory, to be used in tests. +class FakeNearbyShareCertificateManager : public NearbyShareCertificateManager { + public: + // Factory that creates FakeNearbyShareCertificateManager instances. Use in + // NearbyShareCertificateManagerImpl::Factor::SetFactoryForTesting() in unit + // tests. + class Factory : public NearbyShareCertificateManagerImpl::Factory { + public: + Factory(); + ~Factory() override; + + // Returns all FakeNearbyShareCertificateManager instances created by + // CreateInstance(). + std::vector& instances() { + return instances_; + } + + private: + // NearbyShareCertificateManagerImpl::Factory: + std::unique_ptr CreateInstance( + Context* context, + NearbyShareLocalDeviceDataManager* local_device_data_manager, + NearbyShareContactManager* contact_manager, + absl::string_view profile_path, + nearby::sharing::api::SharingRpcClientFactory* client_factory) override; + + std::vector instances_; + }; + + struct GetDecryptedPublicCertificateCall { + public: + GetDecryptedPublicCertificateCall( + NearbyShareEncryptedMetadataKey encrypted_metadata_key, + CertDecryptedCallback callback); + GetDecryptedPublicCertificateCall( + GetDecryptedPublicCertificateCall&& other); + GetDecryptedPublicCertificateCall& operator=( + GetDecryptedPublicCertificateCall&& other); + GetDecryptedPublicCertificateCall( + const GetDecryptedPublicCertificateCall&) = delete; + GetDecryptedPublicCertificateCall& operator=( + const GetDecryptedPublicCertificateCall&) = delete; + ~GetDecryptedPublicCertificateCall(); + + NearbyShareEncryptedMetadataKey encrypted_metadata_key; + CertDecryptedCallback callback; + }; + + FakeNearbyShareCertificateManager(); + ~FakeNearbyShareCertificateManager() override; + + // NearbyShareCertificateManager: + std::vector + GetPrivateCertificatesAsPublicCertificates( + proto::DeviceVisibility visibility) override; + void GetDecryptedPublicCertificate( + NearbyShareEncryptedMetadataKey encrypted_metadata_key, + CertDecryptedCallback callback) override; + void DownloadPublicCertificates() override; + void ClearPublicCertificates(std::function callback) override; + std::string Dump() const override { return ""; } + + // Make protected methods from base class public in this fake class. + using NearbyShareCertificateManager::NotifyPrivateCertificatesChanged; + using NearbyShareCertificateManager::NotifyPublicCertificatesDownloaded; + + void set_next_salt(const std::vector& salt) { next_salt_ = salt; } + + size_t num_get_private_certificates_as_public_certificates_calls() { + return num_get_private_certificates_as_public_certificates_calls_; + } + + size_t num_download_public_certificates_calls() { + return num_download_public_certificates_calls_; + } + + size_t num_clear_public_certificates_calls() { + return num_clear_public_certificates_calls_; + } + + std::vector& + get_decrypted_public_certificate_calls() { + return get_decrypted_public_certificate_calls_; + } + + private: + // NearbyShareCertificateManager: + void OnStart() override; + void OnStop() override; + std::optional GetValidPrivateCertificate( + proto::DeviceVisibility visibility) const override; + void UpdatePrivateCertificateInStorage( + const NearbySharePrivateCertificate& private_certificate) override; + + size_t num_get_private_certificates_as_public_certificates_calls_ = 0; + size_t num_download_public_certificates_calls_ = 0; + size_t num_clear_public_certificates_calls_ = 0; + std::vector + get_decrypted_public_certificate_calls_; + std::vector next_salt_; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_CERTIFICATES_FAKE_NEARBY_SHARE_CERTIFICATE_MANAGER_H_ diff --git a/sharing/certificates/fake_nearby_share_certificate_storage.cc b/sharing/certificates/fake_nearby_share_certificate_storage.cc new file mode 100644 index 00000000..b168db61 --- /dev/null +++ b/sharing/certificates/fake_nearby_share_certificate_storage.cc @@ -0,0 +1,165 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/certificates/fake_nearby_share_certificate_storage.h" + +#include +#include +#include +#include +#include + +#include "absl/strings/string_view.h" +#include "absl/time/time.h" +#include "absl/types/span.h" +#include "sharing/certificates/nearby_share_certificate_storage.h" +#include "sharing/certificates/nearby_share_private_certificate.h" +#include "sharing/internal/api/preference_manager.h" +#include "sharing/internal/api/public_certificate_database.h" +#include "sharing/proto/rpc_resources.pb.h" + +namespace nearby { +namespace sharing { + +using ::nearby::sharing::proto::PublicCertificate; + +FakeNearbyShareCertificateStorage::Factory::Factory() = default; + +FakeNearbyShareCertificateStorage::Factory::~Factory() = default; + +std::shared_ptr +FakeNearbyShareCertificateStorage::Factory::CreateInstance( + nearby::sharing::api::PreferenceManager& preference_manager, + std::unique_ptr + public_certificate_database) { + auto instance = std::make_shared(); + instances_.push_back(instance.get()); + + return instance; +} + +FakeNearbyShareCertificateStorage::ReplacePublicCertificatesCall:: + ReplacePublicCertificatesCall( + const std::vector& public_certificates, + ResultCallback callback) + : public_certificates(public_certificates), callback(std::move(callback)) {} + +FakeNearbyShareCertificateStorage::ReplacePublicCertificatesCall:: + ReplacePublicCertificatesCall(ReplacePublicCertificatesCall&& other) = + default; + +FakeNearbyShareCertificateStorage::ReplacePublicCertificatesCall:: + ~ReplacePublicCertificatesCall() = default; + +FakeNearbyShareCertificateStorage::AddPublicCertificatesCall:: + AddPublicCertificatesCall( + const std::vector& public_certificates, + ResultCallback callback) + : public_certificates(public_certificates), callback(std::move(callback)) {} + +FakeNearbyShareCertificateStorage::AddPublicCertificatesCall:: + AddPublicCertificatesCall(AddPublicCertificatesCall&& other) = default; + +FakeNearbyShareCertificateStorage::AddPublicCertificatesCall:: + ~AddPublicCertificatesCall() = default; + +FakeNearbyShareCertificateStorage::RemoveExpiredPublicCertificatesCall:: + RemoveExpiredPublicCertificatesCall(absl::Time now, ResultCallback callback) + : now(now), callback(std::move(callback)) {} + +FakeNearbyShareCertificateStorage::RemoveExpiredPublicCertificatesCall:: + RemoveExpiredPublicCertificatesCall( + RemoveExpiredPublicCertificatesCall&& other) = default; + +FakeNearbyShareCertificateStorage::RemoveExpiredPublicCertificatesCall:: + ~RemoveExpiredPublicCertificatesCall() = default; + +FakeNearbyShareCertificateStorage::FakeNearbyShareCertificateStorage() = + default; + +FakeNearbyShareCertificateStorage::~FakeNearbyShareCertificateStorage() = + default; + +std::vector +FakeNearbyShareCertificateStorage::GetPublicCertificateIds() const { + return public_certificate_ids_; +} + +void FakeNearbyShareCertificateStorage::GetPublicCertificates( + PublicCertificateCallback callback) { + get_public_certificates_callbacks_.push_back(std::move(callback)); +} + +std::optional> +FakeNearbyShareCertificateStorage::GetPrivateCertificates() const { + return private_certificates_; +} + +std::optional +FakeNearbyShareCertificateStorage::NextPublicCertificateExpirationTime() const { + return next_public_certificate_expiration_time_; +} + +void FakeNearbyShareCertificateStorage::ReplacePrivateCertificates( + absl::Span private_certificates) { + private_certificates_ = std::vector( + private_certificates.begin(), private_certificates.end()); +} + +void FakeNearbyShareCertificateStorage::ReplacePublicCertificates( + absl::Span public_certificates, + ResultCallback callback) { + replace_public_certificates_calls_.emplace_back( + std::vector(public_certificates.begin(), + public_certificates.end()), + std::move(callback)); +} + +void FakeNearbyShareCertificateStorage::AddPublicCertificates( + absl::Span public_certificates, + ResultCallback callback) { + add_public_certificates_calls_.emplace_back( + std::vector(public_certificates.begin(), + public_certificates.end()), + callback); + if (is_sync_mode_) { + callback(add_public_certificates_result_); + } +} + +void FakeNearbyShareCertificateStorage::RemoveExpiredPublicCertificates( + absl::Time now, ResultCallback callback) { + remove_expired_public_certificates_calls_.emplace_back(now, callback); + if (is_sync_mode_) { + callback(remove_expired_public_certificates_result_); + } +} + +void FakeNearbyShareCertificateStorage::ClearPublicCertificates( + ResultCallback callback) { + clear_public_certificates_callbacks_.push_back(std::move(callback)); +} + +void FakeNearbyShareCertificateStorage::SetPublicCertificateIds( + absl::Span ids) { + public_certificate_ids_ = std::vector(ids.begin(), ids.end()); +} + +void FakeNearbyShareCertificateStorage::SetNextPublicCertificateExpirationTime( + std::optional time) { + next_public_certificate_expiration_time_ = time; +} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/certificates/fake_nearby_share_certificate_storage.h b/sharing/certificates/fake_nearby_share_certificate_storage.h new file mode 100644 index 00000000..f3f1a98e --- /dev/null +++ b/sharing/certificates/fake_nearby_share_certificate_storage.h @@ -0,0 +1,178 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_CERTIFICATES_FAKE_NEARBY_SHARE_CERTIFICATE_STORAGE_H_ +#define THIRD_PARTY_NEARBY_SHARING_CERTIFICATES_FAKE_NEARBY_SHARE_CERTIFICATE_STORAGE_H_ + +#include +#include +#include +#include + +#include "absl/strings/string_view.h" +#include "absl/time/time.h" +#include "absl/types/span.h" +#include "sharing/certificates/nearby_share_certificate_storage.h" +#include "sharing/certificates/nearby_share_certificate_storage_impl.h" +#include "sharing/certificates/nearby_share_private_certificate.h" +#include "sharing/internal/api/preference_manager.h" +#include "sharing/internal/api/public_certificate_database.h" +#include "sharing/proto/rpc_resources.pb.h" + +namespace nearby { +namespace sharing { + +// A fake implementation of NearbyShareCertificateStorage, along with a fake +// factory, to be used in tests. +class FakeNearbyShareCertificateStorage : public NearbyShareCertificateStorage { + public: + // Factory that creates FakeNearbyShareCertificateStorage instances. Use + // in NearbyShareCertificateStorageImpl::Factory::SetFactoryForTesting() + // in unit tests. + class Factory : public NearbyShareCertificateStorageImpl::Factory { + public: + Factory(); + ~Factory() override; + + // Returns all FakeNearbyShareCertificateStorage instances created by + // CreateInstance(). + std::vector& instances() { + return instances_; + } + + private: + // NearbyShareCertificateStorageImpl::Factory: + std::shared_ptr CreateInstance( + nearby::sharing::api::PreferenceManager& preference_manager, + std::unique_ptr + public_certificate_database) override; + + std::vector instances_; + }; + + struct ReplacePublicCertificatesCall { + ReplacePublicCertificatesCall( + const std::vector& + public_certificates, + ResultCallback callback); + ReplacePublicCertificatesCall(ReplacePublicCertificatesCall&& other); + ~ReplacePublicCertificatesCall(); + + std::vector public_certificates; + ResultCallback callback; + }; + + struct AddPublicCertificatesCall { + AddPublicCertificatesCall( + const std::vector& + public_certificates, + ResultCallback callback); + AddPublicCertificatesCall(AddPublicCertificatesCall&& other); + ~AddPublicCertificatesCall(); + + std::vector public_certificates; + ResultCallback callback; + }; + + struct RemoveExpiredPublicCertificatesCall { + RemoveExpiredPublicCertificatesCall(absl::Time now, + ResultCallback callback); + RemoveExpiredPublicCertificatesCall( + RemoveExpiredPublicCertificatesCall&& other); + ~RemoveExpiredPublicCertificatesCall(); + + absl::Time now; + ResultCallback callback; + }; + + FakeNearbyShareCertificateStorage(); + ~FakeNearbyShareCertificateStorage() override; + + // NearbyShareCertificateStorage: + std::vector GetPublicCertificateIds() const override; + void GetPublicCertificates(PublicCertificateCallback callback) override; + std::optional> + GetPrivateCertificates() const override; + std::optional NextPublicCertificateExpirationTime() + const override; + void ReplacePrivateCertificates( + absl::Span private_certificates) + override; + void ReplacePublicCertificates( + absl::Span + public_certificates, + ResultCallback callback) override; + void AddPublicCertificates( + absl::Span + public_certificates, + ResultCallback callback) override; + void RemoveExpiredPublicCertificates(absl::Time now, + ResultCallback callback) override; + void ClearPublicCertificates(ResultCallback callback) override; + + void SetPublicCertificateIds(absl::Span ids); + void SetNextPublicCertificateExpirationTime(std::optional time); + + std::vector& get_public_certificates_callbacks() { + return get_public_certificates_callbacks_; + } + + std::vector& + replace_public_certificates_calls() { + return replace_public_certificates_calls_; + } + + std::vector& add_public_certificates_calls() { + return add_public_certificates_calls_; + } + + std::vector& + remove_expired_public_certificates_calls() { + return remove_expired_public_certificates_calls_; + } + + std::vector& clear_public_certificates_callbacks() { + return clear_public_certificates_callbacks_; + } + + void set_is_sync_mode(bool is_sync_mode) { is_sync_mode_ = is_sync_mode; } + + void SetAddPublicCertificatesResult(bool result) { + add_public_certificates_result_ = result; + } + + void SetRemoveExpiredPublicCertificatesResult(bool result) { + remove_expired_public_certificates_result_ = result; + } + + private: + std::optional next_public_certificate_expiration_time_; + std::vector public_certificate_ids_; + std::optional> + private_certificates_; + std::vector get_public_certificates_callbacks_; + std::vector replace_public_certificates_calls_; + std::vector add_public_certificates_calls_; + std::vector + remove_expired_public_certificates_calls_; + std::vector clear_public_certificates_callbacks_; + bool is_sync_mode_ = false; + bool add_public_certificates_result_ = false; + bool remove_expired_public_certificates_result_ = false; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_CERTIFICATES_FAKE_NEARBY_SHARE_CERTIFICATE_STORAGE_H_ diff --git a/sharing/certificates/nearby_share_certificate_manager.cc b/sharing/certificates/nearby_share_certificate_manager.cc new file mode 100644 index 00000000..8b44c00c --- /dev/null +++ b/sharing/certificates/nearby_share_certificate_manager.cc @@ -0,0 +1,108 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/certificates/nearby_share_certificate_manager.h" + +#include + +#include +#include + +#include "absl/types/span.h" +#include "sharing/certificates/nearby_share_encrypted_metadata_key.h" +#include "sharing/certificates/nearby_share_private_certificate.h" +#include "sharing/proto/enums.pb.h" + +namespace nearby { +namespace sharing { + +using ::nearby::sharing::proto::DeviceVisibility; + +NearbyShareCertificateManager::NearbyShareCertificateManager() = default; + +NearbyShareCertificateManager::~NearbyShareCertificateManager() = default; + +void NearbyShareCertificateManager::AddObserver(Observer* observer) { + observers_.AddObserver(observer); +} + +void NearbyShareCertificateManager::RemoveObserver(Observer* observer) { + observers_.RemoveObserver(observer); +} + +void NearbyShareCertificateManager::Start() { + if (is_running_) return; + + is_running_ = true; + OnStart(); +} + +void NearbyShareCertificateManager::Stop() { + if (!is_running_) return; + + is_running_ = false; + OnStop(); +} + +std::optional +NearbyShareCertificateManager::EncryptPrivateCertificateMetadataKey( + DeviceVisibility visibility) { + std::optional cert = + GetValidPrivateCertificate(visibility); + if (!cert) return std::nullopt; + + std::optional encrypted_key = + cert->EncryptMetadataKey(); + + // Every salt consumed to encrypt the metadata encryption key is tracked by + // the NearbySharePrivateCertificate. Update the private certificate in + // storage to reflect the new list of consumed salts. + UpdatePrivateCertificateInStorage(*cert); + + return encrypted_key; +} + +std::optional> +NearbyShareCertificateManager::SignWithPrivateCertificate( + DeviceVisibility visibility, absl::Span payload) const { + std::optional cert = + GetValidPrivateCertificate(visibility); + if (!cert) return std::nullopt; + + return cert->Sign(payload); +} + +std::optional> +NearbyShareCertificateManager::HashAuthenticationTokenWithPrivateCertificate( + DeviceVisibility visibility, + absl::Span authentication_token) const { + std::optional cert = + GetValidPrivateCertificate(visibility); + if (!cert) return std::nullopt; + + return cert->HashAuthenticationToken(authentication_token); +} + +void NearbyShareCertificateManager::NotifyPublicCertificatesDownloaded() { + for (const auto& observer : observers_.GetObservers()) + observer->OnPublicCertificatesDownloaded(); +} + +void NearbyShareCertificateManager::NotifyPrivateCertificatesChanged() { + for (const auto& observer : observers_.GetObservers()) + observer->OnPrivateCertificatesChanged(); +} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/certificates/nearby_share_certificate_manager.h b/sharing/certificates/nearby_share_certificate_manager.h new file mode 100644 index 00000000..81c6fdad --- /dev/null +++ b/sharing/certificates/nearby_share_certificate_manager.h @@ -0,0 +1,154 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_CERTIFICATES_NEARBY_SHARE_CERTIFICATE_MANAGER_H_ +#define THIRD_PARTY_NEARBY_SHARING_CERTIFICATES_NEARBY_SHARE_CERTIFICATE_MANAGER_H_ + +#include + +#include +#include +#include +#include + +#include "absl/types/span.h" +#include "internal/base/observer_list.h" +#include "sharing/certificates/nearby_share_decrypted_public_certificate.h" +#include "sharing/certificates/nearby_share_encrypted_metadata_key.h" +#include "sharing/certificates/nearby_share_private_certificate.h" +#include "sharing/common/nearby_share_enums.h" +#include "sharing/proto/rpc_resources.pb.h" + +namespace nearby { +namespace sharing { + +// The Nearby Share certificate manager maintains the local device's private +// certificates and contacts' public certificates. The manager communicates with +// the Nearby server to 1) download contacts' public certificates and 2) upload +// local device public certificates to be distributed to contacts. +// +// The class contains methods for performing crypto operations with the +// currently valid private certificate of a given visibility, such as signing a +// payload or generating an encrypted metadata key for an advertisement. For +// crypto operations related to public certificates, such as verifying a +// payload, find and decrypt the relevant certificate with +// DecryptPublicCertificate(), then use the +// NearbyShareDecryptedPublicCertificate class to perform the crypto operations. +// NOTE: The NearbySharePrivateCertificate class is not directly returned +// because storage needs to be updated whenever salts are consumed for metadata +// key encryption. +// +// Observers are notified of any changes to private/public certificates. +class NearbyShareCertificateManager { + public: + class Observer { + public: + virtual ~Observer() = default; + + virtual void OnPublicCertificatesDownloaded() = 0; + virtual void OnPrivateCertificatesChanged() = 0; + }; + + using CertDecryptedCallback = + std::function)>; + + NearbyShareCertificateManager(); + virtual ~NearbyShareCertificateManager(); + + void AddObserver(Observer* observer); + void RemoveObserver(Observer* observer); + + // Starts/Stops certificate task scheduling. + void Start(); + void Stop(); + bool is_running() { return is_running_; } + + // Encrypts the metadata encryption key of the currently valid private + // certificate with |visibility|. Returns absl::nullopt if there is no valid + // private certificate with |visibility|, if the encryption fails, or if + // there are no remaining salts. + std::optional + EncryptPrivateCertificateMetadataKey(proto::DeviceVisibility visibility); + + // Signs the input |payload| using the currently valid private certificate + // with |visibility|. Returns absl::nullopt if there is no valid private + // certificate with |visibility| or if the signing was unsuccessful. + std::optional> SignWithPrivateCertificate( + proto::DeviceVisibility visibility, + absl::Span payload) const; + + // Creates a hash of the |authentication_token| using the currently valid + // private certificate. Returns absl::nullopt if there is no valid private + // certificate with |visibility|. + std::optional> + HashAuthenticationTokenWithPrivateCertificate( + proto::DeviceVisibility visibility, + absl::Span authentication_token) const; + + // Returns all local device private certificates of |visibility| converted to + // public certificates. The public certificates' for_selected_contacts fields + // will be set to reflect the |visibility|. NOTE: Only certificates with the + // requested visibility will be returned; if selected-contacts visibility is + // passed in, the all-contacts visibility certificates will *not* be returned + // as well. + virtual std::vector + GetPrivateCertificatesAsPublicCertificates( + proto::DeviceVisibility visibility) = 0; + + // Returns in |callback| the public certificate that is able to be decrypted + // using |encrypted_metadata_key|, and returns absl::nullopt if no such public + // certificate exists. + virtual void GetDecryptedPublicCertificate( + NearbyShareEncryptedMetadataKey encrypted_metadata_key, + CertDecryptedCallback callback) = 0; + + // Makes an RPC call to the Nearby server to retrieve all public certificates + // available to the local device. These are also downloaded periodically. + // Observers are notified when all public certificate downloads succeed via + // OnPublicCertificatesDownloaded(). + virtual void DownloadPublicCertificates() = 0; + + // Clears all public certificates. when account logout,the public certificates + // should be cleared. + virtual void ClearPublicCertificates(std::function callback) = 0; + + // Dump certificates ID information for troubleshooting. + virtual std::string Dump() const = 0; + + protected: + virtual void OnStart() = 0; + virtual void OnStop() = 0; + + // Returns the currently valid private certificate with |visibility|, or + // returns std::nullopt if one does not exist. + virtual std::optional + GetValidPrivateCertificate(proto::DeviceVisibility visibility) const = 0; + + // Updates the existing record for |private_certificate|. If no such record + // exists, this function does nothing. + virtual void UpdatePrivateCertificateInStorage( + const NearbySharePrivateCertificate& private_certificate) = 0; + + void NotifyPublicCertificatesDownloaded(); + void NotifyPrivateCertificatesChanged(); + + private: + bool is_running_ = false; + nearby::ObserverList observers_; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_CERTIFICATES_NEARBY_SHARE_CERTIFICATE_MANAGER_H_ diff --git a/sharing/certificates/nearby_share_certificate_manager_impl.cc b/sharing/certificates/nearby_share_certificate_manager_impl.cc new file mode 100644 index 00000000..33913d9b --- /dev/null +++ b/sharing/certificates/nearby_share_certificate_manager_impl.cc @@ -0,0 +1,706 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/certificates/nearby_share_certificate_manager_impl.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "absl/container/flat_hash_map.h" +#include "absl/functional/bind_front.h" +#include "absl/memory/memory.h" +#include "absl/status/statusor.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "absl/synchronization/notification.h" +#include "absl/time/time.h" +#include "absl/types/span.h" +#include "internal/platform/implementation/account_manager.h" +#include "sharing/certificates/common.h" +#include "sharing/certificates/constants.h" +#include "sharing/certificates/nearby_share_certificate_manager.h" +#include "sharing/certificates/nearby_share_certificate_storage.h" +#include "sharing/certificates/nearby_share_certificate_storage_impl.h" +#include "sharing/certificates/nearby_share_decrypted_public_certificate.h" +#include "sharing/certificates/nearby_share_encrypted_metadata_key.h" +#include "sharing/certificates/nearby_share_private_certificate.h" +#include "sharing/common/nearby_share_prefs.h" +#include "sharing/contacts/nearby_share_contact_manager.h" +#include "sharing/internal/api/bluetooth_adapter.h" +#include "sharing/internal/api/preference_manager.h" +#include "sharing/internal/api/public_certificate_database.h" +#include "sharing/internal/api/sharing_platform.h" +#include "sharing/internal/api/sharing_rpc_client.h" +#include "sharing/internal/base/encode.h" +#include "sharing/internal/public/context.h" +#include "sharing/internal/public/logging.h" +#include "sharing/local_device_data/nearby_share_local_device_data_manager.h" +#include "sharing/proto/certificate_rpc.pb.h" +#include "sharing/proto/encrypted_metadata.pb.h" +#include "sharing/proto/enums.pb.h" +#include "sharing/proto/rpc_resources.pb.h" +#include "sharing/scheduling/nearby_share_scheduler.h" +#include "sharing/scheduling/nearby_share_scheduler_factory.h" +#include "google/protobuf/repeated_ptr_field.h" + +namespace nearby { +namespace sharing { +namespace { + +using ::nearby::sharing::api::PreferenceManager; +using ::nearby::sharing::api::PublicCertificateDatabase; +using ::nearby::sharing::api::SharingPlatform; +using ::nearby::sharing::proto::DeviceVisibility; +using ::nearby::sharing::proto::EncryptedMetadata; +using ::nearby::sharing::proto::ListPublicCertificatesRequest; +using ::nearby::sharing::proto::ListPublicCertificatesResponse; +using ::nearby::sharing::proto::PublicCertificate; + +constexpr char kDeviceIdPrefix[] = "users/me/devices/"; + +constexpr std::array kVisibilities = { + DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS, + DeviceVisibility::DEVICE_VISIBILITY_SELECTED_CONTACTS, + DeviceVisibility::DEVICE_VISIBILITY_SELF_SHARE, +}; + +const absl::string_view kPublicCertificateDatabaseName = + "NearbySharePublicCertificateDatabase"; + +// These values are persisted to logs. Entries should not be renumbered and +// numeric values should never be reused. +enum class GetDecryptedPublicCertificateResult { + kSuccess = 0, + kNoMatch = 1, + kStorageFailure = 2, + kMaxValue = kStorageFailure +}; + +size_t NumExpectedPrivateCertificates() { + return kVisibilities.size() * kNearbyShareNumPrivateCertificates; +} + +std::optional BuildMetadata( + std::string device_name, std::optional full_name, + std::optional icon_url, + std::optional account_name, Context* context) { + EncryptedMetadata metadata; + if (device_name.empty()) { + NL_LOG(WARNING) << __func__ + << ": Failed to create private certificate metadata; " + << "missing device name."; + return std::nullopt; + } + + metadata.set_device_name(device_name); + if (full_name.has_value()) { + metadata.set_full_name(*full_name); + } + if (icon_url.has_value()) { + metadata.set_icon_url(*icon_url); + } + if (account_name.has_value()) { + metadata.set_account_name(*account_name); + } + + auto bluetooth_mac_address = context->GetBluetoothAdapter().GetAddress(); + if (!bluetooth_mac_address) return std::nullopt; + + metadata.set_bluetooth_mac_address(bluetooth_mac_address->data(), 6u); + return metadata; +} + +void TryDecryptPublicCertificates( + const NearbyShareEncryptedMetadataKey& encrypted_metadata_key, + NearbyShareCertificateManager::CertDecryptedCallback callback, bool success, + std::unique_ptr> public_certificates) { + if (!success || !public_certificates) { + NL_LOG(ERROR) << __func__ + << ": Failed to read public certificates from storage."; + std::move(callback)(std::nullopt); + return; + } + + for (const auto& cert : *public_certificates) { + std::optional decrypted = + NearbyShareDecryptedPublicCertificate::DecryptPublicCertificate( + cert, encrypted_metadata_key); + if (decrypted) { + NL_VLOG(1) << __func__ + << ": Successfully decrypted public certificate with ID " + << nearby::utils::HexEncode(decrypted->id()); + std::move(callback)(std::move(decrypted)); + return; + } + } + NL_VLOG(1) << __func__ + << ": Metadata key could not decrypt any public certificates."; + std::move(callback)(std::nullopt); +} + +void DumpCertificateId(std::stringstream& sstream, absl::string_view cert_id, + bool is_public_cert) { + if (is_public_cert) { + sstream << " Public certificates:["; + } else { + sstream << " Private certificates:["; + } + for (int i = 0; i < cert_id.size() - 1; ++i) { + sstream << static_cast(static_cast(cert_id[i])) << ", "; + } + sstream << static_cast(static_cast(cert_id[cert_id.size() - 1])) + << "]" << std::endl; +} + +} // namespace + +// static +NearbyShareCertificateManagerImpl::Factory* + NearbyShareCertificateManagerImpl::Factory::test_factory_ = nullptr; + +// static +std::unique_ptr +NearbyShareCertificateManagerImpl::Factory::Create( + Context* context, SharingPlatform& sharing_platform, + NearbyShareLocalDeviceDataManager* local_device_data_manager, + NearbyShareContactManager* contact_manager, absl::string_view profile_path, + nearby::sharing::api::SharingRpcClientFactory* client_factory) { + NL_DCHECK(context); + + if (test_factory_) { + return test_factory_->CreateInstance(context, local_device_data_manager, + contact_manager, profile_path, + client_factory); + } + + return absl::WrapUnique(new NearbyShareCertificateManagerImpl( + context, sharing_platform.GetPreferenceManager(), + sharing_platform.GetAccountManager(), + sharing_platform.CreatePublicCertificateDatabase( + absl::StrCat(profile_path, "/", kPublicCertificateDatabaseName)), + local_device_data_manager, contact_manager, client_factory)); +} + +// static +void NearbyShareCertificateManagerImpl::Factory::SetFactoryForTesting( + Factory* test_factory) { + test_factory_ = test_factory; +} + +NearbyShareCertificateManagerImpl::Factory::~Factory() = default; + +NearbyShareCertificateManagerImpl::NearbyShareCertificateManagerImpl( + Context* context, PreferenceManager& preference_manager, + AccountManager& account_manager, + std::unique_ptr public_certificate_database, + NearbyShareLocalDeviceDataManager* local_device_data_manager, + NearbyShareContactManager* contact_manager, + nearby::sharing::api::SharingRpcClientFactory* client_factory) + : context_(context), + account_manager_(account_manager), + local_device_data_manager_(local_device_data_manager), + contact_manager_(contact_manager), + nearby_client_(client_factory->CreateInstance()), + certificate_storage_(NearbyShareCertificateStorageImpl::Factory::Create( + preference_manager, std::move(public_certificate_database))), + private_certificate_expiration_scheduler_( + NearbyShareSchedulerFactory::CreateExpirationScheduler( + context, preference_manager, + [&] { return NextPrivateCertificateExpirationTime(); }, + /*retry_failures=*/true, + /*require_connectivity=*/false, + prefs::kNearbySharingSchedulerPrivateCertificateExpirationName, + [&] { + NL_LOG(INFO) + << ": Private certificate expiration scheduler is called."; + OnPrivateCertificateExpiration(); + })), + public_certificate_expiration_scheduler_( + NearbyShareSchedulerFactory::CreateExpirationScheduler( + context, preference_manager, + [&] { return NextPublicCertificateExpirationTime(); }, + /*retry_failures=*/true, + /*require_connectivity=*/false, + prefs::kNearbySharingSchedulerPublicCertificateExpirationName, + [&] { + NL_LOG(INFO) + << ": Public certificate expiration scheduler is called."; + OnPublicCertificateExpiration(); + })), + upload_local_device_certificates_scheduler_( + NearbyShareSchedulerFactory::CreateOnDemandScheduler( + context, preference_manager, + /*retry_failures=*/true, + /*require_connectivity=*/true, + prefs::kNearbySharingSchedulerUploadLocalDeviceCertificatesName, + [&] { + NL_LOG(INFO) << ": Upload local device certificates scheduler " + "is called."; + UploadLocalDeviceCertificates(); + })), + download_public_certificates_scheduler_( + NearbyShareSchedulerFactory::CreatePeriodicScheduler( + context, preference_manager, + kNearbySharePublicCertificateDownloadPeriod, + /*retry_failures=*/true, + /*require_connectivity=*/true, + prefs::kNearbySharingSchedulerDownloadPublicCertificatesName, + [&] { + NL_LOG(INFO) + << ": Download public certificates scheduler is called."; + DownloadPublicCertificates(); + })), + executor_(context->CreateSequencedTaskRunner()) { + local_device_data_manager_->AddObserver(this); + contact_manager_->AddObserver(this); +} + +NearbyShareCertificateManagerImpl::~NearbyShareCertificateManagerImpl() { + local_device_data_manager_->RemoveObserver(this); + contact_manager_->RemoveObserver(this); +} + +void NearbyShareCertificateManagerImpl::CertificateDownloadContext:: + FetchNextPage() { + NL_LOG(INFO) << __func__ << ": Downloading page=" << page_number_++; + ListPublicCertificatesRequest request; + request.set_parent(device_id_); + if (next_page_token_.has_value()) { + request.set_page_token(*next_page_token_); + } + nearby_share_client_->ListPublicCertificates( + request, [this]( + const absl::StatusOr& + response) mutable { + if (!response.ok()) { + NL_LOG(ERROR) << __func__ << ": Failed to download certificates."; + std::move(download_failure_callback_)(); + return; + } + + certificates_.insert(certificates_.end(), + response->public_certificates().begin(), + response->public_certificates().end()); + + if (response->next_page_token().empty()) { + NL_LOG(INFO) << __func__ << ": Completed to download " + << certificates_.size() + << " certificates from backend"; + std::move(download_success_callback_)(certificates_); + return; + } + next_page_token_ = response->next_page_token(); + FetchNextPage(); + }); +} + +void NearbyShareCertificateManagerImpl::OnPublicCertificatesDownloadSuccess( + const std::vector& certificates) { + // Save certificates to store. + absl::Notification notification; + bool is_added_to_store = false; + certificate_storage_->AddPublicCertificates( + absl::MakeSpan(certificates.data(), + certificates.size()), + [&](bool success) { + is_added_to_store = success; + notification.Notify(); + }); + notification.WaitForNotification(); + if (!is_added_to_store) { + NL_LOG(ERROR) << __func__ << ": Failed to add certificates to store."; + OnPublicCertificatesDownloadFailure(); + return; + } + + // Succeeded to download public certificates. + NotifyPublicCertificatesDownloaded(); + + // Recompute the expiration timer to account for new certificates. + public_certificate_expiration_scheduler_->Reschedule(); + download_public_certificates_scheduler_->HandleResult(true); +} + +void NearbyShareCertificateManagerImpl::OnPublicCertificatesDownloadFailure() { + download_public_certificates_scheduler_->HandleResult(false); +} + +void NearbyShareCertificateManagerImpl::DownloadPublicCertificates() { + executor_->PostTask([&]() { + NL_LOG(INFO) << __func__ << ": Start to download certificates."; + if (!is_running()) { + NL_LOG(WARNING) << __func__ + << ": Ignore to download certificates due to manager is " + "not running."; + return; + } + + if (!account_manager_.GetCurrentAccount().has_value()) { + NL_LOG(WARNING) + << __func__ + << ": Ignore to download certificates due to no login account."; + download_public_certificates_scheduler_->HandleResult(/*success=*/true); + return; + } + + // Currently certificates download is synchronous. It completes after + // FetchNextPage() returns. + auto context = std::make_unique( + nearby_client_.get(), + kDeviceIdPrefix + local_device_data_manager_->GetId(), + absl::bind_front(&NearbyShareCertificateManagerImpl:: + OnPublicCertificatesDownloadFailure, + this), + absl::bind_front(&NearbyShareCertificateManagerImpl:: + OnPublicCertificatesDownloadSuccess, + this)); + context->FetchNextPage(); + }); +} + +void NearbyShareCertificateManagerImpl::UploadLocalDeviceCertificates() { + executor_->PostTask([&]() { + NL_LOG(INFO) << __func__ << ": Start to upload local device certificates."; + + if (!is_running()) { + NL_LOG(WARNING) + << __func__ + << ": Ignore to upload local device certificates due to manager is " + "not running."; + return; + } + + if (!account_manager_.GetCurrentAccount().has_value()) { + NL_LOG(WARNING) + << __func__ + << ": Ignore to upload local device certificates due to no " + "login account."; + upload_local_device_certificates_scheduler_->HandleResult( + /*success=*/true); + return; + } + + std::vector public_certs; + std::vector private_certs = + *certificate_storage_->GetPrivateCertificates(); + public_certs.reserve(private_certs.size()); + for (const NearbySharePrivateCertificate& private_cert : private_certs) { + public_certs.push_back(*private_cert.ToPublicCertificate()); + } + + NL_LOG(INFO) << __func__ << ": Uploading " << public_certs.size() + << " local device certificates."; + bool upload_certificates_result = false; + absl::Notification notification; + local_device_data_manager_->UploadCertificates( + std::move(public_certs), [&](bool success) { + upload_certificates_result = success; + notification.Notify(); + }); + notification.WaitForNotification(); + NL_LOG(INFO) << __func__ << ": Upload of local device certificates " + << (upload_certificates_result ? "succeeded" : "failed."); + upload_local_device_certificates_scheduler_->HandleResult( + upload_certificates_result); + }); +} + +std::vector +NearbyShareCertificateManagerImpl::GetPrivateCertificatesAsPublicCertificates( + DeviceVisibility visibility) { + return std::vector(); +} + +void NearbyShareCertificateManagerImpl::GetDecryptedPublicCertificate( + NearbyShareEncryptedMetadataKey encrypted_metadata_key, + CertDecryptedCallback callback) { + certificate_storage_->GetPublicCertificates( + [encrypted_metadata_key = std::move(encrypted_metadata_key), + callback = std::move(callback)]( + bool success, + std::unique_ptr> result) { + TryDecryptPublicCertificates(encrypted_metadata_key, + std::move(callback), success, + std::move(result)); + }); +} + +void NearbyShareCertificateManagerImpl::ClearPublicCertificates( + std::function callback) { + certificate_storage_->ClearPublicCertificates(std::move(callback)); +} + +void NearbyShareCertificateManagerImpl::OnStart() { + private_certificate_expiration_scheduler_->Start(); + public_certificate_expiration_scheduler_->Start(); + upload_local_device_certificates_scheduler_->Start(); + download_public_certificates_scheduler_->Start(); +} + +void NearbyShareCertificateManagerImpl::OnStop() { + private_certificate_expiration_scheduler_->Stop(); + public_certificate_expiration_scheduler_->Stop(); + upload_local_device_certificates_scheduler_->Stop(); + download_public_certificates_scheduler_->Stop(); +} + +std::optional +NearbyShareCertificateManagerImpl::GetValidPrivateCertificate( + DeviceVisibility visibility) const { + std::optional> certs = + *certificate_storage_->GetPrivateCertificates(); + for (auto& cert : *certs) { + if (IsNearbyShareCertificateWithinValidityPeriod( + context_->GetClock()->Now(), cert.not_before(), cert.not_after(), + /*use_public_certificate_tolerance=*/false) && + cert.visibility() == visibility) { + return std::move(cert); + } + } + + NL_LOG(WARNING) << __func__ + << ": No valid private certificate found with visibility " + << static_cast(visibility); + return std::nullopt; +} + +void NearbyShareCertificateManagerImpl::UpdatePrivateCertificateInStorage( + const NearbySharePrivateCertificate& private_certificate) { + certificate_storage_->UpdatePrivateCertificate(private_certificate); +} + +void NearbyShareCertificateManagerImpl::OnContactsDownloaded( + const std::set& allowed_contact_ids, + const std::vector& contacts, + uint32_t num_unreachable_contacts_filtered_out) { + NL_LOG(INFO) << __func__ << ": Contacts downloaded."; +} + +void NearbyShareCertificateManagerImpl::OnContactsUploaded( + bool did_contacts_change_since_last_upload) { + executor_->PostTask([&, did_contacts_change_since_last_upload]() { + NL_LOG(INFO) << __func__ << ": Handle to Contacts uploaded."; + if (!did_contacts_change_since_last_upload) return; + + // If any of the uploaded contact data - the contact list or the allowlist - + // has changed since the previous successful upload, recreate certificates. + // We do not want to continue using the current certificates because they + // might have been shared with contacts no longer on the contact list or + // allowlist. NOTE: Ideally, we would only recreate all-contacts visibility + // certificates when contacts are removed from the contact list, and we + // would only recreate selected-contacts visibility certificates when + // contacts are removed from the allowlist, but our information is not that + // granular. + certificate_storage_->ClearPrivateCertificates(); + private_certificate_expiration_scheduler_->MakeImmediateRequest(); + }); +} + +void NearbyShareCertificateManagerImpl::OnLocalDeviceDataChanged( + bool did_device_name_change, bool did_full_name_change, + bool did_icon_change) { + executor_->PostTask([&, did_device_name_change, did_full_name_change, + did_icon_change]() { + NL_LOG(INFO) << __func__ << ": Handle to local device data changed."; + if (!did_device_name_change && !did_full_name_change && !did_icon_change) + return; + + // Recreate all private certificates to ensure up-to-date metadata. + certificate_storage_->ClearPrivateCertificates(); + private_certificate_expiration_scheduler_->MakeImmediateRequest(); + }); +} + +std::string NearbyShareCertificateManagerImpl::Dump() const { + std::stringstream sstream; + sstream << "Public Certificates" << std::endl; + std::vector ids = + certificate_storage_->GetPublicCertificateIds(); + sstream << " Total count:" << ids.size() << std::endl; + for (const auto& id : ids) { + DumpCertificateId(sstream, id, true); + } + sstream << std::endl; + + sstream << "Private Certificates" << std::endl; + std::optional> private_certs = + certificate_storage_->GetPrivateCertificates(); + if (private_certs.has_value()) { + sstream << " Total count:" << private_certs->size() << std::endl; + for (const auto& cert : *private_certs) { + std::string id(cert.id().begin(), cert.id().end()); + DumpCertificateId(sstream, id, false); + } + } else { + sstream << " Total count: 0" << std::endl; + } + + return sstream.str(); +} + +std::optional +NearbyShareCertificateManagerImpl::NextPrivateCertificateExpirationTime() { + // We enforce that a fixed number--kNearbyShareNumPrivateCertificates for each + // visibility--of private certificates be present at all times. This might not + // be true the first time the user enables Nearby Share or after certificates + // are revoked. For simplicity, consider the case of missing certificates an + // "expired" state. Return the minimum time to immediately trigger the private + // certificate creation flow. + if (certificate_storage_->GetPrivateCertificates()->size() < + NumExpectedPrivateCertificates()) { + return absl::InfinitePast(); + } + + std::optional expiration_time = + certificate_storage_->NextPrivateCertificateExpirationTime(); + NL_DCHECK(expiration_time); + + return *expiration_time; +} + +void NearbyShareCertificateManagerImpl::OnPrivateCertificateExpiration() { + NL_VLOG(1) + << __func__ + << ": Private certificate expiration detected; refreshing certificates."; + + FinishPrivateCertificateRefresh(); +} + +void NearbyShareCertificateManagerImpl::FinishPrivateCertificateRefresh() { + executor_->PostTask([&]() { + NL_LOG(INFO) << __func__ << ": Refresh private certificates."; + absl::Time now = context_->GetClock()->Now(); + certificate_storage_->RemoveExpiredPrivateCertificates(now); + + std::vector certs = + *certificate_storage_->GetPrivateCertificates(); + if (certs.size() == NumExpectedPrivateCertificates()) { + NL_VLOG(1) << __func__ << ": All private certificates are still valid."; + private_certificate_expiration_scheduler_->HandleResult(/*success=*/true); + return; + } + + // Determine how many private certificates of each visibility need to be + // created, and determine the validity period for the new certificates. + absl::flat_hash_map num_valid_certs; + absl::flat_hash_map latest_not_after; + for (DeviceVisibility visibility : kVisibilities) { + num_valid_certs[visibility] = 0; + latest_not_after[visibility] = now; + } + for (const NearbySharePrivateCertificate& cert : certs) { + ++num_valid_certs[cert.visibility()]; + latest_not_after[cert.visibility()] = + std::max(latest_not_after[cert.visibility()], cert.not_after()); + } + + std::optional account = + account_manager_.GetCurrentAccount(); + std::optional email = + account.has_value() + ? account->email + : static_cast>(std::nullopt); + + std::optional metadata = BuildMetadata( + local_device_data_manager_->GetDeviceName(), + local_device_data_manager_->GetFullName(), + local_device_data_manager_->GetIconUrl(), email, context_); + if (!metadata.has_value()) { + NL_LOG(WARNING) + << __func__ + << "Failed to create private certificates; cannot create metadata"; + private_certificate_expiration_scheduler_->HandleResult( + /*success=*/false); + return; + } + + // Add new certificates if necessary. Each visibility should have + // kNearbyShareNumPrivateCertificates. + NL_LOG(INFO) + << __func__ << ": Creating " + << kNearbyShareNumPrivateCertificates - + num_valid_certs[DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS] + << " all-contacts visibility and " + << kNearbyShareNumPrivateCertificates - + num_valid_certs + [DeviceVisibility::DEVICE_VISIBILITY_SELECTED_CONTACTS] + << " selected-contacts visibility private certificates."; + + for (DeviceVisibility visibility : kVisibilities) { + while (num_valid_certs[visibility] < kNearbyShareNumPrivateCertificates) { + certs.emplace_back(visibility, + /*not_before=*/latest_not_after[visibility], + *metadata); + ++num_valid_certs[visibility]; + latest_not_after[visibility] = certs.back().not_after(); + } + } + + certificate_storage_->ReplacePrivateCertificates( + absl::MakeSpan(certs.data(), certs.size())); + NotifyPrivateCertificatesChanged(); + private_certificate_expiration_scheduler_->HandleResult(/*success=*/true); + + upload_local_device_certificates_scheduler_->MakeImmediateRequest(); + }); +} + +std::optional +NearbyShareCertificateManagerImpl::NextPublicCertificateExpirationTime() { + std::optional next_expiration_time = + certificate_storage_->NextPublicCertificateExpirationTime(); + + // Supposedly there are no store public certificates. + if (!next_expiration_time) return std::nullopt; + + // To account for clock skew between devices, we accept public certificates + // that are slightly past their validity period. This conforms with the + // GmsCore implementation. + return *next_expiration_time + + kNearbySharePublicCertificateValidityBoundOffsetTolerance; +} + +void NearbyShareCertificateManagerImpl::OnPublicCertificateExpiration() { + executor_->PostTask([&]() { + NL_LOG(INFO) << __func__ << ": Removing expired public certificates."; + absl::Notification notification; + bool result = false; + certificate_storage_->RemoveExpiredPublicCertificates( + context_->GetClock()->Now(), [&](bool success) { + result = success; + notification.Notify(); + }); + notification.WaitForNotification(); + if (!result) { + NL_LOG(ERROR) << __func__ + << ": Failed to remove expired public certificates."; + } + public_certificate_expiration_scheduler_->HandleResult(result); + }); +} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/certificates/nearby_share_certificate_manager_impl.h b/sharing/certificates/nearby_share_certificate_manager_impl.h new file mode 100644 index 00000000..abd89427 --- /dev/null +++ b/sharing/certificates/nearby_share_certificate_manager_impl.h @@ -0,0 +1,219 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_CERTIFICATES_NEARBY_SHARE_CERTIFICATE_MANAGER_IMPL_H_ +#define THIRD_PARTY_NEARBY_SHARING_CERTIFICATES_NEARBY_SHARE_CERTIFICATE_MANAGER_IMPL_H_ +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "absl/functional/any_invocable.h" +#include "absl/strings/string_view.h" +#include "absl/time/time.h" +#include "internal/platform/implementation/account_manager.h" +#include "internal/platform/task_runner.h" +#include "sharing/certificates/nearby_share_certificate_manager.h" +#include "sharing/certificates/nearby_share_certificate_storage.h" +#include "sharing/certificates/nearby_share_encrypted_metadata_key.h" +#include "sharing/certificates/nearby_share_private_certificate.h" +#include "sharing/contacts/nearby_share_contact_manager.h" +#include "sharing/internal/api/preference_manager.h" +#include "sharing/internal/api/public_certificate_database.h" +#include "sharing/internal/api/sharing_platform.h" +#include "sharing/internal/api/sharing_rpc_client.h" +#include "sharing/internal/public/context.h" +#include "sharing/local_device_data/nearby_share_local_device_data_manager.h" +#include "sharing/proto/enums.pb.h" +#include "sharing/proto/rpc_resources.pb.h" + +namespace nearby { +namespace sharing { + +class NearbyShareScheduler; + +// An implementation of the NearbyShareCertificateManager that handles +// 1) creating, storing, and uploading local device certificates, as well as +// removing expired/revoked local device certificates; +// 2) downloading, storing, and decrypting public certificates from trusted +// contacts, as well as removing expired public certificates. +// +// This implementation destroys and recreates all private certificates if there +// are any changes to the user's contact list or allowlist, or if there are any +// changes to the local device data, such as the device name. +class NearbyShareCertificateManagerImpl + : public NearbyShareCertificateManager, + public NearbyShareContactManager::Observer, + public NearbyShareLocalDeviceDataManager::Observer { + public: + class Factory { + public: + static std::unique_ptr Create( + Context* context, + nearby::sharing::api::SharingPlatform& sharing_platform, + NearbyShareLocalDeviceDataManager* local_device_data_manager, + NearbyShareContactManager* contact_manager, + absl::string_view profile_path, + nearby::sharing::api::SharingRpcClientFactory* client_factory); + static void SetFactoryForTesting(Factory* test_factory); + + protected: + virtual ~Factory(); + virtual std::unique_ptr CreateInstance( + Context* context, + NearbyShareLocalDeviceDataManager* local_device_data_manager, + NearbyShareContactManager* contact_manager, + absl::string_view profile_path, + nearby::sharing::api::SharingRpcClientFactory* client_factory) = 0; + + private: + static Factory* test_factory_; + }; + + ~NearbyShareCertificateManagerImpl() override; + + private: + // Class for maintaining a single instance of public certificate download + // request. It is responsible for downloading all available pages and making + // the results or error available. + class CertificateDownloadContext { + public: + CertificateDownloadContext( + nearby::sharing::api::SharingRpcClient* nearby_share_client, + std::string device_id, + absl::AnyInvocable download_failure_callback, + absl::AnyInvocable< + void(const std::vector& + certificates) &&> + download_success_callback) + : nearby_share_client_(nearby_share_client), + device_id_(std::move(device_id)), + download_failure_callback_(std::move(download_failure_callback)), + download_success_callback_(std::move(download_success_callback)) {} + + // Fetches the next page of certificates. + // If |next_page_token_| is empty, it fetches the first page. + // On successful download, if page token in the response is empty, the + // |download_success_callback_| is invoked with all downloaded certificates. + void FetchNextPage(); + + private: + nearby::sharing::api::SharingRpcClient* const nearby_share_client_; + std::string device_id_; + std::optional next_page_token_; + int page_number_ = 1; + std::vector certificates_; + absl::AnyInvocable download_failure_callback_; + absl::AnyInvocable< + void(const std::vector& + certificates) &&> + download_success_callback_; + }; + + NearbyShareCertificateManagerImpl( + Context* context, + nearby::sharing::api::PreferenceManager& preference_manager, + AccountManager& account_manager, + std::unique_ptr + public_certificate_database, + NearbyShareLocalDeviceDataManager* local_device_data_manager, + NearbyShareContactManager* contact_manager, + nearby::sharing::api::SharingRpcClientFactory* client_factory); + + // NearbyShareCertificateManager: + std::vector + GetPrivateCertificatesAsPublicCertificates( + proto::DeviceVisibility visibility) override; + void GetDecryptedPublicCertificate( + NearbyShareEncryptedMetadataKey encrypted_metadata_key, + CertDecryptedCallback callback) override; + void DownloadPublicCertificates() override; + void ClearPublicCertificates(std::function callback) override; + void OnStart() override; + void OnStop() override; + std::optional GetValidPrivateCertificate( + proto::DeviceVisibility visibility) const override; + void UpdatePrivateCertificateInStorage( + const NearbySharePrivateCertificate& private_certificate) override; + + // NearbyShareContactManager::Observer: + void OnContactsDownloaded( + const std::set& allowed_contact_ids, + const std::vector& contacts, + uint32_t num_unreachable_contacts_filtered_out) override; + void OnContactsUploaded(bool did_contacts_change_since_last_upload) override; + + // NearbyShareLocalDeviceDataManager::Observer: + void OnLocalDeviceDataChanged(bool did_device_name_change, + bool did_full_name_change, + bool did_icon_change) override; + + // Dump certs information. + std::string Dump() const override; + + // Used by the private certificate expiration scheduler to determine the next + // private certificate expiration time. Returns base::Time::Min() if + // certificates are missing. This function never returns absl::nullopt. + std::optional NextPrivateCertificateExpirationTime(); + + // Used by the public certificate expiration scheduler to determine the next + // public certificate expiration time. Returns absl::nullopt if no public + // certificates are present, and no expiration event is scheduled. + std::optional NextPublicCertificateExpirationTime(); + + // Invoked by the private certificate expiration scheduler when an expired + // private certificate needs to be removed or if no private certificates exist + // yet. New certificate(s) will be created, and an upload to the Nearby Share + // server will be requested. + void OnPrivateCertificateExpiration(); + void FinishPrivateCertificateRefresh(); + + // Invoked by the public certificate expiration scheduler when an expired + // public certificate needs to be removed from storage. + void OnPublicCertificateExpiration(); + + void UploadLocalDeviceCertificates(); + + void OnPublicCertificatesDownloadSuccess( + const std::vector& + certificates); + void OnPublicCertificatesDownloadFailure(); + + Context* const context_; + AccountManager& account_manager_; + NearbyShareLocalDeviceDataManager* const local_device_data_manager_; + NearbyShareContactManager* const contact_manager_; + std::unique_ptr< nearby::sharing::api::SharingRpcClient> nearby_client_; + + std::shared_ptr certificate_storage_; + std::unique_ptr + private_certificate_expiration_scheduler_; + std::unique_ptr + public_certificate_expiration_scheduler_; + std::unique_ptr + upload_local_device_certificates_scheduler_; + std::unique_ptr download_public_certificates_scheduler_; + + std::unique_ptr executor_; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_CERTIFICATES_NEARBY_SHARE_CERTIFICATE_MANAGER_IMPL_H_ diff --git a/sharing/certificates/nearby_share_certificate_manager_impl_test.cc b/sharing/certificates/nearby_share_certificate_manager_impl_test.cc new file mode 100644 index 00000000..83195f17 --- /dev/null +++ b/sharing/certificates/nearby_share_certificate_manager_impl_test.cc @@ -0,0 +1,807 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/certificates/nearby_share_certificate_manager_impl.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "gmock/gmock.h" +#include "protobuf-matchers/protocol-buffer-matchers.h" +#include "gtest/gtest.h" +#include "absl/container/flat_hash_map.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "absl/time/time.h" +#include "absl/types/span.h" +#include "internal/platform/implementation/account_manager.h" +#include "internal/test/fake_account_manager.h" +#include "internal/test/fake_task_runner.h" +#include "sharing/certificates/constants.h" +#include "sharing/certificates/fake_nearby_share_certificate_storage.h" +#include "sharing/certificates/nearby_share_certificate_manager.h" +#include "sharing/certificates/nearby_share_certificate_storage_impl.h" +#include "sharing/certificates/nearby_share_decrypted_public_certificate.h" +#include "sharing/certificates/nearby_share_encrypted_metadata_key.h" +#include "sharing/certificates/nearby_share_private_certificate.h" +#include "sharing/certificates/test_util.h" +#include "sharing/common/nearby_share_prefs.h" +#include "sharing/contacts/fake_nearby_share_contact_manager.h" +#include "sharing/internal/api/fake_nearby_share_client.h" +#include "sharing/internal/api/mock_sharing_platform.h" +#include "sharing/internal/public/logging.h" +#include "sharing/internal/test/fake_bluetooth_adapter.h" +#include "sharing/internal/test/fake_context.h" +#include "sharing/internal/test/fake_preference_manager.h" +#include "sharing/local_device_data/fake_nearby_share_local_device_data_manager.h" +#include "sharing/proto/certificate_rpc.pb.h" +#include "sharing/proto/encrypted_metadata.pb.h" +#include "sharing/proto/enums.pb.h" +#include "sharing/proto/rpc_resources.pb.h" +#include "sharing/scheduling/fake_nearby_share_scheduler.h" +#include "sharing/scheduling/fake_nearby_share_scheduler_factory.h" +#include "sharing/scheduling/nearby_share_scheduler_factory.h" + +namespace nearby { +namespace sharing { +namespace { +using ::nearby::sharing::proto::DeviceVisibility; +using ::nearby::sharing::proto::PublicCertificate; +using ::testing::ReturnRef; + +const absl::Time t0 = absl::UnixEpoch() + absl::Hours(365 * 50 * 24); + +constexpr char kPageTokenPrefix[] = "page_token_"; +constexpr char kSecretIdPrefix[] = "secret_id_"; +constexpr char kDeviceId[] = "123456789A"; +constexpr char kDefaultDeviceName[] = "Josh's Chromebook"; + +constexpr absl::string_view kPublicCertificateIds[3] = {"id1", "id2", "id3"}; + +void CaptureDecryptedPublicCertificateCallback( + std::optional* dest, + std::optional src) { + *dest = std::move(src); +} + +} // namespace + +class NearbyShareCertificateManagerImplTest + : public ::testing::Test, + public NearbyShareCertificateManager::Observer { + public: + NearbyShareCertificateManagerImplTest() = default; + ~NearbyShareCertificateManagerImplTest() override = default; + + void SetUp() override { + ON_CALL(mock_sharing_platform_, GetPreferenceManager) + .WillByDefault(ReturnRef(preference_manager_)); + ON_CALL(mock_sharing_platform_, GetAccountManager) + .WillByDefault(ReturnRef(fake_account_manager_)); + // Set time to t0. + FastForward(t0 - fake_context_.GetClock()->Now()); + + local_device_data_manager_ = + std::make_unique( + kDefaultDeviceName); + local_device_data_manager_->set_is_sync_mode(true); + local_device_data_manager_->SetId(kDeviceId); + + contact_manager_ = std::make_unique(); + + AccountManager::Account account{ + .email = kTestMetadataAccountName, + }; + + fake_account_manager_.SetAccount(account); + + fake_account_manager_.Login([](AccountManager::Account account) {}, + []() {}); + + NearbyShareSchedulerFactory::SetFactoryForTesting(&scheduler_factory_); + NearbyShareCertificateStorageImpl::Factory::SetFactoryForTesting( + &cert_store_factory_); + + // Set default device data. + local_device_data_manager_->SetDeviceName( + GetNearbyShareTestMetadata().device_name()); + local_device_data_manager_->SetFullName( + GetNearbyShareTestMetadata().full_name()); + local_device_data_manager_->SetIconUrl( + GetNearbyShareTestMetadata().icon_url()); + SetBluetoothMacAddress(kTestUnparsedBluetoothMacAddress); + SetMockBluetoothAddress(kTestUnparsedBluetoothMacAddress); + + cert_manager_ = NearbyShareCertificateManagerImpl::Factory::Create( + &fake_context_, mock_sharing_platform_, + local_device_data_manager_.get(), contact_manager_.get(), std::string(), + &client_factory_); + cert_manager_->AddObserver(this); + + cert_store_ = cert_store_factory_.instances().back(); + cert_store_->set_is_sync_mode(true); + + private_cert_exp_scheduler_ = + scheduler_factory_.pref_name_to_expiration_instance() + .find( + prefs::kNearbySharingSchedulerPrivateCertificateExpirationName) + ->second.fake_scheduler; + public_cert_exp_scheduler_ = + scheduler_factory_.pref_name_to_expiration_instance() + .find(prefs::kNearbySharingSchedulerPublicCertificateExpirationName) + ->second.fake_scheduler; + upload_scheduler_ = + scheduler_factory_.pref_name_to_on_demand_instance() + .find( + prefs:: + kNearbySharingSchedulerUploadLocalDeviceCertificatesName) // NOLINT + ->second.fake_scheduler; + download_scheduler_ = + scheduler_factory_.pref_name_to_periodic_instance() + .find(prefs::kNearbySharingSchedulerDownloadPublicCertificatesName) + ->second.fake_scheduler; + + PopulatePrivateCertificates(); + PopulatePublicCertificates(); + cert_manager_->Start(); + } + + void TearDown() override { + cert_manager_->RemoveObserver(this); + NearbyShareSchedulerFactory::SetFactoryForTesting(nullptr); + NearbyShareCertificateStorageImpl::Factory::SetFactoryForTesting(nullptr); + } + + void SetBluetoothMacAddress(absl::string_view bluetooth_mac_address) { + bluetooth_mac_address_ = bluetooth_mac_address; + } + + // NearbyShareCertificateManager::Observer: + void OnPublicCertificatesDownloaded() override { + ++num_public_certs_downloaded_notifications_; + } + void OnPrivateCertificatesChanged() override { + ++num_private_certs_changed_notifications_; + } + + protected: + enum class DownloadPublicCertificatesResult { + kSuccess, + kTimeout, + kHttpError, + kStorageError + }; + + absl::Time Now() { return fake_context_.GetClock()->Now(); } + + // Fast-forwards mock time by |delta| and fires relevant timers. + void FastForward(absl::Duration delta) { + fake_context_.fake_clock()->FastForward(delta); + } + + void Sync() { + EXPECT_TRUE(FakeTaskRunner::WaitForRunningTasksWithTimeout( + absl::Milliseconds(1000))); + } + + void SetMockBluetoothAddress(absl::string_view bluetooth_mac_address) { + FakeBluetoothAdapter& bluetooth_adapter = + dynamic_cast( + fake_context_.GetBluetoothAdapter()); + bluetooth_adapter.SetAddress(bluetooth_mac_address); + } + + void SetBluetoothAdapterIsPresent(bool is_present) { + if (!is_present) { + FakeBluetoothAdapter& bluetooth_adapter = + dynamic_cast( + fake_context_.GetBluetoothAdapter()); + bluetooth_adapter.SetAddress(""); + } + } + + void GetPublicCertificatesCallback( + bool success, const std::vector& certs) { + auto& callbacks = cert_store_->get_public_certificates_callbacks(); + auto callback = std::move(callbacks.back()); + callbacks.pop_back(); + auto pub_certs = std::make_unique>( + certs.begin(), certs.end()); + std::move(callback)(success, std::move(pub_certs)); + } + + void HandlePrivateCertificateRefresh(bool expect_private_cert_refresh, + bool expected_success) { + if (expect_private_cert_refresh) { + private_cert_exp_scheduler_->InvokeRequestCallback(); + } + Sync(); + EXPECT_EQ(expect_private_cert_refresh ? 1u : 0u, + private_cert_exp_scheduler_->handled_results().size()); + if (expect_private_cert_refresh) { + EXPECT_EQ(expected_success, + private_cert_exp_scheduler_->handled_results().back()); + } + EXPECT_EQ(expect_private_cert_refresh && expected_success ? 1u : 0u, + num_private_certs_changed_notifications_); + EXPECT_EQ(expect_private_cert_refresh && expected_success ? 1u : 0u, + upload_scheduler_->num_immediate_requests()); + } + + void VerifyPrivateCertificates( + const nearby::sharing::proto::EncryptedMetadata& expected_metadata) { + // Expect a full set of certificates for all-contacts, selected-contacts, + // and self-share + std::vector certs = + *cert_store_->GetPrivateCertificates(); + EXPECT_EQ(3 * kNearbyShareNumPrivateCertificates, certs.size()); + + absl::Time min_not_before_all_contacts = absl::InfiniteFuture(); + absl::Time min_not_before_selected_contacts = absl::InfiniteFuture(); + absl::Time min_not_before_self_share = absl::InfiniteFuture(); + + absl::Time max_not_after_all_contacts = absl::InfinitePast(); + absl::Time max_not_after_selected_contacts = absl::InfinitePast(); + absl::Time max_not_after_self_share = absl::InfinitePast(); + + for (const auto& cert : certs) { + EXPECT_EQ(cert.not_after() - cert.not_before(), + kNearbyShareCertificateValidityPeriod); + switch (cert.visibility()) { + case DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS: + min_not_before_all_contacts = + std::min(min_not_before_all_contacts, cert.not_before()); + max_not_after_all_contacts = + std::max(max_not_after_all_contacts, cert.not_after()); + break; + case DeviceVisibility::DEVICE_VISIBILITY_SELECTED_CONTACTS: + min_not_before_selected_contacts = + std::min(min_not_before_selected_contacts, cert.not_before()); + max_not_after_selected_contacts = + std::max(max_not_after_selected_contacts, cert.not_after()); + break; + case DeviceVisibility::DEVICE_VISIBILITY_SELF_SHARE: + min_not_before_self_share = + std::min(min_not_before_self_share, cert.not_before()); + max_not_after_self_share = + std::max(max_not_after_self_share, cert.not_after()); + break; + default: + NL_DCHECK(false); + break; + } + + // Verify metadata. + EXPECT_EQ(expected_metadata.SerializeAsString(), + cert.unencrypted_metadata().SerializeAsString()); + } + + // Verify contiguous validity periods + EXPECT_EQ(max_not_after_all_contacts - min_not_before_all_contacts, + kNearbyShareNumPrivateCertificates * + kNearbyShareCertificateValidityPeriod); + EXPECT_EQ( + max_not_after_selected_contacts - min_not_before_selected_contacts, + kNearbyShareNumPrivateCertificates * + kNearbyShareCertificateValidityPeriod); + } + + void RunUpload(bool success) { + size_t initial_num_upload_calls = + local_device_data_manager_->upload_certificates_calls().size(); + local_device_data_manager_->SetUploadCertificatesResult(success); + size_t initial_num_handled_results = + upload_scheduler_->handled_results().size(); + + upload_scheduler_->InvokeRequestCallback(); + Sync(); + EXPECT_EQ(local_device_data_manager_->upload_certificates_calls().size(), + initial_num_upload_calls + 1); + + EXPECT_EQ(local_device_data_manager_->upload_certificates_calls() + .back() + .certificates.size(), + 3 * kNearbyShareNumPrivateCertificates); + + EXPECT_EQ(upload_scheduler_->handled_results().size(), + initial_num_handled_results + 1); + EXPECT_EQ(upload_scheduler_->handled_results().back(), success); + } + + // Test downloading public certificates with or without errors. The RPC is + // paginated, and |num_pages| will be simulated. Any failures, as indicated by + // |result|, will be simulated on the last page. + void DownloadPublicCertificatesFlow(size_t num_pages, + DownloadPublicCertificatesResult result) { + size_t prev_num_results = download_scheduler_->handled_results().size(); + cert_store_->SetPublicCertificateIds(kPublicCertificateIds); + + size_t initial_num_notifications = + num_public_certs_downloaded_notifications_; + size_t initial_num_public_cert_exp_reschedules = + public_cert_exp_scheduler_->num_reschedule_calls(); + + // Build RPC responses. + std::vector> + responses; + std::string page_token; + for (size_t page_number = 0; page_number < num_pages; ++page_number) { + bool last_page = page_number == num_pages - 1; + if (last_page && result == DownloadPublicCertificatesResult::kHttpError) { + responses.push_back(absl::InternalError("")); + break; + } + page_token = last_page ? std::string() + : absl::StrCat(kPageTokenPrefix, page_number); + responses.push_back(BuildRpcResponse(page_number, page_token)); + } + + client_factory_.instances().back()->SetListPublicCertificatesResponses( + responses); + cert_store_->SetAddPublicCertificatesResult( + result != DownloadPublicCertificatesResult::kStorageError); + download_scheduler_->InvokeRequestCallback(); + Sync(); + + CheckRpcRequest(num_pages); + ASSERT_EQ(download_scheduler_->handled_results().size(), + prev_num_results + 1); + + bool success = result == DownloadPublicCertificatesResult::kSuccess; + EXPECT_EQ(download_scheduler_->handled_results().back(), success); + EXPECT_EQ(num_public_certs_downloaded_notifications_, + initial_num_notifications + (success ? 1u : 0u)); + EXPECT_EQ(public_cert_exp_scheduler_->num_reschedule_calls(), + initial_num_public_cert_exp_reschedules + (success ? 1u : 0u)); + } + + void CheckRpcRequest(int num_pages) { + std::vector requests = + client_factory_.instances().back()->list_public_certificates_requests(); + EXPECT_EQ(requests.size(), num_pages); + } + + nearby::sharing::proto::ListPublicCertificatesResponse BuildRpcResponse( + size_t page_number, absl::string_view page_token) { + nearby::sharing::proto::ListPublicCertificatesResponse response; + for (size_t i = 0; i < public_certificates_.size(); ++i) { + public_certificates_[i].set_secret_id( + absl::StrCat(kSecretIdPrefix, page_number, "_", i)); + response.add_public_certificates(); + *response.mutable_public_certificates(i) = public_certificates_[i]; + } + response.set_next_page_token(page_token); + return response; + } + + void CheckStorageAddCertificates( + const FakeNearbyShareCertificateStorage::AddPublicCertificatesCall& + add_cert_call) { + ASSERT_EQ(add_cert_call.public_certificates.size(), + public_certificates_.size()); + for (size_t i = 0; i < public_certificates_.size(); ++i) { + EXPECT_EQ(add_cert_call.public_certificates[i].secret_id(), + public_certificates_[i].secret_id()); + } + } + + void PopulatePrivateCertificates() { + private_certificates_.clear(); + const auto& metadata = GetNearbyShareTestMetadata(); + for (auto visibility : + {DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS, + DeviceVisibility::DEVICE_VISIBILITY_SELECTED_CONTACTS, + DeviceVisibility::DEVICE_VISIBILITY_SELF_SHARE}) { + private_certificates_.emplace_back(visibility, t0, metadata); + private_certificates_.emplace_back( + visibility, t0 + kNearbyShareCertificateValidityPeriod, metadata); + private_certificates_.emplace_back( + visibility, t0 + kNearbyShareCertificateValidityPeriod * 2, metadata); + } + } + + void PopulatePublicCertificates() { + public_certificates_.clear(); + metadata_encryption_keys_.clear(); + auto& metadata1 = GetNearbyShareTestMetadata(); + nearby::sharing::proto::EncryptedMetadata metadata2; + metadata2.set_device_name("device_name2"); + metadata2.set_full_name("full_name2"); + metadata2.set_icon_url("icon_url2"); + metadata2.set_bluetooth_mac_address("bluetooth_mac_address2"); + for (auto metadata : {metadata1, metadata2}) { + auto private_cert = NearbySharePrivateCertificate( + DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS, t0, metadata); + public_certificates_.push_back(*private_cert.ToPublicCertificate()); + metadata_encryption_keys_.push_back(*private_cert.EncryptMetadataKey()); + } + } + + nearby::sharing::api::MockSharingPlatform mock_sharing_platform_; + nearby::FakePreferenceManager preference_manager_; + FakeAccountManager fake_account_manager_; + FakeContext fake_context_; + FakeNearbyShareCertificateStorage* cert_store_ = nullptr; + FakeNearbyShareScheduler* private_cert_exp_scheduler_ = nullptr; + FakeNearbyShareScheduler* public_cert_exp_scheduler_ = nullptr; + FakeNearbyShareScheduler* upload_scheduler_ = nullptr; + FakeNearbyShareScheduler* download_scheduler_ = nullptr; + std::string bluetooth_mac_address_ = kTestUnparsedBluetoothMacAddress; + size_t num_public_certs_downloaded_notifications_ = 0; + size_t num_private_certs_changed_notifications_ = 0; + std::vector private_certificates_; + std::vector public_certificates_; + std::vector metadata_encryption_keys_; + + FakeNearbyShareClientFactory client_factory_; + FakeNearbyShareSchedulerFactory scheduler_factory_; + FakeNearbyShareCertificateStorage::Factory cert_store_factory_; + std::unique_ptr + local_device_data_manager_; + std::unique_ptr contact_manager_; + std::unique_ptr cert_manager_; +}; + +TEST_F(NearbyShareCertificateManagerImplTest, + EncryptPrivateCertificateMetadataKey) { + // No valid certificates exist. + cert_store_->ReplacePrivateCertificates({}); + EXPECT_FALSE(cert_manager_->EncryptPrivateCertificateMetadataKey( + DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS)); + EXPECT_FALSE(cert_manager_->EncryptPrivateCertificateMetadataKey( + DeviceVisibility::DEVICE_VISIBILITY_SELECTED_CONTACTS)); + + // Set up valid all-contacts visibility certificate. + NearbySharePrivateCertificate private_certificate = + GetNearbyShareTestPrivateCertificate( + DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS); + cert_store_->ReplacePrivateCertificates({private_certificate}); + FastForward(GetNearbyShareTestNotBefore() + + kNearbyShareCertificateValidityPeriod * 0.5 - Now()); + + // Sanity check that the cert storage is as expected. + std::optional> stored_certs = + cert_store_->GetPrivateCertificates(); + EXPECT_EQ(stored_certs->at(0).ToCertificateData(), + private_certificate.ToCertificateData()); + + std::optional encrypted_metadata_key = + cert_manager_->EncryptPrivateCertificateMetadataKey( + DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS); + EXPECT_EQ(GetNearbyShareTestEncryptedMetadataKey().encrypted_key(), + encrypted_metadata_key->encrypted_key()); + EXPECT_EQ(GetNearbyShareTestEncryptedMetadataKey().salt(), + encrypted_metadata_key->salt()); + EXPECT_FALSE(cert_manager_->EncryptPrivateCertificateMetadataKey( + DeviceVisibility::DEVICE_VISIBILITY_SELECTED_CONTACTS)); + + // Verify that storage is updated when salts are consumed during encryption. + EXPECT_NE(cert_store_->GetPrivateCertificates()->at(0).ToCertificateData(), + private_certificate.ToCertificateData()); + + // No valid certificates exist. + FastForward(kNearbyShareCertificateValidityPeriod); + EXPECT_FALSE(cert_manager_->EncryptPrivateCertificateMetadataKey( + DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS)); + EXPECT_FALSE(cert_manager_->EncryptPrivateCertificateMetadataKey( + DeviceVisibility::DEVICE_VISIBILITY_SELECTED_CONTACTS)); +} + +TEST_F(NearbyShareCertificateManagerImplTest, SignWithPrivateCertificate) { + NearbySharePrivateCertificate private_certificate = + GetNearbyShareTestPrivateCertificate( + DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS); + cert_store_->ReplacePrivateCertificates({private_certificate}); + FastForward(GetNearbyShareTestNotBefore() + + kNearbyShareCertificateValidityPeriod * 0.5 - Now()); + + // Perform sign/verify round trip. + EXPECT_TRUE(GetNearbyShareTestDecryptedPublicCertificate().VerifySignature( + GetNearbyShareTestPayloadToSign(), + *cert_manager_->SignWithPrivateCertificate( + DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS, + GetNearbyShareTestPayloadToSign()))); + + // No selected-contact visibility certificate in storage. + EXPECT_FALSE(cert_manager_->SignWithPrivateCertificate( + DeviceVisibility::DEVICE_VISIBILITY_SELECTED_CONTACTS, + GetNearbyShareTestPayloadToSign())); +} + +TEST_F(NearbyShareCertificateManagerImplTest, + HashAuthenticationTokenWithPrivateCertificate) { + NearbySharePrivateCertificate private_certificate = + GetNearbyShareTestPrivateCertificate( + DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS); + cert_store_->ReplacePrivateCertificates({private_certificate}); + FastForward(GetNearbyShareTestNotBefore() + + kNearbyShareCertificateValidityPeriod * 0.5 - Now()); + + EXPECT_EQ(private_certificate.HashAuthenticationToken( + GetNearbyShareTestPayloadToSign()), + cert_manager_->HashAuthenticationTokenWithPrivateCertificate( + DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS, + GetNearbyShareTestPayloadToSign())); + + // No selected-contact visibility certificate in storage. + EXPECT_FALSE(cert_manager_->HashAuthenticationTokenWithPrivateCertificate( + DeviceVisibility::DEVICE_VISIBILITY_SELECTED_CONTACTS, + GetNearbyShareTestPayloadToSign())); +} + +TEST_F(NearbyShareCertificateManagerImplTest, + GetDecryptedPublicCertificateSuccess) { + std::optional decrypted_pub_cert; + cert_manager_->GetDecryptedPublicCertificate( + metadata_encryption_keys_[0], + [&](std::optional cert) { + CaptureDecryptedPublicCertificateCallback(&decrypted_pub_cert, cert); + }); + GetPublicCertificatesCallback(true, public_certificates_); + + ASSERT_TRUE(decrypted_pub_cert); + std::vector id(public_certificates_[0].secret_id().begin(), + public_certificates_[0].secret_id().end()); + EXPECT_EQ(decrypted_pub_cert->id(), id); + EXPECT_EQ(decrypted_pub_cert->unencrypted_metadata().SerializeAsString(), + GetNearbyShareTestMetadata().SerializeAsString()); +} + +TEST_F(NearbyShareCertificateManagerImplTest, + GetDecryptedPublicCertificateCertNotFound) { + auto private_cert = NearbySharePrivateCertificate( + DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS, t0, + GetNearbyShareTestMetadata()); + auto metadata_key = private_cert.EncryptMetadataKey(); + ASSERT_TRUE(metadata_key); + + std::optional decrypted_pub_cert; + cert_manager_->GetDecryptedPublicCertificate( + *metadata_key, + [&](std::optional cert) { + CaptureDecryptedPublicCertificateCallback(&decrypted_pub_cert, cert); + }); + + GetPublicCertificatesCallback(true, public_certificates_); + + EXPECT_FALSE(decrypted_pub_cert); +} + +TEST_F(NearbyShareCertificateManagerImplTest, + GetDecryptedPublicCertificateGetPublicCertificatesFailure) { + std::optional decrypted_pub_cert; + cert_manager_->GetDecryptedPublicCertificate( + metadata_encryption_keys_[0], + [&](std::optional cert) { + CaptureDecryptedPublicCertificateCallback(&decrypted_pub_cert, cert); + }); + + GetPublicCertificatesCallback(false, {}); + + EXPECT_FALSE(decrypted_pub_cert); +} + +TEST_F(NearbyShareCertificateManagerImplTest, + DownloadPublicCertificatesSuccess) { + ASSERT_NO_FATAL_FAILURE(DownloadPublicCertificatesFlow( + /*num_pages=*/2, DownloadPublicCertificatesResult::kSuccess)); +} + +TEST_F(NearbyShareCertificateManagerImplTest, + DownloadPublicCertificatesRPCFailure) { + ASSERT_NO_FATAL_FAILURE(DownloadPublicCertificatesFlow( + /*num_pages=*/2, DownloadPublicCertificatesResult::kHttpError)); +} + +TEST_F(NearbyShareCertificateManagerImplTest, ClearPublicCertificates) { + cert_manager_->ClearPublicCertificates([&](bool result) {}); + EXPECT_THAT(cert_store_->clear_public_certificates_callbacks(), + ::testing::SizeIs(1)); +} + +TEST_F(NearbyShareCertificateManagerImplTest, + DownloadPublicCertificatesStoreFailure) { + ASSERT_NO_FATAL_FAILURE(DownloadPublicCertificatesFlow( + /*num_pages=*/2, DownloadPublicCertificatesResult::kStorageError)); +} + +TEST_F(NearbyShareCertificateManagerImplTest, + RefreshPrivateCertificates_ValidCertificates) { + cert_store_->ReplacePrivateCertificates(private_certificates_); + + HandlePrivateCertificateRefresh(/*expect_private_cert_refresh=*/false, + /*expected_success=*/true); + VerifyPrivateCertificates(/*expected_metadata=*/GetNearbyShareTestMetadata()); +} + +TEST_F(NearbyShareCertificateManagerImplTest, + RefreshPrivateCertificates_NoCertificates_UploadSuccess) { + cert_store_->ReplacePrivateCertificates({}); + + HandlePrivateCertificateRefresh(/*expect_private_cert_refresh=*/true, + /*expected_success=*/true); + RunUpload(/*success=*/true); + VerifyPrivateCertificates(/*expected_metadata=*/GetNearbyShareTestMetadata()); +} + +TEST_F(NearbyShareCertificateManagerImplTest, + RefreshPrivateCertificates_NoCertificates_UploadFailure) { + cert_store_->ReplacePrivateCertificates({}); + + HandlePrivateCertificateRefresh(/*expect_private_cert_refresh=*/true, + /*expected_success=*/true); + RunUpload(/*success=*/false); + VerifyPrivateCertificates(/*expected_metadata=*/GetNearbyShareTestMetadata()); +} + +TEST_F(NearbyShareCertificateManagerImplTest, + RevokePrivateCertificates_OnContactsUploaded) { + // Destroy and recreate private certificates if contact data has changed since + // the last successful upload. + cert_manager_->Stop(); + size_t num_expected_calls = 0; + for (bool did_contacts_change_since_last_upload : {true, false}) { + cert_store_->ReplacePrivateCertificates(private_certificates_); + contact_manager_->NotifyContactsUploaded( + did_contacts_change_since_last_upload); + + Sync(); + std::vector certs = + *cert_store_->GetPrivateCertificates(); + + if (did_contacts_change_since_last_upload) { + ++num_expected_calls; + EXPECT_TRUE(certs.empty()); + } else { + EXPECT_EQ(certs.size(), 9u); + } + + EXPECT_EQ(num_expected_calls, + private_cert_exp_scheduler_->num_immediate_requests()); + } +} + +TEST_F(NearbyShareCertificateManagerImplTest, + RefreshPrivateCertificates_OnLocalDeviceMetadataChanged) { + cert_manager_->Start(); + + // Destroy and recreate private certificates if any metadata fields change. + size_t num_expected_calls = 0; + for (bool did_device_name_change : {true, false}) { + for (bool did_full_name_change : {true, false}) { + for (bool did_icon_change : {true, false}) { + local_device_data_manager_->NotifyLocalDeviceDataChanged( + did_device_name_change, did_full_name_change, did_icon_change); + Sync(); + + if (did_device_name_change || did_full_name_change || did_icon_change) { + ++num_expected_calls; + EXPECT_TRUE(cert_store_->GetPrivateCertificates()->empty()); + } + + EXPECT_EQ(num_expected_calls, + private_cert_exp_scheduler_->num_immediate_requests()); + } + } + } +} + +TEST_F(NearbyShareCertificateManagerImplTest, + RefreshPrivateCertificates_ExpiredCertificate) { + // First certificates are expired; + FastForward(kNearbyShareCertificateValidityPeriod * 1.5); + cert_store_->ReplacePrivateCertificates(private_certificates_); + + cert_manager_->Start(); + HandlePrivateCertificateRefresh(/*expect_private_cert_refresh=*/true, + /*expected_success=*/true); + RunUpload(/*success=*/true); + VerifyPrivateCertificates(/*expected_metadata=*/GetNearbyShareTestMetadata()); +} + +TEST_F(NearbyShareCertificateManagerImplTest, + RefreshPrivateCertificates_InvalidDeviceName) { + cert_store_->ReplacePrivateCertificates({}); + + // Device name is missing in local device data manager. + local_device_data_manager_->SetDeviceName(std::string()); + + cert_manager_->Start(); + + // Expect failure because a device name is required. + HandlePrivateCertificateRefresh(/*expect_private_cert_refresh=*/true, + /*expected_success=*/false); +} + +TEST_F(NearbyShareCertificateManagerImplTest, + RefreshPrivateCertificates_BluetoothAdapterNotPresent) { + cert_store_->ReplacePrivateCertificates({}); + + SetBluetoothAdapterIsPresent(false); + + cert_manager_->Start(); + + // Expect failure because a Bluetooth MAC address is required. + HandlePrivateCertificateRefresh(/*expect_private_cert_refresh=*/true, + /*expected_success=*/false); +} + +TEST_F(NearbyShareCertificateManagerImplTest, + RefreshPrivateCertificates_MissingFullNameAndIconUrl) { + cert_store_->ReplacePrivateCertificates({}); + + // Full name and icon URL are missing in the local device data manager. + local_device_data_manager_->SetFullName(std::nullopt); + local_device_data_manager_->SetIconUrl(std::nullopt); + + cert_manager_->Start(); + HandlePrivateCertificateRefresh(/*expect_private_cert_refresh=*/true, + /*expected_success=*/true); + RunUpload(/*success=*/true); + + // The full name and icon URL are not set. + nearby::sharing::proto::EncryptedMetadata metadata = + GetNearbyShareTestMetadata(); + metadata.clear_full_name(); + metadata.clear_icon_url(); + + VerifyPrivateCertificates(/*expected_metadata=*/metadata); +} + +TEST_F(NearbyShareCertificateManagerImplTest, + RemoveExpiredPublicCertificates_Success) { + // The public certificate expiration scheduler notifies the certificate + // manager that a public certificate has expired. + EXPECT_EQ(cert_store_->remove_expired_public_certificates_calls().size(), 0u); + EXPECT_EQ(public_cert_exp_scheduler_->handled_results().size(), 0u); + cert_store_->SetRemoveExpiredPublicCertificatesResult(true); + public_cert_exp_scheduler_->InvokeRequestCallback(); + Sync(); + EXPECT_EQ(cert_store_->remove_expired_public_certificates_calls().size(), 1u); + EXPECT_EQ(cert_store_->remove_expired_public_certificates_calls().back().now, + t0); + EXPECT_EQ(public_cert_exp_scheduler_->handled_results().size(), 1u); + EXPECT_TRUE(public_cert_exp_scheduler_->handled_results().back()); +} + +TEST_F(NearbyShareCertificateManagerImplTest, + RemoveExpiredPublicCertificates_Failure) { + // The public certificate expiration scheduler notifies the certificate + // manager that a public certificate has expired. + EXPECT_EQ(cert_store_->remove_expired_public_certificates_calls().size(), 0u); + cert_store_->SetRemoveExpiredPublicCertificatesResult(false); + EXPECT_EQ(public_cert_exp_scheduler_->handled_results().size(), 0u); + + public_cert_exp_scheduler_->InvokeRequestCallback(); + Sync(); + EXPECT_EQ(cert_store_->remove_expired_public_certificates_calls().size(), 1u); + EXPECT_EQ(cert_store_->remove_expired_public_certificates_calls().back().now, + t0); + + EXPECT_EQ(public_cert_exp_scheduler_->handled_results().size(), 1u); + EXPECT_FALSE(public_cert_exp_scheduler_->handled_results().back()); +} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/certificates/nearby_share_certificate_storage.cc b/sharing/certificates/nearby_share_certificate_storage.cc new file mode 100644 index 00000000..f5fba1d9 --- /dev/null +++ b/sharing/certificates/nearby_share_certificate_storage.cc @@ -0,0 +1,130 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/certificates/nearby_share_certificate_storage.h" + +#include +#include + +#include +#include +#include +#include + +#include "absl/time/time.h" +#include "sharing/certificates/common.h" +#include "sharing/certificates/nearby_share_private_certificate.h" +#include "sharing/common/nearby_share_enums.h" +#include "sharing/internal/base/encode.h" +#include "sharing/internal/public/logging.h" +#include "sharing/proto/enums.pb.h" + +namespace nearby { +namespace sharing { + +using ::nearby::sharing::proto::DeviceVisibility; + +std::optional +NearbyShareCertificateStorage::NextPrivateCertificateExpirationTime() { + std::optional> certs = + GetPrivateCertificates(); + if (!certs || certs->empty()) return std::nullopt; + + absl::Time min_time = absl::InfiniteFuture(); + for (const NearbySharePrivateCertificate& cert : *certs) + min_time = std::min(min_time, cert.not_after()); + + return min_time; +} + +void NearbyShareCertificateStorage::UpdatePrivateCertificate( + const NearbySharePrivateCertificate& private_certificate) { + std::optional> certs = + GetPrivateCertificates(); + if (!certs) { + NL_LOG(WARNING) << __func__ << ": No private certificates to update."; + return; + } + + auto it = std::find_if( + certs->begin(), certs->end(), + [&private_certificate](const NearbySharePrivateCertificate& cert) { + return cert.id() == private_certificate.id(); + }); + if (it == certs->end()) { + NL_VLOG(1) << __func__ << ": No private certificate with id=" + << nearby::utils::HexEncode(private_certificate.id()); + return; + } + + NL_VLOG(1) << __func__ << ": Updating private certificate id=" + << nearby::utils::HexEncode(private_certificate.id()); + *it = private_certificate; + ReplacePrivateCertificates(*certs); +} + +void NearbyShareCertificateStorage::RemoveExpiredPrivateCertificates( + absl::Time now) { + std::optional> certs = + GetPrivateCertificates(); + if (!certs) return; + + std::vector unexpired_certs; + for (const NearbySharePrivateCertificate& cert : *certs) { + if (!IsNearbyShareCertificateExpired( + now, cert.not_after(), + /*use_public_certificate_tolerance=*/false)) { + unexpired_certs.push_back(cert); + } + } + + size_t num_removed = certs->size() - unexpired_certs.size(); + if (num_removed == 0) return; + + NL_VLOG(1) << __func__ << ": Removing " << num_removed + << " expired private certificates."; + ReplacePrivateCertificates(unexpired_certs); +} + +void NearbyShareCertificateStorage::ClearPrivateCertificates() { + NL_VLOG(1) << __func__ << ": Removing all private certificates."; + ReplacePrivateCertificates({}); +} + +void NearbyShareCertificateStorage::ClearPrivateCertificatesOfVisibility( + DeviceVisibility visibility) { + std::optional> certs = + GetPrivateCertificates(); + if (!certs) return; + + bool were_certs_removed = false; + std::vector new_certs; + for (const NearbySharePrivateCertificate& cert : *certs) { + if (cert.visibility() == visibility) { + were_certs_removed = true; + } else { + new_certs.push_back(cert); + } + } + + if (were_certs_removed) { + NL_VLOG(1) << __func__ + << ": Removing all private certificates of visibility " + << static_cast(visibility); + ReplacePrivateCertificates(new_certs); + } +} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/certificates/nearby_share_certificate_storage.h b/sharing/certificates/nearby_share_certificate_storage.h new file mode 100644 index 00000000..1003409f --- /dev/null +++ b/sharing/certificates/nearby_share_certificate_storage.h @@ -0,0 +1,115 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_CERTIFICATES_NEARBY_SHARE_CERTIFICATE_STORAGE_H_ +#define THIRD_PARTY_NEARBY_SHARING_CERTIFICATES_NEARBY_SHARE_CERTIFICATE_STORAGE_H_ + +#include +#include +#include +#include +#include + +#include "absl/time/time.h" +#include "absl/types/span.h" +#include "sharing/certificates/nearby_share_private_certificate.h" +#include "sharing/common/nearby_share_enums.h" +#include "sharing/proto/enums.pb.h" +#include "sharing/proto/rpc_resources.pb.h" + +namespace nearby { +namespace sharing { + +// Stores local-device private certificates and remote-device public +// certificates. Provides methods to help manage certificate expiration. Due to +// the potentially large number of public certificates, some methods are +// asynchronous. +class NearbyShareCertificateStorage { + public: + using ResultCallback = std::function; + using PublicCertificateCallback = std::function>)>; + + NearbyShareCertificateStorage() = default; + virtual ~NearbyShareCertificateStorage() = default; + + // Returns the secret ids of all stored public certificates + virtual std::vector GetPublicCertificateIds() const = 0; + + // Returns all public certificates currently in storage. No RPC call is made. + virtual void GetPublicCertificates(PublicCertificateCallback callback) = 0; + + // Returns all private certificates currently in storage. Will return + // absl::nullopt if deserialization from prefs fails -- not expected to happen + // under normal circumstances. + virtual std::optional> + GetPrivateCertificates() const = 0; + + // Returns the next time a certificate expires or absl::nullopt if no + // certificates are present. + std::optional NextPrivateCertificateExpirationTime(); + virtual std::optional NextPublicCertificateExpirationTime() + const = 0; + + // Deletes existing private certificates and replaces them with + // |private_certificates|. + virtual void ReplacePrivateCertificates( + absl::Span private_certificates) = 0; + + // Deletes existing public certificates and replaces them with + // |public_certificates|. + virtual void ReplacePublicCertificates( + absl::Span + public_certificates, + ResultCallback callback) = 0; + + // Overwrites an existing record with |private_certificate| if that record + // has the same ID . If no such record exists in storage, no action is taken. + // This method is necessary for updating the private certificate's list of + // consumed salts. + void UpdatePrivateCertificate( + const NearbySharePrivateCertificate& private_certificate); + + // Adds public certificates, or replaces existing certificates + // by secret_id + virtual void AddPublicCertificates( + absl::Span + public_certificates, + ResultCallback callback) = 0; + + // Removes all private certificates from storage with expiration date after + // |now|. + void RemoveExpiredPrivateCertificates(absl::Time now); + + // Removes all public certificates from storage with expiration date after + // |now|. + virtual void RemoveExpiredPublicCertificates(absl::Time now, + ResultCallback callback) = 0; + + // Delete all private certificates from memory and persistent storage. + void ClearPrivateCertificates(); + + // Delete private certificates with |visibility| from memory and persistent + // storage. + void ClearPrivateCertificatesOfVisibility(proto::DeviceVisibility visibility); + + // Delete all public certificates from memory and persistent storage. + virtual void ClearPublicCertificates(ResultCallback callback) = 0; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_CERTIFICATES_NEARBY_SHARE_CERTIFICATE_STORAGE_H_ diff --git a/sharing/certificates/nearby_share_certificate_storage_impl.cc b/sharing/certificates/nearby_share_certificate_storage_impl.cc new file mode 100644 index 00000000..77ae5ceb --- /dev/null +++ b/sharing/certificates/nearby_share_certificate_storage_impl.cc @@ -0,0 +1,602 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/certificates/nearby_share_certificate_storage_impl.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "absl/container/btree_map.h" +#include "absl/container/flat_hash_set.h" +#include "absl/strings/escaping.h" +#include "absl/strings/string_view.h" +#include "absl/time/clock.h" +#include "absl/time/time.h" +#include "absl/types/span.h" +#include "sharing/certificates/common.h" +#include "sharing/certificates/constants.h" +#include "sharing/certificates/nearby_share_certificate_storage.h" +#include "sharing/certificates/nearby_share_private_certificate.h" +#include "sharing/common/nearby_share_prefs.h" +#include "sharing/internal/api/preference_manager.h" +#include "sharing/internal/api/private_certificate_data.h" +#include "sharing/internal/api/public_certificate_database.h" +#include "sharing/internal/public/logging.h" +#include "sharing/proto/rpc_resources.pb.h" +#include "sharing/proto/timestamp.pb.h" + +namespace nearby { +namespace sharing { +namespace { +using ::nearby::sharing::api::PreferenceManager; +using ::nearby::sharing::api::PrivateCertificateData; +using ::nearby::sharing::api::PublicCertificateDatabase; + +// Compare to leveldb_proto::Enums::InitStatus. Using a separate enum so that +// the values don't change. +// These values are persisted to logs. Entries should not be renumbered and +// numeric values should never be reused. +enum InitStatusMetric { + kOK = 0, + kNotInitialized = 1, + kError = 2, + kCorrupt = 3, + kInvalidOperation = 4, + kMaxValue = kInvalidOperation +}; + +std::string EncodeString(absl::string_view unencoded_string) { + std::string result; + absl::WebSafeBase64Escape(unencoded_string, &result); + return result; +} + +std::optional DecodeString(const std::string* encoded_string) { + std::string result; + if (!encoded_string) return std::nullopt; + + if (absl::WebSafeBase64Unescape(*encoded_string, &result)) { + return result; + } + return std::nullopt; +} + +bool SortBySecond(const std::pair& pair1, + const std::pair& pair2) { + return pair1.second < pair2.second; +} + +NearbyShareCertificateStorageImpl::ExpirationList MergeExpirations( + const NearbyShareCertificateStorageImpl::ExpirationList& old_exp, + const NearbyShareCertificateStorageImpl::ExpirationList& new_exp) { + // Remove duplicates with a preference for new entries. + absl::btree_map merged_map(new_exp.begin(), + new_exp.end()); + merged_map.insert(old_exp.begin(), old_exp.end()); + // Convert map to vector and sort by expiration time. + NearbyShareCertificateStorageImpl::ExpirationList merged(merged_map.begin(), + merged_map.end()); + std::sort(merged.begin(), merged.end(), SortBySecond); + return merged; +} + +absl::Time TimestampToTime(nearby::sharing::proto::Timestamp timestamp) { + return absl::UnixEpoch() + absl::Seconds(timestamp.seconds()) + + absl::Nanoseconds(timestamp.nanos()); +} + +} // namespace + +// static +NearbyShareCertificateStorageImpl::Factory* + NearbyShareCertificateStorageImpl::Factory::test_factory_ = nullptr; + +// static +std::shared_ptr +NearbyShareCertificateStorageImpl::Factory::Create( + PreferenceManager& preference_manager, + std::unique_ptr public_certificate_database) { + if (test_factory_) { + return test_factory_->CreateInstance( + preference_manager, std::move(public_certificate_database)); + } + + auto storage = std::shared_ptr( + new NearbyShareCertificateStorageImpl( + preference_manager, std::move(public_certificate_database))); + // Initialize cannot be called from c'tor since it tries to create a weak_ptr. + storage->Initialize(); + return storage; +} + +// static +void NearbyShareCertificateStorageImpl::Factory::SetFactoryForTesting( + Factory* test_factory) { + test_factory_ = test_factory; +} + +NearbyShareCertificateStorageImpl::Factory::~Factory() = default; + +NearbyShareCertificateStorageImpl::NearbyShareCertificateStorageImpl( + PreferenceManager& preference_manager, + std::unique_ptr public_certificate_database) + : preference_manager_(preference_manager), + public_certificate_database_(std::move(public_certificate_database)) { + FetchPublicCertificateExpirations(); +} + +NearbyShareCertificateStorageImpl::~NearbyShareCertificateStorageImpl() = + default; + +void NearbyShareCertificateStorageImpl::Initialize() { + switch (init_status_) { + case InitStatus::kUninitialized: + case InitStatus::kFailed: + num_initialize_attempts_++; + if (num_initialize_attempts_ > + kNearbyShareCertificateStorageMaxNumInitializeAttempts) { + FinishInitialization(false); + break; + } + + NL_VLOG(1) << __func__ + << ": Attempting to initialize public certificate " + "database. Number of attempts: " + << num_initialize_attempts_; + public_certificate_database_->Initialize( + [weak_this = + weak_from_this()](PublicCertificateDatabase::InitStatus status) { + if (auto storage = weak_this.lock()) { + storage->OnDatabaseInitialized(absl::Now(), status); + } + }); + break; + case InitStatus::kInitialized: + NL_LOG(INFO) << __func__ << " already initialized."; + break; + } +} + +void NearbyShareCertificateStorageImpl::DestroyAndReinitialize() { + NL_LOG(ERROR) << __func__ + << ": Public certificate database corrupt. Erasing and " + "initializing new database."; + init_status_ = InitStatus::kUninitialized; + public_certificate_database_->Destroy( + [weak_this = weak_from_this()](bool success) { + if (auto storage = weak_this.lock()) { + storage->OnDatabaseDestroyedReinitialize( + [&](bool result) { + NL_LOG(INFO) + << "Destroy and reinitialize database. result: " << result; + }, + success); + } + }); +} + +void NearbyShareCertificateStorageImpl::OnDatabaseInitialized( + absl::Time initialize_start_time, + PublicCertificateDatabase::InitStatus status) { + NL_LOG(INFO) << "Database is initialized for certificates. status=" + << static_cast(status); + switch (status) { + case PublicCertificateDatabase::InitStatus::kOk: + FinishInitialization(true); + break; + case PublicCertificateDatabase::InitStatus::kError: + Initialize(); + break; + case PublicCertificateDatabase::InitStatus::kCorrupt: + DestroyAndReinitialize(); + break; + } +} + +void NearbyShareCertificateStorageImpl::FinishInitialization(bool success) { + init_status_ = success ? InitStatus::kInitialized : InitStatus::kFailed; + if (success) { + // Need to reset the initialize attempts. + num_initialize_attempts_ = 0; + + NL_VLOG(1) << __func__ + << "Public certificate database initialization succeeded."; + } else { + NL_LOG(ERROR) << __func__ + << "Public certificate database initialization failed."; + } + + // We run deferred callbacks even if initialization failed not to cause + // possible client-side blocks of next calls to the database. + while (!deferred_callbacks_.empty()) { + auto deferred_task = std::move(deferred_callbacks_.front()); + deferred_callbacks_.pop(); + deferred_task(); + } +} + +void NearbyShareCertificateStorageImpl::OnDatabaseDestroyedReinitialize( + ResultCallback callback, bool success) { + if (!success) { + NL_LOG(ERROR) << __func__ + << ": Failed to destroy public certificate database."; + FinishInitialization(false); + callback(false); + return; + } + + public_certificate_expirations_.clear(); + SavePublicCertificateExpirations(); + + Initialize(); + callback(true); +} + +void NearbyShareCertificateStorageImpl::OnDatabaseDestroyed( + ResultCallback callback, bool success) { + if (!success) { + NL_LOG(ERROR) << __func__ + << ": Failed to destroy public certificate database."; + std::move(callback)(false); + return; + } + + public_certificate_expirations_.clear(); + SavePublicCertificateExpirations(); + + std::move(callback)(true); +} + +void NearbyShareCertificateStorageImpl:: + ReplacePublicCertificatesDestroyCallback( + const std::vector& + new_certificates, + const ExpirationList& new_expirations, ResultCallback callback, + bool proceed) { + if (!proceed) { + std::move(callback)(false); + return; + } + + NL_VLOG(1) << __func__ << ": Inserting " << new_certificates.size() + << " public certificates."; + + public_certificate_database_->AddCertificates( + absl::MakeConstSpan(new_certificates), + [weak_this = weak_from_this(), new_expirations, + callback = std::move(callback)](bool success) { + if (auto storage = weak_this.lock()) { + storage->ReplacePublicCertificatesUpdateEntriesCallback( + std::make_unique(std::move(new_expirations)), + std::move(callback), success); + } + }); +} + +void NearbyShareCertificateStorageImpl:: + ReplacePublicCertificatesUpdateEntriesCallback( + std::unique_ptr expirations, ResultCallback callback, + bool proceed) { + if (!proceed) { + NL_LOG(ERROR) << __func__ << ": Failed to replace public certificates."; + std::move(callback)(false); + return; + } + NL_VLOG(1) << __func__ << ": Successfully replaced public certificates."; + + NL_CHECK(expirations); + public_certificate_expirations_.swap(*expirations); + SavePublicCertificateExpirations(); + std::move(callback)(true); +} + +void NearbyShareCertificateStorageImpl::AddPublicCertificatesCallback( + std::unique_ptr new_expirations, ResultCallback callback, + bool proceed) { + if (!proceed) { + NL_LOG(ERROR) << __func__ << ": Failed to add public certificates."; + std::move(callback)(false); + return; + } + NL_VLOG(1) << __func__ << ": Successfully added public certificates."; + + public_certificate_expirations_ = + MergeExpirations(public_certificate_expirations_, *new_expirations); + SavePublicCertificateExpirations(); + std::move(callback)(true); +} + +void NearbyShareCertificateStorageImpl::RemoveExpiredPublicCertificatesCallback( + const absl::flat_hash_set& ids_to_remove, + ResultCallback callback, bool proceed) { + if (!proceed) { + NL_LOG(ERROR) << __func__ + << ": Failed to remove expired public certificates."; + std::move(callback)(false); + return; + } + NL_VLOG(1) << __func__ + << ": Expired public certificates successfully removed."; + + auto should_remove = + [&](const std::pair& pair) -> bool { + return ids_to_remove.contains(pair.first); + }; + + public_certificate_expirations_.erase( + std::remove_if(public_certificate_expirations_.begin(), + public_certificate_expirations_.end(), should_remove), + public_certificate_expirations_.end()); + SavePublicCertificateExpirations(); + std::move(callback)(true); +} + +std::vector +NearbyShareCertificateStorageImpl::GetPublicCertificateIds() const { + std::vector ids; + for (const auto& pair : public_certificate_expirations_) { + ids.emplace_back(pair.first); + } + return ids; +} + +void NearbyShareCertificateStorageImpl::GetPublicCertificates( + PublicCertificateCallback callback) { + if (init_status_ == InitStatus::kFailed) { + std::move(callback)(false, nullptr); + return; + } + + if (init_status_ == InitStatus::kUninitialized) { + deferred_callbacks_.push([this, callback = std::move(callback)] { + GetPublicCertificates(std::move(callback)); + }); + return; + } + + NL_VLOG(1) << __func__ << ": Calling LoadEntries on database."; + public_certificate_database_->LoadEntries(std::move(callback)); +} + +std::optional> +NearbyShareCertificateStorageImpl::GetPrivateCertificates() const { + std::vector list = + preference_manager_.GetPrivateCertificateArray( + prefs::kNearbySharingPrivateCertificateListName); + std::vector certs; + certs.reserve(list.size()); + for (const PrivateCertificateData& cert_data : list) { + std::optional cert( + NearbySharePrivateCertificate::FromCertificateData(cert_data)); + if (!cert) return std::nullopt; + + certs.push_back(*std::move(cert)); + } + return certs; +} + +std::optional +NearbyShareCertificateStorageImpl::NextPublicCertificateExpirationTime() const { + if (public_certificate_expirations_.empty()) return std::nullopt; + + // |public_certificate_expirations_| is sorted by expiration date. + return public_certificate_expirations_.front().second; +} + +void NearbyShareCertificateStorageImpl::ReplacePrivateCertificates( + absl::Span private_certificates) { + std::vector list; + list.reserve(private_certificates.size()); + for (const NearbySharePrivateCertificate& cert : private_certificates) { + list.push_back(cert.ToCertificateData()); + } + preference_manager_.SetPrivateCertificateArray( + prefs::kNearbySharingPrivateCertificateListName, list); +} + +void NearbyShareCertificateStorageImpl::ReplacePublicCertificates( + absl::Span + public_certificates, + ResultCallback callback) { + if (init_status_ == InitStatus::kFailed) { + std::move(callback)(false); + return; + } + + if (init_status_ == InitStatus::kUninitialized) { + deferred_callbacks_.push( + [this, public_certificates, callback = std::move(callback)]() { + ReplacePublicCertificates(public_certificates, std::move(callback)); + }); + + return; + } + + auto new_entries = std::vector(); + ExpirationList new_expirations; + for (const nearby::sharing::proto::PublicCertificate& cert : + public_certificates) { + new_entries.emplace_back(cert); + new_expirations.emplace_back(cert.secret_id(), + TimestampToTime(cert.end_time())); + } + std::sort(new_expirations.begin(), new_expirations.end(), SortBySecond); + + NL_VLOG(1) << __func__ << ": Clearing public certificate database."; + + public_certificate_database_->Destroy( + [weak_this = weak_from_this(), new_entries, new_expirations, + callback = std::move(callback)](bool success) { + if (auto storage = weak_this.lock()) { + storage->ReplacePublicCertificatesDestroyCallback( + new_entries, new_expirations, std::move(callback), success); + } + }); +} + +void NearbyShareCertificateStorageImpl::AddPublicCertificates( + absl::Span + public_certificates, + ResultCallback callback) { + if (init_status_ == InitStatus::kFailed) { + std::move(callback)(false); + return; + } + + if (init_status_ == InitStatus::kUninitialized) { + deferred_callbacks_.push( + [this, public_certificates, callback = std::move(callback)]() { + AddPublicCertificates(public_certificates, std::move(callback)); + }); + return; + } + + ExpirationList new_expirations; + for (const nearby::sharing::proto::PublicCertificate& cert : + public_certificates) { + new_expirations.emplace_back(cert.secret_id(), + TimestampToTime(cert.end_time())); + } + std::sort(new_expirations.begin(), new_expirations.end(), SortBySecond); + + NL_VLOG(1) << __func__ + << ": Calling UpdateEntries on public certificate database with " + << public_certificates.size() << " certificates."; + public_certificate_database_->AddCertificates( + public_certificates, [weak_this = weak_from_this(), new_expirations, + callback = std::move(callback)](bool success) { + if (auto storage = weak_this.lock()) { + storage->AddPublicCertificatesCallback( + std::make_unique(new_expirations), + std::move(callback), success); + } + }); +} + +void NearbyShareCertificateStorageImpl::RemoveExpiredPublicCertificates( + absl::Time now, ResultCallback callback) { + if (init_status_ == InitStatus::kFailed) { + std::move(callback)(false); + return; + } + + if (init_status_ == InitStatus::kUninitialized) { + deferred_callbacks_.push([this, now, callback = std::move(callback)]() { + RemoveExpiredPublicCertificates(now, std::move(callback)); + }); + return; + } + + auto ids_to_remove = std::vector(); + for (const auto& pair : public_certificate_expirations_) { + // Because the list is sorted by expiration time, break as soon as we + // encounter an unexpired certificate. Apply a tolerance when evaluating + // whether the certificate is expired to account for clock skew between + // devices. This conforms this the GmsCore implementation. + if (!IsNearbyShareCertificateExpired( + now, + /*not_after=*/pair.second, + /*use_public_certificate_tolerance=*/true)) { + break; + } + + ids_to_remove.emplace_back(pair.first); + } + if (ids_to_remove.empty()) { + std::move(callback)(true); + return; + } + + NL_VLOG(1) + << __func__ + << ": Calling UpdateEntries on public certificate database to remove " + << ids_to_remove.size() << " expired certificates."; + absl::flat_hash_set remove_set(ids_to_remove.begin(), + ids_to_remove.end()); + public_certificate_database_->RemoveCertificatesById( + std::move(ids_to_remove), + [weak_this = weak_from_this(), remove_set = std::move(remove_set), + callback = std::move(callback)](bool success) { + if (auto storage = weak_this.lock()) { + storage->RemoveExpiredPublicCertificatesCallback( + remove_set, std::move(callback), success); + } + }); +} + +void NearbyShareCertificateStorageImpl::ClearPublicCertificates( + ResultCallback callback) { + if (init_status_ == InitStatus::kFailed) { + std::move(callback)(false); + return; + } + + NL_VLOG(1) << __func__ << ": Calling Destroy on public certificate database."; + init_status_ = InitStatus::kUninitialized; + public_certificate_database_->Destroy( + [weak_this = weak_from_this(), + callback = std::move(callback)](bool success) { + if (auto storage = weak_this.lock()) { + storage->OnDatabaseDestroyedReinitialize(callback, success); + } + }); +} + +bool NearbyShareCertificateStorageImpl::FetchPublicCertificateExpirations() { + std::vector> expirations = + preference_manager_.GetCertificateExpirationArray( + prefs::kNearbySharingPublicCertificateExpirationDictName); + public_certificate_expirations_.clear(); + if (expirations.empty()) { + return false; + } + + public_certificate_expirations_.reserve(expirations.size()); + + for (auto it = expirations.begin(); it != expirations.end(); ++it) { + std::optional id = DecodeString(&it->first); + std::optional expiration = absl::FromUnixNanos(it->second); + if (!id || !expiration) return false; + + public_certificate_expirations_.emplace_back(*id, *expiration); + } + std::sort(public_certificate_expirations_.begin(), + public_certificate_expirations_.end(), SortBySecond); + + return true; +} + +void NearbyShareCertificateStorageImpl::SavePublicCertificateExpirations() { + std::vector> expirations; + expirations.reserve(public_certificate_expirations_.size()); + for (const std::pair& pair : + public_certificate_expirations_) { + expirations.emplace_back(EncodeString(pair.first), + absl::ToUnixNanos(pair.second)); + } + + preference_manager_.SetCertificateExpirationArray( + prefs::kNearbySharingPublicCertificateExpirationDictName, expirations); +} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/certificates/nearby_share_certificate_storage_impl.h b/sharing/certificates/nearby_share_certificate_storage_impl.h new file mode 100644 index 00000000..7b4ec90d --- /dev/null +++ b/sharing/certificates/nearby_share_certificate_storage_impl.h @@ -0,0 +1,146 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_CERTIFICATES_NEARBY_SHARE_CERTIFICATE_STORAGE_IMPL_H_ +#define THIRD_PARTY_NEARBY_SHARING_CERTIFICATES_NEARBY_SHARE_CERTIFICATE_STORAGE_IMPL_H_ + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "absl/container/flat_hash_set.h" +#include "absl/time/time.h" +#include "absl/types/span.h" +#include "sharing/certificates/nearby_share_certificate_storage.h" +#include "sharing/certificates/nearby_share_private_certificate.h" +#include "sharing/internal/api/preference_manager.h" +#include "sharing/internal/api/public_certificate_database.h" +#include "sharing/proto/rpc_resources.pb.h" + +namespace nearby { +namespace sharing { + +// Implements NearbyShareCertificateStorage using Prefs to store private +// certificates and LevelDB Proto to store public certificates. Must be +// initialized by calling Initialize before retrieving or storing certificates. +// Callbacks are guaranteed to not be invoked after +// NearbyShareCertificateStorageImpl is destroyed. +class NearbyShareCertificateStorageImpl : public NearbyShareCertificateStorage, + public std::enable_shared_from_this { + public: + class Factory { + public: + static std::shared_ptr Create( + nearby::sharing::api::PreferenceManager& preference_manager, + std::unique_ptr + public_certificate_database); + static void SetFactoryForTesting(Factory* test_factory); + + protected: + virtual ~Factory(); + virtual std::shared_ptr CreateInstance( + nearby::sharing::api::PreferenceManager& preference_manager, + std::unique_ptr + public_certificate_database) = 0; + + private: + static Factory* test_factory_; + }; + + using ExpirationList = std::vector>; + + ~NearbyShareCertificateStorageImpl() override; + NearbyShareCertificateStorageImpl(NearbyShareCertificateStorageImpl&) = + delete; + void operator=(NearbyShareCertificateStorageImpl&) = delete; + + // NearbyShareCertificateStorage + std::vector GetPublicCertificateIds() const override; + void GetPublicCertificates(PublicCertificateCallback callback) override; + std::optional> + GetPrivateCertificates() const override; + std::optional NextPublicCertificateExpirationTime() + const override; + void ReplacePrivateCertificates( + absl::Span private_certificates) + override; + void ReplacePublicCertificates( + absl::Span + public_certificates, + ResultCallback callback) override; + void AddPublicCertificates( + absl::Span + public_certificates, + ResultCallback callback) override; + void RemoveExpiredPublicCertificates(absl::Time now, + ResultCallback callback) override; + void ClearPublicCertificates(ResultCallback callback) override; + + private: + enum class InitStatus { kUninitialized, kInitialized, kFailed }; + + NearbyShareCertificateStorageImpl( + nearby::sharing::api::PreferenceManager& preference_manager, + std::unique_ptr + public_certificate_database); + void Initialize(); + void OnDatabaseInitialized( + absl::Time initialize_start_time, + nearby::sharing::api::PublicCertificateDatabase::InitStatus + init_dataset_status); + void FinishInitialization(bool success); + + void OnDatabaseDestroyedReinitialize(ResultCallback callback, bool success); + void OnDatabaseDestroyed(ResultCallback callback, bool success); + + void DestroyAndReinitialize(); + + void ReplacePublicCertificatesDestroyCallback( + const std::vector& + new_certificates, + const ExpirationList& new_expirations, ResultCallback callback, + bool proceed); + void ReplacePublicCertificatesUpdateEntriesCallback( + std::unique_ptr expirations, ResultCallback callback, + bool proceed); + void AddPublicCertificatesCallback( + std::unique_ptr new_expirations, ResultCallback callback, + bool proceed); + void RemoveExpiredPublicCertificatesCallback( + const absl::flat_hash_set& ids_to_remove, + ResultCallback callback, bool proceed); + + bool FetchPublicCertificateExpirations(); + void SavePublicCertificateExpirations(); + + nearby::sharing::api::PreferenceManager& preference_manager_; + InitStatus init_status_ = InitStatus::kUninitialized; + size_t num_initialize_attempts_ = 0; + std::unique_ptr + public_certificate_database_; + ExpirationList public_certificate_expirations_; + + std::queue> deferred_callbacks_; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_CERTIFICATES_NEARBY_SHARE_CERTIFICATE_STORAGE_IMPL_H_ diff --git a/sharing/certificates/nearby_share_certificate_storage_impl_test.cc b/sharing/certificates/nearby_share_certificate_storage_impl_test.cc new file mode 100644 index 00000000..3101f555 --- /dev/null +++ b/sharing/certificates/nearby_share_certificate_storage_impl_test.cc @@ -0,0 +1,810 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/certificates/nearby_share_certificate_storage_impl.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "gmock/gmock.h" +#include "protobuf-matchers/protocol-buffer-matchers.h" +#include "gtest/gtest.h" +#include "absl/container/flat_hash_map.h" +#include "absl/functional/any_invocable.h" +#include "absl/meta/type_traits.h" +#include "absl/strings/escaping.h" +#include "absl/strings/string_view.h" +#include "absl/time/clock.h" +#include "absl/time/time.h" +#include "sharing/certificates/constants.h" +#include "sharing/certificates/nearby_share_certificate_storage.h" +#include "sharing/certificates/nearby_share_private_certificate.h" +#include "sharing/certificates/test_util.h" +#include "sharing/common/nearby_share_prefs.h" +#include "sharing/internal/api/mock_public_certificate_db.h" +#include "sharing/internal/test/fake_preference_manager.h" +#include "sharing/internal/test/fake_public_certificate_db.h" +#include "sharing/proto/enums.pb.h" +#include "sharing/proto/rpc_resources.pb.h" +#include "sharing/proto/timestamp.pb.h" + +namespace nearby { +namespace sharing { +namespace { +using ::nearby::sharing::api::MockPublicCertificateDb; +using ::nearby::sharing::proto::DeviceVisibility; +using ::nearby::sharing::proto::PublicCertificate; +using ::testing::_; +using ::testing::Eq; +using ::testing::Invoke; +using ::testing::StrictMock; + +// NOTE: Make sure secret ID alphabetical ordering does not match the 1,2,3,... +// ordering to test sorting expiration times. +constexpr char kSecretId1[] = "b_secretid1"; +constexpr char kSecretKey1[] = "secretkey1"; +constexpr char kPublicKey1[] = "publickey1"; +constexpr int64_t kStartSeconds1 = 0; +constexpr int32_t kStartNanos1 = 10; +constexpr int64_t kEndSeconds1 = 100; +constexpr int32_t kEndNanos1 = 30; +constexpr bool kForSelectedContacts1 = false; +constexpr char kMetadataEncryptionKey1[] = "metadataencryptionkey1"; +constexpr char kEncryptedMetadataBytes1[] = "encryptedmetadatabytes1"; +constexpr char kMetadataEncryptionKeyTag1[] = "metadataencryptionkeytag1"; +constexpr char kSecretId2[] = "c_secretid2"; +constexpr char kSecretKey2[] = "secretkey2"; +constexpr char kPublicKey2[] = "publickey2"; +constexpr int64_t kStartSeconds2 = 0; +constexpr int32_t kStartNanos2 = 20; +constexpr int64_t kEndSeconds2 = 200; +constexpr int32_t kEndNanos2 = 30; +constexpr bool kForSelectedContacts2 = false; +constexpr char kMetadataEncryptionKey2[] = "metadataencryptionkey2"; +constexpr char kEncryptedMetadataBytes2[] = "encryptedmetadatabytes2"; +constexpr char kMetadataEncryptionKeyTag2[] = "metadataencryptionkeytag2"; +constexpr char kSecretId3[] = "a_secretid3"; +constexpr char kSecretKey3[] = "secretkey3"; +constexpr char kPublicKey3[] = "publickey3"; +constexpr int64_t kStartSeconds3 = 0; +constexpr int32_t kStartNanos3 = 30; +constexpr int64_t kEndSeconds3 = 300; +constexpr int32_t kEndNanos3 = 30; +constexpr bool kForSelectedContacts3 = false; +constexpr char kMetadataEncryptionKey3[] = "metadataencryptionkey3"; +constexpr char kEncryptedMetadataBytes3[] = "encryptedmetadatabytes3"; +constexpr char kMetadataEncryptionKeyTag3[] = "metadataencryptionkeytag3"; +constexpr char kSecretId4[] = "d_secretid4"; +constexpr char kSecretKey4[] = "secretkey4"; +constexpr char kPublicKey4[] = "publickey4"; +constexpr int64_t kStartSeconds4 = 0; +constexpr int32_t kStartNanos4 = 10; +constexpr int64_t kEndSeconds4 = 100; +constexpr int32_t kEndNanos4 = 30; +constexpr bool kForSelectedContacts4 = false; +constexpr char kMetadataEncryptionKey4[] = "metadataencryptionkey4"; +constexpr char kEncryptedMetadataBytes4[] = "encryptedmetadatabytes4"; +constexpr char kMetadataEncryptionKeyTag4[] = "metadataencryptionkeytag4"; + +std::string EncodeString(absl::string_view unencoded_string) { + std::string result; + absl::WebSafeBase64Escape(unencoded_string, &result); + return result; +} + +PublicCertificate CreatePublicCertificate( + absl::string_view secret_id, absl::string_view secret_key, + absl::string_view public_key, int64_t start_seconds, int32_t start_nanos, + int64_t end_seconds, int32_t end_nanos, bool for_selected_contacts, + absl::string_view metadata_encryption_key, + absl::string_view encrypted_metadata_bytes, + absl::string_view metadata_encryption_key_tag) { + 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 CreatePrivateCertificates( + size_t n, DeviceVisibility visibility) { + std::vector certs; + certs.reserve(n); + for (size_t i = 0; i < n; ++i) { + certs.emplace_back(visibility, absl::Now(), GetNearbyShareTestMetadata()); + } + return certs; +} + +absl::Time TimestampToTime(nearby::sharing::proto::Timestamp timestamp) { + return absl::UnixEpoch() + absl::Seconds(timestamp.seconds()) + + absl::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 { + preference_manager_.Remove( + prefs::kNearbySharingPublicCertificateExpirationDictName); + preference_manager_.Remove( + prefs::kNearbySharingPrivateCertificateListName); + } + + void PrepopulatePublicCertificates(nearby::FakePublicCertificateDb* db) { + std::vector 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)); + db->AddCertificates(pub_certs, [](bool) {}); + std::vector> expirations; + for (const auto& cert : pub_certs) { + expirations.emplace_back( + EncodeString(cert.secret_id()), + absl::ToUnixNanos(TimestampToTime(cert.end_time()))); + } + preference_manager_.SetCertificateExpirationArray( + prefs::kNearbySharingPublicCertificateExpirationDictName, + expirations); + } + + void CaptureBoolCallback(bool* dest, bool src) { *dest = src; } + + void PublicCertificateCallback( + std::vector* public_certificates, + std::function complete, bool success, + std::unique_ptr> result) { + if (success && result) { + public_certificates->swap(*result); + } + std::move(complete)(); + } + + protected: + nearby::FakePreferenceManager preference_manager_; +}; + +TEST_F(NearbyShareCertificateStorageImplTest, InitializeRetrySucceed) { + // This test only makes sense if initialization will be attempted at least + // twice. + if (kNearbyShareCertificateStorageMaxNumInitializeAttempts < 2) return; + + auto db = std::make_unique>(); + MockPublicCertificateDb* mock_db = db.get(); + EXPECT_CALL(*mock_db, Initialize(_)) + .WillOnce(Invoke( + [](absl::AnyInvocable + callback) { + std::move(callback)(MockPublicCertificateDb::InitStatus::kError); + })) + .WillRepeatedly(Invoke( + [](absl::AnyInvocable + callback) { + std::move(callback)(MockPublicCertificateDb::InitStatus::kOk); + })); + EXPECT_CALL(*mock_db, Destroy(_)) + .WillOnce(Invoke([](absl::AnyInvocable callback) { + std::move(callback)(true); + })); + + auto cert_store = NearbyShareCertificateStorageImpl::Factory::Create( + preference_manager_, std::move(db)); + + bool clear_succeeded = false; + // Use this to trigger call to Destroy. + cert_store->ClearPublicCertificates([this, &clear_succeeded](bool success) { + CaptureBoolCallback(&clear_succeeded, success); + }); + + EXPECT_TRUE(clear_succeeded); + EXPECT_THAT(cert_store.use_count(), Eq(1)); +} + +TEST_F(NearbyShareCertificateStorageImplTest, InitializeRetryFailed) { + auto db = std::make_unique>(); + MockPublicCertificateDb* mock_db = db.get(); + EXPECT_CALL(*mock_db, Initialize(_)) + .WillRepeatedly(Invoke( + [](absl::AnyInvocable + callback) { + std::move(callback)(MockPublicCertificateDb::InitStatus::kError); + })); + + auto cert_store = NearbyShareCertificateStorageImpl::Factory::Create( + preference_manager_, std::move(db)); + + bool clear_succeeded = true; + // Call to Destroy not made because of initialization failure. + cert_store->ClearPublicCertificates([this, &clear_succeeded](bool success) { + CaptureBoolCallback(&clear_succeeded, success); + }); + + EXPECT_FALSE(clear_succeeded); + EXPECT_THAT(cert_store.use_count(), Eq(1)); +} + +TEST_F(NearbyShareCertificateStorageImplTest, + InitializeCorruptDestroySucceeds) { + auto db = std::make_unique>(); + MockPublicCertificateDb* mock_db = db.get(); + EXPECT_CALL(*mock_db, Initialize(_)) + .WillOnce(Invoke( + [](absl::AnyInvocable + callback) { + std::move(callback)(MockPublicCertificateDb::InitStatus::kCorrupt); + })) + .WillRepeatedly(Invoke( + [](absl::AnyInvocable + callback) { + std::move(callback)(MockPublicCertificateDb::InitStatus::kOk); + })); + // Destroy called once from corrupted initialization and once from cal to + // ClearPublicCertificates. + EXPECT_CALL(*mock_db, Destroy(_)) + .Times(2) + .WillRepeatedly(Invoke([](absl::AnyInvocable callback) { + std::move(callback)(true); + })); + + auto cert_store = NearbyShareCertificateStorageImpl::Factory::Create( + preference_manager_, std::move(db)); + + bool clear_succeeded = false; + cert_store->ClearPublicCertificates([this, &clear_succeeded](bool success) { + CaptureBoolCallback(&clear_succeeded, success); + }); + + EXPECT_TRUE(clear_succeeded); + EXPECT_THAT(cert_store.use_count(), Eq(1)); +} + +TEST_F(NearbyShareCertificateStorageImplTest, InitializeCorruptDestroyFails) { + auto db = std::make_unique>(); + MockPublicCertificateDb* mock_db = db.get(); + EXPECT_CALL(*mock_db, Initialize(_)) + .WillOnce(Invoke( + [](absl::AnyInvocable + callback) { + std::move(callback)(MockPublicCertificateDb::InitStatus::kCorrupt); + })); + EXPECT_CALL(*mock_db, Destroy(_)) + .WillOnce(Invoke([](absl::AnyInvocable callback) { + std::move(callback)(false); + })); + + auto cert_store = NearbyShareCertificateStorageImpl::Factory::Create( + preference_manager_, std::move(db)); + + bool clear_succeeded = true; + cert_store->ClearPublicCertificates([this, &clear_succeeded](bool success) { + CaptureBoolCallback(&clear_succeeded, success); + }); + + EXPECT_FALSE(clear_succeeded); + EXPECT_THAT(cert_store.use_count(), Eq(1)); +} + +TEST_F(NearbyShareCertificateStorageImplTest, DeferredCallbackQueue) { + absl::AnyInvocable + init_status_callback; + absl::AnyInvocable destroy_callback; + absl::AnyInvocable>) &&> + load_callback; + auto db = std::make_unique>(); + MockPublicCertificateDb* mock_db = db.get(); + EXPECT_CALL(*mock_db, Initialize(_)) + .WillOnce(Invoke( + [&](absl::AnyInvocable + callback) { init_status_callback = std::move(callback); })); + EXPECT_CALL(*mock_db, Destroy(_)) + .WillOnce(Invoke([&](absl::AnyInvocable callback) { + destroy_callback = std::move(callback); + })); + EXPECT_CALL(*mock_db, LoadEntries(_)) + .WillOnce(Invoke( + [&](absl::AnyInvocable>)&&> + callback) { load_callback = std::move(callback); })); + + auto cert_store = NearbyShareCertificateStorageImpl::Factory::Create( + preference_manager_, std::move(db)); + + bool clear_succeeded = false; + std::vector public_certificates; + + cert_store->ClearPublicCertificates([this, &clear_succeeded](bool success) { + CaptureBoolCallback(&clear_succeeded, success); + }); + cert_store->GetPublicCertificates( + [this, &public_certificates, complete = []() { + }](bool success, std::unique_ptr> result) { + PublicCertificateCallback(&public_certificates, std::move(complete), + success, std::move(result)); + }); + + std::move(init_status_callback)(MockPublicCertificateDb::InitStatus::kOk); + + // These callbacks have to be posted to ensure that they run after the + // deferred callbacks posted during initialization. + std::move(destroy_callback)(true); + std::move(load_callback)(true, nullptr); + + EXPECT_TRUE(clear_succeeded); + EXPECT_TRUE(public_certificates.empty()); + EXPECT_THAT(cert_store.use_count(), Eq(1)); +} + +TEST_F(NearbyShareCertificateStorageImplTest, GetPublicCertificateIds) { + auto db = std::make_unique(); + nearby::FakePublicCertificateDb* fake_db = db.get(); + PrepopulatePublicCertificates(fake_db); + + auto cert_store = NearbyShareCertificateStorageImpl::Factory::Create( + preference_manager_, std::move(db)); + + auto ids = cert_store->GetPublicCertificateIds(); + ASSERT_EQ(ids.size(), 3u); + EXPECT_EQ(ids[0], kSecretId1); + EXPECT_EQ(ids[1], kSecretId2); + EXPECT_EQ(ids[2], kSecretId3); + EXPECT_THAT(cert_store.use_count(), Eq(1)); +} + +TEST_F(NearbyShareCertificateStorageImplTest, GetPublicCertificates) { + auto db = std::make_unique(); + nearby::FakePublicCertificateDb* fake_db = db.get(); + PrepopulatePublicCertificates(fake_db); + + auto cert_store = NearbyShareCertificateStorageImpl::Factory::Create( + preference_manager_, std::move(db)); + + std::vector public_certificates; + cert_store->GetPublicCertificates([this, &public_certificates, complete = [] { + }](bool success, std::unique_ptr> result) { + PublicCertificateCallback(&public_certificates, std::move(complete), + success, std::move(result)); + }); + + ASSERT_EQ(3u, public_certificates.size()); + for (const PublicCertificate& cert : public_certificates) { + std::string expected_serialized, actual_serialized; + ASSERT_TRUE(cert.SerializeToString(&actual_serialized)); + ASSERT_TRUE(fake_db->GetCertificatesMap() + .find(cert.secret_id()) + ->second.SerializeToString(&expected_serialized)); + ASSERT_EQ(expected_serialized, actual_serialized); + } + EXPECT_THAT(cert_store.use_count(), Eq(1)); +} + +TEST_F(NearbyShareCertificateStorageImplTest, ReplacePublicCertificates) { + auto db = std::make_unique(); + nearby::FakePublicCertificateDb* fake_db = db.get(); + PrepopulatePublicCertificates(fake_db); + std::vector new_certs = { + CreatePublicCertificate(kSecretId4, kSecretKey4, kPublicKey4, + kStartSeconds4, kStartNanos4, kEndSeconds4, + kEndNanos4, kForSelectedContacts4, + kMetadataEncryptionKey4, kEncryptedMetadataBytes4, + kMetadataEncryptionKeyTag4), + }; + auto cert_store = NearbyShareCertificateStorageImpl::Factory::Create( + preference_manager_, std::move(db)); + + bool succeeded = false; + cert_store->ReplacePublicCertificates( + new_certs, [this, &succeeded](bool success) { + CaptureBoolCallback(&succeeded, success); + }); + + ASSERT_TRUE(succeeded); + auto cert_map = fake_db->GetCertificatesMap(); + ASSERT_EQ(cert_map.size(), 1u); + ASSERT_EQ(cert_map.count(kSecretId4), 1u); + auto& cert = cert_map.find(kSecretId4)->second; + EXPECT_EQ(cert.secret_key(), kSecretKey4); + EXPECT_EQ(cert.public_key(), kPublicKey4); + EXPECT_EQ(cert.start_time().seconds(), kStartSeconds4); + EXPECT_EQ(cert.start_time().nanos(), kStartNanos4); + EXPECT_EQ(cert.end_time().seconds(), kEndSeconds4); + EXPECT_EQ(cert.end_time().nanos(), kEndNanos4); + EXPECT_EQ(cert.for_selected_contacts(), kForSelectedContacts4); + EXPECT_EQ(cert.metadata_encryption_key(), kMetadataEncryptionKey4); + EXPECT_EQ(cert.encrypted_metadata_bytes(), kEncryptedMetadataBytes4); + EXPECT_EQ(cert.metadata_encryption_key_tag(), kMetadataEncryptionKeyTag4); + EXPECT_THAT(cert_store.use_count(), Eq(1)); +} + +TEST_F(NearbyShareCertificateStorageImplTest, AddPublicCertificates) { + auto db = std::make_unique(); + nearby::FakePublicCertificateDb* fake_db = db.get(); + PrepopulatePublicCertificates(fake_db); + std::vector 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), + }; + + auto cert_store = NearbyShareCertificateStorageImpl::Factory::Create( + preference_manager_, std::move(db)); + + bool succeeded = false; + cert_store->AddPublicCertificates(new_certs, + [this, &succeeded](bool success) { + CaptureBoolCallback(&succeeded, success); + }); + + ASSERT_TRUE(succeeded); + auto cert_map = fake_db->GetCertificatesMap(); + ASSERT_EQ(cert_map.size(), 4u); + ASSERT_EQ(cert_map.count(kSecretId3), 1u); + ASSERT_EQ(cert_map.count(kSecretId4), 1u); + auto& cert = cert_map.find(kSecretId3)->second; + EXPECT_EQ(cert.secret_key(), kSecretKey2); + EXPECT_EQ(cert.public_key(), kPublicKey2); + EXPECT_EQ(cert.start_time().seconds(), kStartSeconds2); + EXPECT_EQ(cert.start_time().nanos(), kStartNanos2); + EXPECT_EQ(cert.end_time().seconds(), kEndSeconds2); + EXPECT_EQ(cert.end_time().nanos(), kEndNanos2); + EXPECT_EQ(cert.for_selected_contacts(), kForSelectedContacts2); + EXPECT_EQ(cert.metadata_encryption_key(), kMetadataEncryptionKey2); + EXPECT_EQ(cert.encrypted_metadata_bytes(), kEncryptedMetadataBytes2); + EXPECT_EQ(cert.metadata_encryption_key_tag(), kMetadataEncryptionKeyTag2); + cert = cert_map.find(kSecretId4)->second; + EXPECT_EQ(cert.secret_key(), kSecretKey4); + EXPECT_EQ(cert.public_key(), kPublicKey4); + EXPECT_EQ(cert.start_time().seconds(), kStartSeconds4); + EXPECT_EQ(cert.start_time().nanos(), kStartNanos4); + EXPECT_EQ(cert.end_time().seconds(), kEndSeconds4); + EXPECT_EQ(cert.end_time().nanos(), kEndNanos4); + EXPECT_EQ(cert.for_selected_contacts(), kForSelectedContacts4); + EXPECT_EQ(cert.metadata_encryption_key(), kMetadataEncryptionKey4); + EXPECT_EQ(cert.encrypted_metadata_bytes(), kEncryptedMetadataBytes4); + EXPECT_EQ(cert.metadata_encryption_key_tag(), kMetadataEncryptionKeyTag4); + EXPECT_THAT(cert_store.use_count(), Eq(1)); +} + +TEST_F(NearbyShareCertificateStorageImplTest, ClearPublicCertificates) { + auto db = std::make_unique(); + nearby::FakePublicCertificateDb* fake_db = db.get(); + PrepopulatePublicCertificates(fake_db); + + auto cert_store = NearbyShareCertificateStorageImpl::Factory::Create( + preference_manager_, std::move(db)); + + bool succeeded = false; + cert_store->ClearPublicCertificates([this, &succeeded](bool success) { + CaptureBoolCallback(&succeeded, success); + }); + + ASSERT_TRUE(succeeded); + ASSERT_EQ(0u, fake_db->GetCertificatesMap().size()); + EXPECT_THAT(cert_store.use_count(), Eq(1)); +} + +TEST_F(NearbyShareCertificateStorageImplTest, + RemoveExpiredPrivateCertificates) { + auto db = std::make_unique(); + nearby::FakePublicCertificateDb* fake_db = db.get(); + PrepopulatePublicCertificates(fake_db); + + auto cert_store = NearbyShareCertificateStorageImpl::Factory::Create( + preference_manager_, std::move(db)); + + std::vector certs = CreatePrivateCertificates( + 3, DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS); + cert_store->ReplacePrivateCertificates(certs); + + std::vector expiration_times; + for (const NearbySharePrivateCertificate& cert : certs) { + expiration_times.push_back(cert.not_after()); + } + std::sort(expiration_times.begin(), expiration_times.end()); + + // Set the current time to exceed the expiration times of the first two + // certificates. + absl::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()); + } + EXPECT_THAT(cert_store.use_count(), Eq(1)); +} + +TEST_F(NearbyShareCertificateStorageImplTest, RemoveExpiredPublicCertificates) { + auto db = std::make_unique(); + nearby::FakePublicCertificateDb* fake_db = db.get(); + PrepopulatePublicCertificates(fake_db); + + auto cert_store = NearbyShareCertificateStorageImpl::Factory::Create( + preference_manager_, std::move(db)); + + std::vector expiration_times; + for (const auto& pair : fake_db->GetCertificatesMap()) { + 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. + absl::Time now = expiration_times[1] + + kNearbySharePublicCertificateValidityBoundOffsetTolerance; + + bool succeeded = false; + cert_store->RemoveExpiredPublicCertificates( + now, [this, &succeeded](bool success) { + CaptureBoolCallback(&succeeded, success); + }); + + ASSERT_TRUE(succeeded); + auto cert_map = fake_db->GetCertificatesMap(); + ASSERT_EQ(cert_map.size(), 1u); + for (const auto& pair : cert_map) { + EXPECT_LE(now - kNearbySharePublicCertificateValidityBoundOffsetTolerance, + TimestampToTime(pair.second.end_time())); + } + EXPECT_THAT(cert_store.use_count(), Eq(1)); +} + +TEST_F(NearbyShareCertificateStorageImplTest, ReplaceGetPrivateCertificates) { + auto db = std::make_unique(); + nearby::FakePublicCertificateDb* fake_db = db.get(); + PrepopulatePublicCertificates(fake_db); + + auto cert_store = NearbyShareCertificateStorageImpl::Factory::Create( + preference_manager_, std::move(db)); + + auto certs_before = CreatePrivateCertificates( + 3, DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS); + 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].ToCertificateData(), + (*certs_after)[i].ToCertificateData()); + } + + certs_before = CreatePrivateCertificates( + 1, DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS); + 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].ToCertificateData(), + (*certs_after)[i].ToCertificateData()); + } + EXPECT_THAT(cert_store.use_count(), Eq(1)); +} + +TEST_F(NearbyShareCertificateStorageImplTest, UpdatePrivateCertificates) { + auto db = std::make_unique(); + nearby::FakePublicCertificateDb* fake_db = db.get(); + PrepopulatePublicCertificates(fake_db); + + auto cert_store = NearbyShareCertificateStorageImpl::Factory::Create( + preference_manager_, std::move(db)); + + std::vector initial_certs = + CreatePrivateCertificates( + 3, DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS); + cert_store->ReplacePrivateCertificates(initial_certs); + + NearbySharePrivateCertificate cert_to_update = initial_certs[1]; + EXPECT_EQ(initial_certs[1].ToCertificateData(), + cert_to_update.ToCertificateData()); + cert_to_update.EncryptMetadataKey(); + EXPECT_NE(initial_certs[1].ToCertificateData(), + cert_to_update.ToCertificateData()); + + cert_store->UpdatePrivateCertificate(cert_to_update); + + std::vector 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.ToCertificateData(), + new_certs[i].ToCertificateData()); + } + EXPECT_THAT(cert_store.use_count(), Eq(1)); +} + +TEST_F(NearbyShareCertificateStorageImplTest, + NextPrivateCertificateExpirationTime) { + auto db = std::make_unique(); + nearby::FakePublicCertificateDb* fake_db = db.get(); + PrepopulatePublicCertificates(fake_db); + + auto cert_store = NearbyShareCertificateStorageImpl::Factory::Create( + preference_manager_, std::move(db)); + + auto certs = CreatePrivateCertificates( + 3, DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS); + cert_store->ReplacePrivateCertificates(certs); + std::optional 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); + EXPECT_THAT(cert_store.use_count(), Eq(1)); +} + +TEST_F(NearbyShareCertificateStorageImplTest, + NextPublicCertificateExpirationTime) { + auto db = std::make_unique(); + nearby::FakePublicCertificateDb* fake_db = db.get(); + PrepopulatePublicCertificates(fake_db); + + auto cert_store = NearbyShareCertificateStorageImpl::Factory::Create( + preference_manager_, std::move(db)); + + std::optional next_expiration = + cert_store->NextPublicCertificateExpirationTime(); + + ASSERT_TRUE(next_expiration.has_value()); + bool found = false; + for (const auto& pair : fake_db->GetCertificatesMap()) { + absl::Time curr_expiration = TimestampToTime(pair.second.end_time()); + EXPECT_GE(curr_expiration, *next_expiration); + if (curr_expiration == *next_expiration) found = true; + } + EXPECT_TRUE(found); + EXPECT_THAT(cert_store.use_count(), Eq(1)); +} + +TEST_F(NearbyShareCertificateStorageImplTest, ClearPrivateCertificates) { + auto db = std::make_unique(); + nearby::FakePublicCertificateDb* fake_db = db.get(); + PrepopulatePublicCertificates(fake_db); + + auto cert_store = NearbyShareCertificateStorageImpl::Factory::Create( + preference_manager_, std::move(db)); + + std::vector certs_before = + CreatePrivateCertificates( + 3, DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS); + 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()); + EXPECT_THAT(cert_store.use_count(), Eq(1)); +} + +TEST_F(NearbyShareCertificateStorageImplTest, + ClearPrivateCertificatesOfVisibility) { + auto db = std::make_unique(); + nearby::FakePublicCertificateDb* fake_db = db.get(); + PrepopulatePublicCertificates(fake_db); + + auto cert_store = NearbyShareCertificateStorageImpl::Factory::Create( + preference_manager_, std::move(db)); + + std::vector certs_all_contacts = + CreatePrivateCertificates( + 3, DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS); + std::vector certs_selected_contacts = + CreatePrivateCertificates( + 3, DeviceVisibility::DEVICE_VISIBILITY_SELECTED_CONTACTS); + std::vector 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( + DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS); + 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].ToCertificateData(), + (*certs_after)[i].ToCertificateData()); + } + + cert_store->ClearPrivateCertificatesOfVisibility( + DeviceVisibility::DEVICE_VISIBILITY_SELECTED_CONTACTS); + certs_after = cert_store->GetPrivateCertificates(); + ASSERT_TRUE(certs_after.has_value()); + EXPECT_EQ(certs_after->size(), 0u); + } + + // Remove selected-contacts certs then all-contacts certs. + { + cert_store->ReplacePrivateCertificates(all_certs); + cert_store->ClearPrivateCertificatesOfVisibility( + DeviceVisibility::DEVICE_VISIBILITY_SELECTED_CONTACTS); + 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].ToCertificateData(), + (*certs_after)[i].ToCertificateData()); + } + + cert_store->ClearPrivateCertificatesOfVisibility( + DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS); + certs_after = cert_store->GetPrivateCertificates(); + ASSERT_TRUE(certs_after.has_value()); + EXPECT_EQ(certs_after->size(), 0u); + } + EXPECT_THAT(cert_store.use_count(), Eq(1)); +} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/certificates/nearby_share_decrypted_public_certificate.cc b/sharing/certificates/nearby_share_decrypted_public_certificate.cc new file mode 100644 index 00000000..892c939b --- /dev/null +++ b/sharing/certificates/nearby_share_decrypted_public_certificate.cc @@ -0,0 +1,261 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/certificates/nearby_share_decrypted_public_certificate.h" + +#include + +#include +#include +#include +#include +#include +#include + +#include "absl/time/time.h" +#include "absl/types/span.h" +#include "internal/crypto_cros/aead.h" +#include "internal/crypto_cros/encryptor.h" +#include "internal/crypto_cros/hmac.h" +#include "internal/crypto_cros/signature_verifier.h" +#include "sharing/certificates/common.h" +#include "sharing/certificates/constants.h" +#include "sharing/certificates/nearby_share_encrypted_metadata_key.h" +#include "sharing/internal/public/logging.h" +#include "sharing/proto/encrypted_metadata.pb.h" +#include "sharing/proto/rpc_resources.pb.h" +#include "sharing/proto/timestamp.pb.h" + +namespace nearby { +namespace sharing { +namespace { + +bool IsDataValid(absl::Time not_before, absl::Time not_after, + absl::Span public_key, + crypto::SymmetricKey* secret_key, absl::Span id, + absl::Span encrypted_metadata, + absl::Span metadata_encryption_key_tag) { + return not_before < not_after && !public_key.empty() && secret_key && + secret_key->key().size() == kNearbyShareNumBytesSecretKey && + id.size() == kNearbyShareNumBytesCertificateId && + !encrypted_metadata.empty() && + metadata_encryption_key_tag.size() == + kNearbyShareNumBytesMetadataEncryptionKeyTag; +} + +// Attempts to decrypt |encrypted_metadata_key| using the |secret_key|. +// Return std::nullopt if the decryption was unsuccessful. +std::optional> DecryptMetadataKey( + const NearbyShareEncryptedMetadataKey& encrypted_metadata_key, + const crypto::SymmetricKey* secret_key) { + std::unique_ptr encryptor = + CreateNearbyShareCtrEncryptor(secret_key, encrypted_metadata_key.salt()); + if (!encryptor) { + NL_LOG(ERROR) + << "Cannot decrypt metadata key: Could not create CTR encryptor."; + return std::nullopt; + } + + std::vector decrypted_metadata_key; + if (!encryptor->Decrypt( + as_bytes(absl::MakeSpan(encrypted_metadata_key.encrypted_key())), + &decrypted_metadata_key)) { + return std::nullopt; + } + + return decrypted_metadata_key; +} + +// Attempts to decrypt |encrypted_metadata| with |metadata_encryption_key|, +// using |authentication_key| as the IV. Returns std::nullopt if the decryption +// was unsuccessful. +std::optional> DecryptMetadataPayload( + absl::Span encrypted_metadata, + absl::Span metadata_encryption_key, + const crypto::SymmetricKey* secret_key) { + // Init() keeps a reference to the input key, so that reference must outlive + // the lifetime of |aead|. + std::vector derived_key = DeriveNearbyShareKey( + metadata_encryption_key, kNearbyShareNumBytesAesGcmKey); + + crypto::Aead aead(crypto::Aead::AeadAlgorithm::AES_256_GCM); + aead.Init(derived_key); + + auto result = aead.Open( + encrypted_metadata, + /*nonce=*/ + DeriveNearbyShareKey(as_bytes(absl::MakeSpan(secret_key->key())), + kNearbyShareNumBytesAesGcmIv), + /*additional_data=*/absl::Span()); + if (result) { + return result.value(); + } + + return std::nullopt; +} + +// Returns true if the HMAC of |decrypted_metadata_key| is +// |metadata_encryption_key_tag|. +bool VerifyMetadataEncryptionKeyTag( + absl::Span decrypted_metadata_key, + absl::Span metadata_encryption_key_tag) { + // This array of 0x00 is used to conform with the GmsCore implementation. + std::vector key(kNearbyShareNumBytesMetadataEncryptionKeyTag, 0x00); + + std::vector result(kNearbyShareNumBytesMetadataEncryptionKeyTag); + crypto::HMAC hmac(crypto::HMAC::HashAlgorithm::SHA256); + return hmac.Init(key) && + hmac.Verify(decrypted_metadata_key, metadata_encryption_key_tag); +} + +} // namespace + +// static +std::optional +NearbyShareDecryptedPublicCertificate::DecryptPublicCertificate( + const nearby::sharing::proto::PublicCertificate& public_certificate, + const NearbyShareEncryptedMetadataKey& encrypted_metadata_key) { + // Note: The PublicCertificate.metadata_encryption_key and + // PublicCertificate.for_selected_contacts are not returned from the server + // for remote devices. + absl::Time not_before = + FromJavaTime(public_certificate.start_time().seconds() * 1000); + absl::Time not_after = + FromJavaTime(public_certificate.end_time().seconds() * 1000); + std::vector public_key(public_certificate.public_key().begin(), + public_certificate.public_key().end()); + std::unique_ptr secret_key = + crypto::SymmetricKey::Import(crypto::SymmetricKey::Algorithm::AES, + public_certificate.secret_key()); + std::vector id(public_certificate.secret_id().begin(), + public_certificate.secret_id().end()); + std::vector encrypted_metadata( + public_certificate.encrypted_metadata_bytes().begin(), + public_certificate.encrypted_metadata_bytes().end()); + std::vector metadata_encryption_key_tag( + public_certificate.metadata_encryption_key_tag().begin(), + public_certificate.metadata_encryption_key_tag().end()); + + if (!IsDataValid(not_before, not_after, public_key, secret_key.get(), id, + encrypted_metadata, metadata_encryption_key_tag)) { + return std::nullopt; + } + + // Note: Failure to decrypt the metadata key or failure to confirm that the + // decrypted metadata key agrees with the key commitment tag should not log an + // error. When another device advertises their encrypted metadata key, we do + // not know what public certificate that corresponds to. So, we will + // potentially be calling DecryptPublicCertificate() on all of our public + // certificates with the same encrypted metadata key until we find the correct + // one. + auto decrypted_metadata_key = + DecryptMetadataKey(encrypted_metadata_key, secret_key.get()); + if (!decrypted_metadata_key || + !VerifyMetadataEncryptionKeyTag(*decrypted_metadata_key, + metadata_encryption_key_tag)) { + return std::nullopt; + } + + // If the key was able to be decrypted, we expect the metadata to be able to + // be decrypted. + auto decrypted_metadata_bytes = DecryptMetadataPayload( + encrypted_metadata, *decrypted_metadata_key, secret_key.get()); + if (!decrypted_metadata_bytes) { + NL_LOG(ERROR) << "Metadata decryption failed: Failed to decrypt metadata" + << "payload."; + return std::nullopt; + } + + nearby::sharing::proto::EncryptedMetadata unencrypted_metadata; + if (!unencrypted_metadata.ParseFromArray(decrypted_metadata_bytes->data(), + decrypted_metadata_bytes->size())) { + NL_LOG(ERROR) << "Metadata decryption failed: Failed to parse decrypted " + << "metadata payload."; + return std::nullopt; + } + + return NearbyShareDecryptedPublicCertificate( + not_before, not_after, std::move(secret_key), std::move(public_key), + std::move(id), std::move(unencrypted_metadata), + public_certificate.for_self_share()); +} + +NearbyShareDecryptedPublicCertificate::NearbyShareDecryptedPublicCertificate( + absl::Time not_before, absl::Time not_after, + std::unique_ptr secret_key, + std::vector public_key, std::vector id, + nearby::sharing::proto::EncryptedMetadata unencrypted_metadata, + bool for_self_share) + : not_before_(not_before), + not_after_(not_after), + secret_key_(std::move(secret_key)), + public_key_(std::move(public_key)), + id_(std::move(id)), + unencrypted_metadata_(std::move(unencrypted_metadata)), + for_self_share_(for_self_share) {} + +NearbyShareDecryptedPublicCertificate::NearbyShareDecryptedPublicCertificate( + const NearbyShareDecryptedPublicCertificate& other) { + *this = other; +} + +NearbyShareDecryptedPublicCertificate& +NearbyShareDecryptedPublicCertificate::operator=( + const NearbyShareDecryptedPublicCertificate& other) { + if (this == &other) return *this; + + not_before_ = other.not_before_; + not_after_ = other.not_after_; + secret_key_ = crypto::SymmetricKey::Import( + crypto::SymmetricKey::Algorithm::AES, other.secret_key_->key()); + public_key_ = other.public_key_; + id_ = other.id_; + unencrypted_metadata_ = other.unencrypted_metadata_; + for_self_share_ = other.for_self_share_; + return *this; +} + +NearbyShareDecryptedPublicCertificate::NearbyShareDecryptedPublicCertificate( + NearbyShareDecryptedPublicCertificate&&) = default; + +NearbyShareDecryptedPublicCertificate& +NearbyShareDecryptedPublicCertificate::operator=( + NearbyShareDecryptedPublicCertificate&&) = default; + +NearbyShareDecryptedPublicCertificate:: + ~NearbyShareDecryptedPublicCertificate() = default; + +bool NearbyShareDecryptedPublicCertificate::VerifySignature( + absl::Span payload, + absl::Span signature) const { + crypto::SignatureVerifier verifier; + if (!verifier.VerifyInit(crypto::SignatureVerifier::ECDSA_SHA256, signature, + public_key_)) { + NL_LOG(ERROR) << "Verification failed: Initialization unsuccessful."; + return false; + } + + verifier.VerifyUpdate(payload); + return verifier.VerifyFinal(); +} + +std::vector +NearbyShareDecryptedPublicCertificate::HashAuthenticationToken( + absl::Span authentication_token) const { + return ComputeAuthenticationTokenHash( + authentication_token, as_bytes(absl::MakeSpan(secret_key_->key()))); +} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/certificates/nearby_share_decrypted_public_certificate.h b/sharing/certificates/nearby_share_decrypted_public_certificate.h new file mode 100644 index 00000000..58e4a137 --- /dev/null +++ b/sharing/certificates/nearby_share_decrypted_public_certificate.h @@ -0,0 +1,119 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_CERTIFICATES_NEARBY_SHARE_DECRYPTED_PUBLIC_CERTIFICATE_H_ +#define THIRD_PARTY_NEARBY_SHARING_CERTIFICATES_NEARBY_SHARE_DECRYPTED_PUBLIC_CERTIFICATE_H_ + +#include + +#include +#include +#include + +#include "absl/time/time.h" +#include "absl/types/span.h" +#include "internal/crypto_cros/symmetric_key.h" +#include "sharing/certificates/nearby_share_encrypted_metadata_key.h" +#include "sharing/proto/encrypted_metadata.pb.h" +#include "sharing/proto/rpc_resources.pb.h" + +namespace nearby { +namespace sharing { + +// Stores decrypted metadata and crypto keys for the remote device that uploaded +// this certificate to the Nearby Share server. Use DecryptPublicCertificate() +// to generate an instance. This class provides a method for verifying a signed +// payload during the authentication flow. +class NearbyShareDecryptedPublicCertificate { + public: + // Attempts to decrypt the encrypted metadata of the PublicCertificate proto + // by first decrypting the |encrypted_metadata_key| using the secret key + // then using the decrypted key to decrypt the metadata. Returns absl::nullopt + // if the metadata was not successfully decrypted or if the proto data is + // invalid. + static std::optional + DecryptPublicCertificate( + const nearby::sharing::proto::PublicCertificate& public_certificate, + const NearbyShareEncryptedMetadataKey& encrypted_metadata_key); + + NearbyShareDecryptedPublicCertificate( + const NearbyShareDecryptedPublicCertificate& other); + NearbyShareDecryptedPublicCertificate& operator=( + const NearbyShareDecryptedPublicCertificate& other); + NearbyShareDecryptedPublicCertificate( + NearbyShareDecryptedPublicCertificate&&); + NearbyShareDecryptedPublicCertificate& operator=( + NearbyShareDecryptedPublicCertificate&&); + + virtual ~NearbyShareDecryptedPublicCertificate(); + + const std::vector& id() const { return id_; } + absl::Time not_before() const { return not_before_; } + absl::Time not_after() const { return not_after_; } + const nearby::sharing::proto::EncryptedMetadata& unencrypted_metadata() + const { + return unencrypted_metadata_; + } + + bool for_self_share() const { return for_self_share_; } + + // Verifies the |signature| of the signed |payload| using |public_key_|. + // Returns true if verification was successful. + bool VerifySignature(absl::Span payload, + absl::Span signature) const; + + // Creates a hash of the |authentication_token|, using |secret_key_|. The use + // of HKDF and the output vector size is part of the Nearby Share protocol and + // conforms with the GmsCore implementation. + std::vector HashAuthenticationToken( + absl::Span authentication_token) const; + + private: + NearbyShareDecryptedPublicCertificate( + absl::Time not_before, absl::Time not_after, + std::unique_ptr secret_key, + std::vector public_key, std::vector id, + nearby::sharing::proto::EncryptedMetadata unencrypted_metadata, + bool for_self_share); + + // The start and end times of the certificate's validity period. To avoid + // issues with clock skew, these times may be offset compared to the + // corresponding private certificate. + absl::Time not_before_; + absl::Time not_after_; + + // A 32-byte AES key that was used for metadata key and metadata decryption. + // Also, used to generate an authentication token hash. + std::unique_ptr secret_key_; + + // A P-256 public key used for verification. The bytes comprise a DER-encoded + // ASN.1 SubjectPublicKeyInfo from the X.509 specification (RFC 5280). + std::vector public_key_; + + // An ID for the certificate, most likely generated from the secret key. + std::vector id_; + + // Unencrypted device metadata. The proto name is misleading; it holds data + // that was previously serialized and encrypted. + nearby::sharing::proto::EncryptedMetadata unencrypted_metadata_; + + // Indicates if this public certificate is from another device owned by the + // same user. + bool for_self_share_ = false; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_CERTIFICATES_NEARBY_SHARE_DECRYPTED_PUBLIC_CERTIFICATE_H_ diff --git a/sharing/certificates/nearby_share_decrypted_public_certificate_test.cc b/sharing/certificates/nearby_share_decrypted_public_certificate_test.cc new file mode 100644 index 00000000..ec650da6 --- /dev/null +++ b/sharing/certificates/nearby_share_decrypted_public_certificate_test.cc @@ -0,0 +1,143 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/certificates/nearby_share_decrypted_public_certificate.h" + +#include + +#include +#include +#include + +#include "gtest/gtest.h" +#include "absl/types/span.h" +#include "sharing/certificates/common.h" +#include "sharing/certificates/constants.h" +#include "sharing/certificates/nearby_share_encrypted_metadata_key.h" +#include "sharing/certificates/test_util.h" +#include "sharing/common/nearby_share_enums.h" +#include "sharing/proto/encrypted_metadata.pb.h" +#include "sharing/proto/enums.pb.h" +#include "sharing/proto/rpc_resources.pb.h" +#include "sharing/proto/timestamp.pb.h" + +namespace nearby { +namespace sharing { +namespace { + +using ::nearby::sharing::proto::DeviceVisibility; +using ::nearby::sharing::proto::PublicCertificate; + +// The for_selected_contacts field of a public certificate proto is irrelevant +// for remote device certificates. Even if it is set, it is meaningless. It only +// has meaning for private certificates converted to public certificates and +// uploaded to the Nearby server. +const DeviceVisibility kTestPublicCertificateVisibility = + DeviceVisibility::DEVICE_VISIBILITY_SELF_SHARE; + +TEST(NearbyShareDecryptedPublicCertificateTest, Decrypt) { + PublicCertificate proto_cert = + GetNearbyShareTestPublicCertificate(kTestPublicCertificateVisibility); + proto_cert.set_for_self_share(true); + + std::optional cert = + NearbyShareDecryptedPublicCertificate::DecryptPublicCertificate( + proto_cert, GetNearbyShareTestEncryptedMetadataKey()); + EXPECT_TRUE(cert); + EXPECT_EQ(FromJavaTime(proto_cert.start_time().seconds() * 1000), + cert->not_before()); + EXPECT_EQ(FromJavaTime(proto_cert.end_time().seconds() * 1000), + cert->not_after()); + EXPECT_EQ(std::vector(proto_cert.secret_id().begin(), + proto_cert.secret_id().end()), + cert->id()); + EXPECT_EQ(GetNearbyShareTestMetadata().SerializeAsString(), + cert->unencrypted_metadata().SerializeAsString()); + EXPECT_EQ(proto_cert.for_self_share(), cert->for_self_share()); +} + +TEST(NearbyShareDecryptedPublicCertificateTest, Decrypt_IncorrectKeyFailure) { + // Input incorrect metadata encryption key. + EXPECT_FALSE(NearbyShareDecryptedPublicCertificate::DecryptPublicCertificate( + GetNearbyShareTestPublicCertificate(kTestPublicCertificateVisibility), + NearbyShareEncryptedMetadataKey( + std::vector(kNearbyShareNumBytesMetadataEncryptionKeySalt, + 0x00), + std::vector(kNearbyShareNumBytesMetadataEncryptionKey, + 0x00)))); +} + +TEST(NearbyShareDecryptedPublicCertificateTest, + Decrypt_MetadataDecryptionFailure) { + // Use metadata that cannot be decrypted with the given key. + PublicCertificate proto_cert = + GetNearbyShareTestPublicCertificate(kTestPublicCertificateVisibility); + proto_cert.set_encrypted_metadata_bytes("invalid metadata"); + EXPECT_FALSE(NearbyShareDecryptedPublicCertificate::DecryptPublicCertificate( + proto_cert, GetNearbyShareTestEncryptedMetadataKey())); +} + +TEST(NearbyShareDecryptedPublicCertificateTest, Decrypt_InvalidDataFailure) { + // Do not accept the input PublicCertificate because the validity period does + // not make sense. + PublicCertificate proto_cert = + GetNearbyShareTestPublicCertificate(kTestPublicCertificateVisibility); + proto_cert.mutable_end_time()->set_seconds(proto_cert.start_time().seconds() - + 1); + EXPECT_FALSE(NearbyShareDecryptedPublicCertificate::DecryptPublicCertificate( + proto_cert, GetNearbyShareTestEncryptedMetadataKey())); +} + +TEST(NearbyShareDecryptedPublicCertificateTest, Verify) { + std::optional cert = + NearbyShareDecryptedPublicCertificate::DecryptPublicCertificate( + GetNearbyShareTestPublicCertificate(kTestPublicCertificateVisibility), + GetNearbyShareTestEncryptedMetadataKey()); + EXPECT_TRUE(cert->VerifySignature(GetNearbyShareTestPayloadToSign(), + GetNearbyShareTestSampleSignature())); +} + +TEST(NearbyShareDecryptedPublicCertificateTest, Verify_InitFailure) { + // Public key has invalid SubjectPublicKeyInfo format. + PublicCertificate proto_cert = + GetNearbyShareTestPublicCertificate(kTestPublicCertificateVisibility); + proto_cert.set_public_key("invalid public key"); + + auto cert = NearbyShareDecryptedPublicCertificate::DecryptPublicCertificate( + proto_cert, GetNearbyShareTestEncryptedMetadataKey()); + ASSERT_TRUE(cert); + EXPECT_FALSE(cert->VerifySignature(GetNearbyShareTestPayloadToSign(), + GetNearbyShareTestSampleSignature())); +} + +TEST(NearbyShareDecryptedPublicCertificateTest, Verify_WrongSignature) { + auto cert = NearbyShareDecryptedPublicCertificate::DecryptPublicCertificate( + GetNearbyShareTestPublicCertificate(kTestPublicCertificateVisibility), + GetNearbyShareTestEncryptedMetadataKey()); + EXPECT_FALSE( + cert->VerifySignature(GetNearbyShareTestPayloadToSign(), + /*signature=*/absl::Span())); +} + +TEST(NearbyShareDecryptedPublicCertificateTest, HashAuthenticationToken) { + auto cert = NearbyShareDecryptedPublicCertificate::DecryptPublicCertificate( + GetNearbyShareTestPublicCertificate(kTestPublicCertificateVisibility), + GetNearbyShareTestEncryptedMetadataKey()); + EXPECT_EQ(GetNearbyShareTestPayloadHashUsingSecretKey(), + cert->HashAuthenticationToken(GetNearbyShareTestPayloadToSign())); +} + +} // namespace +} // namespace sharing +} // namespace nearby diff --git a/sharing/certificates/nearby_share_encrypted_metadata_key.cc b/sharing/certificates/nearby_share_encrypted_metadata_key.cc new file mode 100644 index 00000000..9fca55ec --- /dev/null +++ b/sharing/certificates/nearby_share_encrypted_metadata_key.cc @@ -0,0 +1,51 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/certificates/nearby_share_encrypted_metadata_key.h" + +#include + +#include +#include + +#include "sharing/certificates/constants.h" +#include "sharing/internal/public/logging.h" + +namespace nearby { +namespace sharing { + +NearbyShareEncryptedMetadataKey::NearbyShareEncryptedMetadataKey( + std::vector salt, std::vector encrypted_key) + : salt_(std::move(salt)), encrypted_key_(std::move(encrypted_key)) { + NL_DCHECK_EQ(kNearbyShareNumBytesMetadataEncryptionKeySalt, salt_.size()); + NL_DCHECK_EQ(kNearbyShareNumBytesMetadataEncryptionKey, + encrypted_key_.size()); +} + +NearbyShareEncryptedMetadataKey::NearbyShareEncryptedMetadataKey( + const NearbyShareEncryptedMetadataKey&) = default; + +NearbyShareEncryptedMetadataKey& NearbyShareEncryptedMetadataKey::operator=( + const NearbyShareEncryptedMetadataKey&) = default; + +NearbyShareEncryptedMetadataKey::NearbyShareEncryptedMetadataKey( + NearbyShareEncryptedMetadataKey&&) = default; + +NearbyShareEncryptedMetadataKey& NearbyShareEncryptedMetadataKey::operator=( + NearbyShareEncryptedMetadataKey&&) = default; + +NearbyShareEncryptedMetadataKey::~NearbyShareEncryptedMetadataKey() = default; + +} // namespace sharing +} // namespace nearby diff --git a/sharing/certificates/nearby_share_encrypted_metadata_key.h b/sharing/certificates/nearby_share_encrypted_metadata_key.h new file mode 100644 index 00000000..b1ad118a --- /dev/null +++ b/sharing/certificates/nearby_share_encrypted_metadata_key.h @@ -0,0 +1,48 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_CERTIFICATES_NEARBY_SHARE_ENCRYPTED_METADATA_KEY_H_ +#define THIRD_PARTY_NEARBY_SHARING_CERTIFICATES_NEARBY_SHARE_ENCRYPTED_METADATA_KEY_H_ + +#include +#include + +namespace nearby { +namespace sharing { + +// Holds the encrypted symmetric key--the key used to encrypt user/device +// metadata--as well as the salt used to encrypt the key. +class NearbyShareEncryptedMetadataKey { + public: + NearbyShareEncryptedMetadataKey(std::vector salt, + std::vector encrypted_key); + NearbyShareEncryptedMetadataKey(const NearbyShareEncryptedMetadataKey&); + NearbyShareEncryptedMetadataKey& operator=( + const NearbyShareEncryptedMetadataKey&); + NearbyShareEncryptedMetadataKey(NearbyShareEncryptedMetadataKey&&); + NearbyShareEncryptedMetadataKey& operator=(NearbyShareEncryptedMetadataKey&&); + ~NearbyShareEncryptedMetadataKey(); + + const std::vector& salt() const { return salt_; } + const std::vector& encrypted_key() const { return encrypted_key_; } + + private: + std::vector salt_; + std::vector encrypted_key_; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_CERTIFICATES_NEARBY_SHARE_ENCRYPTED_METADATA_KEY_H_ diff --git a/sharing/certificates/nearby_share_private_certificate.cc b/sharing/certificates/nearby_share_private_certificate.cc new file mode 100644 index 00000000..461de510 --- /dev/null +++ b/sharing/certificates/nearby_share_private_certificate.cc @@ -0,0 +1,420 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/certificates/nearby_share_private_certificate.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "absl/random/random.h" +#include "absl/strings/escaping.h" +#include "absl/strings/string_view.h" +#include "absl/time/time.h" +#include "absl/types/span.h" +#include "internal/crypto_cros/aead.h" +#include "internal/crypto_cros/ec_private_key.h" +#include "internal/crypto_cros/ec_signature_creator.h" +#include "internal/crypto_cros/encryptor.h" +#include "internal/crypto_cros/hmac.h" +#include "internal/crypto_cros/sha2.h" +#include "internal/crypto_cros/symmetric_key.h" +#include "sharing/certificates/common.h" +#include "sharing/certificates/constants.h" +#include "sharing/certificates/nearby_share_encrypted_metadata_key.h" +#include "sharing/internal/api/private_certificate_data.h" +#include "sharing/internal/base/encode.h" +#include "sharing/internal/public/logging.h" +#include "sharing/proto/encrypted_metadata.pb.h" +#include "sharing/proto/enums.pb.h" +#include "sharing/proto/rpc_resources.pb.h" +#include "sharing/proto/timestamp.pb.h" + +namespace nearby { +namespace sharing { +namespace { + +using ::nearby::sharing::api::PrivateCertificateData; +using ::nearby::sharing::proto::DeviceVisibility; + +// Generates a random validity bound offset in the interval +// [0, kNearbyShareMaxPrivateCertificateValidityBoundOffset). +absl::Duration GenerateRandomOffset() { + absl::BitGen bitgen; + return absl::Microseconds( + absl::Uniform(bitgen, 0, + kNearbyShareMaxPrivateCertificateValidityBoundOffset / + absl::Microseconds(1))); +} + +// Generates a certificate identifier by hashing the input secret |key|. +std::vector CreateCertificateIdFromSecretKey( + const crypto::SymmetricKey& key) { + NL_DCHECK_EQ(crypto::kSHA256Length, kNearbyShareNumBytesCertificateId); + std::vector id(kNearbyShareNumBytesCertificateId); + crypto::SHA256HashString(key.key(), id.data(), id.size()); + + return id; +} + +// Creates an HMAC from |metadata_encryption_key| to be used as a key commitment +// in certificates. +std::optional> CreateMetadataEncryptionKeyTag( + absl::Span metadata_encryption_key) { + // This array of 0x00 is used to conform with the GmsCore implementation. + std::vector key(kNearbyShareNumBytesMetadataEncryptionKeyTag, 0x00); + + std::vector result(kNearbyShareNumBytesMetadataEncryptionKeyTag); + crypto::HMAC hmac(crypto::HMAC::HashAlgorithm::SHA256); + if (!hmac.Init(key) || + !hmac.Sign(metadata_encryption_key, + absl::MakeSpan(result.data(), result.size()))) + return std::nullopt; + + return result; +} + +std::string EncodeString(absl::string_view unencoded_string) { + std::string result; + absl::WebSafeBase64Escape(unencoded_string, &result); + return result; +} + +std::optional DecodeString(const std::string* encoded_string) { + std::string result; + if (!encoded_string) return std::nullopt; + + if (absl::WebSafeBase64Unescape(*encoded_string, &result)) { + return result; + } + return std::nullopt; +} + +std::string BytesToEncodedString(const std::vector& bytes) { + return EncodeString(std::string(bytes.begin(), bytes.end())); +} + +std::optional> EncodedStringToBytes( + const std::string* str) { + std::optional decoded_str = DecodeString(str); + return decoded_str ? std::make_optional>( + decoded_str->begin(), decoded_str->end()) + : std::nullopt; +} + +std::string SaltsToString(const std::set>& salts) { + std::string str; + str.reserve(salts.size() * 2 * kNearbyShareNumBytesMetadataEncryptionKeySalt); + for (const std::vector& salt : salts) { + str += nearby::utils::HexEncode(salt); + } + return str; +} + +std::set> StringToSalts(absl::string_view str) { + const size_t chars_per_salt = + 2 * kNearbyShareNumBytesMetadataEncryptionKeySalt; + NL_DCHECK_EQ(str.size() % chars_per_salt, 0); + std::set> salts; + for (size_t i = 0; i < str.size(); i += chars_per_salt) { + std::string bytes = + absl::HexStringToBytes(absl::string_view(&str[i], chars_per_salt)); + std::vector salt(bytes.begin(), bytes.end()); + salts.insert(std::move(salt)); + } + return salts; +} + +} // namespace + +NearbySharePrivateCertificate::NearbySharePrivateCertificate( + DeviceVisibility visibility, absl::Time not_before, + nearby::sharing::proto::EncryptedMetadata unencrypted_metadata) + : visibility_(visibility), + not_before_(not_before), + not_after_(not_before_ + kNearbyShareCertificateValidityPeriod), + key_pair_(crypto::ECPrivateKey::Create()), + secret_key_(crypto::SymmetricKey::GenerateRandomKey( + crypto::SymmetricKey::Algorithm::AES, + /*key_size_in_bits=*/8 * kNearbyShareNumBytesSecretKey)), + metadata_encryption_key_( + GenerateRandomBytes(kNearbyShareNumBytesMetadataEncryptionKey)), + id_(CreateCertificateIdFromSecretKey(*secret_key_)), + unencrypted_metadata_(std::move(unencrypted_metadata)) { + NL_DCHECK_NE( + static_cast(visibility), + static_cast(DeviceVisibility::DEVICE_VISIBILITY_UNSPECIFIED)); +} + +NearbySharePrivateCertificate::NearbySharePrivateCertificate( + DeviceVisibility visibility, absl::Time not_before, absl::Time not_after, + std::unique_ptr key_pair, + std::unique_ptr secret_key, + std::vector metadata_encryption_key, std::vector id, + nearby::sharing::proto::EncryptedMetadata unencrypted_metadata, + std::set> consumed_salts) + : visibility_(visibility), + not_before_(not_before), + not_after_(not_after), + key_pair_(std::move(key_pair)), + secret_key_(std::move(secret_key)), + metadata_encryption_key_(std::move(metadata_encryption_key)), + id_(std::move(id)), + unencrypted_metadata_(std::move(unencrypted_metadata)), + consumed_salts_(std::move(consumed_salts)) { + NL_DCHECK_NE( + static_cast(visibility), + static_cast(DeviceVisibility::DEVICE_VISIBILITY_UNSPECIFIED)); +} + +NearbySharePrivateCertificate::NearbySharePrivateCertificate( + const NearbySharePrivateCertificate& other) { + *this = other; +} + +NearbySharePrivateCertificate& NearbySharePrivateCertificate::operator=( + const NearbySharePrivateCertificate& other) { + if (this == &other) return *this; + + visibility_ = other.visibility_; + not_before_ = other.not_before_; + not_after_ = other.not_after_; + key_pair_ = other.key_pair_->Copy(); + secret_key_ = crypto::SymmetricKey::Import( + crypto::SymmetricKey::Algorithm::AES, other.secret_key_->key()); + metadata_encryption_key_ = other.metadata_encryption_key_; + id_ = other.id_; + unencrypted_metadata_ = other.unencrypted_metadata_; + consumed_salts_ = other.consumed_salts_; + next_salts_for_testing_ = other.next_salts_for_testing_; + offset_for_testing_ = other.offset_for_testing_; + return *this; +} + +NearbySharePrivateCertificate::NearbySharePrivateCertificate( + NearbySharePrivateCertificate&& other) = default; + +NearbySharePrivateCertificate& NearbySharePrivateCertificate::operator=( + NearbySharePrivateCertificate&& other) = default; + +NearbySharePrivateCertificate::~NearbySharePrivateCertificate() = default; + +std::optional +NearbySharePrivateCertificate::EncryptMetadataKey() { + std::optional> salt = GenerateUnusedSalt(); + if (!salt) { + NL_LOG(ERROR) << "Encryption failed: Salt generation unsuccessful."; + return std::nullopt; + } + + std::unique_ptr encryptor = + CreateNearbyShareCtrEncryptor(secret_key_.get(), *salt); + if (!encryptor) { + NL_LOG(ERROR) << "Encryption failed: Could not create CTR encryptor."; + return std::nullopt; + } + + NL_DCHECK_EQ(kNearbyShareNumBytesMetadataEncryptionKey, + metadata_encryption_key_.size()); + std::vector encrypted_metadata_key; + if (!encryptor->Encrypt(metadata_encryption_key_, &encrypted_metadata_key)) { + NL_LOG(ERROR) << "Encryption failed: Could not encrypt metadata key."; + return std::nullopt; + } + + return NearbyShareEncryptedMetadataKey(*salt, encrypted_metadata_key); +} + +std::optional> NearbySharePrivateCertificate::Sign( + absl::Span payload) const { + std::unique_ptr signer( + crypto::ECSignatureCreator::Create(key_pair_.get())); + + std::vector signature; + if (!signer->Sign(payload, &signature)) { + NL_LOG(ERROR) << "Signing failed."; + return std::nullopt; + } + + return signature; +} + +std::vector NearbySharePrivateCertificate::HashAuthenticationToken( + absl::Span authentication_token) const { + return ComputeAuthenticationTokenHash( + authentication_token, as_bytes(absl::MakeSpan(secret_key_->key()))); +} + +std::optional +NearbySharePrivateCertificate::ToPublicCertificate() const { + std::vector public_key; + if (!key_pair_->ExportPublicKey(&public_key)) { + NL_LOG(ERROR) << "Failed to export public key."; + return std::nullopt; + } + + std::optional> encrypted_metadata_bytes = + EncryptMetadata(); + if (!encrypted_metadata_bytes) { + NL_LOG(ERROR) << "Failed to encrypt metadata."; + return std::nullopt; + } + + std::optional> metadata_encryption_key_tag = + CreateMetadataEncryptionKeyTag(metadata_encryption_key_); + if (!metadata_encryption_key_tag) { + NL_LOG(ERROR) << "Failed to compute metadata encryption key tag."; + return std::nullopt; + } + + absl::Duration not_before_offset = + offset_for_testing_.value_or(GenerateRandomOffset()); + absl::Duration not_after_offset = + offset_for_testing_.value_or(GenerateRandomOffset()); + + nearby::sharing::proto::PublicCertificate public_certificate; + public_certificate.set_secret_id(std::string(id_.begin(), id_.end())); + public_certificate.set_secret_key(secret_key_->key()); + public_certificate.set_public_key( + std::string(public_key.begin(), public_key.end())); + public_certificate.mutable_start_time()->set_seconds( + ToJavaTime(not_before_ - not_before_offset) / 1000); + public_certificate.mutable_end_time()->set_seconds( + ToJavaTime(not_after_ + not_after_offset) / 1000); + public_certificate.set_for_selected_contacts( + visibility_ == DeviceVisibility::DEVICE_VISIBILITY_SELECTED_CONTACTS); + public_certificate.set_metadata_encryption_key(std::string( + metadata_encryption_key_.begin(), metadata_encryption_key_.end())); + public_certificate.set_encrypted_metadata_bytes(std::string( + encrypted_metadata_bytes->begin(), encrypted_metadata_bytes->end())); + public_certificate.set_metadata_encryption_key_tag( + std::string(metadata_encryption_key_tag->begin(), + metadata_encryption_key_tag->end())); + public_certificate.set_for_self_share( + visibility_ == DeviceVisibility::DEVICE_VISIBILITY_SELF_SHARE); + + return public_certificate; +} + +PrivateCertificateData NearbySharePrivateCertificate::ToCertificateData() + const { + std::vector key_pair; + key_pair_->ExportPrivateKey(&key_pair); + return PrivateCertificateData{ + .visibility = static_cast(visibility_), + .not_before = absl::ToUnixNanos(not_before_), + .not_after = absl::ToUnixNanos(not_after_), + .key_pair = BytesToEncodedString(key_pair), + .secret_key = EncodeString(secret_key_->key()), + .metadata_encryption_key = BytesToEncodedString(metadata_encryption_key_), + .id = BytesToEncodedString(id_), + .unencrypted_metadata_proto = + EncodeString(unencrypted_metadata_.SerializeAsString()), + .consumed_salts = SaltsToString(consumed_salts_), + }; +} + +std::optional +NearbySharePrivateCertificate::FromCertificateData( + const PrivateCertificateData& cert_data) { + auto bytes_opt = EncodedStringToBytes(&cert_data.key_pair); + if (!bytes_opt) return std::nullopt; + std::unique_ptr key_pair = + crypto::ECPrivateKey::CreateFromPrivateKeyInfo(*bytes_opt); + auto str_opt = DecodeString(&cert_data.secret_key); + if (!str_opt) return std::nullopt; + std::unique_ptr secret_key = + crypto::SymmetricKey::Import(crypto::SymmetricKey::Algorithm::AES, + *str_opt); + bytes_opt = EncodedStringToBytes(&cert_data.metadata_encryption_key); + if (!bytes_opt) return std::nullopt; + std::vector metadata_encryption_key = *bytes_opt; + bytes_opt = EncodedStringToBytes(&cert_data.id); + if (!bytes_opt) return std::nullopt; + std::vector id = *bytes_opt; + str_opt = DecodeString(&cert_data.unencrypted_metadata_proto); + if (!str_opt) return std::nullopt; + nearby::sharing::proto::EncryptedMetadata unencrypted_metadata; + if (!unencrypted_metadata.ParseFromString(*str_opt)) return std::nullopt; + + return NearbySharePrivateCertificate( + static_cast(cert_data.visibility), + absl::FromUnixNanos(cert_data.not_before), + absl::FromUnixNanos(cert_data.not_after), std::move(key_pair), + std::move(secret_key), std::move(metadata_encryption_key), std::move(id), + std::move(unencrypted_metadata), + StringToSalts(cert_data.consumed_salts)); +} + +std::optional> +NearbySharePrivateCertificate::GenerateUnusedSalt() { + if (consumed_salts_.size() >= kNearbyShareMaxNumMetadataEncryptionKeySalts) { + NL_LOG(ERROR) << "All salts exhausted for certificate."; + return std::nullopt; + } + + for (size_t attempt = 0; + attempt < kNearbyShareMaxNumMetadataEncryptionKeySaltGenerationRetries; + ++attempt) { + std::vector salt; + if (next_salts_for_testing_.empty()) { + salt = GenerateRandomBytes(2u); + } else { + salt = next_salts_for_testing_.front(); + next_salts_for_testing_.pop(); + } + NL_DCHECK_EQ(2u, salt.size()); + + if (consumed_salts_.find(salt) == consumed_salts_.end()) { + consumed_salts_.insert(salt); + return salt; + } + } + + NL_LOG(ERROR) << "Salt generation exceeded max number of retries. This is " + "highly improbable."; + return std::nullopt; +} + +std::optional> +NearbySharePrivateCertificate::EncryptMetadata() const { + // Init() keeps a reference to the input key, so that reference must outlive + // the lifetime of |aead|. + std::vector derived_key = DeriveNearbyShareKey( + metadata_encryption_key_, kNearbyShareNumBytesAesGcmKey); + + crypto::Aead aead(crypto::Aead::AeadAlgorithm::AES_256_GCM); + aead.Init(derived_key); + + std::vector metadata_array(unencrypted_metadata_.ByteSizeLong()); + unencrypted_metadata_.SerializeToArray(metadata_array.data(), + metadata_array.size()); + + return aead.Seal( + metadata_array, + /*nonce=*/ + DeriveNearbyShareKey(as_bytes(absl::MakeSpan(secret_key_->key())), + kNearbyShareNumBytesAesGcmIv), + /*additional_data=*/absl::Span()); +} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/certificates/nearby_share_private_certificate.h b/sharing/certificates/nearby_share_private_certificate.h new file mode 100644 index 00000000..e664c391 --- /dev/null +++ b/sharing/certificates/nearby_share_private_certificate.h @@ -0,0 +1,189 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_CERTIFICATES_NEARBY_SHARE_PRIVATE_CERTIFICATE_H_ +#define THIRD_PARTY_NEARBY_SHARING_CERTIFICATES_NEARBY_SHARE_PRIVATE_CERTIFICATE_H_ + +#include + +#include +#include +#include +#include +#include + +#include "absl/time/time.h" +#include "absl/types/span.h" +#include "internal/crypto_cros/ec_private_key.h" +#include "internal/crypto_cros/symmetric_key.h" +#include "sharing/certificates/nearby_share_encrypted_metadata_key.h" +#include "sharing/internal/api/private_certificate_data.h" +#include "sharing/proto/encrypted_metadata.pb.h" +#include "sharing/proto/enums.pb.h" +#include "sharing/proto/rpc_resources.pb.h" + +#ifndef FRIEND_TEST_ALL_PREFIXES +#define FRIEND_TEST(test_case_name, test_name) \ + friend class test_case_name##_##test_name##_Test + +#define FRIEND_TEST_ALL_PREFIXES(test_case_name, test_name) \ + FRIEND_TEST(test_case_name, test_name); \ + FRIEND_TEST(test_case_name, DISABLED_##test_name); \ + FRIEND_TEST(test_case_name, FLAKY_##test_name) +#endif + +namespace nearby { +namespace sharing { + +// Stores metadata and crypto keys for the local device. This certificate +// can be converted to a public certificate and sent to select contacts, who +// will then use the certificate for authenticating the local device before +// transferring data. Provides method for signing a payload during +// authentication with a remote device. Provides method for encrypting the +// metadata encryption key, which can then be advertised. +class NearbySharePrivateCertificate { + public: + // Inverse operation of ToCertificateData(). Returns absl::nullopt if the + // conversion is not successful + static std::optional FromCertificateData( + const nearby::sharing::api::PrivateCertificateData& cert_data); + + // Generates a random EC key pair, secret key, and metadata encryption + // key. Derives the certificate ID from the secret key. Derives the + // not-after time from |not_before| and the certificate validity period fixed + // by the Nearby Share protocol. Visibility cannot be "no one". + NearbySharePrivateCertificate( + proto::DeviceVisibility visibility, absl::Time not_before, + nearby::sharing::proto::EncryptedMetadata unencrypted_metadata); + + NearbySharePrivateCertificate( + proto::DeviceVisibility visibility, absl::Time not_before, + absl::Time not_after, std::unique_ptr key_pair, + std::unique_ptr secret_key, + std::vector metadata_encryption_key, std::vector id, + nearby::sharing::proto::EncryptedMetadata unencrypted_metadata, + std::set> consumed_salts); + + NearbySharePrivateCertificate(const NearbySharePrivateCertificate& other); + NearbySharePrivateCertificate& operator=( + const NearbySharePrivateCertificate& other); + NearbySharePrivateCertificate(NearbySharePrivateCertificate&& other); + NearbySharePrivateCertificate& operator=( + NearbySharePrivateCertificate&& other); + + virtual ~NearbySharePrivateCertificate(); + + const std::vector& id() const { return id_; } + proto::DeviceVisibility visibility() const { return visibility_; } + absl::Time not_before() const { return not_before_; } + absl::Time not_after() const { return not_after_; } + const nearby::sharing::proto::EncryptedMetadata& unencrypted_metadata() + const { + return unencrypted_metadata_; + } + + // Encrypts |metadata_encryption_key_| with the |secret_key_|, using a + // randomly generated 2-byte salt that has not already been consumed. Returns + // std::nullopt if the encryption fails or if there are no remaining salts. + // Note: Due to the generation and storage of an unconsumed salt, this method + // is not thread safe. + std::optional EncryptMetadataKey(); + + // Signs the input |payload| with the private key from |key_pair_|. Returns + // std::nullopt if the signing was unsuccessful. + std::optional> Sign( + absl::Span payload) const; + + // Creates a hash of the |authentication_token|, using |secret_key_|. The use + // of HKDF and the output vector size is part of the Nearby Share protocol and + // conforms with the GmsCore implementation. + std::vector HashAuthenticationToken( + absl::Span authentication_token) const; + + // Converts this private certificate to a public certificate proto that can be + // shared with select contacts. Returns std::nullopt if the conversion was + // unsuccessful. + std::optional ToPublicCertificate() + const; + + // Converts this private certificate to PrivateCertificateData for storage + // in Prefs. + nearby::sharing::api::PrivateCertificateData ToCertificateData() const; + + // For testing only. + std::queue>& next_salts_for_testing() { + return next_salts_for_testing_; + } + std::optional& offset_for_testing() { + return offset_for_testing_; + } + + private: + // Generates a random 2-byte salt used for encrypting the metadata encryption + // key. Adds returned salt to |consumed_salts_|. Returns absl::nullopt if the + // maximum number of salts have been exhausted or if an unconsumed salt cannot + // be found in a fixed number of attempts, though this is highly improbably. + // Note: This function is not thread safe. + std::optional> GenerateUnusedSalt(); + + // Encrypts |unencrypted_metadata_| with the |metadata_encryption_key_|, using + // the |secret_key_| as salt. + std::optional> EncryptMetadata() const; + + // Specifies which contacts can receive the public certificate corresponding + // to this private certificate. + proto::DeviceVisibility visibility_; + + // The start and end times of the certificate's validity period. Note: An + // offset is not yet applied to these values. To avoid issues with clock skew, + // offsets should be applied during conversion to a public certificate. + absl::Time not_before_; + absl::Time not_after_; + + // The public/private P-256 key pair used for verification/signing to ensure + // secret of public certificates. The public key is included in the + // public certificate, but the private key will never leave the device. + std::unique_ptr key_pair_; + + // A 32-byte AES key used, along with a salt, to encrypt the + // |metadata_encryption_key_|, after which it can be safely advertised. Also, + // used to generate an authentication token hash. Included in the public + // certificate. + std::unique_ptr secret_key_; + + // A 14-byte symmetric key used to encrypt |unencrypted_metadata_|. Not + // included in the public certificate. + std::vector metadata_encryption_key_; + + // An ID for the certificate, generated from the secret key. + std::vector id_; + + // Unencrypted device metadata. The proto name is misleading; it holds data + // that will eventually be serialized and encrypted. + nearby::sharing::proto::EncryptedMetadata unencrypted_metadata_; + + // The set of 2-byte salts already used to encrypt the metadata key. + std::set> consumed_salts_; + + // For testing only. + std::queue> next_salts_for_testing_; + std::optional offset_for_testing_; + + FRIEND_TEST_ALL_PREFIXES(NearbySharePrivateCertificateTest, ToFromDictionary); +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_CERTIFICATES_NEARBY_SHARE_PRIVATE_CERTIFICATE_H_ diff --git a/sharing/certificates/nearby_share_private_certificate_test.cc b/sharing/certificates/nearby_share_private_certificate_test.cc new file mode 100644 index 00000000..015cefc1 --- /dev/null +++ b/sharing/certificates/nearby_share_private_certificate_test.cc @@ -0,0 +1,188 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/certificates/nearby_share_private_certificate.h" + +#include +#include + +#include +#include +#include +#include + +#include "gtest/gtest.h" +#include "absl/time/time.h" +#include "sharing/certificates/constants.h" +#include "sharing/certificates/nearby_share_decrypted_public_certificate.h" +#include "sharing/certificates/nearby_share_encrypted_metadata_key.h" +#include "sharing/certificates/test_util.h" +#include "sharing/proto/encrypted_metadata.pb.h" +#include "sharing/proto/enums.pb.h" +#include "sharing/proto/rpc_resources.pb.h" + +namespace nearby { +namespace sharing { + +using ::nearby::sharing::proto::DeviceVisibility; + +TEST(NearbySharePrivateCertificateTest, Construction) { + NearbySharePrivateCertificate private_certificate( + DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS, + GetNearbyShareTestNotBefore(), GetNearbyShareTestMetadata()); + EXPECT_EQ(kNearbyShareNumBytesCertificateId, private_certificate.id().size()); + EXPECT_EQ(DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS, + private_certificate.visibility()); + EXPECT_EQ(GetNearbyShareTestNotBefore(), private_certificate.not_before()); + EXPECT_EQ( + GetNearbyShareTestNotBefore() + kNearbyShareCertificateValidityPeriod, + private_certificate.not_after()); + EXPECT_EQ(GetNearbyShareTestMetadata().SerializeAsString(), + private_certificate.unencrypted_metadata().SerializeAsString()); +} + +TEST(NearbySharePrivateCertificateTest, ToFromDictionary) { + NearbySharePrivateCertificate before( + DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS, + GetNearbyShareTestNotBefore(), GetNearbyShareTestMetadata()); + // Generate a few consumed salts. + for (size_t i = 0; i < 5; ++i) ASSERT_TRUE(before.EncryptMetadataKey()); + + NearbySharePrivateCertificate after( + *NearbySharePrivateCertificate::FromCertificateData( + before.ToCertificateData())); + + EXPECT_EQ(before.id(), after.id()); + EXPECT_EQ(before.visibility(), after.visibility()); + EXPECT_EQ(before.not_before(), after.not_before()); + EXPECT_EQ(before.not_after(), after.not_after()); + EXPECT_EQ(before.unencrypted_metadata().SerializeAsString(), + after.unencrypted_metadata().SerializeAsString()); + EXPECT_EQ(before.secret_key_->key(), after.secret_key_->key()); + EXPECT_EQ(before.metadata_encryption_key_, after.metadata_encryption_key_); + EXPECT_EQ(before.consumed_salts_, after.consumed_salts_); + + std::vector before_private_key, after_private_key; + before.key_pair_->ExportPrivateKey(&before_private_key); + after.key_pair_->ExportPrivateKey(&after_private_key); + EXPECT_EQ(before_private_key, after_private_key); +} + +TEST(NearbySharePrivateCertificateTest, EncryptMetadataKey) { + NearbySharePrivateCertificate private_certificate( + DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS, + GetNearbyShareTestNotBefore(), GetNearbyShareTestMetadata()); + std::optional encrypted_metadata_key = + private_certificate.EncryptMetadataKey(); + ASSERT_TRUE(encrypted_metadata_key); + EXPECT_EQ(kNearbyShareNumBytesMetadataEncryptionKeySalt, + encrypted_metadata_key->salt().size()); + EXPECT_EQ(kNearbyShareNumBytesMetadataEncryptionKey, + encrypted_metadata_key->encrypted_key().size()); +} + +TEST(NearbySharePrivateCertificateTest, EncryptMetadataKey_FixedData) { + NearbySharePrivateCertificate private_certificate = + GetNearbyShareTestPrivateCertificate( + DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS); + std::optional encrypted_metadata_key = + private_certificate.EncryptMetadataKey(); + EXPECT_EQ(GetNearbyShareTestEncryptedMetadataKey().encrypted_key(), + encrypted_metadata_key->encrypted_key()); + EXPECT_EQ(GetNearbyShareTestEncryptedMetadataKey().salt(), + encrypted_metadata_key->salt()); +} + +TEST(NearbySharePrivateCertificateTest, + EncryptMetadataKey_SaltsExhaustedFailure) { + NearbySharePrivateCertificate private_certificate = + GetNearbyShareTestPrivateCertificate( + DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS); + for (size_t i = 0; i < kNearbyShareMaxNumMetadataEncryptionKeySalts; ++i) { + EXPECT_TRUE(private_certificate.EncryptMetadataKey()); + } + EXPECT_FALSE(private_certificate.EncryptMetadataKey()); +} + +TEST(NearbySharePrivateCertificateTest, + EncryptMetadataKey_TooManySaltGenerationRetriesFailure) { + NearbySharePrivateCertificate private_certificate = + GetNearbyShareTestPrivateCertificate( + DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS); + EXPECT_TRUE(private_certificate.EncryptMetadataKey()); + while (private_certificate.next_salts_for_testing().size() < + kNearbyShareMaxNumMetadataEncryptionKeySaltGenerationRetries) { + private_certificate.next_salts_for_testing().push(GetNearbyShareTestSalt()); + } + EXPECT_FALSE(private_certificate.EncryptMetadataKey()); +} + +TEST(NearbySharePrivateCertificateTest, PublicCertificateConversion) { + NearbySharePrivateCertificate private_certificate = + GetNearbyShareTestPrivateCertificate( + DeviceVisibility::DEVICE_VISIBILITY_SELECTED_CONTACTS); + private_certificate.offset_for_testing() = GetNearbyShareTestValidityOffset(); + std::optional public_certificate = + private_certificate.ToPublicCertificate(); + ASSERT_TRUE(public_certificate); + EXPECT_EQ(GetNearbyShareTestPublicCertificate( + DeviceVisibility::DEVICE_VISIBILITY_SELECTED_CONTACTS) + .SerializeAsString(), + public_certificate->SerializeAsString()); +} + +TEST(NearbySharePrivateCertificateTest, EncryptDecryptRoundtrip) { + NearbySharePrivateCertificate private_certificate = + GetNearbyShareTestPrivateCertificate( + DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS); + + std::optional + decrypted_public_certificate = + NearbyShareDecryptedPublicCertificate::DecryptPublicCertificate( + *private_certificate.ToPublicCertificate(), + *private_certificate.EncryptMetadataKey()); + ASSERT_TRUE(decrypted_public_certificate); + EXPECT_EQ( + private_certificate.unencrypted_metadata().SerializeAsString(), + decrypted_public_certificate->unencrypted_metadata().SerializeAsString()); +} + +TEST(NearbySharePrivateCertificateTest, SignVerifyRoundtrip) { + NearbySharePrivateCertificate private_certificate = + GetNearbyShareTestPrivateCertificate( + DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS); + std::optional> signature = + private_certificate.Sign(GetNearbyShareTestPayloadToSign()); + ASSERT_TRUE(signature); + + std::optional + decrypted_public_certificate = + NearbyShareDecryptedPublicCertificate::DecryptPublicCertificate( + *private_certificate.ToPublicCertificate(), + *private_certificate.EncryptMetadataKey()); + EXPECT_TRUE(decrypted_public_certificate->VerifySignature( + GetNearbyShareTestPayloadToSign(), *signature)); +} + +TEST(NearbySharePrivateCertificateTest, HashAuthenticationToken) { + NearbySharePrivateCertificate private_certificate = + GetNearbyShareTestPrivateCertificate( + DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS); + EXPECT_EQ(GetNearbyShareTestPayloadHashUsingSecretKey(), + private_certificate.HashAuthenticationToken( + GetNearbyShareTestPayloadToSign())); +} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/certificates/test_util.cc b/sharing/certificates/test_util.cc new file mode 100644 index 00000000..499a38be --- /dev/null +++ b/sharing/certificates/test_util.cc @@ -0,0 +1,343 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/certificates/test_util.h" + +#include +#include + +#include +#include +#include +#include + +#include "absl/time/time.h" +#include "absl/types/span.h" +#include "internal/base/bluetooth_address.h" +#include "sharing/certificates/common.h" +#include "sharing/certificates/constants.h" +#include "sharing/certificates/nearby_share_decrypted_public_certificate.h" +#include "sharing/certificates/nearby_share_encrypted_metadata_key.h" +#include "sharing/certificates/nearby_share_private_certificate.h" +#include "sharing/common/nearby_share_enums.h" +#include "sharing/proto/encrypted_metadata.pb.h" +#include "sharing/proto/rpc_resources.pb.h" +#include "sharing/proto/timestamp.pb.h" + +namespace nearby { +namespace sharing { + +namespace { + +using ::nearby::sharing::proto::DeviceVisibility; + +// Sample P-256 public and private keys from RFC 6979 A.2.5 in their respective +// ASN.1 formats: SubjectPublicKeyInfo (RFC 5280) and PKCS #8 PrivateKeyInfo +// (RFC 5208). +constexpr uint8_t kTestPublicKeyBytes[] = { + 0x30, 0x59, + // Begin AlgorithmIdentifier: ecPublicKey, prime256v1 + 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, + 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, + // End AlgorithmIdentifier + 0x03, 0x42, 0x00, 0x04, + // Public key bytes (Ux): + 0x60, 0xfe, 0xd4, 0xba, 0x25, 0x5a, 0x9d, 0x31, 0xc9, 0x61, 0xeb, 0x74, + 0xc6, 0x35, 0x6d, 0x68, 0xc0, 0x49, 0xb8, 0x92, 0x3b, 0x61, 0xfa, 0x6c, + 0xe6, 0x69, 0x62, 0x2e, 0x60, 0xf2, 0x9f, 0xb6, + // Public key bytes (Uy): + 0x79, 0x03, 0xfe, 0x10, 0x08, 0xb8, 0xbc, 0x99, 0xa4, 0x1a, 0xe9, 0xe9, + 0x56, 0x28, 0xbc, 0x64, 0xf2, 0xf1, 0xb2, 0x0c, 0x2d, 0x7e, 0x9f, 0x51, + 0x77, 0xa3, 0xc2, 0x94, 0xd4, 0x46, 0x22, 0x99}; +constexpr uint8_t kTestPrivateKeyBytes[] = { + 0x30, 0x81, 0x87, 0x02, 0x01, 0x00, + // Begin AlgorithmIdentifier: ecPublicKey, prime256v1 + 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, + 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, + // End AlgorithmIdentifier + 0x04, 0x6d, 0x30, 0x6b, 0x02, 0x01, 0x01, 0x04, 0x20, + // Begin private key bytes + 0xc9, 0xaf, 0xa9, 0xd8, 0x45, 0xba, 0x75, 0x16, 0x6b, 0x5c, 0x21, 0x57, + 0x67, 0xb1, 0xd6, 0x93, 0x4e, 0x50, 0xc3, 0xdb, 0x36, 0xe8, 0x9b, 0x12, + 0x7b, 0x8a, 0x62, 0x2b, 0x12, 0x0f, 0x67, 0x21, + // End private key bytes + 0xa1, 0x44, 0x03, 0x42, 0x00, 0x04, + // Public key: + 0x60, 0xfe, 0xd4, 0xba, 0x25, 0x5a, 0x9d, 0x31, 0xc9, 0x61, 0xeb, 0x74, + 0xc6, 0x35, 0x6d, 0x68, 0xc0, 0x49, 0xb8, 0x92, 0x3b, 0x61, 0xfa, 0x6c, + 0xe6, 0x69, 0x62, 0x2e, 0x60, 0xf2, 0x9f, 0xb6, 0x79, 0x03, 0xfe, 0x10, + 0x08, 0xb8, 0xbc, 0x99, 0xa4, 0x1a, 0xe9, 0xe9, 0x56, 0x28, 0xbc, 0x64, + 0xf2, 0xf1, 0xb2, 0x0c, 0x2d, 0x7e, 0x9f, 0x51, 0x77, 0xa3, 0xc2, 0x94, + 0xd4, 0x46, 0x22, 0x99}; + +constexpr uint8_t kTestSecretKey[] = { + 0x60, 0x3d, 0xeb, 0x10, 0x15, 0xca, 0x71, 0xbe, 0x2b, 0x73, 0xae, + 0xf0, 0x85, 0x7d, 0x77, 0x81, 0x1f, 0x35, 0x2c, 0x07, 0x3b, 0x61, + 0x08, 0xd7, 0x2d, 0x98, 0x10, 0xa3, 0x09, 0x14, 0xdf, 0xf4}; + +constexpr uint8_t kTestCertificateId[] = { + 0xb9, 0x3c, 0x72, 0xb7, 0x4b, 0xc8, 0x48, 0x7d, 0x29, 0x82, 0x70, + 0x05, 0xf8, 0x0d, 0x63, 0x59, 0x18, 0xf9, 0x1b, 0xc2, 0x2b, 0x14, + 0xd7, 0xed, 0x05, 0x71, 0x4d, 0x58, 0xf9, 0x67, 0x02, 0xdd}; + +constexpr uint8_t kTestMetadataEncryptionKey[] = {0x60, 0x1e, 0xc3, 0x13, 0x77, + 0x57, 0x89, 0xa5, 0xb7, 0xa7, + 0xf5, 0x04, 0xbb, 0xf3}; + +constexpr uint8_t kTestMetadataEncryptionKeyTag[] = { + 0x51, 0x9b, 0x16, 0xd8, 0x91, 0xb4, 0x0d, 0x81, 0x11, 0x21, 0xe3, + 0x70, 0x42, 0x80, 0x8f, 0x87, 0x23, 0x6a, 0x84, 0x9b, 0xcd, 0xac, + 0xbc, 0xe3, 0x54, 0xd7, 0xff, 0x53, 0xdf, 0x5d, 0x8a, 0xda}; + +constexpr uint8_t kTestSalt[] = {0xf0, 0xf1}; + +constexpr uint8_t kTestEncryptedMetadataKey[] = {0x52, 0x0e, 0x7e, 0x6b, 0x8e, + 0xb5, 0x40, 0xe8, 0xe2, 0xbd, + 0xa0, 0xee, 0x9d, 0x7b}; + +constexpr uint8_t kTestEncryptedMetadata[] = { + 0x4d, 0x59, 0x5d, 0xb6, 0xac, 0x70, 0x00, 0x8f, 0x32, 0x9d, 0x0d, 0xcf, + 0xc3, 0x8b, 0x01, 0x19, 0x1d, 0xad, 0x2e, 0xb4, 0x62, 0xec, 0xf3, 0xa5, + 0xe4, 0x89, 0x51, 0x37, 0x0d, 0x78, 0xad, 0x9d, 0x2e, 0xe5, 0x99, 0xd5, + 0xf7, 0x1d, 0x71, 0x47, 0xef, 0x33, 0xae, 0x4b, 0xe2, 0xda, 0x57, 0xfb, + 0x3c, 0xa9, 0x1b, 0xbb, 0x00, 0x67, 0x99, 0xf3, 0xa4, 0x03, 0xab, 0x73, + 0xe5, 0x1a, 0xf6, 0x5c, 0x5f, 0x15, 0xa0, 0x00, 0xa5, 0x41, 0xf9}; + +// Plaintext "sample" (from RFC 6979 A.2.5). +constexpr uint8_t kTestPayloadToSign[] = {0x73, 0x61, 0x6d, 0x70, 0x6c, 0x65}; + +// One possible signature (from RFC 6979 A.2.5). +constexpr uint8_t kTestSampleSignature[] = { + 0x30, + 0x46, // length of remaining data + 0x02, + 0x21, // length of r + // begin r (note 0x00 padding since leading bit of 0xEF is 1) + 0x00, 0xEF, 0xD4, 0x8B, 0x2A, 0xAC, 0xB6, 0xA8, 0xFD, 0x11, 0x40, 0xDD, + 0x9C, 0xD4, 0x5E, 0x81, 0xD6, 0x9D, 0x2C, 0x87, 0x7B, 0x56, 0xAA, 0xF9, + 0x91, 0xC3, 0x4D, 0x0E, 0xA8, 0x4E, 0xAF, 0x37, 0x16, + // end r + 0x02, + 0x21, // length of s + // begin s (note 0x00 padding since leading bit of 0xF7 is 1) + 0x00, 0xF7, 0xCB, 0x1C, 0x94, 0x2D, 0x65, 0x7C, 0x41, 0xD4, 0x36, 0xC7, + 0xA1, 0xB6, 0xE2, 0x9F, 0x65, 0xF3, 0xE9, 0x00, 0xDB, 0xB9, 0xAF, 0xF4, + 0x06, 0x4D, 0xC4, 0xAB, 0x2F, 0x84, 0x3A, 0xCD, 0xA8 + // end s +}; + +// The result of HKDF of kTestPayloadToSign, using kTestSecretKey as salt. A +// trivial info parameter is used, and the output length is fixed to be +// kNearbyShareNumBytesAuthenticationTokenHash. +constexpr uint8_t kTestPayloadHashUsingSecretKey[] = {0xE2, 0xCB, 0x90, + 0x58, 0xDE, 0x3A}; + +constexpr int64_t kTestNotBeforeMillis = 1881702000000; + +constexpr int64_t kTestValidityOffsetMillis = 1800000; // 30 minutes + +} // namespace + +// Do not change. Values align with kTestEncryptedMetadata. +constexpr char kTestDeviceName[] = "device_name"; +constexpr char kTestMetadataFullName[] = "full_name"; +constexpr char kTestMetadataIconUrl[] = "icon_url"; +constexpr char kTestMetadataAccountName[] = "foo@bar.org"; +constexpr char kTestUnparsedBluetoothMacAddress[] = "4E:65:61:72:62:79"; + +std::unique_ptr GetNearbyShareTestP256KeyPair() { + return crypto::ECPrivateKey::CreateFromPrivateKeyInfo(kTestPrivateKeyBytes); +} + +const std::vector& GetNearbyShareTestP256PublicKey() { + static std::vector* public_key = new std::vector( + std::begin(kTestPublicKeyBytes), std::end(kTestPublicKeyBytes)); + return *public_key; +} + +std::unique_ptr GetNearbyShareTestSecretKey() { + return crypto::SymmetricKey::Import( + crypto::SymmetricKey::Algorithm::AES, + std::string(reinterpret_cast(kTestSecretKey), + kNearbyShareNumBytesSecretKey)); +} + +const std::vector& GetNearbyShareTestCertificateId() { + static std::vector* id = new std::vector( + std::begin(kTestCertificateId), std::end(kTestCertificateId)); + return *id; +} + +const std::vector& GetNearbyShareTestMetadataEncryptionKey() { + static const std::vector* metadata_encryption_key = + new std::vector(kTestMetadataEncryptionKey, + kTestMetadataEncryptionKey + + kNearbyShareNumBytesMetadataEncryptionKey); + return *metadata_encryption_key; +} + +const std::vector& GetNearbyShareTestMetadataEncryptionKeyTag() { + static const std::vector* tag = + new std::vector(std::begin(kTestMetadataEncryptionKeyTag), + std::end(kTestMetadataEncryptionKeyTag)); + return *tag; +} + +const std::vector& GetNearbyShareTestSalt() { + static const std::vector* salt = + new std::vector(std::begin(kTestSalt), std::end(kTestSalt)); + return *salt; +} + +const NearbyShareEncryptedMetadataKey& +GetNearbyShareTestEncryptedMetadataKey() { + static const NearbyShareEncryptedMetadataKey* encrypted_metadata_key = + new NearbyShareEncryptedMetadataKey( + GetNearbyShareTestSalt(), + std::vector(std::begin(kTestEncryptedMetadataKey), + std::end(kTestEncryptedMetadataKey))); + return *encrypted_metadata_key; +} + +absl::Time GetNearbyShareTestNotBefore() { + static const absl::Time not_before = FromJavaTime(kTestNotBeforeMillis); + return not_before; +} + +absl::Duration GetNearbyShareTestValidityOffset() { + static const absl::Duration offset = + absl::Milliseconds(kTestValidityOffsetMillis); + return offset; +} + +const nearby::sharing::proto::EncryptedMetadata& GetNearbyShareTestMetadata() { + static const nearby::sharing::proto::EncryptedMetadata* metadata = + new nearby::sharing::proto::EncryptedMetadata([] { + std::array bytes; + nearby::device::ParseBluetoothAddress(kTestUnparsedBluetoothMacAddress, + absl::MakeSpan(bytes.data(), 6)); + + nearby::sharing::proto::EncryptedMetadata metadata; + metadata.set_device_name(kTestDeviceName); + metadata.set_full_name(kTestMetadataFullName); + metadata.set_icon_url(kTestMetadataIconUrl); + metadata.set_account_name(kTestMetadataAccountName); + + metadata.set_bluetooth_mac_address(bytes.data(), 6u); + return metadata; + }()); + return *metadata; +} + +const std::vector& GetNearbyShareTestEncryptedMetadata() { + static const std::vector* bytes = new std::vector( + std::begin(kTestEncryptedMetadata), std::end(kTestEncryptedMetadata)); + return *bytes; +} + +const std::vector& GetNearbyShareTestPayloadToSign() { + static const std::vector* payload = new std::vector( + std::begin(kTestPayloadToSign), std::end(kTestPayloadToSign)); + return *payload; +} + +const std::vector& GetNearbyShareTestSampleSignature() { + static const std::vector* signature = new std::vector( + std::begin(kTestSampleSignature), std::end(kTestSampleSignature)); + return *signature; +} + +const std::vector& GetNearbyShareTestPayloadHashUsingSecretKey() { + static const std::vector* hash = + new std::vector(std::begin(kTestPayloadHashUsingSecretKey), + std::end(kTestPayloadHashUsingSecretKey)); + return *hash; +} + +NearbySharePrivateCertificate GetNearbyShareTestPrivateCertificate( + DeviceVisibility visibility, absl::Time not_before) { + NearbySharePrivateCertificate cert( + visibility, not_before, + not_before + kNearbyShareCertificateValidityPeriod, + GetNearbyShareTestP256KeyPair(), GetNearbyShareTestSecretKey(), + GetNearbyShareTestMetadataEncryptionKey(), + GetNearbyShareTestCertificateId(), GetNearbyShareTestMetadata(), + /*consumed_salts=*/std::set>()); + cert.next_salts_for_testing().push(GetNearbyShareTestSalt()); + return cert; +} + +nearby::sharing::proto::PublicCertificate GetNearbyShareTestPublicCertificate( + DeviceVisibility visibility, absl::Time not_before) { + nearby::sharing::proto::PublicCertificate cert; + cert.set_secret_id(std::string(GetNearbyShareTestCertificateId().begin(), + GetNearbyShareTestCertificateId().end())); + cert.set_secret_key(GetNearbyShareTestSecretKey()->key()); + cert.set_public_key(std::string(GetNearbyShareTestP256PublicKey().begin(), + GetNearbyShareTestP256PublicKey().end())); + cert.mutable_start_time()->set_seconds( + ToJavaTime(not_before - GetNearbyShareTestValidityOffset()) / 1000); + cert.mutable_end_time()->set_seconds( + ToJavaTime(not_before + kNearbyShareCertificateValidityPeriod + + GetNearbyShareTestValidityOffset()) / + 1000); + cert.set_for_selected_contacts( + visibility == DeviceVisibility::DEVICE_VISIBILITY_SELECTED_CONTACTS); + cert.set_metadata_encryption_key( + std::string(GetNearbyShareTestMetadataEncryptionKey().begin(), + GetNearbyShareTestMetadataEncryptionKey().end())); + cert.set_encrypted_metadata_bytes( + std::string(GetNearbyShareTestEncryptedMetadata().begin(), + GetNearbyShareTestEncryptedMetadata().end())); + cert.set_metadata_encryption_key_tag( + std::string(GetNearbyShareTestMetadataEncryptionKeyTag().begin(), + GetNearbyShareTestMetadataEncryptionKeyTag().end())); + return cert; +} + +std::vector +GetNearbyShareTestPrivateCertificateList(DeviceVisibility visibility) { + std::vector list; + for (size_t i = 0; i < kNearbyShareNumPrivateCertificates; ++i) { + list.push_back(GetNearbyShareTestPrivateCertificate( + visibility, GetNearbyShareTestNotBefore() + + i * kNearbyShareCertificateValidityPeriod)); + } + return list; +} + +std::vector +GetNearbyShareTestPublicCertificateList(DeviceVisibility visibility) { + std::vector list; + for (size_t i = 0; i < kNearbyShareNumPrivateCertificates; ++i) { + list.push_back(GetNearbyShareTestPublicCertificate( + visibility, GetNearbyShareTestNotBefore() + + i * kNearbyShareCertificateValidityPeriod)); + } + return list; +} + +const NearbyShareDecryptedPublicCertificate& +GetNearbyShareTestDecryptedPublicCertificate() { + static const NearbyShareDecryptedPublicCertificate* cert = + new NearbyShareDecryptedPublicCertificate( + *NearbyShareDecryptedPublicCertificate::DecryptPublicCertificate( + GetNearbyShareTestPublicCertificate( + DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS), + GetNearbyShareTestEncryptedMetadataKey())); + return *cert; +} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/certificates/test_util.h b/sharing/certificates/test_util.h new file mode 100644 index 00000000..bbb230eb --- /dev/null +++ b/sharing/certificates/test_util.h @@ -0,0 +1,84 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef LOCATION_NEARBY_CPP_SHARING_IMPLEMENTATION_CERTIFICATES_TEST_UTIL_H_ +#define LOCATION_NEARBY_CPP_SHARING_IMPLEMENTATION_CERTIFICATES_TEST_UTIL_H_ + +#include + +#include +#include + +#include "absl/time/time.h" +#include "internal/crypto_cros/ec_private_key.h" +#include "internal/crypto_cros/symmetric_key.h" +#include "sharing/certificates/nearby_share_decrypted_public_certificate.h" +#include "sharing/certificates/nearby_share_encrypted_metadata_key.h" +#include "sharing/certificates/nearby_share_private_certificate.h" +#include "sharing/common/nearby_share_enums.h" +#include "sharing/proto/encrypted_metadata.pb.h" +#include "sharing/proto/rpc_resources.pb.h" + +namespace nearby { +namespace sharing { + +extern const char kTestMetadataFullName[]; +extern const char kTestMetadataIconUrl[]; +extern const char kTestMetadataAccountName[]; + +// Test Bluetooth MAC address in the format "XX:XX:XX:XX:XX:XX". +extern const char kTestUnparsedBluetoothMacAddress[]; + +std::unique_ptr GetNearbyShareTestP256KeyPair(); +const std::vector& GetNearbyShareTestP256PublicKey(); + +std::unique_ptr GetNearbyShareTestSecretKey(); +const std::vector& GetNearbyShareTestCertificateId(); + +const std::vector& GetNearbyShareTestMetadataEncryptionKey(); +const std::vector& GetNearbyShareTestMetadataEncryptionKeyTag(); +const std::vector& GetNearbyShareTestSalt(); +const NearbyShareEncryptedMetadataKey& GetNearbyShareTestEncryptedMetadataKey(); + +absl::Time GetNearbyShareTestNotBefore(); +absl::Duration GetNearbyShareTestValidityOffset(); + +const nearby::sharing::proto::EncryptedMetadata& GetNearbyShareTestMetadata(); +const std::vector& GetNearbyShareTestEncryptedMetadata(); + +const std::vector& GetNearbyShareTestPayloadToSign(); +const std::vector& GetNearbyShareTestSampleSignature(); +const std::vector& GetNearbyShareTestPayloadHashUsingSecretKey(); + +NearbySharePrivateCertificate GetNearbyShareTestPrivateCertificate( + proto::DeviceVisibility visibility, + absl::Time not_before = GetNearbyShareTestNotBefore()); +nearby::sharing::proto::PublicCertificate GetNearbyShareTestPublicCertificate( + proto::DeviceVisibility visibility, + absl::Time not_before = GetNearbyShareTestNotBefore()); + +// Returns a list of |kNearbyShareNumPrivateCertificates| private/public +// certificates, spanning contiguous validity periods. +std::vector +GetNearbyShareTestPrivateCertificateList(proto::DeviceVisibility visibility); +std::vector +GetNearbyShareTestPublicCertificateList(proto::DeviceVisibility visibility); + +const NearbyShareDecryptedPublicCertificate& +GetNearbyShareTestDecryptedPublicCertificate(); + +} // namespace sharing +} // namespace nearby + +#endif // LOCATION_NEARBY_CPP_SHARING_IMPLEMENTATION_CERTIFICATES_TEST_UTIL_H_ diff --git a/sharing/contacts/BUILD b/sharing/contacts/BUILD new file mode 100644 index 00000000..11cb050d --- /dev/null +++ b/sharing/contacts/BUILD @@ -0,0 +1,87 @@ +licenses(["notice"]) + +cc_library( + name = "contacts", + srcs = [ + "nearby_share_contact_manager.cc", + "nearby_share_contact_manager_impl.cc", + "nearby_share_contacts_sorter.cc", + ], + hdrs = [ + "nearby_share_contact_manager.h", + "nearby_share_contact_manager_impl.h", + "nearby_share_contacts_sorter.h", + ], + visibility = ["//visibility:public"], + deps = [ + "//internal/base", + "//internal/crypto_cros", + "//internal/platform:types", + "//internal/platform/implementation:types", + "//sharing/common", + "//sharing/internal/api:platform", + "//sharing/internal/base", + "//sharing/internal/public:logging", + "//sharing/internal/public:types", + "//sharing/local_device_data", + "//sharing/proto:share_cc_proto", + "//sharing/scheduling", + "@com_google_absl//absl/functional:any_invocable", + "@com_google_absl//absl/functional:bind_front", + "@com_google_absl//absl/memory", + "@com_google_absl//absl/status:statusor", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/synchronization", + "@com_google_absl//absl/time", + "@com_google_protobuf//:protobuf_lite", + ], +) + +cc_library( + name = "test_support", + testonly = True, + srcs = [ + "fake_nearby_share_contact_manager.cc", + ], + hdrs = [ + "fake_nearby_share_contact_manager.h", + ], + visibility = ["//visibility:public"], + deps = [ + ":contacts", + "//internal/platform/implementation:types", + "//sharing/internal/api:platform", + "//sharing/internal/public:types", + "//sharing/local_device_data", + ], +) + +cc_test( + name = "contacts_test", + srcs = [ + "nearby_share_contact_manager_impl_test.cc", + "nearby_share_contacts_sorter_test.cc", + ], + deps = [ + ":contacts", + "//internal/platform/implementation:types", + "//internal/platform/implementation/g3", # fixdeps: keep + "//internal/test", + "//sharing/common", + "//sharing/internal/api:mock_sharing_platform", + "//sharing/internal/api:platform", + "//sharing/internal/test:nearby_test", + "//sharing/local_device_data:test_support", + "//sharing/proto:share_cc_proto", + "//sharing/scheduling", + "//sharing/scheduling:test_support", + "@com_github_protobuf_matchers//protobuf-matchers", + "@com_google_absl//absl/container:flat_hash_map", + "@com_google_absl//absl/container:flat_hash_set", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:statusor", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/time", + "@com_google_googletest//:gtest_main", + ], +) diff --git a/sharing/contacts/fake_nearby_share_contact_manager.cc b/sharing/contacts/fake_nearby_share_contact_manager.cc new file mode 100644 index 00000000..f1c61afc --- /dev/null +++ b/sharing/contacts/fake_nearby_share_contact_manager.cc @@ -0,0 +1,68 @@ +// Copyright 2021-2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/contacts/fake_nearby_share_contact_manager.h" + +#include +#include +#include +#include + +#include "internal/platform/implementation/account_manager.h" +#include "sharing/contacts/nearby_share_contact_manager.h" +#include "sharing/internal/api/sharing_rpc_client.h" +#include "sharing/internal/public/context.h" +#include "sharing/local_device_data/nearby_share_local_device_data_manager.h" + +namespace nearby { +namespace sharing { + +FakeNearbyShareContactManager::Factory::Factory() = default; + +FakeNearbyShareContactManager::Factory::~Factory() = default; + +std::unique_ptr +FakeNearbyShareContactManager::Factory::CreateInstance( + Context* context, AccountManager& account_manager, + nearby::sharing::api::SharingRpcClientFactory* nearby_client_factory, + NearbyShareLocalDeviceDataManager* local_device_data_manager) { + latest_nearby_client_factory_ = nearby_client_factory; + latest_local_device_data_manager_ = local_device_data_manager; + latest_account_manager_ = &account_manager; + + auto instance = std::make_unique(); + instances_.push_back(instance.get()); + + return instance; +} + +FakeNearbyShareContactManager::FakeNearbyShareContactManager() = default; + +FakeNearbyShareContactManager::~FakeNearbyShareContactManager() = default; + +void FakeNearbyShareContactManager::DownloadContacts() { + ++num_download_contacts_calls_; +} + +void FakeNearbyShareContactManager::SetAllowedContacts( + const std::set& allowed_contact_ids) { + set_allowed_contacts_calls_.push_back(allowed_contact_ids); +} + +void FakeNearbyShareContactManager::OnStart() {} + +void FakeNearbyShareContactManager::OnStop() {} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/contacts/fake_nearby_share_contact_manager.h b/sharing/contacts/fake_nearby_share_contact_manager.h new file mode 100644 index 00000000..77909fde --- /dev/null +++ b/sharing/contacts/fake_nearby_share_contact_manager.h @@ -0,0 +1,116 @@ +// Copyright 2021-2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_CONTACTS_FAKE_NEARBY_SHARE_CONTACT_MANAGER_H_ +#define THIRD_PARTY_NEARBY_SHARING_CONTACTS_FAKE_NEARBY_SHARE_CONTACT_MANAGER_H_ + +#include + +#include +#include +#include +#include + +#include "internal/platform/implementation/account_manager.h" +#include "sharing/contacts/nearby_share_contact_manager.h" +#include "sharing/contacts/nearby_share_contact_manager_impl.h" +#include "sharing/internal/api/sharing_rpc_client.h" +#include "sharing/internal/public/context.h" +#include "sharing/local_device_data/nearby_share_local_device_data_manager.h" + +namespace nearby { +namespace sharing { + +// A fake implementation of NearbyShareContactManager, along with a fake +// factory, to be used in tests. Stores parameters input into +// NearbyShareContactManager method calls. Use the notification methods from the +// base class--NotifyContactsDownloaded() and NotifyContactsUploaded()--to alert +// observers of changes; these methods are made public in this fake class. +class FakeNearbyShareContactManager : public NearbyShareContactManager { + public: + // Factory that creates FakeNearbyShareContactManager instances. Use in + // NearbyShareContactManagerImpl::Factor::SetFactoryForTesting() in unit + // tests. + class Factory : public NearbyShareContactManagerImpl::Factory { + public: + Factory(); + ~Factory() override; + + // Returns all FakeNearbyShareContactManager instances created by + // CreateInstance(). + std::vector& instances() { + return instances_; + } + + nearby::sharing::api::SharingRpcClientFactory* latest_http_client_factory() + const { + return latest_nearby_client_factory_; + } + + NearbyShareLocalDeviceDataManager* latest_local_device_data_manager() + const { + return latest_local_device_data_manager_; + } + + AccountManager* latest_account_manager() const { + return latest_account_manager_; + } + + private: + // NearbyShareContactManagerImpl::Factory: + std::unique_ptr CreateInstance( + Context* context, AccountManager& account_manager, + nearby::sharing::api::SharingRpcClientFactory* nearby_client_factory, + NearbyShareLocalDeviceDataManager* local_device_data_manager) override; + + std::vector instances_; + nearby::sharing::api::SharingRpcClientFactory* + latest_nearby_client_factory_ = nullptr; + NearbyShareLocalDeviceDataManager* latest_local_device_data_manager_ = + nullptr; + AccountManager* latest_account_manager_ = nullptr; + }; + + FakeNearbyShareContactManager(); + ~FakeNearbyShareContactManager() override; + + size_t num_download_contacts_calls() const { + return num_download_contacts_calls_; + } + + // Returns inputs of all SetAllowedContacts() calls. + const std::vector>& set_allowed_contacts_calls() const { + return set_allowed_contacts_calls_; + } + + // Make protected methods from base class public in this fake class. + using NearbyShareContactManager::NotifyContactsDownloaded; + using NearbyShareContactManager::NotifyContactsUploaded; + + private: + // NearbyShareContactsManager: + void DownloadContacts() override; + void SetAllowedContacts( + const std::set& allowed_contact_ids) override; + void OnStart() override; + void OnStop() override; + + size_t num_download_contacts_calls_ = 0; + std::vector> set_allowed_contacts_calls_; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_CONTACTS_FAKE_NEARBY_SHARE_CONTACT_MANAGER_H_ diff --git a/sharing/contacts/nearby_share_contact_manager.cc b/sharing/contacts/nearby_share_contact_manager.cc new file mode 100644 index 00000000..75361dfc --- /dev/null +++ b/sharing/contacts/nearby_share_contact_manager.cc @@ -0,0 +1,72 @@ +// Copyright 2021-2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/contacts/nearby_share_contact_manager.h" + +#include + +#include +#include +#include + +#include "sharing/proto/rpc_resources.pb.h" + +namespace nearby { +namespace sharing { + +NearbyShareContactManager::NearbyShareContactManager() = default; + +NearbyShareContactManager::~NearbyShareContactManager() = default; + +void NearbyShareContactManager::AddObserver(Observer* observer) { + observers_.AddObserver(observer); +} + +void NearbyShareContactManager::RemoveObserver(Observer* observer) { + observers_.RemoveObserver(observer); +} + +void NearbyShareContactManager::Start() { + if (is_running_) return; + + is_running_ = true; + OnStart(); +} + +void NearbyShareContactManager::Stop() { + if (!is_running_) return; + + is_running_ = false; + OnStop(); +} + +void NearbyShareContactManager::NotifyContactsDownloaded( + const std::set& allowed_contact_ids, + const std::vector& contacts, + uint32_t num_unreachable_contacts_filtered_out) { + for (Observer* observer : observers_.GetObservers()) { + observer->OnContactsDownloaded(allowed_contact_ids, contacts, + num_unreachable_contacts_filtered_out); + } +} + +void NearbyShareContactManager::NotifyContactsUploaded( + bool did_contacts_change_since_last_upload) { + for (Observer* observer : observers_.GetObservers()) { + observer->OnContactsUploaded(did_contacts_change_since_last_upload); + } +} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/contacts/nearby_share_contact_manager.h b/sharing/contacts/nearby_share_contact_manager.h new file mode 100644 index 00000000..afea7de3 --- /dev/null +++ b/sharing/contacts/nearby_share_contact_manager.h @@ -0,0 +1,106 @@ +// Copyright 2021-2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_CONTACTS_NEARBY_SHARE_CONTACT_MANAGER_H_ +#define THIRD_PARTY_NEARBY_SHARING_CONTACTS_NEARBY_SHARE_CONTACT_MANAGER_H_ + +#include + +#include +#include +#include + +#include "internal/base/observer_list.h" +#include "sharing/proto/rpc_resources.pb.h" + +namespace nearby { +namespace sharing { + +// The Nearby Share contacts manager interfaces with the Nearby server in the +// following ways: +// 1) The user's contacts are downloaded from People API, using the Nearby +// server as a proxy. +// 2) All the user's contacts are uploaded to Nearby server, along with an +// indication of what contacts are allowed for selected-contacts visibility +// mode. The Nearby server will distribute all-contacts and selected-contacts +// visibility certificates accordingly. For privacy reasons, the Nearby server +// needs to explicitly receive the list of contacts from the device instead of +// pulling them directly from People API. +// +// All contact data and update notifications are conveyed via observer methods; +// the manager does not return data directly from function calls. +class NearbyShareContactManager { + public: + class Observer { + public: + virtual ~Observer() = default; + + virtual void OnContactsDownloaded( + const std::set& allowed_contact_ids, + const std::vector& contacts, + uint32_t num_unreachable_contacts_filtered_out) = 0; + virtual void OnContactsUploaded( + bool did_contacts_change_since_last_upload) = 0; + }; + + NearbyShareContactManager(); + virtual ~NearbyShareContactManager(); + + void AddObserver(Observer* observer); + void RemoveObserver(Observer* observer); + + // Starts/Stops contact task scheduling. + void Start(); + void Stop(); + bool is_running() { return is_running_; } + + // nearby_share::mojom::ContactManager: + // Downloads the user's contact list from the server. The locally persisted + // list of allowed contacts is reconciled with the newly downloaded contacts. + // If the user's contact list or the allowlist has changed since the last + // successful contacts upload to the Nearby Share server, via the UpdateDevice + // RPC, an upload is requested. Contact downloads (and uploads if necessary) + // are also scheduled periodically. The results are sent to observers via + // OnContactsDownloaded(), and if an upload occurs, observers are notified via + // OnContactsUploaded(). + virtual void DownloadContacts() = 0; + + // Assigns the set of contacts that the local device allows sharing with when + // in selected-contacts visibility mode. (Note: This set is irrelevant for + // all-contact visibility mode.) The allowed contact list determines what + // contacts receive the local device's "selected-contacts" visibility public + // certificates. Changes to the allowlist will trigger RPC calls to upload the + // new allowlist to the Nearby Share server. + virtual void SetAllowedContacts( + const std::set& allowed_contact_ids) = 0; + + protected: + virtual void OnStart() = 0; + virtual void OnStop() = 0; + + void NotifyContactsDownloaded( + const std::set& allowed_contact_ids, + const std::vector& contacts, + uint32_t num_unreachable_contacts_filtered_out); + void NotifyContactsUploaded(bool did_contacts_change_since_last_upload); + + private: + bool is_running_ = false; + ObserverList observers_; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_CONTACTS_NEARBY_SHARE_CONTACT_MANAGER_H_ diff --git a/sharing/contacts/nearby_share_contact_manager_impl.cc b/sharing/contacts/nearby_share_contact_manager_impl.cc new file mode 100644 index 00000000..c4f322c7 --- /dev/null +++ b/sharing/contacts/nearby_share_contact_manager_impl.cc @@ -0,0 +1,442 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/contacts/nearby_share_contact_manager_impl.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "absl/functional/bind_front.h" +#include "absl/memory/memory.h" +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" +#include "absl/synchronization/notification.h" +#include "absl/time/time.h" +#include "internal/crypto_cros/secure_hash.h" +#include "internal/platform/implementation/account_manager.h" +#include "sharing/common/nearby_share_prefs.h" +#include "sharing/contacts/nearby_share_contact_manager.h" +#include "sharing/contacts/nearby_share_contacts_sorter.h" +#include "sharing/internal/api/preference_manager.h" +#include "sharing/internal/api/sharing_rpc_client.h" +#include "sharing/internal/base/encode.h" +#include "sharing/internal/public/context.h" +#include "sharing/internal/public/logging.h" +#include "sharing/local_device_data/nearby_share_local_device_data_manager.h" +#include "sharing/local_device_data/nearby_share_local_device_data_manager_impl.h" +#include "sharing/proto/contact_rpc.pb.h" +#include "sharing/proto/rpc_resources.pb.h" +#include "sharing/scheduling/nearby_share_scheduler.h" +#include "sharing/scheduling/nearby_share_scheduler_factory.h" + +namespace nearby { +namespace sharing { +namespace { + +using ::nearby::sharing::api::PreferenceManager; +using ::nearby::sharing::proto::Contact; +using ::nearby::sharing::proto::ContactRecord; +using ::nearby::sharing::proto::ListContactPeopleRequest; +using ::nearby::sharing::proto::ListContactPeopleResponse; + +constexpr absl::Duration kContactUploadPeriod = absl::Hours(24); +constexpr absl::Duration kContactDownloadPeriod = absl::Hours(12); + +// Removes contact IDs from the allowlist if they are not in |contacts|. +std::set RemoveNonexistentContactsFromAllowlist( + const std::set& allowed_contact_ids, + const std::vector& contacts) { + std::set new_allowed_contact_ids; + for (const ContactRecord& contact : contacts) { + if (allowed_contact_ids.find(contact.id()) != allowed_contact_ids.end()) + new_allowed_contact_ids.insert(contact.id()); + } + return new_allowed_contact_ids; +} + +// Converts a list of ContactRecord protos, along with the allowlist, into a +// list of Contact protos. +std::vector ContactRecordsToContacts( + const std::set& allowed_contact_ids, + const std::vector& contact_records) { + std::vector contacts; + for (const ContactRecord& contact_record : contact_records) { + bool is_selected = allowed_contact_ids.find(contact_record.id()) != + allowed_contact_ids.end(); + for (const proto::Contact_Identifier& identifier : + contact_record.identifiers()) { + Contact contact; + *contact.mutable_identifier() = identifier; + contact.set_is_selected(is_selected); + contacts.push_back(contact); + } + } + return contacts; +} + +Contact CreateLocalContact(absl::string_view profile_user_name) { + Contact contact; + contact.mutable_identifier()->set_account_name( + std::string(profile_user_name)); + // Always consider your own account a selected contact. + contact.set_is_selected(true); + contact.set_is_self(true); + return contact; +} + +// Creates a hex-encoded hash of the contact data, implicitly including the +// allowlist, to be sent to the Nearby Share server. This hash is persisted and +// used to detect any changes to the user's contact list or allowlist since the +// last successful upload to the server. The hash is invariant under the +// ordering of |contacts|. +std::string ComputeHash(const std::vector& contacts) { + // To ensure that the hash is invariant under ordering of input |contacts|, + // add all serialized protos to an ordered set. Then, incrementally calculate + // the hash as we iterate through the set. + std::set serialized_contacts_set; + for (const Contact& contact : contacts) { + serialized_contacts_set.insert(contact.SerializeAsString()); + } + + std::unique_ptr hasher = + crypto::SecureHash::Create(crypto::SecureHash::Algorithm::SHA256); + for (const std::string& serialized_contact : serialized_contacts_set) { + hasher->Update(serialized_contact.data(), serialized_contact.size()); + } + std::vector hash(hasher->GetHashLength()); + hasher->Finish(hash.data(), hash.size()); + + return nearby::utils::HexEncode(hash); +} + +} // namespace + +// static +NearbyShareContactManagerImpl::Factory* + NearbyShareContactManagerImpl::Factory::test_factory_ = nullptr; + +// static +std::unique_ptr +NearbyShareContactManagerImpl::Factory::Create( + Context* context, PreferenceManager& preference_manager, + AccountManager& account_manager, + nearby::sharing::api::SharingRpcClientFactory* nearby_client_factory, + NearbyShareLocalDeviceDataManager* local_device_data_manager) { + if (test_factory_) { + return test_factory_->CreateInstance(context, account_manager, + nearby_client_factory, + local_device_data_manager); + } + + return absl::WrapUnique(new NearbyShareContactManagerImpl( + context, preference_manager, account_manager, nearby_client_factory, + local_device_data_manager)); +} + +// static +void NearbyShareContactManagerImpl::Factory::SetFactoryForTesting( + Factory* test_factory) { + test_factory_ = test_factory; +} + +NearbyShareContactManagerImpl::Factory::~Factory() = default; + +NearbyShareContactManagerImpl::NearbyShareContactManagerImpl( + Context* context, PreferenceManager& preference_manager, + AccountManager& account_manager, + nearby::sharing::api::SharingRpcClientFactory* nearby_client_factory, + NearbyShareLocalDeviceDataManager* local_device_data_manager) + : preference_manager_(preference_manager), + account_manager_(account_manager), + nearby_client_factory_(nearby_client_factory), + nearby_share_client_(nearby_client_factory_->CreateInstance()), + local_device_data_manager_(local_device_data_manager), + periodic_contact_upload_scheduler_( + NearbyShareSchedulerFactory::CreatePeriodicScheduler( + context, preference_manager_, kContactUploadPeriod, + /*retry_failures=*/false, + /*require_connectivity=*/true, + prefs::kNearbySharingSchedulerPeriodicContactUploadName, + [&] { OnPeriodicContactsUploadRequested(); })), + contact_download_and_upload_scheduler_( + NearbyShareSchedulerFactory::CreatePeriodicScheduler( + context, preference_manager_, kContactDownloadPeriod, + /*retry_failures=*/true, + /*require_connectivity=*/true, + prefs::kNearbySharingSchedulerContactDownloadAndUploadName, + [&] { DownloadContacts(); })), + executor_(context->CreateSequencedTaskRunner()) {} + +NearbyShareContactManagerImpl::~NearbyShareContactManagerImpl() = default; + +void NearbyShareContactManagerImpl::OnContactsDownloadCompleted( + std::vector contacts) { + NL_LOG(INFO) << __func__ << ": Completed to download contacts from backend"; + size_t initial_num_contacts = contacts.size(); + contacts.erase( + std::remove_if(contacts.begin(), contacts.end(), + [](const nearby::sharing::proto::ContactRecord& contact) { + return !contact.is_reachable(); + }), + contacts.end()); + uint32_t num_unreachable_contacts_filtered_out = + initial_num_contacts - contacts.size(); + NL_VLOG(1) << __func__ << ": Removed " + << num_unreachable_contacts_filtered_out + << " unreachable contacts."; + OnContactsDownloadSuccess(std::move(contacts), + num_unreachable_contacts_filtered_out); +} + +void NearbyShareContactManagerImpl::ContactDownloadContext::FetchNextPage() { + NL_LOG(INFO) << __func__ << ": Downloading page=" << page_number_++; + ListContactPeopleRequest request; + if (next_page_token_.has_value()) { + request.set_page_token(*next_page_token_); + } + nearby_share_client_->ListContactPeople( + request, + [this]( + const absl::StatusOr& response) mutable { + if (!response.ok()) { + NL_LOG(ERROR) << __func__ << ": Failed to download contacts."; + std::move(download_failure_callback_)(); + return; + } + + contacts_.insert(contacts_.end(), response->contact_records().begin(), + response->contact_records().end()); + + if (response->next_page_token().empty()) { + std::move(download_success_callback_)(std::move(contacts_)); + return; + } + // Continue with next page. + next_page_token_ = response->next_page_token(); + FetchNextPage(); + }); +} + +void NearbyShareContactManagerImpl::DownloadContacts() { + executor_->PostTask([this]() { + NL_LOG(INFO) << __func__ << ": Start to download contacts"; + if (!is_running()) { + NL_LOG(WARNING) << __func__ + << ": Ignore to download contacts due to manager is not " + "running."; + return; + } + + std::vector contacts; + if (!account_manager_.GetCurrentAccount().has_value()) { + NL_LOG(WARNING) + << __func__ + << ": Ignore to download certificates due to no login account."; + OnContactsDownloadSuccess(contacts, 0); + return; + } + + // Currently Contacts download is synchronous. It completes after + // FetchNextPage() returns. + auto context = std::make_unique( + nearby_share_client_.get(), + absl::bind_front( + &NearbyShareContactManagerImpl::OnContactsDownloadFailure, this), + absl::bind_front( + &NearbyShareContactManagerImpl::OnContactsDownloadCompleted, this)); + context->FetchNextPage(); + }); +} + +void NearbyShareContactManagerImpl::SetAllowedContacts( + const std::set& allowed_contact_ids) { + // If the allowlist changed, re-upload contacts to Nearby server. + if (SetAllowlist(allowed_contact_ids)) + contact_download_and_upload_scheduler_->MakeImmediateRequest(); +} + +void NearbyShareContactManagerImpl::OnStart() { + periodic_contact_upload_scheduler_->Start(); + contact_download_and_upload_scheduler_->Start(); +} + +void NearbyShareContactManagerImpl::OnStop() { + periodic_contact_upload_scheduler_->Stop(); + contact_download_and_upload_scheduler_->Stop(); +} + +std::set NearbyShareContactManagerImpl::GetAllowedContacts() + const { + std::set allowlist; + for (const std::string& id : preference_manager_.GetStringArray( + prefs::kNearbySharingAllowedContactsName, {})) { + allowlist.insert(id); + } + return allowlist; +} + +void NearbyShareContactManagerImpl::OnPeriodicContactsUploadRequested() { + NL_VLOG(1) << __func__ + << ": Periodic Nearby Share contacts upload requested. " + << "Upload will occur after next contacts download."; +} + +void NearbyShareContactManagerImpl::OnContactsDownloadSuccess( + std::vector contacts, + uint32_t num_unreachable_contacts_filtered_out) { + NL_LOG(INFO) << __func__ << ": Nearby Share download of " << contacts.size() + << " contacts succeeded."; + + // Remove contacts from the allowlist that are not in the contact list. + SetAllowlist( + RemoveNonexistentContactsFromAllowlist(GetAllowedContacts(), contacts)); + + // Notify observers that the contact list was downloaded. + std::set allowed_contact_ids = GetAllowedContacts(); + NotifyAllObserversContactsDownloaded(allowed_contact_ids, contacts, + num_unreachable_contacts_filtered_out); + + std::vector contacts_to_upload = + ContactRecordsToContacts(GetAllowedContacts(), contacts); + + // Enable cross-device self-share by adding your account to the list of + // contacts. It is also marked as a selected contact. + std::optional account = + account_manager_.GetCurrentAccount(); + + if (!account.has_value()) { + NL_LOG(WARNING) << __func__ + << ": Profile user name is not valid; could not " + << "add self to list of contacts to upload."; + } else { + contacts_to_upload.push_back(CreateLocalContact(account->email)); + } + + std::string last_contact_upload_hash = preference_manager_.GetString( + prefs::kNearbySharingContactUploadHashName, ""); + std::string contact_upload_hash = ComputeHash(contacts_to_upload); + bool did_contacts_change_since_last_upload = + contact_upload_hash != last_contact_upload_hash; + + if (did_contacts_change_since_last_upload) { + NL_VLOG(1) << __func__ << ": Contact list or allowlist changed since last " + << "successful upload to the Nearby Share server."; + } + + // Request a contacts upload if the contact list or allowlist has changed + // since the last successful upload. Also request an upload periodically. + if (did_contacts_change_since_last_upload || + periodic_contact_upload_scheduler_->IsWaitingForResult()) { + absl::Notification notification; + bool upload_success = false; + local_device_data_manager_->UploadContacts(std::move(contacts_to_upload), + [&](bool success) { + upload_success = success; + notification.Notify(); + }); + + notification.WaitForNotification(); + NL_LOG(INFO) << __func__ << ": Completed to upload contacts with result:" + << upload_success; + + OnContactsUploadFinished(did_contacts_change_since_last_upload, + contact_upload_hash, upload_success); + return; + } + + // No upload is needed. + contact_download_and_upload_scheduler_->HandleResult(/*success=*/true); +} + +void NearbyShareContactManagerImpl::OnContactsDownloadFailure() { + NL_LOG(WARNING) << __func__ << ": Nearby Share contacts download failed."; + + contact_download_and_upload_scheduler_->HandleResult(/*success=*/false); +} + +void NearbyShareContactManagerImpl::OnContactsUploadFinished( + bool did_contacts_change_since_last_upload, + absl::string_view contact_upload_hash, bool success) { + NL_LOG(INFO) << __func__ << ": Upload of contacts to Nearby Share server " + << (success ? "succeeded." : "failed.") + << " Contact upload hash: " << contact_upload_hash; + if (success) { + // Only resolve the periodic upload request on success; let the + // download-and-upload scheduler handle any failure retries. The periodic + // upload scheduler will remember that it has an outstanding request even + // after reboot. + if (periodic_contact_upload_scheduler_->IsWaitingForResult()) { + periodic_contact_upload_scheduler_->HandleResult(success); + } + + std::string last_contact_upload_hash = preference_manager_.GetString( + prefs::kNearbySharingContactUploadHashName, ""); + + preference_manager_.SetString(prefs::kNearbySharingContactUploadHashName, + contact_upload_hash); + + if (last_contact_upload_hash.empty()) { + // If no contacts are uploaded before, set the flag to false in order to + // prevent the certificate manager from regenerating certificates. + NL_LOG(WARNING) << __func__ + << ": Mark contacts change flag to false due to no " + "contacts upload before."; + did_contacts_change_since_last_upload = false; + } + NotifyContactsUploaded(did_contacts_change_since_last_upload); + } + + contact_download_and_upload_scheduler_->HandleResult(success); +} + +bool NearbyShareContactManagerImpl::SetAllowlist( + const std::set& new_allowlist) { + if (new_allowlist == GetAllowedContacts()) return false; + + std::vector allowlist_value; + allowlist_value.reserve(new_allowlist.size()); + for (const std::string& id : new_allowlist) { + allowlist_value.push_back(id); + } + + preference_manager_.SetStringArray(prefs::kNearbySharingAllowedContactsName, + allowlist_value); + return true; +} + +void NearbyShareContactManagerImpl::NotifyAllObserversContactsDownloaded( + const std::set& allowed_contact_ids, + const std::vector& contacts, + uint32_t num_unreachable_contacts_filtered_out) { + // Sort the contacts before sending the list to observers. + std::vector sorted_contacts = contacts; + SortNearbyShareContactRecords(&sorted_contacts); + + // First, notify NearbyShareContactManager::Observers. + // Note: These are direct observers of the NearbyShareContactManager base + // class, distinct from the mojo remote observers that we notify below. + NotifyContactsDownloaded(allowed_contact_ids, sorted_contacts, + num_unreachable_contacts_filtered_out); +} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/contacts/nearby_share_contact_manager_impl.h b/sharing/contacts/nearby_share_contact_manager_impl.h new file mode 100644 index 00000000..d2ad2097 --- /dev/null +++ b/sharing/contacts/nearby_share_contact_manager_impl.h @@ -0,0 +1,166 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_CONTACTS_NEARBY_SHARE_CONTACT_MANAGER_IMPL_H_ +#define THIRD_PARTY_NEARBY_SHARING_CONTACTS_NEARBY_SHARE_CONTACT_MANAGER_IMPL_H_ + +#include + +#include +#include +#include +#include +#include +#include + +#include "absl/functional/any_invocable.h" +#include "absl/strings/string_view.h" +#include "internal/platform/implementation/account_manager.h" +#include "internal/platform/task_runner.h" +#include "sharing/contacts/nearby_share_contact_manager.h" +#include "sharing/internal/api/preference_manager.h" +#include "sharing/internal/api/sharing_rpc_client.h" +#include "sharing/internal/public/context.h" +#include "sharing/local_device_data/nearby_share_local_device_data_manager.h" +#include "sharing/proto/rpc_resources.pb.h" +#include "sharing/scheduling/nearby_share_scheduler.h" + +namespace nearby { +namespace sharing { + +// Implementation of NearbyShareContactManager that persists the set of allowed +// contact IDs--for selected-contacts visibility mode--in prefs. Other +// contact data is downloaded from People API, via the NearbyShare server, as +// needed. +// +// The Nearby Share server must be explicitly informed of all contacts this +// device is aware of--needed for all-contacts visibility mode--as well as what +// contacts are allowed for selected-contacts visibility mode. These uploaded +// contact lists are used by the server to distribute the device's public +// certificates accordingly. This implementation persists a hash of the last +// uploaded contact data, and after every contacts download, a subsequent upload +// request is made if we detect that the contact list or allowlist has changed +// since the last successful upload. We also schedule periodic contact uploads +// just in case the server removed the record. +// +// In addition to supporting on-demand contact downloads, this implementation +// periodically checks in with the Nearby Share server to see if the user's +// contact list has changed since the last upload. +class NearbyShareContactManagerImpl : public NearbyShareContactManager { + public: + class Factory { + public: + static std::unique_ptr Create( + Context* context, + nearby::sharing::api::PreferenceManager& preference_manager, + AccountManager& account_manager, + nearby::sharing::api::SharingRpcClientFactory* nearby_client_factory, + NearbyShareLocalDeviceDataManager* local_device_data_manager); + static void SetFactoryForTesting(Factory* test_factory); + + protected: + virtual ~Factory(); + virtual std::unique_ptr CreateInstance( + Context* context, AccountManager& account_manager, + nearby::sharing::api::SharingRpcClientFactory* nearby_client_factory, + NearbyShareLocalDeviceDataManager* local_device_data_manager) = 0; + + private: + static Factory* test_factory_; + }; + + ~NearbyShareContactManagerImpl() override; + + private: + // Class for maintaining a single instance of contacts download request. It + // is responsible for downloading all available pages and making the results + // or error available. + class ContactDownloadContext { + public: + ContactDownloadContext( + nearby::sharing::api::SharingRpcClient* nearby_share_client, + absl::AnyInvocable download_failure_callback, + absl::AnyInvocable< + void( + std::vector contacts) &&> + download_success_callback) + : nearby_share_client_(nearby_share_client), + download_failure_callback_(std::move(download_failure_callback)), + download_success_callback_(std::move(download_success_callback)) {} + + // Fetches the next page of contacts. + // If |next_page_token_| is empty, it fetches the first page. + // On successful download, if page token in the response is empty, the + // |download_success_callback_| is invoked with all downloaded contacts. + void FetchNextPage(); + + private: + nearby::sharing::api::SharingRpcClient* const nearby_share_client_; + std::optional next_page_token_; + int page_number_ = 1; + std::vector contacts_; + absl::AnyInvocable download_failure_callback_; + absl::AnyInvocable contacts) &&> + download_success_callback_; + }; + + NearbyShareContactManagerImpl( + Context* context, + nearby::sharing::api::PreferenceManager& preference_manager, + AccountManager& account_manager, + nearby::sharing::api::SharingRpcClientFactory* nearby_client_factory, + NearbyShareLocalDeviceDataManager* local_device_data_manager); + + // NearbyShareContactsManager: + void DownloadContacts() override; + void SetAllowedContacts( + const std::set& allowed_contact_ids) override; + void OnStart() override; + void OnStop() override; + + std::set GetAllowedContacts() const; + bool SetAllowlist(const std::set& new_allowlist); + void OnContactsDownloadCompleted( + std::vector contacts); + void OnContactsDownloadSuccess( + std::vector<::nearby::sharing::proto::ContactRecord> contacts, + uint32_t num_unreachable_contacts_filtered_out); + void OnContactsDownloadFailure(); + void OnPeriodicContactsUploadRequested(); + void OnContactsUploadFinished(bool did_contacts_change_since_last_upload, + absl::string_view contact_upload_hash, + bool success); + + // Notify the base-class and mojo observers that contacts were downloaded. + void NotifyAllObserversContactsDownloaded( + const std::set& allowed_contact_ids, + const std::vector& contacts, + uint32_t num_unreachable_contacts_filtered_out); + + nearby::sharing::api::PreferenceManager& preference_manager_; + AccountManager& account_manager_; + nearby::sharing::api::SharingRpcClientFactory* const nearby_client_factory_; + std::unique_ptr nearby_share_client_; + NearbyShareLocalDeviceDataManager* local_device_data_manager_ = nullptr; + std::unique_ptr periodic_contact_upload_scheduler_; + std::unique_ptr contact_download_and_upload_scheduler_; + + std::unique_ptr executor_ = nullptr; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_CONTACTS_NEARBY_SHARE_CONTACT_MANAGER_IMPL_H_ diff --git a/sharing/contacts/nearby_share_contact_manager_impl_test.cc b/sharing/contacts/nearby_share_contact_manager_impl_test.cc new file mode 100644 index 00000000..d8841ab5 --- /dev/null +++ b/sharing/contacts/nearby_share_contact_manager_impl_test.cc @@ -0,0 +1,672 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/contacts/nearby_share_contact_manager_impl.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "gtest/gtest.h" +#include "absl/container/flat_hash_map.h" +#include "absl/container/flat_hash_set.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/str_cat.h" +#include "absl/time/time.h" +#include "internal/platform/implementation/account_manager.h" +#include "internal/test/fake_account_manager.h" +#include "internal/test/fake_task_runner.h" +#include "sharing/common/nearby_share_prefs.h" +#include "sharing/contacts/nearby_share_contact_manager.h" +#include "sharing/contacts/nearby_share_contacts_sorter.h" +#include "sharing/internal/api/fake_nearby_share_client.h" +#include "sharing/internal/api/preference_manager.h" +#include "sharing/internal/test/fake_context.h" +#include "sharing/internal/test/fake_preference_manager.h" +#include "sharing/local_device_data/fake_nearby_share_local_device_data_manager.h" +#include "sharing/proto/contact_rpc.pb.h" +#include "sharing/proto/rpc_resources.pb.h" +#include "sharing/scheduling/fake_nearby_share_scheduler.h" +#include "sharing/scheduling/fake_nearby_share_scheduler_factory.h" +#include "sharing/scheduling/nearby_share_scheduler_factory.h" + +namespace nearby { +namespace sharing { +namespace { + +using ::nearby::sharing::api::PreferenceManager; +using ::nearby::sharing::proto::Contact; +using ::nearby::sharing::proto::ContactRecord; + +constexpr char kTestContactIdPrefix[] = "id_"; +constexpr char kTestContactEmailPrefix[] = "email_"; +constexpr char kTestContactPhonePrefix[] = "phone_"; +constexpr char kTestDefaultDeviceName[] = "Josh's Chromebook"; +constexpr char kTestProfileUserName[] = "test@google.com"; +constexpr char kTestAccountId[] = "test_account_id"; +constexpr char kTestDefaultContactsHash[] = "last_hash"; +const char* kTestPersonNames[] = {"BBB BBB", "CCC CCC", "AAA AAA"}; + +// From nearby_share_contact_manager_impl.cc. +constexpr absl::Duration kContactUploadPeriod = absl::Hours(24); +constexpr absl::Duration kContactDownloadPeriod = absl::Hours(12); + +std::string GetTestContactId(size_t index) { + return absl::StrCat(kTestContactIdPrefix, index); +} +std::string GetTestContactEmail(size_t index) { + return absl::StrCat(kTestContactEmailPrefix, index); +} +std::string GetTestContactPhone(size_t index) { + return absl::StrCat(kTestContactPhonePrefix, index); +} + +std::set TestContactIds(size_t num_contacts) { + std::set ids; + for (size_t i = 0; i < num_contacts; ++i) { + ids.insert(GetTestContactId(i)); + } + return ids; +} + +std::vector TestContactRecordList(size_t num_contacts) { + std::vector contact_list; + for (size_t i = 0; i < num_contacts; ++i) { + ContactRecord contact; + contact.set_id(GetTestContactId(i)); + contact.set_image_url("https://www.google.com/"); + contact.set_person_name(kTestPersonNames[i % 3]); + contact.set_is_reachable(true); + // only one of these fields should be set... + switch ((i % 3)) { + case 0: + contact.add_identifiers()->set_account_name(GetTestContactEmail(i)); + break; + case 1: + contact.add_identifiers()->set_phone_number(GetTestContactPhone(i)); + break; + case 2: + contact.add_identifiers()->set_obfuscated_gaia("4938tyah"); + break; + } + contact_list.push_back(contact); + } + return contact_list; +} + +// Converts a list of ContactRecord protos, along with the allowlist, into a +// list of Contact protos. To enable self-sharing across devices, we expect the +// local device to include itself in the contact list as an allowed contact. +// Partially from nearby_share_contact_manager_impl.cc. +std::vector BuildContactListToUpload( + const std::set& allowed_contact_ids, + const std::vector& contact_records) { + std::vector contacts; + for (const auto& contact_record : contact_records) { + bool is_selected = allowed_contact_ids.find(contact_record.id()) != + allowed_contact_ids.end(); + for (const auto& identifier : contact_record.identifiers()) { + Contact contact; + *contact.mutable_identifier() = identifier; + contact.set_is_selected(is_selected); + contacts.push_back(contact); + } + } + + // Add self to list of contacts. + Contact contact; + contact.mutable_identifier()->set_account_name(kTestProfileUserName); + contact.set_is_selected(true); + contacts.push_back(contact); + + return contacts; +} + +void VerifyDownloadNotificationContacts( + const std::set& expected_allowed_contact_ids, + const std::vector& expected_unordered_contacts, + const std::set& notification_allowed_contact_ids, + const std::vector& notification_contacts) { + EXPECT_EQ(notification_allowed_contact_ids, expected_allowed_contact_ids); + EXPECT_EQ(notification_contacts.size(), expected_unordered_contacts.size()); + + // Verify that observers receive contacts in sorted order. + std::vector expected_ordered_contacts = + expected_unordered_contacts; + SortNearbyShareContactRecords(&expected_ordered_contacts); + for (size_t i = 0; i < expected_ordered_contacts.size(); ++i) { + EXPECT_EQ(notification_contacts[i].SerializeAsString(), + expected_ordered_contacts[i].SerializeAsString()); + } +} + +class NearbyShareContactManagerImplTest + : public ::testing::Test, + public NearbyShareContactManager::Observer { + protected: + struct AllowlistChangedNotification { + bool were_contacts_added_to_allowlist; + bool were_contacts_removed_from_allowlist; + }; + struct ContactsDownloadedNotification { + std::set allowed_contact_ids; + std::vector contacts; + uint32_t num_unreachable_contacts_filtered_out; + }; + struct ContactsUploadedNotification { + bool did_contacts_change_since_last_upload; + }; + + NearbyShareContactManagerImplTest() + : local_device_data_manager_(kTestDefaultDeviceName) { + local_device_data_manager_.set_is_sync_mode(true); + } + + ~NearbyShareContactManagerImplTest() override = default; + + void SetUp() override { + prefs::RegisterNearbySharingPrefs(preference_manager_); + NearbyShareSchedulerFactory::SetFactoryForTesting(&scheduler_factory_); + AccountManager::Account account; + account.id = kTestAccountId; + account.email = kTestProfileUserName; + fake_account_manager_.SetAccount(account); + + manager_ = NearbyShareContactManagerImpl::Factory::Create( + &fake_context_, preference_manager_, + fake_account_manager_, &nearby_client_factory_, + &local_device_data_manager_); + + VerifySchedulerInitialization(); + manager_->AddObserver(this); + + preference_manager().SetString(prefs::kNearbySharingContactUploadHashName, + kTestDefaultContactsHash); + manager_->Start(); + } + + void TearDown() override { + manager_->RemoveObserver(this); + manager_.reset(); + NearbyShareSchedulerFactory::SetFactoryForTesting(nullptr); + preference_manager().SetString(prefs::kNearbySharingContactUploadHashName, + ""); + } + + void Sync() { + EXPECT_TRUE(FakeTaskRunner::WaitForRunningTasksWithTimeout( + absl::Milliseconds(1000))); + } + + void SetUploadResult(bool success) { + local_device_data_manager_.SetUploadContactsResult(success); + } + + void SetDownloadSuccessResult(const std::vector& contacts) { + proto::ListContactPeopleResponse response; + response.set_next_page_token(nullptr); + response.mutable_contact_records()->Add(contacts.begin(), contacts.end()); + std::vector> responses; + responses.push_back(response); + client()->SetListContactPeopleResponses(responses); + } + + void SetDownloadFailureResult() { + std::vector> responses; + responses.push_back(absl::InternalError("")); + client()->SetListContactPeopleResponses(responses); + } + + void DownloadContacts( + bool download_success, bool expect_upload, bool upload_success, + std::optional> allowed_contact_ids, + std::optional> contacts) { + // Track for download contacts. + size_t num_handled_results = + download_and_upload_scheduler()->handled_results().size(); + size_t num_download_notifications = + contacts_downloaded_notifications_.size(); + size_t num_upload_contacts_calls = + local_device_data_manager_.upload_contacts_calls().size(); + // Invoke upload callback from local device data manager. + size_t num_upload_notifications = contacts_uploaded_notifications_.size(); + size_t num_download_and_upload_handled_results = + download_and_upload_scheduler()->handled_results().size(); + size_t num_periodic_upload_handled_results = + periodic_upload_scheduler()->handled_results().size(); + + manager_->DownloadContacts(); + Sync(); + if (download_success) { + VerifyDownloadNotificationSent( + /*initial_num_notifications=*/num_download_notifications, + *allowed_contact_ids, *contacts); + + // Verify that contacts start uploading if needed. + EXPECT_EQ(local_device_data_manager_.upload_contacts_calls().size(), + num_upload_contacts_calls + (expect_upload ? 1 : 0)); + + // Verify that the success result is sent to the download/upload scheduler + // if a subsequent upload isn't required. + EXPECT_EQ(download_and_upload_scheduler()->handled_results().size(), + num_handled_results + 1); + if (!expect_upload) { + EXPECT_TRUE(download_and_upload_scheduler()->handled_results().back()); + return; + } + + // Check on upload. + FakeNearbyShareLocalDeviceDataManager::UploadContactsCall& call = + local_device_data_manager_.upload_contacts_calls().back(); + + std::vector expected_upload_contacts = + BuildContactListToUpload(*allowed_contact_ids, *contacts); + // Ordering doesn't matter. Otherwise, because of internal sorting, + // comparison would be difficult. + ASSERT_EQ(expected_upload_contacts.size(), call.contacts.size()); + absl::flat_hash_set expected_contacts_set; + absl::flat_hash_set call_contacts_set; + for (size_t i = 0; i < expected_upload_contacts.size(); ++i) { + expected_contacts_set.insert( + expected_upload_contacts[i].SerializeAsString()); + call_contacts_set.insert(call.contacts[i].SerializeAsString()); + } + + // Verify upload notification was sent on success. + EXPECT_EQ(contacts_uploaded_notifications_.size(), + num_upload_notifications + (upload_success ? 1 : 0)); + if (upload_success) { + // We only expect uploads to occur if contacts have changed since the + // last + // upload or if a periodic upload was requested. + EXPECT_TRUE(contacts_uploaded_notifications_.back() + .did_contacts_change_since_last_upload || + periodic_upload_scheduler()->IsWaitingForResult()); + + if (periodic_upload_scheduler()->IsWaitingForResult()) { + EXPECT_EQ(periodic_upload_scheduler()->handled_results().size(), + num_periodic_upload_handled_results + 1); + EXPECT_TRUE(periodic_upload_scheduler()->handled_results().back()); + periodic_upload_scheduler()->SetIsWaitingForResult(false); + } else { + EXPECT_EQ(periodic_upload_scheduler()->handled_results().size(), + num_periodic_upload_handled_results); + } + } + // Verify that the result is sent to download/upload scheduler. + EXPECT_EQ(download_and_upload_scheduler()->handled_results().size(), + num_download_and_upload_handled_results + 1); + EXPECT_EQ(download_and_upload_scheduler()->handled_results().back(), + upload_success); + } else { + EXPECT_EQ(download_and_upload_scheduler()->handled_results().size(), + num_handled_results + 1); + EXPECT_FALSE(download_and_upload_scheduler()->handled_results().back()); + } + } + + void MakePeriodicUploadRequest() { + periodic_upload_scheduler()->InvokeRequestCallback(); + periodic_upload_scheduler()->SetIsWaitingForResult(true); + Sync(); + } + + void SetAllowedContacts(const std::set& allowed_contact_ids, + bool expect_allowlist_changed) { + size_t num_download_and_upload_requests = + download_and_upload_scheduler()->num_immediate_requests(); + + manager_->SetAllowedContacts(allowed_contact_ids); + + // Verify that download/upload is requested if the allowlist changed. + EXPECT_EQ( + download_and_upload_scheduler()->num_immediate_requests(), + num_download_and_upload_requests + (expect_allowlist_changed ? 1 : 0)); + } + + PreferenceManager& preference_manager() { return preference_manager_; } + + private: + // NearbyShareContactManager::Observer: + void OnContactsDownloaded( + const std::set& allowed_contact_ids, + const std::vector& contacts, + uint32_t num_unreachable_contacts_filtered_out) override { + ContactsDownloadedNotification notification; + notification.allowed_contact_ids = allowed_contact_ids; + notification.contacts = contacts; + contacts_downloaded_notifications_.push_back(notification); + } + void OnContactsUploaded(bool did_contacts_change_since_last_upload) override { + ContactsUploadedNotification notification; + notification.did_contacts_change_since_last_upload = + did_contacts_change_since_last_upload; + contacts_uploaded_notifications_.push_back(notification); + } + + FakeNearbyShareClient* client() { + return nearby_client_factory_.instances().back(); + } + + FakeNearbyShareScheduler* periodic_upload_scheduler() { + return scheduler_factory_.pref_name_to_periodic_instance() + .at(prefs::kNearbySharingSchedulerPeriodicContactUploadName) + .fake_scheduler; + } + + FakeNearbyShareScheduler* download_and_upload_scheduler() { + return scheduler_factory_.pref_name_to_periodic_instance() + .at(prefs::kNearbySharingSchedulerContactDownloadAndUploadName) + .fake_scheduler; + } + + // Verify scheduler input parameters. + void VerifySchedulerInitialization() { + FakeNearbyShareSchedulerFactory::PeriodicInstance + download_and_upload_scheduler_instance = + scheduler_factory_.pref_name_to_periodic_instance().at( + prefs::kNearbySharingSchedulerContactDownloadAndUploadName); + EXPECT_TRUE(download_and_upload_scheduler_instance.fake_scheduler); + EXPECT_EQ(download_and_upload_scheduler_instance.request_period, + kContactDownloadPeriod); + EXPECT_TRUE(download_and_upload_scheduler_instance.retry_failures); + EXPECT_TRUE(download_and_upload_scheduler_instance.require_connectivity); + + FakeNearbyShareSchedulerFactory::PeriodicInstance + periodic_upload_scheduler_instance = + scheduler_factory_.pref_name_to_periodic_instance().at( + prefs::kNearbySharingSchedulerPeriodicContactUploadName); + EXPECT_TRUE(periodic_upload_scheduler_instance.fake_scheduler); + EXPECT_EQ(periodic_upload_scheduler_instance.request_period, + kContactUploadPeriod); + EXPECT_FALSE(periodic_upload_scheduler_instance.retry_failures); + EXPECT_TRUE(periodic_upload_scheduler_instance.require_connectivity); + } + + void TriggerDownloadScheduler() { + // Fire scheduler and verify downloader creation. + download_and_upload_scheduler()->InvokeRequestCallback(); + } + + void VerifyDownloadNotificationSent( + size_t initial_num_notifications, + const std::set& expected_allowed_contact_ids, + const std::vector& expected_unordered_contacts) { + EXPECT_EQ(contacts_downloaded_notifications_.size(), + initial_num_notifications + 1); + + // Verify notification sent to regular (not mojo) observers. + VerifyDownloadNotificationContacts( + expected_allowed_contact_ids, expected_unordered_contacts, + contacts_downloaded_notifications_.back().allowed_contact_ids, + contacts_downloaded_notifications_.back().contacts); + } + + nearby::FakePreferenceManager preference_manager_; + FakeAccountManager fake_account_manager_; + FakeContext fake_context_; + std::vector allowlist_changed_notifications_; + std::vector + contacts_downloaded_notifications_; + std::vector contacts_uploaded_notifications_; + FakeNearbyShareClientFactory nearby_client_factory_; + FakeNearbyShareLocalDeviceDataManager local_device_data_manager_; + std::unique_ptr account_manager_; + FakeNearbyShareSchedulerFactory scheduler_factory_; + std::unique_ptr manager_; +}; + +TEST_F(NearbyShareContactManagerImplTest, SetAllowlist) { + // Add initial allowed contacts. + SetAllowedContacts(TestContactIds(/*num_contacts=*/3u), + /*expect_allowlist_changed=*/true); + // Remove last allowed contact. + SetAllowedContacts(TestContactIds(/*num_contacts=*/2u), + /*expect_allowlist_changed=*/true); + // Add back last allowed contact. + SetAllowedContacts(TestContactIds(/*num_contacts=*/3u), + /*expect_allowlist_changed=*/true); + // Set list without any changes. + SetAllowedContacts(TestContactIds(/*num_contacts=*/3u), + /*expect_allowlist_changed=*/false); +} + +TEST_F(NearbyShareContactManagerImplTest, DownloadContacts_WithFirstUpload) { + std::vector contact_records = + TestContactRecordList(/*num_contacts=*/4u); + std::set allowlist = TestContactIds(/*num_contacts=*/2u); + SetAllowedContacts(allowlist, /*expect_allowlist_changed=*/true); + + SetDownloadSuccessResult(contact_records); + SetUploadResult(true); + // Because contacts have never been uploaded, a subsequent upload should be + // requested, which succeeds. + DownloadContacts(/*download_success=*/true, /*expect_upload=*/true, + /*upload_success=*/true, + /*allowed_contact_ids=*/allowlist, + /*contacts=*/contact_records); + + SetDownloadSuccessResult(contact_records); + SetUploadResult(true); + // When contacts are downloaded again, we detect that contacts have not + // changed, so no upload should be made + DownloadContacts(/*download_success=*/true, /*expect_upload=*/false, + /*upload_success=*/true, + /*allowed_contact_ids=*/allowlist, + /*contacts=*/contact_records); +} + +TEST_F(NearbyShareContactManagerImplTest, + DownloadContacts_DetectContactListChanged) { + std::vector contact_records = + TestContactRecordList(/*num_contacts=*/3u); + std::set allowlist = TestContactIds(/*num_contacts=*/2u); + SetAllowedContacts(allowlist, /*expect_allowlist_changed=*/true); + + SetDownloadSuccessResult(contact_records); + SetUploadResult(true); + // Because contacts have never been uploaded, a subsequent upload is + // requested, which succeeds. + DownloadContacts(/*download_success=*/true, /*expect_upload=*/true, + /*upload_success=*/true, + /*allowed_contact_ids=*/allowlist, + /*contacts=*/contact_records); + + // When contacts are downloaded again, we detect that contacts have changed + // since the last upload. + contact_records = TestContactRecordList(/*num_contacts=*/4u); + SetDownloadSuccessResult(contact_records); + SetUploadResult(true); + DownloadContacts(/*download_success=*/true, /*expect_upload=*/true, + /*upload_success=*/true, + /*allowed_contact_ids=*/allowlist, + /*contacts=*/contact_records); +} + +TEST_F(NearbyShareContactManagerImplTest, + DownloadContacts_DetectAllowlistChanged) { + std::vector contact_records = + TestContactRecordList(/*num_contacts=*/3u); + std::set allowlist = TestContactIds(/*num_contacts=*/2u); + SetAllowedContacts(allowlist, /*expect_allowlist_changed=*/true); + + SetDownloadSuccessResult(contact_records); + SetUploadResult(true); + + // Because contacts have never been uploaded, a subsequent upload is + // requested, which succeeds. + DownloadContacts(/*download_success=*/true, /*expect_upload=*/true, + /*upload_success=*/true, + /*allowed_contact_ids=*/allowlist, + /*contacts=*/contact_records); + + // When contacts are downloaded again, we detect that the allowlist has + // changed since the last upload. + allowlist = TestContactIds(/*num_contacts=*/1u); + SetAllowedContacts(allowlist, /*expect_allowlist_changed=*/true); + + SetDownloadSuccessResult(contact_records); + SetUploadResult(true); + DownloadContacts(/*download_success=*/true, /*expect_upload=*/true, + /*upload_success=*/true, + /*allowed_contact_ids=*/allowlist, + /*contacts=*/contact_records); +} + +TEST_F(NearbyShareContactManagerImplTest, + DownloadContacts_PeriodicUploadRequest) { + std::vector contact_records = + TestContactRecordList(/*num_contacts=*/3u); + std::set allowlist = TestContactIds(/*num_contacts=*/2u); + SetAllowedContacts(allowlist, /*expect_allowlist_changed=*/true); + + SetDownloadSuccessResult(contact_records); + SetUploadResult(true); + + // Because contacts have never been uploaded, a subsequent upload is + // requested, which succeeds. + DownloadContacts(/*download_success=*/true, /*expect_upload=*/true, + /*upload_success=*/true, + /*allowed_contact_ids=*/allowlist, + /*contacts=*/contact_records); + + // Because device records on the server will be removed after a few days if + // the device does not contact the server, we ensure that contacts are + // uploaded periodically. Make that request now. Contacts will be uploaded + // after the next contact download. It will not force a download now, + // however. + MakePeriodicUploadRequest(); + + SetDownloadSuccessResult(contact_records); + SetUploadResult(true); + // When contacts are downloaded again, we detect that contacts have not + // changed. However, we expect an upload because a periodic request was + // made. + DownloadContacts(/*download_success=*/true, /*expect_upload=*/true, + /*upload_success=*/true, + /*allowed_contact_ids=*/allowlist, + /*contacts=*/contact_records); +} + +TEST_F(NearbyShareContactManagerImplTest, DownloadContacts_FailDownload) { + SetDownloadFailureResult(); + DownloadContacts(/*download_success=*/false, /*expect_upload=*/false, + /*upload_success=*/false, + /*allowed_contact_ids=*/std::nullopt, + /*contacts=*/std::nullopt); +} + +TEST_F(NearbyShareContactManagerImplTest, DownloadContacts_RetryFailedUpload) { + std::vector contact_records = + TestContactRecordList(/*num_contacts=*/3u); + std::set allowlist = TestContactIds(/*num_contacts=*/2u); + SetAllowedContacts(allowlist, /*expect_allowlist_changed=*/true); + + SetDownloadSuccessResult(contact_records); + SetUploadResult(true); + + // Because contacts have never been uploaded, a subsequent upload is + // requested, which succeeds. + DownloadContacts(/*download_success=*/true, /*expect_upload=*/true, + /*upload_success=*/true, + /*allowed_contact_ids=*/allowlist, + /*contacts=*/contact_records); + + // When contacts are downloaded again, we detect that contacts have changed + // since the last upload. Fail this upload. + contact_records = TestContactRecordList(/*num_contacts=*/4u); + SetDownloadSuccessResult(contact_records); + SetUploadResult(false); + DownloadContacts(/*download_success=*/true, /*expect_upload=*/true, + /*upload_success=*/false, + /*allowed_contact_ids=*/allowlist, + /*contacts=*/contact_records); + + // When contacts are downloaded again, we should continue to indicate that + // contacts have changed since the last upload, and attempt another upload. + // (In other words, this tests that the contact-upload hash isn't updated + // prematurely.) + SetDownloadSuccessResult(contact_records); + SetUploadResult(true); + DownloadContacts(/*download_success=*/true, /*expect_upload=*/true, + /*upload_success=*/true, + /*allowed_contact_ids=*/allowlist, + /*contacts=*/contact_records); +} + +TEST_F(NearbyShareContactManagerImplTest, ContactUploadHash) { + EXPECT_EQ(preference_manager().GetString( + prefs::kNearbySharingContactUploadHashName, std::string()), + std::string(kTestDefaultContactsHash)); + + std::vector contact_records = + TestContactRecordList(/*num_contacts=*/10u); + std::set allowlist = TestContactIds(/*num_contacts=*/2u); + SetAllowedContacts(allowlist, /*expect_allowlist_changed=*/true); + + SetDownloadSuccessResult(contact_records); + SetUploadResult(true); + DownloadContacts(/*download_success=*/true, /*expect_upload=*/true, + /*upload_success=*/true, + /*allowed_contact_ids=*/allowlist, + /*contacts=*/contact_records); + + // Hardcode expected contact upload hash to ensure that hashed value is + // consistent across process starts. If this test starts to fail, check one + // of the following: + // 1. Did the test data change? No worries; just update this hash value. + // 2. Did the hashing function change? As long as the function is stable + // across (most) process starts, then everything is okay; just update + // this hash value. A changed hash value will result in an extra + // server call, so as long as the value is stable for the most part, + // it's okay. + const char kExpectedHash[] = + "A6DE36F14A9752DF247D92C4ECBEBC708691C33596C4D0C7D02F09F4BA65A37B"; + EXPECT_EQ(kExpectedHash, + preference_manager().GetString( + prefs::kNearbySharingContactUploadHashName, std::string())); + + // Try a few different permutations of contacts to ensure that the hash is + // invariant under ordering. + std::default_random_engine rng; + for (size_t i = 0; i < 10u; ++i) { + // We do not expect an upload because the contacts did not change in any + // way other than ordering. + std::vector shuffled_contacts = contact_records; + std::shuffle(shuffled_contacts.begin(), shuffled_contacts.end(), rng); + + SetDownloadSuccessResult(shuffled_contacts); + SetUploadResult(true); + DownloadContacts(/*download_success=*/true, /*expect_upload=*/false, + /*upload_success=*/true, + /*allowed_contact_ids=*/allowlist, + /*contacts=*/shuffled_contacts); + + EXPECT_EQ(preference_manager().GetString( + prefs::kNearbySharingContactUploadHashName, std::string()), + kExpectedHash); + } +} + +} // namespace +} // namespace sharing +} // namespace nearby diff --git a/sharing/contacts/nearby_share_contacts_sorter.cc b/sharing/contacts/nearby_share_contacts_sorter.cc new file mode 100644 index 00000000..95bdd493 --- /dev/null +++ b/sharing/contacts/nearby_share_contacts_sorter.cc @@ -0,0 +1,161 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/contacts/nearby_share_contacts_sorter.h" + +#include +#include +#include +#include +#include + +#include "absl/strings/string_view.h" +#include "sharing/proto/rpc_resources.pb.h" + +namespace nearby { +namespace sharing { +namespace { + +struct ContactSortingFields { + // Primary sorting key: person name if not empty; otherwise, email. + std::optional person_name_or_email; + // Secondary sorting key. Note: It is okay if email is also used as the + // primary sorting key. + std::optional email; + // Tertiary sorting key. + std::optional phone_number; + // Last resort sorting key. The contact ID should be unique for each + // contact record, guaranteeing uniquely defined ordering. + std::string id; +}; + +ContactSortingFields GetContactSortingFields( + const nearby::sharing::proto::ContactRecord& contact) { + ContactSortingFields fields; + fields.id = contact.id(); + for (const proto::Contact_Identifier& identifier : contact.identifiers()) { + switch (identifier.identifier_case()) { + case nearby::sharing::proto::Contact_Identifier::IdentifierCase:: + kAccountName: + if (!fields.email) { + fields.email = identifier.account_name(); + } + break; + case nearby::sharing::proto::Contact_Identifier::IdentifierCase:: + kPhoneNumber: + if (!fields.phone_number) { + fields.phone_number = identifier.phone_number(); + } + break; + case nearby::sharing::proto::Contact_Identifier::IdentifierCase:: + kObfuscatedGaia: + break; + case nearby::sharing::proto::Contact_Identifier::IdentifierCase:: + IDENTIFIER_NOT_SET: + break; + } + } + fields.person_name_or_email = + contact.person_name().empty() + ? fields.email + : std::make_optional(contact.person_name()); + + return fields; +} + +class ContactRecordComparator { + public: + explicit ContactRecordComparator(std::locale locale) : locale_(locale) {} + + bool operator()(const nearby::sharing::proto::ContactRecord& c1, + const nearby::sharing::proto::ContactRecord& c2) const { + ContactSortingFields f1 = GetContactSortingFields(c1); + ContactSortingFields f2 = GetContactSortingFields(c2); + + switch (CollatorCompare(f1.person_name_or_email, f2.person_name_or_email)) { + case 0: + // Do nothing. Compare with the next field. + break; + case -1: + return true; + case 1: + return false; + } + + switch (CollatorCompare(f1.email, f2.email)) { + case 0: + // Do nothing. Compare with the next field. + break; + case -1: + return true; + case 1: + return false; + } + + if (f1.phone_number != f2.phone_number) { + if (!f1.phone_number) return false; + if (!f2.phone_number) return true; + return *f1.phone_number < *f2.phone_number; + } + + return f1.id < f2.id; + } + + private: + int CollatorCompare(const std::optional& a, + const std::optional& b) const { + // Sort populated strings before absl::nullopt. + if (!a && !b) return 0; + if (!b) return -1; + if (!a) return 1; + + // Sort using a locale-based collator if available. + if (std::has_facet>(locale_)) { + auto& facet = std::use_facet>(locale_); + std::string s1 = *a; + std::string s2 = *b; + + return facet.compare(&s1[0], &s1[0] + s1.size(), &s2[0], + &s2[0] + s2.size()); + } + + // Fall back on standard string comparison, though we hope and expect + // that locale-based sorting will succeed. + if (*a == *b) { + return 0; + } + return *a < *b ? -1 : 1; + } + + std::locale locale_; +}; + +} // namespace + +void SortNearbyShareContactRecords( + std::vector* contacts, + absl::string_view locale_string) { + // initialized to default program environment locale. + std::locale loc = std::locale(""); + + if (!locale_string.empty()) { + loc = std::locale(locale_string.data()); + } + + ContactRecordComparator comparator(loc); + std::sort(contacts->begin(), contacts->end(), comparator); +} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/contacts/nearby_share_contacts_sorter.h b/sharing/contacts/nearby_share_contacts_sorter.h new file mode 100644 index 00000000..ceaa2dff --- /dev/null +++ b/sharing/contacts/nearby_share_contacts_sorter.h @@ -0,0 +1,45 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_CONTACTS_NEARBY_SHARE_CONTACTS_SORTER_H_ +#define THIRD_PARTY_NEARBY_SHARING_CONTACTS_NEARBY_SHARE_CONTACTS_SORTER_H_ + +#include + +#include "absl/strings/string_view.h" +#include "sharing/proto/rpc_resources.pb.h" + +namespace nearby { +namespace sharing { + +// Sort |contacts| by the following fields: +// - person name or email address if name is empty (primary), +// - email, even if this is also used as the primary (secondary), +// - phone number (tertiary), +// - contact record id (last resort; should always be unique). +// +// This sorted order is unique for a given |locale|, presuming every element of +// |contacts| has a unique ContactRecord::id(). The ordering between fields is +// locale-dependent. For example, 'Å' will be sorted with these 'A's for +// US-based sorting, whereas 'Å' will be sorted after 'Z' for Sweden-based +// sorting, because 'Å' comes after 'Z' in the Swedish alphabet. By default, +// |locale| is inferred from system settings. +void SortNearbyShareContactRecords( + std::vector* contacts, + absl::string_view locale_string = ""); + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_CONTACTS_NEARBY_SHARE_CONTACTS_SORTER_H_ diff --git a/sharing/contacts/nearby_share_contacts_sorter_test.cc b/sharing/contacts/nearby_share_contacts_sorter_test.cc new file mode 100644 index 00000000..4c50a8ff --- /dev/null +++ b/sharing/contacts/nearby_share_contacts_sorter_test.cc @@ -0,0 +1,251 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/contacts/nearby_share_contacts_sorter.h" + +#include + +#include +#include +#include +#include + +#include "gmock/gmock.h" +#include "protobuf-matchers/protocol-buffer-matchers.h" +#include "gtest/gtest.h" +#include "absl/strings/string_view.h" +#include "sharing/proto/rpc_resources.pb.h" + +namespace nearby { +namespace sharing { +namespace { + +using ::nearby::sharing::proto::ContactRecord; +using ::protobuf_matchers::EqualsProto; +using ::testing::Pointwise; + +const std::vector& contacts() { + static const std::vector* contacts = + new std::vector([] { + ContactRecord contact0; + contact0.set_person_name("Claire"); + contact0.set_is_reachable(true); + + ContactRecord contact1; + contact1.set_person_name("Alice"); + contact1.add_identifiers()->set_account_name("y@gmail.com"); + contact1.set_is_reachable(true); + + ContactRecord contact2; + contact2.set_person_name("Alice"); + contact2.add_identifiers()->set_account_name("x@gmail.com"); + contact2.set_is_reachable(true); + + ContactRecord contact3; + contact3.add_identifiers()->set_account_name("bob@gmail.com"); + contact3.set_is_reachable(true); + + ContactRecord contact4; + contact4.add_identifiers()->set_phone_number("222-222-2222"); + contact4.set_is_reachable(true); + + ContactRecord contact5; + contact5.add_identifiers()->set_phone_number("111-111-1111"); + contact5.set_is_reachable(true); + + ContactRecord contact6; + contact6.set_person_name("David"); + contact6.add_identifiers()->set_account_name("z@gmail.com"); + contact6.add_identifiers()->set_phone_number("222-222-2222"); + contact6.set_is_reachable(true); + + ContactRecord contact7; + contact7.set_person_name("David"); + contact7.add_identifiers()->set_account_name("z@gmail.com"); + contact7.add_identifiers()->set_phone_number("111-111-1111"); + contact7.set_id("2"); + contact7.set_is_reachable(true); + + ContactRecord contact8; + contact8.set_person_name("David"); + contact8.add_identifiers()->set_account_name("z@gmail.com"); + contact8.add_identifiers()->set_phone_number("111-111-1111"); + contact8.set_id("1"); + contact8.set_is_reachable(true); + + ContactRecord contact9; + contact9.set_person_name("中村光"); + contact9.set_is_reachable(true); + auto a = contact9.person_name(); + + ContactRecord contact10; + contact10.set_person_name("王皓"); + contact10.set_is_reachable(true); + + ContactRecord contact11; + contact11.set_person_name("中村俊輔"); + contact11.set_is_reachable(true); + + ContactRecord contact12; + contact12.set_person_name("丁立人"); + contact12.set_is_reachable(true); + + ContactRecord contact13; + contact13.set_person_name("Á"); + contact13.set_is_reachable(true); + + ContactRecord contact14; + contact14.set_person_name("Ñ"); + contact14.set_is_reachable(true); + + ContactRecord contact15; + contact15.set_person_name("å"); + contact15.set_id("5"); + contact15.set_is_reachable(true); + + ContactRecord contact16; + contact16.set_person_name("Å"); + contact16.set_id("3"); + contact16.set_is_reachable(true); + + ContactRecord contact17; + contact17.set_person_name("åz"); + contact17.set_id("4"); + contact17.set_is_reachable(true); + + ContactRecord contact18; + contact18.set_person_name("Opus"); + contact18.set_is_reachable(true); + + return std::vector{ + contact0, contact1, contact2, contact3, contact4, + contact5, contact6, contact7, contact8, contact9, + contact10, contact11, contact12, contact13, contact14, + contact15, contact16, contact17, contact18}; + }()); + + return *contacts; +} + +void VerifySort(const std::vector& expected_contacts, + const std::vector& unsorted_contacts, + absl::string_view locale_string) { + // Try a few different permutations of |unsorted_contacts|, which should all + // be sorted to |expected_contacts|. + std::default_random_engine rng; + for (size_t i = 0; i < 10u; ++i) { + std::vector sorted_contacts = contacts(); + std::shuffle(sorted_contacts.begin(), sorted_contacts.end(), rng); + SortNearbyShareContactRecords(&sorted_contacts, locale_string); + ASSERT_EQ(expected_contacts.size(), sorted_contacts.size()); + EXPECT_THAT(sorted_contacts, Pointwise(EqualsProto(), sorted_contacts)); + } +} + +TEST(NearbyShareContactsSorter, US) { + // Expected ordering: + // Á | | | + // å | | | ID: 5 + // Å | | | ID: 3 + // Alice | x@gmail.com | | + // Alice | y@gmail.com | | + // åz | | | ID: 4 + // | bob@gmail.com | | + // Claire | | | + // David | z@gmail.com | 111-111-1111 | ID: 1 + // David | z@gmail.com | 111-111-1111 | ID: 2 + // David | z@gmail.com | 222-222-2222 | + // Ñ | | | + // Opus | | | + // 丁立人 | | | + // 中村俊輔 | | | + // 中村光 | | | + // 王皓 | | | + // | | 111-111-1111 | + // | | 222-222-2222 | + std::vector expected_contacts{ + contacts()[13], contacts()[15], contacts()[16], contacts()[2], + contacts()[1], contacts()[17], contacts()[3], contacts()[0], + contacts()[8], contacts()[7], contacts()[6], contacts()[14], + contacts()[18], contacts()[12], contacts()[11], contacts()[9], + contacts()[10], contacts()[5], contacts()[4]}; + ASSERT_NO_FATAL_FAILURE( + VerifySort(expected_contacts, contacts(), "en_US.UTF-8")); +} + +TEST(NearbyShareContactsSorter, DISABLED_Sweden) { + // Expected ordering: + // Á | | | + // Alice | x@gmail.com | | + // Alice | y@gmail.com | | + // | bob@gmail.com | | + // Claire | | | + // David | z@gmail.com | 111-111-1111 | ID: 1 + // David | z@gmail.com | 111-111-1111 | ID: 2 + // David | z@gmail.com | 222-222-2222 | + // Ñ | | | + // Opus | | | + // å | | | ID: 5 + // Å | | | ID: 3 + // åz | | | ID: 4 + // 丁立人 | | | + // 中村俊輔 | | | + // 中村光 | | | + // 王皓 | | | + // | | 111-111-1111 | + // | | 222-222-2222 | + std::vector expected_contacts{ + contacts()[13], contacts()[2], contacts()[1], contacts()[3], + contacts()[0], contacts()[8], contacts()[7], contacts()[6], + contacts()[14], contacts()[18], contacts()[15], contacts()[16], + contacts()[17], contacts()[12], contacts()[11], contacts()[9], + contacts()[10], contacts()[5], contacts()[4]}; + ASSERT_NO_FATAL_FAILURE( + VerifySort(expected_contacts, contacts(), "sv-SE.UTF-8")); +} + +TEST(NearbyShareContactsSorter, DISABLED_China) { + // Expected ordering: + // Á | | | + // å | | | ID: 5 + // Å | | | ID: 3 + // Alice | x@gmail.com | | + // Alice | y@gmail.com | | + // åz | | | ID: 4 + // | bob@gmail.com | | + // Claire | | | + // David | z@gmail.com | 111-111-1111 | ID: 1 + // David | z@gmail.com | 111-111-1111 | ID: 2 + // David | z@gmail.com | 222-222-2222 | + // Ñ | | | + // Opus | | | + // 丁立人 | | | + // 王皓 | | | + // 中村光 | | | + // 中村俊輔 | | | + // | | 111-111-1111 | + // | | 222-222-2222 | + std::vector expected_contacts{ + contacts()[13], contacts()[15], contacts()[16], contacts()[2], + contacts()[1], contacts()[17], contacts()[3], contacts()[0], + contacts()[8], contacts()[7], contacts()[6], contacts()[14], + contacts()[18], contacts()[12], contacts()[10], contacts()[9], + contacts()[11], contacts()[5], contacts()[4]}; + ASSERT_NO_FATAL_FAILURE( + VerifySort(expected_contacts, contacts(), "zh_CN.UTF-8")); +} + +} // namespace +} // namespace sharing +} // namespace nearby diff --git a/sharing/internal/base/BUILD b/sharing/internal/base/BUILD index 3967f05e..b21a9fce 100644 --- a/sharing/internal/base/BUILD +++ b/sharing/internal/base/BUILD @@ -16,18 +16,10 @@ licenses(["notice"]) cc_library( name = "utf_utils", - srcs = [ - "utf_string_conversions.cc", - ], hdrs = [ - "utf_string_configuration.h", "utf_string_conversions.h", ], visibility = ["//visibility:public"], - deps = [ - "//sharing/internal/public:logging", - "//third_party/icu_utf:icu_utf_assistant", - ], ) cc_library( @@ -54,12 +46,10 @@ cc_test( timeout = "short", srcs = [ "encode_test.cc", - "utf_string_conversions_test.cc", ], shard_count = 8, deps = [ ":base", - ":utf_utils", "//internal/platform/implementation/g3", # fixdeps: keep "@com_github_protobuf_matchers//protobuf-matchers", "@com_google_googletest//:gtest_main", diff --git a/sharing/internal/base/utf_string_configuration.h b/sharing/internal/base/utf_string_configuration.h deleted file mode 100644 index 7f3174fc..00000000 --- a/sharing/internal/base/utf_string_configuration.h +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright 2021 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#ifndef THIRD_PARTY_NEARBY_SHARING_INTERNAL_BASE_UTF_STRING_CONFIGURATION_H_ -#define THIRD_PARTY_NEARBY_SHARING_INTERNAL_BASE_UTF_STRING_CONFIGURATION_H_ - -// A set of macros to use for platform detection. -#if defined(__native_client__) -// __native_client__ must be first, so that other OS_ defines are not set. -#define OS_NACL 1 -#define OS_NACL_SFI -#elif defined(ANDROID) -#define OS_ANDROID 1 -#elif defined(__APPLE__) -// Only include TargetConditionals after testing ANDROID as some Android builds -// on the Mac have this header available and it's not needed unless the target -// is really an Apple platform. -#include -#if defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE -#define OS_IOS 1 -#else -#define OS_MAC 1 -#endif // defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE -#elif defined(__linux__) -#if !defined(OS_CHROMEOS) -// Do not define OS_LINUX on Chrome OS build. -// The OS_CHROMEOS macro is defined in GN. -#define OS_LINUX 1 -#endif // !defined(OS_CHROMEOS) -#if defined(__GLIBC__) && !defined(__UCLIBC__) -// We really are using glibc, not uClibc pretending to be glibc. -#define LIBC_GLIBC 1 -#endif -#elif defined(_WIN32) -#define OS_WIN 1 -#elif defined(__Fuchsia__) -#define OS_FUCHSIA 1 -#elif defined(__FreeBSD__) -#define OS_FREEBSD 1 -#elif defined(__NetBSD__) -#define OS_NETBSD 1 -#elif defined(__OpenBSD__) -#define OS_OPENBSD 1 -#elif defined(__sun) -#define OS_SOLARIS 1 -#elif defined(__QNXNTO__) -#define OS_QNX 1 -#elif defined(_AIX) -#define OS_AIX 1 -#elif defined(__asmjs__) || defined(__wasm__) -#define OS_ASMJS 1 -#elif defined(__MVS__) -#define OS_ZOS 1 -#else -#error Please add support for your platform in build/build_config.h -#endif -// NOTE: Adding a new port? Please follow -// https://chromium.googlesource.com/chromium/src/+/main/docs/new_port_policy.md - -#if defined(OS_MAC) || defined(OS_IOS) -#define OS_APPLE 1 -#endif - -// For access to standard BSD features, use OS_BSD instead of a -// more specific macro. -#if defined(OS_FREEBSD) || defined(OS_NETBSD) || defined(OS_OPENBSD) -#define OS_BSD 1 -#endif - -// For access to standard POSIX features, use OS_POSIX instead of a -// more specific macro. -#if defined(OS_AIX) || defined(OS_ANDROID) || defined(OS_ASMJS) || \ - defined(OS_FREEBSD) || defined(OS_IOS) || defined(OS_LINUX) || \ - defined(OS_CHROMEOS) || defined(OS_MAC) || defined(OS_NACL) || \ - defined(OS_NETBSD) || defined(OS_OPENBSD) || defined(OS_QNX) || \ - defined(OS_SOLARIS) || defined(OS_ZOS) -#define OS_POSIX 1 -#endif - -// Compiler detection. Note: clang masquerades as GCC on POSIX and as MSVC on -// Windows. -#if defined(__GNUC__) -#define COMPILER_GCC 1 -#elif defined(_MSC_VER) -#define COMPILER_MSVC 1 -#else -#error Please add support for your compiler in build/build_config.h -#endif - -// Type detection for wchar_t. -#if defined(OS_WIN) -#define WCHAR_T_IS_UTF16 -#elif defined(OS_FUCHSIA) -#define WCHAR_T_IS_UTF32 -#elif defined(OS_POSIX) && defined(COMPILER_GCC) && defined(__WCHAR_MAX__) && \ - (__WCHAR_MAX__ == 0x7fffffff || __WCHAR_MAX__ == 0xffffffff) -#define WCHAR_T_IS_UTF32 -#elif defined(OS_POSIX) && defined(COMPILER_GCC) && defined(__WCHAR_MAX__) && \ - (__WCHAR_MAX__ == 0x7fff || __WCHAR_MAX__ == 0xffff) -// On Posix, we'll detect short wchar_t, but projects aren't guaranteed to -// compile in this mode (in particular, Chrome doesn't). This is intended for -// other projects using base who manage their own dependencies and make sure -// short wchar works for them. -#define WCHAR_T_IS_UTF16 -#else -#error Please add support for your compiler in build/build_config.h -#endif - -#endif // THIRD_PARTY_NEARBY_SHARING_INTERNAL_BASE_UTF_STRING_CONFIGURATION_H_ diff --git a/sharing/internal/base/utf_string_conversions.cc b/sharing/internal/base/utf_string_conversions.cc deleted file mode 100644 index 2234e8a2..00000000 --- a/sharing/internal/base/utf_string_conversions.cc +++ /dev/null @@ -1,510 +0,0 @@ -// Copyright 2021 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#include "sharing/internal/base/utf_string_conversions.h" - -#include -#include - -#include -#include -#include -#include - -#include "third_party/icu_utf/icu_utf.h" -#include "sharing/internal/public/logging.h" - -namespace nearby { -namespace utils { -namespace { - -using MachineWord = uintptr_t; - -constexpr int32_t kErrorCodePoint = 0xFFFD; - -inline bool IsMachineWordAligned(const void* pointer) { - return !(reinterpret_cast(pointer) & (sizeof(MachineWord) - 1)); -} - -template -bool DoIsStringAscii(const Char* characters, size_t length) { - // Bitmasks to detect non-ASCII characters for character sizes of 8, 16 and 32 - // bits. - constexpr MachineWord NonASCIIMasks[] = { - 0, MachineWord(0x8080808080808080ULL), MachineWord(0xFF80FF80FF80FF80ULL), - 0, MachineWord(0xFFFFFF80FFFFFF80ULL), - }; - - if (!length) return true; - constexpr MachineWord non_ascii_bit_mask = NonASCIIMasks[sizeof(Char)]; - static_assert(non_ascii_bit_mask, "Error: Invalid Mask"); - MachineWord all_char_bits = 0; - const Char* end = characters + length; - - // Prologue: align the input. - while (!IsMachineWordAligned(characters) && characters < end) - all_char_bits |= *characters++; - if (all_char_bits & non_ascii_bit_mask) return false; - - // Compare the values of CPU word size. - constexpr size_t chars_per_word = sizeof(MachineWord) / sizeof(Char); - constexpr int batch_count = 16; - while (characters <= end - batch_count * chars_per_word) { - all_char_bits = 0; - for (int i = 0; i < batch_count; ++i) { - all_char_bits |= *(reinterpret_cast(characters)); - characters += chars_per_word; - } - if (all_char_bits & non_ascii_bit_mask) return false; - } - - // Process the remaining words. - all_char_bits = 0; - while (characters <= end - chars_per_word) { - all_char_bits |= *(reinterpret_cast(characters)); - characters += chars_per_word; - } - - // Process the remaining bytes. - while (characters < end) all_char_bits |= *characters++; - - return !(all_char_bits & non_ascii_bit_mask); -} - -inline bool IsValidCharacter(uint32_t code_point) { - // Excludes non-characters (U+FDD0..U+FDEF, and all code points - // ending in 0xFFFE or 0xFFFF) from the set of valid code points. - // https://unicode.org/faq/private_use.html#nonchar1 - return code_point < 0xD800u || - (code_point >= 0xE000u && code_point < 0xFDD0u) || - (code_point > 0xFDEFu && code_point <= 0x10FFFFu && - (code_point & 0xFFFEu) != 0xFFFEu); -} - -template -inline bool DoIsStringUtf8(std::string_view str) { - const char* src = str.data(); - int32_t src_len = static_cast(str.length()); - int32_t char_index = 0; - - while (char_index < src_len) { - int32_t code_point; - CBU8_NEXT(src, char_index, src_len, code_point); - if (!Validator(code_point)) return false; - } - return true; -} - -// Size coefficient ---------------------------------------------------------- -// The maximum number of codeunits in the destination encoding corresponding to -// one codeunit in the source encoding. - -template -struct SizeCoefficient { - static_assert(sizeof(SrcChar) < sizeof(DestChar), - "Default case: from a smaller encoding to the bigger one"); - - // ASCII symbols are encoded by one codeunit in all encodings. - static constexpr int value = 1; -}; - -template <> -struct SizeCoefficient { - // One UTF-16 code unit corresponds to at most 3 code units in UTF-8. - static constexpr int value = 3; -}; - -#if defined(WCHAR_T_IS_UTF32) -template <> -struct SizeCoefficient { - // UTF-8 uses at most 4 code units per character. - static constexpr int value = 4; -}; - -template <> -struct SizeCoefficient { - // UTF-16 uses at most 2 code units per character. - static constexpr int value = 2; -}; -#endif // defined(WCHAR_T_IS_UTF32) - -template -constexpr int size_coefficient_v = - SizeCoefficient, std::decay_t>::value; - -// UnicodeAppendUnsafe -------------------------------------------------------- -// Function overloads that write code_point to the output string. Output string -// has to have enough space for the codepoint. - -// Convenience typedef that checks whether the passed in type is integral (i.e. -// bool, char, int or their extended versions) and is of the correct size. -template -using EnableIfBitsAre = std::enable_if_t< - std::is_integral::value && CHAR_BIT * sizeof(Char) == N, bool>; - -template = true> -void UnicodeAppendUnsafe(Char* out, int32_t* size, uint32_t code_point) { - CBU8_APPEND_UNSAFE(out, *size, code_point); -} - -template = true> -void UnicodeAppendUnsafe(Char* out, int32_t* size, uint32_t code_point) { - CBU16_APPEND_UNSAFE(out, *size, code_point); -} - -template = true> -void UnicodeAppendUnsafe(Char* out, int32_t* size, uint32_t code_point) { - out[(*size)++] = code_point; -} - -// DoUtfConversion ------------------------------------------------------------ -// Main driver of UtfConversion specialized for different Src encodings. -// dest has to have enough room for the converted text. - -template -bool DoUtfConversion(const char* src, int32_t src_len, DestChar* dest, - int32_t* dest_len) { - bool success = true; - - for (int32_t i = 0; i < src_len;) { - int32_t code_point; - CBU8_NEXT(src, i, src_len, code_point); - - if (!IsValidCodepoint(code_point)) { - success = false; - code_point = kErrorCodePoint; - } - - UnicodeAppendUnsafe(dest, dest_len, code_point); - } - - return success; -} - -template -bool DoUtfConversion(const char16_t* src, int32_t src_len, DestChar* dest, - int32_t* dest_len) { - bool success = true; - - auto ConvertSingleChar = [&success](char16_t in) -> int32_t { - if (!CBU16_IS_SINGLE(in) || !IsValidCodepoint(in)) { - success = false; - return kErrorCodePoint; - } - return in; - }; - - int32_t i = 0; - - // Always have another symbol in order to avoid checking boundaries in the - // middle of the surrogate pair. - while (i < src_len - 1) { - int32_t code_point; - - if (CBU16_IS_LEAD(src[i]) && CBU16_IS_TRAIL(src[i + 1])) { - code_point = CBU16_GET_SUPPLEMENTARY(src[i], src[i + 1]); - if (!IsValidCodepoint(code_point)) { - code_point = kErrorCodePoint; - success = false; - } - i += 2; - } else { - code_point = ConvertSingleChar(src[i]); - ++i; - } - - UnicodeAppendUnsafe(dest, dest_len, code_point); - } - - if (i < src_len) - UnicodeAppendUnsafe(dest, dest_len, ConvertSingleChar(src[i])); - - return success; -} - -#if defined(WCHAR_T_IS_UTF32) - -template -bool DoUtfConversion(const wchar_t* src, int32_t src_len, DestChar* dest, - int32_t* dest_len) { - bool success = true; - - for (int32_t i = 0; i < src_len; ++i) { - int32_t code_point = src[i]; - - if (!IsValidCodepoint(code_point)) { - success = false; - code_point = kErrorCodePoint; - } - - UnicodeAppendUnsafe(dest, dest_len, code_point); - } - - return success; -} - -#endif // defined(WCHAR_T_IS_UTF32) - -// UtfConversion -------------------------------------------------------------- -// Function template for generating all UTF conversions. - -template -bool UtfConversion(const InputString& src_str, DestString* dest_str) { - if (IsStringAscii(src_str)) { - dest_str->assign(src_str.begin(), src_str.end()); - return true; - } - - dest_str->resize(src_str.length() * - size_coefficient_v); - - // Empty string is ASCII => it OK to call operator[]. - auto* dest = &(*dest_str)[0]; - - // ICU requires 32 bit numbers. - int32_t src_len32 = static_cast(src_str.length()); - int32_t dest_len32 = 0; - - bool res = DoUtfConversion(src_str.data(), src_len32, dest, &dest_len32); - - dest_str->resize(dest_len32); - dest_str->shrink_to_fit(); - - return res; -} - -#if defined(WCHAR_T_IS_UTF16) -inline const char16_t* as_u16cstr(const wchar_t* str) { - return reinterpret_cast(str); -} - -inline const char16_t* as_u16cstr(std::wstring_view str) { - return reinterpret_cast(str.data()); -} -#endif - -} // namespace - -// UTF16 <-> UTF8 -------------------------------------------------------------- - -bool Utf8ToUtf16(const char* src, size_t src_len, std::u16string* output) { - return UtfConversion(std::string_view(src, src_len), output); -} - -std::u16string Utf8ToUtf16(std::string_view utf8) { - std::u16string ret; - // Ignore the success flag of this call, it will do the best it can for - // invalid input, which is what we want here. - Utf8ToUtf16(utf8.data(), utf8.size(), &ret); - return ret; -} - -bool Utf16ToUtf8(const char16_t* src, size_t src_len, std::string* output) { - return UtfConversion(std::u16string_view(src, src_len), output); -} - -std::string Utf16ToUtf8(std::u16string_view utf16) { - std::string ret; - // Ignore the success flag of this call, it will do the best it can for - // invalid input, which is what we want here. - Utf16ToUtf8(utf16.data(), utf16.length(), &ret); - return ret; -} - -// UTF-16 <-> Wide ------------------------------------------------------------- - -#if defined(WCHAR_T_IS_UTF16) -// When wide == UTF-16 the conversions are a NOP. - -bool WideToUtf16(const wchar_t* src, size_t src_len, std::u16string* output) { - output->assign(src, src + src_len); - return true; -} - -std::u16string WideToUtf16(std::wstring_view wide) { - return std::u16string(wide.begin(), wide.end()); -} - -bool Utf16ToWide(const char16_t* src, size_t src_len, std::wstring* output) { - output->assign(src, src + src_len); - return true; -} - -std::wstring Utf16ToWide(std::u16string_view utf16) { - return std::wstring(utf16.begin(), utf16.end()); -} - -#elif defined(WCHAR_T_IS_UTF32) - -bool WideToUtf16(const wchar_t* src, size_t src_len, std::u16string* output) { - return UtfConversion(std::wstring_view(src, src_len), output); -} - -std::u16string WideToUtf16(std::wstring_view wide) { - std::u16string ret; - // Ignore the success flag of this call, it will do the best it can for - // invalid input, which is what we want here. - WideToUtf16(wide.data(), wide.length(), &ret); - return ret; -} - -bool Utf16ToWide(const char16_t* src, size_t src_len, std::wstring* output) { - return UtfConversion(std::u16string_view(src, src_len), output); -} - -std::wstring Utf16ToWide(std::u16string_view utf16) { - std::wstring ret; - // Ignore the success flag of this call, it will do the best it can for - // invalid input, which is what we want here. - Utf16ToWide(utf16.data(), utf16.length(), &ret); - return ret; -} - -#endif // defined(WCHAR_T_IS_UTF32) - -// UTF-8 <-> Wide -------------------------------------------------------------- - -// UTF8ToWide is the same code, regardless of whether wide is 16 or 32 bits - -bool Utf8ToWide(const char* src, size_t src_len, std::wstring* output) { - return UtfConversion(std::string_view(src, src_len), output); -} - -std::wstring Utf8ToWide(std::string_view utf8) { - std::wstring ret; - // Ignore the success flag of this call, it will do the best it can for - // invalid input, which is what we want here. - Utf8ToWide(utf8.data(), utf8.length(), &ret); - return ret; -} - -#if defined(WCHAR_T_IS_UTF16) -// Easy case since we can use the "utf" versions we already wrote above. - -bool WideToUtf8(const wchar_t* src, size_t src_len, std::string* output) { - return Utf16ToUtf8(as_u16cstr(src), src_len, output); -} - -std::string WideToUtf8(std::wstring_view wide) { - return Utf16ToUtf8(std::u16string_view(as_u16cstr(wide), wide.size())); -} - -#elif defined(WCHAR_T_IS_UTF32) - -bool WideToUtf8(const wchar_t* src, size_t src_len, std::string* output) { - return UtfConversion(std::wstring_view(src, src_len), output); -} - -std::string WideToUtf8(std::wstring_view wide) { - std::string ret; - // Ignore the success flag of this call, it will do the best it can for - // invalid input, which is what we want here. - WideToUtf8(wide.data(), wide.length(), &ret); - return ret; -} - -#endif // defined(WCHAR_T_IS_UTF32) - -std::u16string AsciiToUtf16(std::string_view ascii) { - NL_DCHECK(IsStringAscii(ascii)); - return std::u16string(ascii.begin(), ascii.end()); -} - -std::string Utf16ToAscii(std::u16string_view utf16) { - NL_DCHECK(IsStringAscii(utf16)); - return std::string(utf16.begin(), utf16.end()); -} - -#if defined(WCHAR_T_IS_UTF16) -std::wstring AsciiToWide(std::string_view ascii) { - NL_DCHECK(IsStringAscii(ascii)); - return std::wstring(ascii.begin(), ascii.end()); -} - -std::string WideToAscii(std::string_view wide) { - NL_DCHECK(IsStringAscii(wide)); - return std::string(wide.begin(), wide.end()); -} -#endif // defined(WCHAR_T_IS_UTF16) - -bool IsStringAscii(std::string_view str) { - return DoIsStringAscii(str.data(), str.length()); -} - -bool IsStringAscii(std::u16string_view str) { - return DoIsStringAscii(str.data(), str.length()); -} - -bool IsStringUtf8(std::string_view str) { - return DoIsStringUtf8(str); -} - -bool IsStringAscii(std::wstring_view str) { - return DoIsStringAscii(str.data(), str.length()); -} - -bool IsValidCodepoint(uint32_t code_point) { - // Excludes code points that are not Unicode scalar values, i.e. - // surrogate code points ([0xD800, 0xDFFF]). Additionally, excludes - // code points larger than 0x10FFFF (the highest codepoint allowed). - // Non-characters and unassigned code points are allowed. - // https://unicode.org/glossary/#unicode_scalar_value - return code_point < 0xD800u || - (code_point >= 0xE000u && code_point <= 0x10FFFFu); -} - -void TruncateUtf8ToByteSize(const std::string& input, size_t byte_size, - std::string* output) { - NL_DCHECK(output); - if (byte_size > input.length()) { - *output = input; - return; - } - - // Note: This cast is necessary because CBU8_NEXT uses int32_ts. - int32_t truncation_length = static_cast(byte_size); - int32_t char_index = truncation_length - 1; - const char* data = input.data(); - - // Using CBU8, we will move backwards from the truncation point - // to the beginning of the string looking for a valid UTF8 - // character. Once a full UTF8 character is found, we will - // truncate the string to the end of that character. - while (char_index >= 0) { - int32_t prev = char_index; - int32_t code_point = 0; - CBU8_NEXT(data, char_index, truncation_length, code_point); - if (!IsValidCharacter(code_point) || !IsValidCodepoint(code_point)) { - char_index = prev - 1; - } else { - break; - } - } - - if (char_index >= 0) - *output = input.substr(0, char_index); - else - output->clear(); -} - -std::string ToString(const char* str) { - if (str == nullptr) { - return ""; - } - return std::string(str); -} - -} // namespace utils -} // namespace nearby diff --git a/sharing/internal/base/utf_string_conversions.h b/sharing/internal/base/utf_string_conversions.h index 14f3acec..ca89f619 100644 --- a/sharing/internal/base/utf_string_conversions.h +++ b/sharing/internal/base/utf_string_conversions.h @@ -1,4 +1,4 @@ -// Copyright 2021 Google LLC +// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -15,103 +15,24 @@ #ifndef THIRD_PARTY_NEARBY_SHARING_INTERNAL_BASE_UTF_STRING_CONVERSIONS_H_ #define THIRD_PARTY_NEARBY_SHARING_INTERNAL_BASE_UTF_STRING_CONVERSIONS_H_ -#include +#if defined(GITHUB_BUILD) +// Stub out string conversion functions for github builds. +namespace nearby::utils { +std::string WideToUtf8(std::wstring_view wide) { return std::string(); } +std::wstring Utf8ToWide(std::string_view utf8) { return std::wstring(); } -#include -#include -#include - -#include "sharing/internal/base/utf_string_configuration.h" - -namespace nearby { -namespace utils { - -// These convert between UTF-8, -16, and -32 strings. They are potentially slow, -// so avoid unnecessary conversions. The low-level versions return a boolean -// indicating whether the conversion was 100% valid. In this case, it will still -// do the best it can and put the result in the output buffer. The versions that -// return strings ignore this error and just return the best conversion -// possible. -bool WideToUtf8(const wchar_t* src, size_t src_len, std::string* output); -std::string WideToUtf8(std::wstring_view wide); -bool Utf8ToWide(const char* src, size_t src_len, std::wstring* output); -std::wstring Utf8ToWide(std::string_view utf8); - -bool WideToUtf16(const wchar_t* src, size_t src_len, std::u16string* output); -std::u16string WideToUtf16(std::wstring_view wide); -bool Utf16ToWide(const char16_t* src, size_t src_len, std::wstring* output); -std::wstring Utf16ToWide(std::u16string_view utf16); - -bool Utf8ToUtf16(const char* src, size_t src_len, std::u16string* output); -std::u16string Utf8ToUtf16(std::string_view utf8); -bool Utf16ToUtf8(const char16_t* src, size_t src_len, std::string* output); -std::string Utf16ToUtf8(std::u16string_view utf16); - -// This converts an ASCII string, typically a hard coded constant, to a UTF16 -// string. -std::u16string AsciiToUtf16(std::string_view ascii); - -// Converts to 7-bit ASCII by truncating. The result must be known to be ASCII -// beforehand. -std::string Utf16ToAscii(std::u16string_view utf16); - -bool IsStringAscii(std::string_view str); -bool IsStringAscii(std::u16string_view str); -bool IsStringAscii(std::wstring_view str); -bool IsStringUtf8(std::string_view str); - -inline bool IsValidCodepoint(uint32_t code_point); +bool IsStringUtf8(std::string_view str) { return true; } void TruncateUtf8ToByteSize(const std::string& input, size_t byte_size, - std::string* output); + std::string* output) {} -#if defined(WCHAR_T_IS_UTF16) -// This converts an ASCII string, typically a hard coded constant, to a wide -// string. -std::wstring AsciiToWide(std::string_view ascii); +} // namespace nearby::utils -// Converts to 7-bit ASCII by truncating. The result must be known to be ASCII -// beforehand. -std::string WideToAscii(std::wstring_view wide); -#endif // defined(WCHAR_T_IS_UTF16) +#elif defined(NEARBY_CHROMIUM) +// Forward to chromium implementations. -// The conversion functions in this file should not be used to convert string -// literals. Instead, the corresponding prefixes (e.g. u"" for UTF16 or L"" for -// Wide) should be used. Deleting the overloads here catches these cases at -// compile time. -template -std::u16string WideToUtf16(const wchar_t (&str)[N]) { - static_assert(N == 0, "Error: Use the u\"...\" prefix instead."); - return std::u16string(); -} - -template -std::u16string Utf8ToUtf16(const char (&str)[N]) { - static_assert(N == 0, "Error: Use the u\"...\" prefix instead."); - return std::u16string(); -} - -template -std::u16string AsciiToUtf16(const char (&str)[N]) { - static_assert(N == 0, "Error: Use the u\"...\" prefix instead."); - return std::u16string(); -} - -// Mutable character arrays are usually only populated during runtime. Continue -// to allow this conversion. -template -std::u16string AsciiToUtf16(char (&str)[N]) { - return AsciiToUtf16(std::string_view(str)); -} - -template -constexpr size_t size(const T (&array)[N]) noexcept { - return N; -} - -std::string ToString(const char* str); - -} // namespace utils -} // namespace nearby +#else // defined(GITHUB_BUILD) +#include "sharing/internal/base/strings/utf_string_conversions.h" // IWYU pragma: export +#endif // defined(GITHUB_BUILD) #endif // THIRD_PARTY_NEARBY_SHARING_INTERNAL_BASE_UTF_STRING_CONVERSIONS_H_ diff --git a/sharing/internal/base/utf_string_conversions_test.cc b/sharing/internal/base/utf_string_conversions_test.cc deleted file mode 100644 index 48d620e4..00000000 --- a/sharing/internal/base/utf_string_conversions_test.cc +++ /dev/null @@ -1,375 +0,0 @@ -// Copyright 2021 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#include "sharing/internal/base/utf_string_conversions.h" - -#include -#include -#include -#include - -#include "gtest/gtest.h" - -namespace nearby { -namespace utils { -namespace { - -const wchar_t* const kConvertRoundtripCases[] = { - L"Google Video", - // "网页 图片 资讯更多 »" - L"\x7f51\x9875\x0020\x56fe\x7247\x0020\x8d44\x8baf\x66f4\x591a\x0020\x00bb", - // "Παγκόσμιος Ιστός" - L"\x03a0\x03b1\x03b3\x03ba\x03cc\x03c3\x03bc\x03b9" - L"\x03bf\x03c2\x0020\x0399\x03c3\x03c4\x03cc\x03c2", - // "Поиск страниц на русском" - L"\x041f\x043e\x0438\x0441\x043a\x0020\x0441\x0442" - L"\x0440\x0430\x043d\x0438\x0446\x0020\x043d\x0430" - L"\x0020\x0440\x0443\x0441\x0441\x043a\x043e\x043c", - // "전체서비스" - L"\xc804\xccb4\xc11c\xbe44\xc2a4", - -// Test characters that take more than 16 bits. This will depend on whether -// wchar_t is 16 or 32 bits. -#if defined(WCHAR_T_IS_UTF16) - L"\xd800\xdf00", - // ????? (Mathematical Alphanumeric Symbols (U+011d40 - U+011d44 : - // A,B,C,D,E) - L"\xd807\xdd40\xd807\xdd41\xd807\xdd42\xd807\xdd43\xd807\xdd44", -#elif defined(WCHAR_T_IS_UTF32) - L"\x10300", - // ????? (Mathematical Alphanumeric Symbols (U+011d40 - U+011d44 : - // A,B,C,D,E) - L"\x11d40\x11d41\x11d42\x11d43\x11d44", -#endif -}; - -// Helper used to test TruncateUtf8ToByteSize. -bool Truncated(const std::string& input, const size_t byte_size, - std::string* output) { - size_t prev = input.length(); - TruncateUtf8ToByteSize(input, byte_size, output); - return prev != output->length(); -} - -} // namespace - -TEST(UtfStringConversionsTest, ConvertUtf8AndWide) { - // we round-trip all the wide strings through UTF-8 to make sure everything - // agrees on the conversion. This uses the stream operators to test them - // simultaneously. - for (auto* i : kConvertRoundtripCases) { - std::ostringstream utf8; - utf8 << WideToUtf8(i); - std::wostringstream wide; - wide << Utf8ToWide(utf8.str()); - - EXPECT_EQ(i, wide.str()); - } -} - -TEST(UtfStringConversionsTest, ConvertUtf8AndWideEmptyString) { - // An empty std::wstring should be converted to an empty std::string, - // and vice versa. - std::wstring wempty; - std::string empty; - EXPECT_EQ(empty, WideToUtf8(wempty)); - EXPECT_EQ(wempty, Utf8ToWide(empty)); -} - -TEST(UtfStringConversionsTest, ConvertUtf8ToWide) { - struct Utf8ToWideCase { - const char* utf8; - const wchar_t* wide; - bool success; - } convert_cases[] = { - // Regular UTF-8 input. - {"\xe4\xbd\xa0\xe5\xa5\xbd", L"\x4f60\x597d", true}, - // Non-character is passed through. - {"\xef\xbf\xbfHello", L"\xffffHello", true}, - // Truncated UTF-8 sequence. - {"\xe4\xa0\xe5\xa5\xbd", L"\xfffd\x597d", false}, - // Truncated off the end. - {"\xe5\xa5\xbd\xe4\xa0", L"\x597d\xfffd", false}, - // Non-shortest-form UTF-8. - {"\xf0\x84\xbd\xa0\xe5\xa5\xbd", L"\xfffd\xfffd\xfffd\xfffd\x597d", false}, - // This UTF-8 character is decoded to a UTF-16 surrogate, which is illegal. - {"\xed\xb0\x80", L"\xfffd\xfffd\xfffd", false}, - // Non-BMP characters. The second is a non-character regarded as valid. - // The result will either be in UTF-16 or UTF-32. -#if defined(WCHAR_T_IS_UTF16) - {"A\xF0\x90\x8C\x80z", L"A\xd800\xdf00z", true}, - {"A\xF4\x8F\xBF\xBEz", L"A\xdbff\xdffez", true}, -#elif defined(WCHAR_T_IS_UTF32) - {"A\xF0\x90\x8C\x80z", L"A\x10300z", true}, - {"A\xF4\x8F\xBF\xBEz", L"A\x10fffez", true}, -#endif - }; - - for (const auto& i : convert_cases) { - std::wstring converted; - EXPECT_EQ(i.success, Utf8ToWide(i.utf8, strlen(i.utf8), &converted)); - std::wstring expected(i.wide); - EXPECT_EQ(expected, converted); - } - - // Manually test an embedded NULL. - std::wstring converted; - EXPECT_TRUE(Utf8ToWide("\00Z\t", 3, &converted)); - ASSERT_EQ(3U, converted.length()); - EXPECT_EQ(static_cast(0), converted[0]); - EXPECT_EQ('Z', converted[1]); - EXPECT_EQ('\t', converted[2]); - - // Make sure that conversion replaces, not appends. - EXPECT_TRUE(Utf8ToWide("B", 1, &converted)); - ASSERT_EQ(1U, converted.length()); - EXPECT_EQ('B', converted[0]); -} - -#if defined(WCHAR_T_IS_UTF16) -// This test is only valid when wchar_t == UTF-16. -TEST(UtfStringConversionsTest, ConvertUtf16ToUtf8) { - struct WideToUtf8Case { - const wchar_t* utf16; - const char* utf8; - bool success; - } convert_cases[] = { - // Regular UTF-16 input. - {L"\x4f60\x597d", "\xe4\xbd\xa0\xe5\xa5\xbd", true}, - // Test a non-BMP character. - {L"\xd800\xdf00", "\xF0\x90\x8C\x80", true}, - // Non-characters are passed through. - {L"\xffffHello", "\xEF\xBF\xBFHello", true}, - {L"\xdbff\xdffeHello", "\xF4\x8F\xBF\xBEHello", true}, - // The first character is a truncated UTF-16 character. - {L"\xd800\x597d", "\xef\xbf\xbd\xe5\xa5\xbd", false}, - // Truncated at the end. - {L"\x597d\xd800", "\xe5\xa5\xbd\xef\xbf\xbd", false}, - }; - - for (const auto& test : convert_cases) { - std::string converted; - EXPECT_EQ(test.success, - WideToUtf8(test.utf16, wcslen(test.utf16), &converted)); - std::string expected(test.utf8); - EXPECT_EQ(expected, converted); - } -} - -#elif defined(WCHAR_T_IS_UTF32) -// This test is only valid when wchar_t == UTF-32. -TEST(UtfStringConversionsTest, ConvertUtf32ToUtf8) { - struct WideToUtf8Case { - const wchar_t* utf32; - const char* utf8; - bool success; - } convert_cases[] = { - // Regular 16-bit input. - {L"\x4f60\x597d", "\xe4\xbd\xa0\xe5\xa5\xbd", true}, - // Test a non-BMP character. - {L"A\x10300z", "A\xF0\x90\x8C\x80z", true}, - // Non-characters are passed through. - {L"\xffffHello", "\xEF\xBF\xBFHello", true}, - {L"\x10fffeHello", "\xF4\x8F\xBF\xBEHello", true}, - // Invalid Unicode code points. - {L"\xfffffffHello", "\xEF\xBF\xBDHello", false}, - // The first character is a truncated UTF-16 character. - {L"\xd800\x597d", "\xef\xbf\xbd\xe5\xa5\xbd", false}, - {L"\xdc01Hello", "\xef\xbf\xbdHello", false}, - }; - - for (const auto& test : convert_cases) { - std::string converted; - EXPECT_EQ(test.success, - WideToUtf8(test.utf32, wcslen(test.utf32), &converted)); - std::string expected(test.utf8); - EXPECT_EQ(expected, converted); - } -} -#endif // defined(WCHAR_T_IS_UTF32) - -TEST(UtfStringConversionsTest, TruncateUtf8ToByteSize) { - std::string output; - - // Empty strings and invalid byte_size arguments - EXPECT_FALSE(Truncated(std::string(), 0, &output)); - EXPECT_EQ(output, ""); - EXPECT_TRUE(Truncated("\xe1\x80\xbf", 0, &output)); - EXPECT_EQ(output, ""); - EXPECT_FALSE(Truncated("\xe1\x80\xbf", static_cast(-1), &output)); - EXPECT_FALSE(Truncated("\xe1\x80\xbf", 4, &output)); - - // Testing the truncation of valid UTF8 correctly - EXPECT_TRUE(Truncated("abc", 2, &output)); - EXPECT_EQ(output, "ab"); - EXPECT_TRUE(Truncated("\xc2\x81\xc2\x81", 2, &output)); - EXPECT_EQ(output.compare("\xc2\x81"), 0); - EXPECT_TRUE(Truncated("\xc2\x81\xc2\x81", 3, &output)); - EXPECT_EQ(output.compare("\xc2\x81"), 0); - EXPECT_FALSE(Truncated("\xc2\x81\xc2\x81", 4, &output)); - EXPECT_EQ(output.compare("\xc2\x81\xc2\x81"), 0); - - { - const char array[] = "\x00\x00\xc2\x81\xc2\x81"; - const std::string array_string(array, size(array)); - EXPECT_TRUE(Truncated(array_string, 4, &output)); - EXPECT_EQ(output.compare(std::string("\x00\x00\xc2\x81", 4)), 0); - } - - { - const char array[] = "\x00\xc2\x81\xc2\x81"; - const std::string array_string(array, size(array)); - EXPECT_TRUE(Truncated(array_string, 4, &output)); - EXPECT_EQ(output.compare(std::string("\x00\xc2\x81", 3)), 0); - } - - // Testing invalid UTF8 - EXPECT_TRUE(Truncated("\xed\xa0\x80\xed\xbf\xbf", 6, &output)); - EXPECT_EQ(output.compare(""), 0); - EXPECT_TRUE(Truncated("\xed\xa0\x8f", 3, &output)); - EXPECT_EQ(output.compare(""), 0); - EXPECT_TRUE(Truncated("\xed\xbf\xbf", 3, &output)); - EXPECT_EQ(output.compare(""), 0); - - // Testing invalid UTF8 mixed with valid UTF8 - EXPECT_FALSE(Truncated("\xe1\x80\xbf", 3, &output)); - EXPECT_EQ(output.compare("\xe1\x80\xbf"), 0); - EXPECT_FALSE(Truncated("\xf1\x80\xa0\xbf", 4, &output)); - EXPECT_EQ(output.compare("\xf1\x80\xa0\xbf"), 0); - EXPECT_FALSE(Truncated("a\xc2\x81\xe1\x80\xbf\xf1\x80\xa0\xbf", 10, &output)); - EXPECT_EQ(output.compare("a\xc2\x81\xe1\x80\xbf\xf1\x80\xa0\xbf"), 0); - EXPECT_TRUE( - Truncated("a\xc2\x81\xe1\x80\xbf\xf1" - "a" - "\x80\xa0", - 10, &output)); - EXPECT_EQ(output.compare("a\xc2\x81\xe1\x80\xbf\xf1" - "a"), - 0); - EXPECT_FALSE( - Truncated("\xef\xbb\xbf" - "abc", - 6, &output)); - EXPECT_EQ(output.compare("\xef\xbb\xbf" - "abc"), - 0); - - // Overlong sequences - EXPECT_TRUE(Truncated("\xc0\x80", 2, &output)); - EXPECT_EQ(output.compare(""), 0); - EXPECT_TRUE(Truncated("\xc1\x80\xc1\x81", 4, &output)); - EXPECT_EQ(output.compare(""), 0); - EXPECT_TRUE(Truncated("\xe0\x80\x80", 3, &output)); - EXPECT_EQ(output.compare(""), 0); - EXPECT_TRUE(Truncated("\xe0\x82\x80", 3, &output)); - EXPECT_EQ(output.compare(""), 0); - EXPECT_TRUE(Truncated("\xe0\x9f\xbf", 3, &output)); - EXPECT_EQ(output.compare(""), 0); - EXPECT_TRUE(Truncated("\xf0\x80\x80\x8D", 4, &output)); - EXPECT_EQ(output.compare(""), 0); - EXPECT_TRUE(Truncated("\xf0\x80\x82\x91", 4, &output)); - EXPECT_EQ(output.compare(""), 0); - EXPECT_TRUE(Truncated("\xf0\x80\xa0\x80", 4, &output)); - EXPECT_EQ(output.compare(""), 0); - EXPECT_TRUE(Truncated("\xf0\x8f\xbb\xbf", 4, &output)); - EXPECT_EQ(output.compare(""), 0); - EXPECT_TRUE(Truncated("\xf8\x80\x80\x80\xbf", 5, &output)); - EXPECT_EQ(output.compare(""), 0); - EXPECT_TRUE(Truncated("\xfc\x80\x80\x80\xa0\xa5", 6, &output)); - EXPECT_EQ(output.compare(""), 0); - - // Beyond U+10FFFF (the upper limit of Unicode codespace) - EXPECT_TRUE(Truncated("\xf4\x90\x80\x80", 4, &output)); - EXPECT_EQ(output.compare(""), 0); - EXPECT_TRUE(Truncated("\xf8\xa0\xbf\x80\xbf", 5, &output)); - EXPECT_EQ(output.compare(""), 0); - EXPECT_TRUE(Truncated("\xfc\x9c\xbf\x80\xbf\x80", 6, &output)); - EXPECT_EQ(output.compare(""), 0); - - // BOMs in UTF-16(BE|LE) and UTF-32(BE|LE) - EXPECT_TRUE(Truncated("\xfe\xff", 2, &output)); - EXPECT_EQ(output.compare(""), 0); - EXPECT_TRUE(Truncated("\xff\xfe", 2, &output)); - EXPECT_EQ(output.compare(""), 0); - - { - const char array[] = "\x00\x00\xfe\xff"; - const std::string array_string(array, size(array)); - EXPECT_TRUE(Truncated(array_string, 4, &output)); - EXPECT_EQ(output.compare(std::string("\x00\x00", 2)), 0); - } - - // Variants on the previous test - { - const char array[] = "\xff\xfe\x00\x00"; - const std::string array_string(array, 4); - EXPECT_FALSE(Truncated(array_string, 4, &output)); - EXPECT_EQ(output.compare(std::string("\xff\xfe\x00\x00", 4)), 0); - } - { - const char array[] = "\xff\x00\x00\xfe"; - const std::string array_string(array, size(array)); - EXPECT_TRUE(Truncated(array_string, 4, &output)); - EXPECT_EQ(output.compare(std::string("\xff\x00\x00", 3)), 0); - } - - // Non-characters : U+xxFFF[EF] where xx is 0x00 through 0x10 and - EXPECT_TRUE(Truncated("\xef\xbf\xbe", 3, &output)); - EXPECT_EQ(output.compare(""), 0); - EXPECT_TRUE(Truncated("\xf0\x8f\xbf\xbe", 4, &output)); - EXPECT_EQ(output.compare(""), 0); - EXPECT_TRUE(Truncated("\xf3\xbf\xbf\xbf", 4, &output)); - EXPECT_EQ(output.compare(""), 0); - EXPECT_TRUE(Truncated("\xef\xb7\x90", 3, &output)); - EXPECT_EQ(output.compare(""), 0); - EXPECT_TRUE(Truncated("\xef\xb7\xaf", 3, &output)); - EXPECT_EQ(output.compare(""), 0); - - // Strings in legacy encodings that are valid in UTF-8, but - // are invalid as UTF-8 in real data. - EXPECT_TRUE(Truncated("caf\xe9", 4, &output)); - EXPECT_EQ(output.compare("caf"), 0); - EXPECT_TRUE(Truncated("\xb0\xa1\xb0\xa2", 4, &output)); - EXPECT_EQ(output.compare(""), 0); - EXPECT_FALSE(Truncated("\xa7\x41\xa6\x6e", 4, &output)); - EXPECT_EQ(output.compare("\xa7\x41\xa6\x6e"), 0); - EXPECT_TRUE(Truncated("\xa7\x41\xa6\x6e\xd9\xee\xe4\xee", 7, &output)); - EXPECT_EQ(output.compare("\xa7\x41\xa6\x6e"), 0); - - // Testing using the same string as input and output. - EXPECT_FALSE(Truncated(output, 4, &output)); - EXPECT_EQ(output.compare("\xa7\x41\xa6\x6e"), 0); - EXPECT_TRUE(Truncated(output, 3, &output)); - EXPECT_EQ(output.compare("\xa7\x41"), 0); - - // "abc" with U+201[CD] in windows-125[0-8] - EXPECT_TRUE( - Truncated("\x93" - "abc\x94", - 5, &output)); - EXPECT_EQ(output.compare("\x93" - "abc"), - 0); - - // U+0639 U+064E U+0644 U+064E in ISO-8859-6 - EXPECT_TRUE(Truncated("\xd9\xee\xe4\xee", 4, &output)); - EXPECT_EQ(output.compare(""), 0); - - // U+03B3 U+03B5 U+03B9 U+03AC in ISO-8859-7 - EXPECT_TRUE(Truncated("\xe3\xe5\xe9\xdC", 4, &output)); - EXPECT_EQ(output.compare(""), 0); -} - -} // namespace utils -} // namespace nearby diff --git a/sharing/local_device_data/BUILD b/sharing/local_device_data/BUILD new file mode 100644 index 00000000..d2cbbe64 --- /dev/null +++ b/sharing/local_device_data/BUILD @@ -0,0 +1,97 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +licenses(["notice"]) + +cc_library( + name = "local_device_data", + srcs = [ + "nearby_share_local_device_data_manager.cc", + "nearby_share_local_device_data_manager_impl.cc", + ], + hdrs = [ + "nearby_share_local_device_data_manager.h", + "nearby_share_local_device_data_manager_impl.h", + ], + visibility = ["//visibility:public"], + deps = [ + "//internal/base", + "//internal/platform:types", + "//internal/platform/implementation:types", + "//sharing/common", + "//sharing/internal/api:platform", + "//sharing/internal/base:utf_utils", + "//sharing/internal/public:logging", + "//sharing/internal/public:types", + "//sharing/proto:share_cc_proto", + "//sharing/scheduling", + "@com_google_absl//absl/base:core_headers", + "@com_google_absl//absl/memory", + "@com_google_absl//absl/random", + "@com_google_absl//absl/status:statusor", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/synchronization", + "@com_google_absl//absl/time", + "@com_google_protobuf//:protobuf_lite", + ], +) + +cc_library( + name = "test_support", + testonly = True, + srcs = [ + "fake_nearby_share_local_device_data_manager.cc", + ], + hdrs = [ + "fake_nearby_share_local_device_data_manager.h", + ], + visibility = ["//visibility:public"], + deps = [ + ":local_device_data", + "//sharing/common", + "//sharing/internal/api:platform", + "//sharing/internal/public:types", + "//sharing/proto:share_cc_proto", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/types:optional", + ], +) + +cc_test( + name = "local_device_data_test", + srcs = [ + "nearby_share_local_device_data_manager_impl_test.cc", + ], + deps = [ + ":local_device_data", + "//internal/platform/implementation:types", + "//internal/platform/implementation/g3", # fixdeps: keep + "//internal/test", + "//sharing/common", + "//sharing/common:test_support", + "//sharing/internal/api:mock_sharing_platform", + "//sharing/internal/test:nearby_test", + "//sharing/proto:share_cc_proto", + "//sharing/scheduling", + "//sharing/scheduling:test_support", + "@com_github_protobuf_matchers//protobuf-matchers", + "@com_google_absl//absl/container:flat_hash_map", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:statusor", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/time", + "@com_google_absl//absl/types:optional", + "@com_google_googletest//:gtest_main", + ], +) diff --git a/sharing/local_device_data/fake_nearby_share_local_device_data_manager.cc b/sharing/local_device_data/fake_nearby_share_local_device_data_manager.cc new file mode 100644 index 00000000..abd347d8 --- /dev/null +++ b/sharing/local_device_data/fake_nearby_share_local_device_data_manager.cc @@ -0,0 +1,179 @@ +// Copyright 2021-2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/local_device_data/fake_nearby_share_local_device_data_manager.h" + +#include +#include +#include +#include +#include + +#include "absl/strings/string_view.h" +#include "absl/types/optional.h" +#include "sharing/common/nearby_share_enums.h" +#include "sharing/internal/api/sharing_rpc_client.h" +#include "sharing/internal/public/context.h" +#include "sharing/local_device_data/nearby_share_local_device_data_manager.h" +#include "sharing/proto/rpc_resources.pb.h" + +namespace nearby { +namespace sharing { +class NearbyShareClientFactory; +class NearbyShareProfileInfoProvider; + +namespace { + +using ::nearby::sharing::api::SharingRpcClientFactory; + +constexpr absl::string_view kDefaultId = "123456789A"; +constexpr absl::string_view kDefaultDeviceName = "Barack's Chromebook"; + +} // namespace + +FakeNearbyShareLocalDeviceDataManager::Factory::Factory() = default; + +FakeNearbyShareLocalDeviceDataManager::Factory::~Factory() = default; + +std::unique_ptr +FakeNearbyShareLocalDeviceDataManager::Factory::CreateInstance( + nearby::Context* context, SharingRpcClientFactory* rpc_client_factory, + NearbyShareProfileInfoProvider* profile_info_provider) { + latest_rpc_client_factory_ = rpc_client_factory; + latest_profile_info_provider_ = profile_info_provider; + + auto instance = std::make_unique( + kDefaultDeviceName); + instances_.push_back(instance.get()); + + return instance; +} + +FakeNearbyShareLocalDeviceDataManager::UploadContactsCall::UploadContactsCall( + std::vector contacts, + UploadCompleteCallback callback) + : contacts(std::move(contacts)), callback(std::move(callback)) {} + +FakeNearbyShareLocalDeviceDataManager::UploadContactsCall::UploadContactsCall( + UploadContactsCall&&) = default; + +FakeNearbyShareLocalDeviceDataManager::UploadContactsCall:: + ~UploadContactsCall() = default; + +FakeNearbyShareLocalDeviceDataManager::UploadCertificatesCall:: + UploadCertificatesCall( + std::vector certificates, + UploadCompleteCallback callback) + : certificates(std::move(certificates)), callback(std::move(callback)) {} + +FakeNearbyShareLocalDeviceDataManager::UploadCertificatesCall:: + UploadCertificatesCall(UploadCertificatesCall&&) = default; + +FakeNearbyShareLocalDeviceDataManager::UploadCertificatesCall:: + ~UploadCertificatesCall() = default; + +FakeNearbyShareLocalDeviceDataManager::FakeNearbyShareLocalDeviceDataManager( + absl::string_view default_device_name) + : id_(kDefaultId), device_name_(default_device_name) {} + +FakeNearbyShareLocalDeviceDataManager:: + ~FakeNearbyShareLocalDeviceDataManager() = default; + +std::string FakeNearbyShareLocalDeviceDataManager::GetId() { return id_; } + +std::string FakeNearbyShareLocalDeviceDataManager::GetDeviceName() const { + return device_name_; +} + +std::optional FakeNearbyShareLocalDeviceDataManager::GetFullName() + const { + return full_name_; +} + +std::optional FakeNearbyShareLocalDeviceDataManager::GetIconUrl() + const { + return icon_url_; +} + +DeviceNameValidationResult +FakeNearbyShareLocalDeviceDataManager::ValidateDeviceName( + absl::string_view name) { + return next_validation_result_; +} + +DeviceNameValidationResult FakeNearbyShareLocalDeviceDataManager::SetDeviceName( + absl::string_view name) { + if (next_validation_result_ != DeviceNameValidationResult::kValid) + return next_validation_result_; + + if (device_name_ != name) { + device_name_ = std::string(name); + NotifyLocalDeviceDataChanged( + /*did_device_name_change=*/true, + /*did_full_name_change=*/false, + /*did_icon_change=*/false); + } + + return DeviceNameValidationResult::kValid; +} + +void FakeNearbyShareLocalDeviceDataManager::DownloadDeviceData() { + ++num_download_device_data_calls_; +} + +void FakeNearbyShareLocalDeviceDataManager::UploadContacts( + std::vector contacts, + UploadCompleteCallback callback) { + upload_contacts_calls_.emplace_back(std::move(contacts), callback); + if (is_sync_mode_) { + callback(upload_contact_result_); + } +} + +void FakeNearbyShareLocalDeviceDataManager::UploadCertificates( + std::vector certificates, + UploadCompleteCallback callback) { + upload_certificates_calls_.emplace_back(std::move(certificates), callback); + if (is_sync_mode_) { + callback(upload_certificate_result_); + } +} +void FakeNearbyShareLocalDeviceDataManager::SetFullName( + const absl::optional& full_name) { + if (full_name_ == full_name) return; + + full_name_ = full_name; + NotifyLocalDeviceDataChanged( + /*did_device_name_change=*/false, + /*did_full_name_change=*/true, + /*did_icon_change=*/false); +} + +void FakeNearbyShareLocalDeviceDataManager::SetIconUrl( + const absl::optional& icon_url) { + if (icon_url_ == icon_url) return; + + icon_url_ = icon_url; + NotifyLocalDeviceDataChanged( + /*did_device_name_change=*/false, + /*did_full_name_change=*/false, + /*did_icon_change=*/true); +} + +void FakeNearbyShareLocalDeviceDataManager::OnStart() {} + +void FakeNearbyShareLocalDeviceDataManager::OnStop() {} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/local_device_data/fake_nearby_share_local_device_data_manager.h b/sharing/local_device_data/fake_nearby_share_local_device_data_manager.h new file mode 100644 index 00000000..35cac526 --- /dev/null +++ b/sharing/local_device_data/fake_nearby_share_local_device_data_manager.h @@ -0,0 +1,176 @@ +// Copyright 2021-2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_LOCAL_DEVICE_DATA_FAKE_NEARBY_SHARE_LOCAL_DEVICE_DATA_MANAGER_H_ +#define THIRD_PARTY_NEARBY_SHARING_LOCAL_DEVICE_DATA_FAKE_NEARBY_SHARE_LOCAL_DEVICE_DATA_MANAGER_H_ + +#include + +#include +#include +#include +#include + +#include "absl/strings/string_view.h" +#include "sharing/common/nearby_share_enums.h" +#include "sharing/internal/api/sharing_rpc_client.h" +#include "sharing/internal/public/context.h" +#include "sharing/local_device_data/nearby_share_local_device_data_manager.h" +#include "sharing/local_device_data/nearby_share_local_device_data_manager_impl.h" +#include "sharing/proto/rpc_resources.pb.h" + +namespace nearby { +namespace sharing { + +// A fake implementation of NearbyShareLocalDeviceDataManager, along with a fake +// factory, to be used in tests. +class FakeNearbyShareLocalDeviceDataManager + : public NearbyShareLocalDeviceDataManager { + public: + // Factory that creates FakeNearbyShareLocalDeviceDataManager instances. Use + // in NearbyShareLocalDeviceDataManagerImpl::Factory::SetFactoryForTesting() + // in unit tests. + class Factory : public NearbyShareLocalDeviceDataManagerImpl::Factory { + public: + Factory(); + ~Factory() override; + + // Returns all FakeNearbyShareLocalDeviceDataManager instances created by + // CreateInstance(). + std::vector& instances() { + return instances_; + } + + nearby::sharing::api::SharingRpcClientFactory* latest_rpc_client_factory() + const { + return latest_rpc_client_factory_; + } + + NearbyShareProfileInfoProvider* latest_profile_info_provider() const { + return latest_profile_info_provider_; + } + + protected: + std::unique_ptr CreateInstance( + nearby::Context* context, + nearby::sharing::api::SharingRpcClientFactory* rpc_client_factory, + NearbyShareProfileInfoProvider* profile_info_provider) override; + + private: + std::vector instances_; + nearby::sharing::api::SharingRpcClientFactory* latest_rpc_client_factory_ = + nullptr; + NearbyShareProfileInfoProvider* latest_profile_info_provider_ = nullptr; + }; + + struct UploadContactsCall { + UploadContactsCall(std::vector contacts, + UploadCompleteCallback callback); + UploadContactsCall(UploadContactsCall&&); + ~UploadContactsCall(); + + std::vector contacts; + UploadCompleteCallback callback; + }; + + struct UploadCertificatesCall { + UploadCertificatesCall( + std::vector certificates, + UploadCompleteCallback callback); + UploadCertificatesCall(UploadCertificatesCall&&); + ~UploadCertificatesCall(); + + std::vector certificates; + UploadCompleteCallback callback; + }; + + explicit FakeNearbyShareLocalDeviceDataManager( + absl::string_view default_device_name); + ~FakeNearbyShareLocalDeviceDataManager() override; + + // NearbyShareLocalDeviceDataManager: + std::string GetId() override; + std::string GetDeviceName() const override; + std::optional GetFullName() const override; + std::optional GetIconUrl() const override; + DeviceNameValidationResult ValidateDeviceName( + absl::string_view name) override; + DeviceNameValidationResult SetDeviceName(absl::string_view name) override; + void DownloadDeviceData() override; + void UploadContacts(std::vector contacts, + UploadCompleteCallback callback) override; + void UploadCertificates( + std::vector certificates, + UploadCompleteCallback callback) override; + + // Make protected observer-notification methods from the base class public in + // this fake class. + using NearbyShareLocalDeviceDataManager::NotifyLocalDeviceDataChanged; + + void SetId(absl::string_view id) { id_ = std::string(id); } + void SetFullName(const std::optional& full_name); + void SetIconUrl(const std::optional& icon_url); + + size_t num_download_device_data_calls() const { + return num_download_device_data_calls_; + } + + std::vector& upload_contacts_calls() { + return upload_contacts_calls_; + } + + std::vector& upload_certificates_calls() { + return upload_certificates_calls_; + } + + void set_next_validation_result(DeviceNameValidationResult result) { + next_validation_result_ = result; + } + + // methods for synchronization test. + void set_is_sync_mode(bool is_sync_mode) { is_sync_mode_ = is_sync_mode; } + + void SetUploadContactsResult(bool upload_contact_result) { + upload_contact_result_ = upload_contact_result; + } + + void SetUploadCertificatesResult(bool upload_certificate_result) { + upload_certificate_result_ = upload_certificate_result; + } + + private: + // NearbyShareLocalDeviceDataManager: + void OnStart() override; + void OnStop() override; + + std::string id_; + std::string device_name_; + std::optional full_name_; + std::optional icon_url_; + size_t num_download_device_data_calls_ = 0; + std::vector upload_contacts_calls_; + std::vector upload_certificates_calls_; + DeviceNameValidationResult next_validation_result_ = + DeviceNameValidationResult::kValid; + + // Used to indicate whether the class is running in synchronization mode. + bool is_sync_mode_ = false; + bool upload_contact_result_ = false; + bool upload_certificate_result_ = false; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_LOCAL_DEVICE_DATA_FAKE_NEARBY_SHARE_LOCAL_DEVICE_DATA_MANAGER_H_ diff --git a/sharing/local_device_data/nearby_share_local_device_data_manager.cc b/sharing/local_device_data/nearby_share_local_device_data_manager.cc new file mode 100644 index 00000000..f9165cd2 --- /dev/null +++ b/sharing/local_device_data/nearby_share_local_device_data_manager.cc @@ -0,0 +1,69 @@ +// Copyright 2021-2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/local_device_data/nearby_share_local_device_data_manager.h" + +#include + +#include "sharing/internal/public/logging.h" + +namespace nearby { +namespace sharing { + +const size_t kNearbyShareDeviceNameMaxLength = 32; + +NearbyShareLocalDeviceDataManager::NearbyShareLocalDeviceDataManager() = + default; + +NearbyShareLocalDeviceDataManager::~NearbyShareLocalDeviceDataManager() = + default; + +void NearbyShareLocalDeviceDataManager::AddObserver(Observer* observer) { + observers_.AddObserver(observer); +} + +void NearbyShareLocalDeviceDataManager::RemoveObserver(Observer* observer) { + observers_.RemoveObserver(observer); +} + +void NearbyShareLocalDeviceDataManager::Start() { + if (is_running_) return; + + is_running_ = true; + OnStart(); +} + +void NearbyShareLocalDeviceDataManager::Stop() { + if (!is_running_) return; + + is_running_ = false; + OnStop(); +} + +void NearbyShareLocalDeviceDataManager::NotifyLocalDeviceDataChanged( + bool did_device_name_change, bool did_full_name_change, + bool did_icon_change) { + NL_LOG(INFO) << __func__ << ": did_device_name_change=" + << (did_device_name_change ? "true" : "false") + << ", did_full_name_change=" + << (did_full_name_change ? "true" : "false") + << ", did_icon_change=" << (did_icon_change ? "true" : "false"); + for (auto& observer : observers_.GetObservers()) { + observer->OnLocalDeviceDataChanged(did_device_name_change, + did_full_name_change, did_icon_change); + } +} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/local_device_data/nearby_share_local_device_data_manager.h b/sharing/local_device_data/nearby_share_local_device_data_manager.h new file mode 100644 index 00000000..ef4f9fc2 --- /dev/null +++ b/sharing/local_device_data/nearby_share_local_device_data_manager.h @@ -0,0 +1,134 @@ +// Copyright 2021-2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_LOCAL_DEVICE_DATA_NEARBY_SHARE_LOCAL_DEVICE_DATA_MANAGER_H_ +#define THIRD_PARTY_NEARBY_SHARING_LOCAL_DEVICE_DATA_NEARBY_SHARE_LOCAL_DEVICE_DATA_MANAGER_H_ + +#include + +#include +#include +#include +#include + +#include "absl/strings/string_view.h" +#include "internal/base/observer_list.h" +#include "sharing/common/nearby_share_enums.h" +#include "sharing/proto/rpc_resources.pb.h" + +namespace nearby { +namespace sharing { + +// The maximum length in bytes allowed for a device name, as encoded in UTF-8 in +// a std::string, which will not contain a null terminator. +extern const size_t kNearbyShareDeviceNameMaxLength; + +// Manages local device data related to the UpdateDevice RPC such as the device +// ID, name, and icon URL; provides the user's full name and icon URL returned +// from the Nearby server; and handles uploading contacts and certificates to +// the Nearby server. The uploading of contacts and certificates might seem out +// of place, but this class is the entry point for all UpdateDevice RPC calls. +class NearbyShareLocalDeviceDataManager { + public: + class Observer { + public: + virtual ~Observer() = default; + + virtual void OnLocalDeviceDataChanged(bool did_device_name_change, + bool did_full_name_change, + bool did_icon_change) = 0; + }; + + using UploadCompleteCallback = std::function; + + NearbyShareLocalDeviceDataManager(); + virtual ~NearbyShareLocalDeviceDataManager(); + + void AddObserver(Observer* observer); + void RemoveObserver(Observer* observer); + + // Starts/Stops local-device-data task scheduling. + void Start(); + void Stop(); + bool is_running() { return is_running_; } + + // Returns the immutable ID generated for the local device, used to + // differentiate a user's devices when communicating with the Nearby server. + virtual std::string GetId() = 0; + + // Returns the name of the local device, for example, "Josh's Chromebook." + // This can be modified by SetDeviceName(). + virtual std::string GetDeviceName() const = 0; + + // Returns the user's full name, for example, "Barack Obama". Returns + // absl::nullopt if the name has not yet been set from an UpdateDevice RPC + // response. + virtual std::optional GetFullName() const = 0; + + // Returns the URL of the user's image. Returns absl::nullopt if the URL has + // not yet been set from an UpdateDevice RPC response. + virtual std::optional GetIconUrl() const = 0; + + // Validates the provided device name and returns an error if validation + // fails. This is just a check and the device name is not persisted. + virtual DeviceNameValidationResult ValidateDeviceName( + absl::string_view name) = 0; + + // Sets and persists the device name in prefs. The device name is first + // validated and if validation fails and error is returned and the device name + // is not persisted. The device name is *not* uploaded to the Nearby Share + // server; the UpdateDevice proto device_name field in an artifact. Observers + // are notified via OnLocalDeviceDataChanged() if the device name changes. + virtual DeviceNameValidationResult SetDeviceName(absl::string_view name) = 0; + + // Makes an UpdateDevice RPC call to the Nearby Share server to retrieve all + // available device data, which includes the full name and icon URL for now. + // This action is also scheduled periodically. Observers are notified via + // OnLocalDeviceDataChanged() if any device data changes. + virtual void DownloadDeviceData() = 0; + + // Uses the UpdateDevice RPC to send the local device's contact list to the + // Nearby Share server, including which contacts are allowed for + // selected-contacts visibility mode. This should only be invoked by the + // contact manager, and the contact manager should handle scheduling, failure + // retry, etc. + virtual void UploadContacts( + std::vector contacts, + UploadCompleteCallback callback) = 0; + + // Uses the UpdateDevice RPC to send the local device's public certificates to + // the Nearby Share server. This should only be invoked by the certificate + // manager, and the certificate manager should handle scheduling, failure + // retry, etc. + virtual void UploadCertificates( + std::vector certificates, + UploadCompleteCallback callback) = 0; + + protected: + virtual void OnStart() = 0; + virtual void OnStop() = 0; + + void NotifyLocalDeviceDataChanged(bool did_device_name_change, + bool did_full_name_change, + bool did_icon_change); + + private: + bool is_running_ = false; + nearby::ObserverList observers_; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_LOCAL_DEVICE_DATA_NEARBY_SHARE_LOCAL_DEVICE_DATA_MANAGER_H_ diff --git a/sharing/local_device_data/nearby_share_local_device_data_manager_impl.cc b/sharing/local_device_data/nearby_share_local_device_data_manager_impl.cc new file mode 100644 index 00000000..01dc86a0 --- /dev/null +++ b/sharing/local_device_data/nearby_share_local_device_data_manager_impl.cc @@ -0,0 +1,414 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/local_device_data/nearby_share_local_device_data_manager_impl.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "absl/memory/memory.h" +#include "absl/random/random.h" +#include "absl/status/statusor.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "absl/strings/substitute.h" +#include "absl/time/time.h" +#include "internal/platform/device_info.h" +#include "internal/platform/implementation/account_manager.h" +#include "sharing/common/nearby_share_enums.h" +#include "sharing/common/nearby_share_prefs.h" +#include "sharing/common/nearby_share_profile_info_provider.h" +#include "sharing/internal/api/preference_manager.h" +#include "sharing/internal/api/sharing_rpc_client.h" +#include "sharing/internal/base/utf_string_conversions.h" +#include "sharing/internal/public/context.h" +#include "sharing/internal/public/logging.h" +#include "sharing/local_device_data/nearby_share_local_device_data_manager.h" +#include "sharing/proto/device_rpc.pb.h" +#include "sharing/proto/field_mask.pb.h" +#include "sharing/proto/rpc_resources.pb.h" +#include "sharing/scheduling/nearby_share_scheduler.h" +#include "sharing/scheduling/nearby_share_scheduler_factory.h" + +namespace nearby { +namespace sharing { +namespace { +using ::nearby::sharing::api::PreferenceManager; +using ::nearby::sharing::api::SharingRpcClientFactory; +using ::nearby::sharing::proto::UpdateDeviceRequest; +using ::nearby::sharing::proto::UpdateDeviceResponse; + +// Using the alphanumeric characters below, this provides 36^10 unique device +// IDs. Note that the uniqueness requirement is not global; the IDs are only +// used to differentiate between devices associated with a single GAIA account. +// This ID length agrees with the GmsCore implementation. +constexpr size_t kDeviceIdLength = 10; + +// Possible characters used in a randomly generated device ID. This agrees with +// the GmsCore implementation. +constexpr std::array kAlphaNumericChars = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', + 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', + 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}; + +constexpr absl::string_view kDeviceIdPrefix = "users/me/devices/"; +constexpr absl::string_view kContactsFieldMaskPath = "contacts"; +constexpr absl::string_view kCertificatesFieldMaskPath = "public_certificates"; + +constexpr absl::Duration kDeviceDataDownloadPeriod = absl::Hours(12); + +constexpr absl::string_view kDefaultDeviceName = "$0\'s $1"; + +// Returns a truncated version of |name| that is |overflow_length| characters +// too long. For example, name="Reallylongname" with overflow_length=5 will +// return "Really...". +std::string GetTruncatedName(std::string name, size_t overflow_length) { + std::string ellipsis("..."); + size_t max_name_length = name.length() - overflow_length - ellipsis.length(); + // DCHECK_GT(max_name_length, 0u); + + std::string truncated; + nearby::utils::TruncateUtf8ToByteSize(name, max_name_length, &truncated); + truncated.append(ellipsis); + return truncated; +} + +} // namespace + +// static +NearbyShareLocalDeviceDataManagerImpl::Factory* + NearbyShareLocalDeviceDataManagerImpl::Factory::test_factory_ = nullptr; + +// static +std::unique_ptr +NearbyShareLocalDeviceDataManagerImpl::Factory::Create( + Context* context, PreferenceManager& preference_manager, + AccountManager& account_manager, nearby::DeviceInfo& device_info, + SharingRpcClientFactory* rpc_client_factory, + NearbyShareProfileInfoProvider* profile_info_provider) { + if (test_factory_) { + return test_factory_->CreateInstance(context, rpc_client_factory, + profile_info_provider); + } + + return absl::WrapUnique(new NearbyShareLocalDeviceDataManagerImpl( + context, preference_manager, account_manager, device_info, + rpc_client_factory, profile_info_provider)); +} + +// static +void NearbyShareLocalDeviceDataManagerImpl::Factory::SetFactoryForTesting( + Factory* test_factory) { + test_factory_ = test_factory; +} + +NearbyShareLocalDeviceDataManagerImpl::Factory::~Factory() = default; + +NearbyShareLocalDeviceDataManagerImpl::NearbyShareLocalDeviceDataManagerImpl( + Context* context, PreferenceManager& preference_manager, + AccountManager& account_manager, nearby::DeviceInfo& device_info, + SharingRpcClientFactory* rpc_client_factory, + NearbyShareProfileInfoProvider* profile_info_provider) + : preference_manager_(preference_manager), + account_manager_(account_manager), + device_info_(device_info), + profile_info_provider_(profile_info_provider), + nearby_share_client_(rpc_client_factory->CreateInstance()), + device_id_(GetId()), + download_device_data_scheduler_( + NearbyShareSchedulerFactory::CreatePeriodicScheduler( + context, preference_manager_, kDeviceDataDownloadPeriod, + /*retry_failures=*/true, + /*require_connectivity=*/true, + prefs::kNearbySharingSchedulerDownloadDeviceDataName, + [&]() { DownloadDeviceData(); })), + executor_(context->CreateSequencedTaskRunner()) {} + +NearbyShareLocalDeviceDataManagerImpl:: + ~NearbyShareLocalDeviceDataManagerImpl() = default; + +std::string NearbyShareLocalDeviceDataManagerImpl::GetId() { + std::string id = + preference_manager_.GetString(prefs::kNearbySharingDeviceIdName, ""); + if (!id.empty()) return id; + + absl::BitGen bitgen; + for (size_t i = 0; i < kDeviceIdLength; ++i) + id += kAlphaNumericChars[absl::Uniform( + bitgen, 0, static_cast(kAlphaNumericChars.size()))]; + + preference_manager_.SetString(prefs::kNearbySharingDeviceIdName, id); + + return id; +} + +std::string NearbyShareLocalDeviceDataManagerImpl::GetDeviceName() const { + std::string device_name = preference_manager_.GetString( + prefs::kNearbySharingDeviceNameName, std::string()); + return device_name.empty() ? GetDefaultDeviceName() : device_name; +} + +std::optional NearbyShareLocalDeviceDataManagerImpl::GetFullName() + const { + return preference_manager_.GetString(prefs::kNearbySharingFullNameName, + std::string()); +} + +std::optional NearbyShareLocalDeviceDataManagerImpl::GetIconUrl() + const { + return preference_manager_.GetString(prefs::kNearbySharingIconUrlName, + std::string()); +} + +std::optional NearbyShareLocalDeviceDataManagerImpl::GetIconToken() + const { + return preference_manager_.GetString(prefs::kNearbySharingIconTokenName, + std::string()); +} + +DeviceNameValidationResult +NearbyShareLocalDeviceDataManagerImpl::ValidateDeviceName( + absl::string_view name) { + if (name.empty()) return DeviceNameValidationResult::kErrorEmpty; + + if (!nearby::utils::IsStringUtf8(std::string_view(name.data(), name.size()))) + return DeviceNameValidationResult::kErrorNotValidUtf8; + + if (name.length() > kNearbyShareDeviceNameMaxLength) + return DeviceNameValidationResult::kErrorTooLong; + + return DeviceNameValidationResult::kValid; +} + +DeviceNameValidationResult NearbyShareLocalDeviceDataManagerImpl::SetDeviceName( + absl::string_view name) { + if (name == GetDeviceName()) return DeviceNameValidationResult::kValid; + + auto error = ValidateDeviceName(name); + if (error != DeviceNameValidationResult::kValid) return error; + + preference_manager_.SetString(prefs::kNearbySharingDeviceNameName, name); + + NotifyLocalDeviceDataChanged(/*did_device_name_change=*/true, + /*did_full_name_change=*/false, + /*did_icon_change=*/false); + + return DeviceNameValidationResult::kValid; +} + +void NearbyShareLocalDeviceDataManagerImpl::DownloadDeviceData() { + executor_->PostTask([&]() { + NL_LOG(INFO) << __func__ << ": started"; + if (!is_running()) { + NL_LOG(WARNING) << "DownloadDeviceData: skip to download device data due " + "to manager is stopped."; + return; + } + + if (!account_manager_.GetCurrentAccount().has_value()) { + NL_LOG(WARNING) << __func__ + << ": skip to download device data due " + "to no login account."; + download_device_data_scheduler_->HandleResult(/*success=*/true); + return; + } + + UpdateDeviceRequest request; + request.mutable_device()->set_name( + absl::StrCat(kDeviceIdPrefix, device_id_)); + nearby_share_client_->UpdateDevice( + request, [this](const absl::StatusOr& response) { + // check whether the manager is running again + if (!is_running()) { + NL_LOG(WARNING) + << "DownloadDeviceData: skip to download device data due " + "to manager is stopped."; + return; + } + + if (response.ok()) { + NL_LOG(WARNING) << "DownloadDeviceData: Got response from backend."; + HandleUpdateDeviceResponse(*response); + } else { + NL_LOG(WARNING) + << "DownloadDeviceData: Failed to get response from backend."; + } + + download_device_data_scheduler_->HandleResult( + /*success=*/response.ok()); + }); + }); +} + +void NearbyShareLocalDeviceDataManagerImpl::UploadContacts( + std::vector contacts, + UploadCompleteCallback callback) { + executor_->PostTask( + [&, contacts = std::move(contacts), callback = std::move(callback)]() { + NL_LOG(INFO) << __func__ << ": size=" << contacts.size(); + if (!is_running()) { + NL_LOG(WARNING) << "UploadContacts: skip to upload contacts due " + "to manager is stopped."; + callback(false); + return; + } + + if (!account_manager_.GetCurrentAccount().has_value()) { + NL_LOG(WARNING) << __func__ + << ": skip to upload contacts due " + "to no login account."; + callback(/*success=*/true); + return; + } + + UpdateDeviceRequest request; + request.mutable_device()->set_name( + absl::StrCat(kDeviceIdPrefix, device_id_)); + request.mutable_device()->mutable_contacts()->Add(contacts.begin(), + contacts.end()); + request.mutable_update_mask()->add_paths( + std::string(kContactsFieldMaskPath)); + nearby_share_client_->UpdateDevice( + request, [callback = std::move(callback)]( + const absl::StatusOr& response) { + callback(/*success=*/response.ok()); + }); + }); +} + +void NearbyShareLocalDeviceDataManagerImpl::UploadCertificates( + std::vector certificates, + UploadCompleteCallback callback) { + executor_->PostTask([&, certificates = std::move(certificates), + callback = std::move(callback)]() { + NL_LOG(INFO) << __func__ << ": Upload " << certificates.size() + << " certificates."; + if (!is_running()) { + NL_LOG(WARNING) << "UploadContacts: skip to upload certificates due " + "to manager is stopped."; + callback(false); + return; + } + + if (!account_manager_.GetCurrentAccount().has_value()) { + NL_LOG(WARNING) << __func__ + << ": skip to upload certificates due " + "to no login account."; + callback(/*success=*/true); + return; + } + + UpdateDeviceRequest request; + request.mutable_device()->set_name( + absl::StrCat(kDeviceIdPrefix, device_id_)); + request.mutable_device()->mutable_public_certificates()->Add( + certificates.begin(), certificates.end()); + request.mutable_update_mask()->add_paths( + std::string(kCertificatesFieldMaskPath)); + nearby_share_client_->UpdateDevice( + request, [this, callback = std::move(callback)]( + const absl::StatusOr& response) { + // check whether the manager is running again + if (!is_running()) { + NL_LOG(WARNING) + << "DownloadDeviceData: skip to upload certificates due " + "to manager is stopped."; + callback(false); + return; + } + + callback(/*success=*/response.ok()); + }); + }); +} + +void NearbyShareLocalDeviceDataManagerImpl::OnStart() { + // This schedules an immediate download of the full name and icon URL from the + // server if that has never happened before. + download_device_data_scheduler_->Start(); +} + +void NearbyShareLocalDeviceDataManagerImpl::OnStop() { + download_device_data_scheduler_->Stop(); +} + +std::string NearbyShareLocalDeviceDataManagerImpl::GetDefaultDeviceName() + const { + std::string device_type_name = device_info_.GetDeviceTypeName(); + std::string device_name = device_info_.GetOsDeviceName(); + std::optional given_name = + profile_info_provider_->GetGivenName(); + if (!given_name.has_value()) return device_name; + + std::string default_device_name = + absl::Substitute(kDefaultDeviceName, *given_name, device_type_name); + + if (default_device_name.length() <= kNearbyShareDeviceNameMaxLength) + return default_device_name; + + std::string truncated_name = + GetTruncatedName(*given_name, default_device_name.length() - + kNearbyShareDeviceNameMaxLength); + + return absl::Substitute(kDefaultDeviceName, truncated_name, device_type_name); +} + +void NearbyShareLocalDeviceDataManagerImpl::HandleUpdateDeviceResponse( + const std::optional& + response) { + if (!response) return; + + bool did_full_name_change = response->person_name() != GetFullName(); + if (did_full_name_change) { + preference_manager_.SetString(prefs::kNearbySharingFullNameName, + response->person_name()); + } + + // NOTE(http://crbug.com/1211189): An icon URL can change without the + // underlying image changing. For example, icon URLs for some child accounts + // can rotate on every UpdateDevice RPC call; a timestamp is included in the + // URL. The icon token is used to detect changes in the underlying image. If a + // new URL is sent and the token doesn't change, the old URL may still be + // valid for a couple of weeks, for example. So, private certificates do not + // necessarily need to update the icon URL whenever it changes. Also, we don't + // expect the token to change without the URL changing; regardless, we don't + // consider the icon changed unless the URL changes. That way, private + // certificates will not be unnecessarily regenerated. + bool did_icon_url_change = response->image_url() != GetIconUrl(); + bool did_icon_token_change = response->image_token() != GetIconToken(); + bool did_icon_change = did_icon_url_change && did_icon_token_change; + if (did_icon_url_change) { + preference_manager_.SetString(prefs::kNearbySharingIconUrlName, + response->image_url()); + } + if (did_icon_token_change) { + preference_manager_.SetString(prefs::kNearbySharingIconTokenName, + response->image_token()); + } + + if (!did_full_name_change && !did_icon_change) return; + + NotifyLocalDeviceDataChanged(/*did_device_name_change=*/false, + did_full_name_change, did_icon_change); +} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/local_device_data/nearby_share_local_device_data_manager_impl.h b/sharing/local_device_data/nearby_share_local_device_data_manager_impl.h new file mode 100644 index 00000000..ebe5fdff --- /dev/null +++ b/sharing/local_device_data/nearby_share_local_device_data_manager_impl.h @@ -0,0 +1,121 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_LOCAL_DEVICE_DATA_NEARBY_SHARE_LOCAL_DEVICE_DATA_MANAGER_IMPL_H_ +#define THIRD_PARTY_NEARBY_SHARING_LOCAL_DEVICE_DATA_NEARBY_SHARE_LOCAL_DEVICE_DATA_MANAGER_IMPL_H_ + +#include +#include +#include +#include + +#include "absl/strings/string_view.h" +#include "internal/platform/device_info.h" +#include "internal/platform/implementation/account_manager.h" +#include "internal/platform/task_runner.h" +#include "sharing/common/nearby_share_enums.h" +#include "sharing/internal/api/preference_manager.h" +#include "sharing/internal/api/sharing_rpc_client.h" +#include "sharing/internal/public/context.h" +#include "sharing/local_device_data/nearby_share_local_device_data_manager.h" +#include "sharing/proto/rpc_resources.pb.h" + +namespace nearby { +namespace sharing { + +class NearbyShareProfileInfoProvider; +class NearbyShareScheduler; + +// Implementation of NearbyShareLocalDeviceDataManager that persists device data +// in prefs. All RPC-related calls are guarded by a timeout, so callbacks are +// guaranteed to be invoked. In addition to supporting on-demand device-data +// downloads, this implementation schedules periodic downloads of device +// data--full name and icon URL--from the server. +class NearbyShareLocalDeviceDataManagerImpl + : public NearbyShareLocalDeviceDataManager { + public: + class Factory { + public: + static std::unique_ptr Create( + Context* context, + nearby::sharing::api::PreferenceManager& preference_manager, + AccountManager& account_manager, nearby::DeviceInfo& device_info, + nearby::sharing::api::SharingRpcClientFactory* rpc_client_factory, + NearbyShareProfileInfoProvider* profile_info_provider); + static void SetFactoryForTesting(Factory* test_factory); + + protected: + virtual ~Factory(); + virtual std::unique_ptr CreateInstance( + Context* context, + nearby::sharing::api::SharingRpcClientFactory* rpc_client_factory, + NearbyShareProfileInfoProvider* profile_info_provider) = 0; + + private: + static Factory* test_factory_; + }; + + ~NearbyShareLocalDeviceDataManagerImpl() override; + + private: + NearbyShareLocalDeviceDataManagerImpl( + Context* context, + nearby::sharing::api::PreferenceManager& preference_manager, + AccountManager& account_manager, nearby::DeviceInfo& device_info, + nearby::sharing::api::SharingRpcClientFactory* rpc_client_factory, + NearbyShareProfileInfoProvider* profile_info_provider); + + // NearbyShareLocalDeviceDataManager: + std::string GetId() override; + std::string GetDeviceName() const override; + std::optional GetFullName() const override; + std::optional GetIconUrl() const override; + DeviceNameValidationResult ValidateDeviceName( + absl::string_view name) override; + DeviceNameValidationResult SetDeviceName(absl::string_view name) override; + void DownloadDeviceData() override; + void UploadContacts(std::vector contacts, + UploadCompleteCallback callback) override; + void UploadCertificates( + std::vector certificates, + UploadCompleteCallback callback) override; + void OnStart() override; + void OnStop() override; + + std::optional GetIconToken() const; + + // Creates a default device name of the form "'s ." + // For example, "Josh's Chromebook." If a given name cannot be found, returns + // just the device type. If the resulting name is too long the user's name + // will be truncated, for example "Mi...'s Chromebook." + std::string GetDefaultDeviceName() const; + + void HandleUpdateDeviceResponse( + const std::optional& + response); + + nearby::sharing::api::PreferenceManager& preference_manager_; + AccountManager& account_manager_; + nearby::DeviceInfo& device_info_; + NearbyShareProfileInfoProvider* const profile_info_provider_; + std::unique_ptr nearby_share_client_; + const std::string device_id_; + std::unique_ptr download_device_data_scheduler_; + std::unique_ptr executor_; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_LOCAL_DEVICE_DATA_NEARBY_SHARE_LOCAL_DEVICE_DATA_MANAGER_IMPL_H_ diff --git a/sharing/local_device_data/nearby_share_local_device_data_manager_impl_test.cc b/sharing/local_device_data/nearby_share_local_device_data_manager_impl_test.cc new file mode 100644 index 00000000..cfe649b2 --- /dev/null +++ b/sharing/local_device_data/nearby_share_local_device_data_manager_impl_test.cc @@ -0,0 +1,530 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/local_device_data/nearby_share_local_device_data_manager_impl.h" + +#include + +#include +#include +#include +#include +#include +#include + +#include "gtest/gtest.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/substitute.h" +#include "absl/time/time.h" +#include "absl/types/optional.h" +#include "internal/platform/implementation/account_manager.h" +#include "internal/test/fake_account_manager.h" +#include "internal/test/fake_device_info.h" +#include "internal/test/fake_task_runner.h" +#include "sharing/common/fake_nearby_share_profile_info_provider.h" +#include "sharing/common/nearby_share_enums.h" +#include "sharing/common/nearby_share_prefs.h" +#include "sharing/internal/api/fake_nearby_share_client.h" +#include "sharing/internal/test/fake_context.h" +#include "sharing/internal/test/fake_preference_manager.h" +#include "sharing/local_device_data/nearby_share_local_device_data_manager.h" +#include "sharing/proto/device_rpc.pb.h" +#include "sharing/proto/rpc_resources.pb.h" +#include "sharing/scheduling/fake_nearby_share_scheduler.h" +#include "sharing/scheduling/fake_nearby_share_scheduler_factory.h" +#include "sharing/scheduling/nearby_share_scheduler_factory.h" + +namespace nearby { +namespace sharing { +namespace { + +using UpdateDeviceResponse = nearby::sharing::proto::UpdateDeviceResponse; +using Contact = nearby::sharing::proto::Contact; + +const char kDefaultDeviceName[] = "$0\'s $1"; +const char kFakeDeviceName[] = "My Cool Chromebook"; +const char kFakeEmptyDeviceName[] = ""; +const char kFakeFullName[] = "Barack Obama"; +const char kFakeGivenName[] = "Barack奥巴马"; +const char kFakeIconUrl[] = "https://www.google.com"; +const char kFakeIconUrl2[] = "https://www.google.com/2"; +const char kFakeIconToken[] = "token"; +const char kFakeIconToken2[] = "token2"; +const char kFakeInvalidDeviceName[] = "\xC0"; +const char kFakeTooLongDeviceName[] = "this string is 33 bytes in UTF-8!"; +const char kFakeTooLongGivenName[] = "this is a 33-byte string in utf-8"; +constexpr char kTestAccountId[] = "test_account_id"; +constexpr char kTestProfileUserName[] = "test@google.com"; + +absl::StatusOr CreateResponse( + const std::optional& full_name, + const std::optional& icon_url, + const std::optional& icon_token) { + absl::StatusOr result; + UpdateDeviceResponse response; + if (full_name) response.set_person_name(*full_name); + + if (icon_url) response.set_image_url(*icon_url); + + if (icon_token) response.set_image_token(*icon_token); + + result = response; + return result; +} + +std::vector GetFakeContacts() { + Contact contact1; + Contact contact2; + contact1.mutable_identifier()->set_account_name("account1"); + contact2.mutable_identifier()->set_account_name("account2"); + return {std::move(contact1), std::move(contact2)}; +} + +std::vector GetFakeCertificates() { + nearby::sharing::proto::PublicCertificate cert1; + nearby::sharing::proto::PublicCertificate cert2; + cert1.set_secret_id("id1"); + cert2.set_secret_id("id2"); + return {std::move(cert1), std::move(cert2)}; +} + +class NearbyShareLocalDeviceDataManagerImplTest + : public ::testing::Test, + public NearbyShareLocalDeviceDataManager::Observer { + protected: + struct ObserverNotification { + ObserverNotification(bool did_device_name_change, bool did_full_name_change, + bool did_icon_change) + : did_device_name_change(did_device_name_change), + did_full_name_change(did_full_name_change), + did_icon_change(did_icon_change) {} + ~ObserverNotification() = default; + + bool operator==(const ObserverNotification& other) const { + return did_device_name_change == other.did_device_name_change && + did_full_name_change == other.did_full_name_change && + did_icon_change == other.did_icon_change; + } + + bool did_device_name_change; + bool did_full_name_change; + bool did_icon_change; + }; + + NearbyShareLocalDeviceDataManagerImplTest() = default; + ~NearbyShareLocalDeviceDataManagerImplTest() override = default; + + void SetUp() override { + prefs::RegisterNearbySharingPrefs(preference_manager_); + NearbyShareSchedulerFactory::SetFactoryForTesting(&scheduler_factory_); + profile_info_provider()->set_given_name(kFakeGivenName); + + AccountManager::Account account; + account.id = kTestAccountId; + account.email = kTestProfileUserName; + fake_account_manager_.SetAccount(account); + } + + void TearDown() override { + NearbyShareSchedulerFactory::SetFactoryForTesting(nullptr); + } + + // NearbyShareLocalDeviceDataManager::Observer: + void OnLocalDeviceDataChanged(bool did_device_name_change, + bool did_full_name_change, + bool did_icon_change) override { + notifications_.emplace_back(did_device_name_change, did_full_name_change, + did_icon_change); + } + + void CreateManager() { + manager_ = NearbyShareLocalDeviceDataManagerImpl::Factory::Create( + &context_, preference_manager_, fake_account_manager_, + fake_device_info_, &nearby_client_factory_, &profile_info_provider_); + manager_->AddObserver(this); + ++num_manager_creations_; + num_download_device_data_ = 0; + VerifyInitialization(); + manager_->Start(); + } + + void DestroyManager() { + manager_->RemoveObserver(this); + manager_.reset(); + } + + void DownloadDeviceData( + const absl::StatusOr& response) { + // The scheduler requests a download of device data from the server. + EXPECT_EQ(client()->update_device_requests().size(), + num_download_device_data_); + device_data_scheduler()->InvokeRequestCallback(); + Sync(); + EXPECT_EQ(client()->update_device_requests().size(), + num_download_device_data_ + 1); + num_download_device_data_++; + EXPECT_TRUE(client()->list_contact_people_requests().empty()); + EXPECT_TRUE(client()->list_public_certificates_requests().empty()); + + size_t num_handled_results = + device_data_scheduler()->handled_results().size(); + + client()->SetUpdateDeviceResponse(response); + manager_->DownloadDeviceData(); + Sync(); + EXPECT_EQ(client()->update_device_requests().size(), + num_download_device_data_ + 1); + num_download_device_data_++; + EXPECT_EQ(num_handled_results + 1, + device_data_scheduler()->handled_results().size()); + EXPECT_EQ(response.ok(), device_data_scheduler()->handled_results().back()); + } + + void UploadContacts(const absl::StatusOr& response) { + std::optional returned_success; + + client()->SetUpdateDeviceResponse(response); + manager_->UploadContacts( + GetFakeContacts(), + [&returned_success](bool success) { returned_success = success; }); + Sync(); + EXPECT_TRUE(client()->list_public_certificates_requests().empty()); + auto device = client()->update_device_requests().back().device().contacts(); + std::vector expected_fake_contacts = GetFakeContacts(); + for (size_t i = 0; i < expected_fake_contacts.size(); ++i) { + EXPECT_EQ(expected_fake_contacts[i].SerializeAsString(), + client() + ->update_device_requests() + .back() + .device() + .contacts() + .at(i) + .SerializeAsString()); + } + + EXPECT_EQ(response.ok(), returned_success); + } + + void UploadCertificates( + const absl::StatusOr& response) { + std::optional returned_success; + EXPECT_TRUE(client()->list_contact_people_requests().empty()); + + client()->SetUpdateDeviceResponse(response); + manager_->UploadCertificates( + GetFakeCertificates(), + [&returned_success](bool success) { returned_success = success; }); + + Sync(); + std::vector + expected_fake_certificates = GetFakeCertificates(); + for (size_t i = 0; i < expected_fake_certificates.size(); ++i) { + EXPECT_EQ(expected_fake_certificates[i].SerializeAsString(), + client() + ->update_device_requests() + .back() + .device() + .public_certificates() + .at(i) + .SerializeAsString()); + } + + EXPECT_EQ(response.ok(), returned_success); + } + + NearbyShareLocalDeviceDataManager* manager() { return manager_.get(); } + + FakeNearbyShareProfileInfoProvider* profile_info_provider() { + return &profile_info_provider_; + } + + const std::vector& notifications() { + return notifications_; + } + + FakeNearbyShareScheduler* device_data_scheduler() { + return scheduler_factory_.pref_name_to_periodic_instance() + .at(prefs::kNearbySharingSchedulerDownloadDeviceDataName) + .fake_scheduler; + } + + std::string GetDeviceName() const { + return fake_device_info_.GetOsDeviceName(); + } + + std::string GetDeviceTypeName() const { + return fake_device_info_.GetDeviceTypeName(); + } + + FakeNearbyShareClient* client() { + return nearby_client_factory_.instances().back(); + } + + void Sync() { + EXPECT_TRUE(FakeTaskRunner::WaitForRunningTasksWithTimeout( + absl::Milliseconds(1000))); + } + + private: + void VerifyInitialization() { + // Verify device data scheduler input parameters. + const FakeNearbyShareSchedulerFactory::PeriodicInstance& + device_data_scheduler_instance = + scheduler_factory_.pref_name_to_periodic_instance().at( + prefs::kNearbySharingSchedulerDownloadDeviceDataName); + EXPECT_TRUE(device_data_scheduler_instance.fake_scheduler); + EXPECT_EQ(absl::Hours(12), device_data_scheduler_instance.request_period); + EXPECT_TRUE(device_data_scheduler_instance.retry_failures); + EXPECT_TRUE(device_data_scheduler_instance.require_connectivity); + } + + nearby::FakePreferenceManager preference_manager_; + nearby::FakeAccountManager fake_account_manager_; + nearby::FakeDeviceInfo fake_device_info_; + nearby::FakeContext context_; + size_t num_manager_creations_ = 0; + size_t num_download_device_data_ = 0; + std::vector notifications_; + FakeNearbyShareClientFactory nearby_client_factory_; + FakeNearbyShareProfileInfoProvider profile_info_provider_; + FakeNearbyShareSchedulerFactory scheduler_factory_; + std::unique_ptr manager_; +}; + +TEST_F(NearbyShareLocalDeviceDataManagerImplTest, DeviceId) { + CreateManager(); + + // A 10-character alphanumeric ID is automatically generated if one doesn't + // already exist. + std::string id = manager()->GetId(); + EXPECT_EQ(id.size(), 10u); + for (const char c : id) EXPECT_TRUE(std::isalnum(c)); + + // The ID is persisted. + DestroyManager(); + CreateManager(); + EXPECT_EQ(manager()->GetId(), id); +} + +TEST_F(NearbyShareLocalDeviceDataManagerImplTest, DefaultDeviceName) { + CreateManager(); + + // If given name is null, only return the device type. + profile_info_provider()->set_given_name(std::nullopt); + EXPECT_EQ(manager()->GetDeviceName(), + GetDeviceName()); + + // Set given name and expect full default device name of the form + // "'s ." + profile_info_provider()->set_given_name(kFakeGivenName); + EXPECT_EQ(absl::Substitute(kDefaultDeviceName, + kFakeGivenName, + GetDeviceTypeName()), + manager()->GetDeviceName()); + + // Make sure that when we use a given name that is very long we truncate + // correctly. + profile_info_provider()->set_given_name(kFakeTooLongGivenName); + EXPECT_EQ(kNearbyShareDeviceNameMaxLength, manager()->GetDeviceName().size()); +} + +TEST_F(NearbyShareLocalDeviceDataManagerImplTest, ValidateDeviceName) { + CreateManager(); + EXPECT_EQ(manager()->ValidateDeviceName(kFakeDeviceName), + DeviceNameValidationResult::kValid); + EXPECT_EQ(manager()->ValidateDeviceName(kFakeEmptyDeviceName), + DeviceNameValidationResult::kErrorEmpty); + EXPECT_EQ(manager()->ValidateDeviceName(kFakeTooLongDeviceName), + DeviceNameValidationResult::kErrorTooLong); + EXPECT_EQ(manager()->ValidateDeviceName(kFakeInvalidDeviceName), + DeviceNameValidationResult::kErrorNotValidUtf8); +} + +TEST_F(NearbyShareLocalDeviceDataManagerImplTest, SetDeviceName) { + CreateManager(); + + profile_info_provider()->set_given_name(kFakeGivenName); + std::string expected_default_device_name = + absl::Substitute(kDefaultDeviceName, kFakeGivenName, GetDeviceTypeName()); + EXPECT_EQ(manager()->GetDeviceName(), expected_default_device_name); + EXPECT_TRUE(notifications().empty()); + + auto error = manager()->SetDeviceName(kFakeEmptyDeviceName); + EXPECT_EQ(error, DeviceNameValidationResult::kErrorEmpty); + EXPECT_EQ(manager()->GetDeviceName(), expected_default_device_name); + EXPECT_TRUE(notifications().empty()); + + error = manager()->SetDeviceName(kFakeTooLongDeviceName); + EXPECT_EQ(error, DeviceNameValidationResult::kErrorTooLong); + EXPECT_EQ(manager()->GetDeviceName(), expected_default_device_name); + EXPECT_TRUE(notifications().empty()); + + error = manager()->SetDeviceName(kFakeInvalidDeviceName); + EXPECT_EQ(error, DeviceNameValidationResult::kErrorNotValidUtf8); + EXPECT_EQ(manager()->GetDeviceName(), expected_default_device_name); + EXPECT_TRUE(notifications().empty()); + + error = manager()->SetDeviceName(kFakeDeviceName); + EXPECT_EQ(error, DeviceNameValidationResult::kValid); + EXPECT_EQ(manager()->GetDeviceName(), kFakeDeviceName); + EXPECT_EQ(notifications().size(), 1u); + EXPECT_EQ(ObserverNotification(/*did_device_name_change=*/true, + /*did_full_name_change=*/false, + /*did_icon_change=*/false), + notifications().back()); + + // Verify that the data is persisted. + DestroyManager(); + CreateManager(); + EXPECT_EQ(manager()->GetDeviceName(), kFakeDeviceName); +} + +TEST_F(NearbyShareLocalDeviceDataManagerImplTest, DownloadDeviceData_Success) { + CreateManager(); + EXPECT_TRUE(notifications().empty()); + + DownloadDeviceData( + CreateResponse(kFakeFullName, kFakeIconUrl, kFakeIconToken)); + EXPECT_EQ(manager()->GetFullName(), kFakeFullName); + EXPECT_EQ(manager()->GetIconUrl(), kFakeIconUrl); + EXPECT_EQ(notifications().size(), 1u); + EXPECT_EQ(ObserverNotification(/*did_device_name_change=*/false, + /*did_full_name_change=*/true, + /*did_icon_change=*/true), + notifications()[0]); + + // Verify that the data is persisted. + DestroyManager(); + CreateManager(); + EXPECT_EQ(manager()->GetFullName(), kFakeFullName); + EXPECT_EQ(manager()->GetIconUrl(), kFakeIconUrl); +} + +TEST_F(NearbyShareLocalDeviceDataManagerImplTest, + DownloadDeviceData_EmptyData) { + CreateManager(); + EXPECT_TRUE(notifications().empty()); + + // The server returns empty strings for the full name and icon URL/token. + // GetFullName() and GetIconUrl() should return non-nullopt values even though + // they are trivial values. + DownloadDeviceData(CreateResponse("", "", "")); + EXPECT_EQ(manager()->GetFullName(), ""); + EXPECT_EQ(manager()->GetIconUrl(), ""); + EXPECT_EQ(notifications().size(), 0u); + + // Return empty strings again. Ensure that the trivial full name and icon + // URL/token values are not considered changed and no notification is sent. + DownloadDeviceData(CreateResponse("", "", "")); + EXPECT_EQ(manager()->GetFullName(), ""); + EXPECT_EQ(manager()->GetIconUrl(), ""); + EXPECT_EQ(notifications().size(), 0u); + + // Verify that the data is persisted. + DestroyManager(); + CreateManager(); + EXPECT_EQ(manager()->GetFullName(), ""); + EXPECT_EQ(manager()->GetIconUrl(), ""); +} + +TEST_F(NearbyShareLocalDeviceDataManagerImplTest, + DownloadDeviceData_IconToken) { + CreateManager(); + EXPECT_TRUE(notifications().empty()); + + DownloadDeviceData( + CreateResponse(kFakeFullName, kFakeIconUrl, kFakeIconToken)); + EXPECT_EQ(manager()->GetFullName(), kFakeFullName); + EXPECT_EQ(manager()->GetIconUrl(), kFakeIconUrl); + EXPECT_EQ(notifications().size(), 1u); + EXPECT_EQ(ObserverNotification(/*did_device_name_change=*/false, + /*did_full_name_change=*/true, + /*did_icon_change=*/true), + notifications()[0]); + + // Destroy and recreate to ensure name, URL, and token are all persisted. + DestroyManager(); + CreateManager(); + + // The icon URL changes but the token does not; no notification sent. + DownloadDeviceData( + CreateResponse(kFakeFullName, kFakeIconUrl2, kFakeIconToken)); + EXPECT_EQ(manager()->GetFullName(), kFakeFullName); + EXPECT_EQ(manager()->GetIconUrl(), kFakeIconUrl2); + EXPECT_EQ(notifications().size(), 1u); + + // The icon token changes but the URL does not; no notification sent. + DestroyManager(); + CreateManager(); + DownloadDeviceData( + CreateResponse(kFakeFullName, kFakeIconUrl2, kFakeIconToken2)); + EXPECT_EQ(manager()->GetFullName(), kFakeFullName); + EXPECT_EQ(manager()->GetIconUrl(), kFakeIconUrl2); + EXPECT_EQ(notifications().size(), 1u); + + // The icon URL and token change; notification sent. + DestroyManager(); + CreateManager(); + DownloadDeviceData( + CreateResponse(kFakeFullName, kFakeIconUrl, kFakeIconToken)); + EXPECT_EQ(manager()->GetFullName(), kFakeFullName); + EXPECT_EQ(manager()->GetIconUrl(), kFakeIconUrl); + EXPECT_EQ(notifications().size(), 2u); + EXPECT_EQ(ObserverNotification(/*did_device_name_change=*/false, + /*did_full_name_change=*/false, + /*did_icon_change=*/true), + notifications()[1]); + + // Verify that the data is persisted. + DestroyManager(); + CreateManager(); + EXPECT_EQ(manager()->GetFullName(), kFakeFullName); + EXPECT_EQ(manager()->GetIconUrl(), kFakeIconUrl); +} + +TEST_F(NearbyShareLocalDeviceDataManagerImplTest, DownloadDeviceData_Failure) { + CreateManager(); + DownloadDeviceData(/*response=*/absl::InternalError("")); + + // No full name or icon URL set because the response was null. + EXPECT_EQ(manager()->GetFullName(), std::string()); + EXPECT_EQ(manager()->GetIconUrl(), std::string()); + EXPECT_TRUE(notifications().empty()); +} + +TEST_F(NearbyShareLocalDeviceDataManagerImplTest, UploadContacts_Success) { + CreateManager(); + UploadContacts(CreateResponse(kFakeFullName, kFakeIconUrl, kFakeIconToken)); +} + +TEST_F(NearbyShareLocalDeviceDataManagerImplTest, UploadContacts_Failure) { + CreateManager(); + UploadContacts(/*response=*/absl::InternalError("")); +} + +TEST_F(NearbyShareLocalDeviceDataManagerImplTest, UploadCertificates_Success) { + CreateManager(); + UploadCertificates( + CreateResponse(kFakeFullName, kFakeIconUrl, kFakeIconToken)); +} + +TEST_F(NearbyShareLocalDeviceDataManagerImplTest, UploadCertificates_Failure) { + CreateManager(); + UploadCertificates(/*response=*/absl::InternalError("")); +} + +} // namespace +} // namespace sharing +} // namespace nearby