diff --git a/fastpair/common/BUILD b/fastpair/common/BUILD index ac60ad24..565daba5 100644 --- a/fastpair/common/BUILD +++ b/fastpair/common/BUILD @@ -3,6 +3,7 @@ licenses(["notice"]) cc_library( name = "common", srcs = [ + "account_key_filter.cc", "battery_notification.cc", "fast_pair_device.cc", "fast_pair_http_result.cc", @@ -11,6 +12,7 @@ cc_library( ], hdrs = [ "account_key.h", + "account_key_filter.h", "battery_notification.h", "constant.h", "fast_pair_device.h", @@ -31,6 +33,22 @@ cc_library( ], ) +cc_test( + name = "account_key_filter_test", + size = "small", + srcs = [ + "account_key_filter_test.cc", + ], + shard_count = 16, + deps = [ + ":common", + "//internal/platform/implementation/g3", # build_cleaner: keep + "@com_github_protobuf_matchers//protobuf-matchers", + "@com_google_absl//absl/strings", + "@com_google_googletest//:gtest_main", + ], +) + cc_test( name = "fast_pair_device_test", size = "small", diff --git a/fastpair/common/account_key.h b/fastpair/common/account_key.h index dae14d6e..1dacd5e0 100644 --- a/fastpair/common/account_key.h +++ b/fastpair/common/account_key.h @@ -17,6 +17,7 @@ #include #include +#include #include "absl/strings/escaping.h" #include "absl/strings/string_view.h" @@ -30,6 +31,9 @@ class AccountKey { public: AccountKey() = default; explicit AccountKey(absl::string_view bytes) : bytes_(bytes) {} + explicit AccountKey(const std::vector bytes) { + bytes_ = std::string(bytes.begin(), bytes.end()); + } static AccountKey CreateRandomKey() { std::string key(kAccountKeySize, 0); diff --git a/fastpair/common/account_key_filter.cc b/fastpair/common/account_key_filter.cc new file mode 100644 index 00000000..9397192a --- /dev/null +++ b/fastpair/common/account_key_filter.cc @@ -0,0 +1,125 @@ +// 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 "fastpair/common/account_key_filter.h" + +#include +#include +#include +#include + +#include "absl/strings/string_view.h" +#include "fastpair/common/battery_notification.h" +#include "fastpair/common/non_discoverable_advertisement.h" +#include "internal/crypto/sha2.h" +#include "internal/platform/logging.h" + +namespace nearby { +namespace fastpair { +namespace { +constexpr int kBitsInByte = 8; + +// SASS enabled peripherals create their Bloom filters with the first byte +// equals to either `the account key in use` +// or `the most recently used account key` +// see this spec: +// https://developers.google.com/nearby/fast-pair/early-access/specifications/extensions/sass#SassInUseAccountKey +constexpr uint8_t kRecentlyUsedByte = 0x05; +constexpr uint8_t kInUseByte = 0x06; +constexpr uint8_t kShowUi = 0b00110011; +constexpr uint8_t kHideUi = 0b00110100; + +// Helper to AccountKeyFilter::IsAccountKeyInFilter(). +// Performs the test to see if |data| is in |bit_sets|, a Bloom filter. +bool AccountKeyFilterChecker(const std::vector& data, + const std::vector& bit_sets) { + std::array hashed = crypto::SHA256Hash(data); + + // Iterate over the hashed input in 4 byte increments, combine those 4 + // bytes into an unsigned int and use it as the index into our + // |bit_sets|. + for (size_t i = 0; i < hashed.size(); i += 4) { + uint32_t hash = uint32_t{hashed[i]} << 24 | uint32_t{hashed[i + 1]} << 16 | + uint32_t{hashed[i + 2]} << 8 | hashed[i + 3]; + + size_t num_bits = bit_sets.size() * kBitsInByte; + size_t n = hash % num_bits; + size_t byte_index = floor(n / kBitsInByte); + size_t bit_index = n % kBitsInByte; + bool is_set = (bit_sets[byte_index] >> bit_index) & 0x01; + + if (!is_set) return false; + } + NEARBY_LOGS(INFO) << __func__ << " The accountkey is possibly in set."; + return true; +} + +} // namespace + +AccountKeyFilter::AccountKeyFilter( + const NonDiscoverableAdvertisement& advertisement) + : bit_sets_(advertisement.account_key_filter) { + salt_values_.resize(advertisement.salt.size()); + std::copy(advertisement.salt.begin(), advertisement.salt.end(), + salt_values_.begin()); + + // If the advertisement contains battery information, then that information + // was also appended to the account keys to generate the filter. We need to + // do the same when checking for matches, so save the values in salt_values_ + // for that purpose later. + if (advertisement.battery_notification) { + salt_values_.push_back(advertisement.battery_notification->type == + BatteryNotification::Type::kShowUi + ? kShowUi + : kHideUi); + for (auto battery_info : + advertisement.battery_notification->battery_infos) { + salt_values_.push_back(battery_info.ToByte()); + } + } +} + +AccountKeyFilter::AccountKeyFilter( + const std::vector& account_key_filter_bytes, + const std::vector& salt_values) + : bit_sets_(account_key_filter_bytes), salt_values_(salt_values) {} + +bool AccountKeyFilter::IsPossiblyInSet(const AccountKey& account_key) { + if (!account_key.Ok()) { + NEARBY_LOGS(INFO) << __func__ << " Invalid account key."; + return false; + } + if (bit_sets_.empty()) return false; + // We first need to append the salt value to the input (see + // https://developers.google.com/nearby/fast-pair/spec#AccountKeyFilter). + std::vector data(account_key.GetAsBytes().begin(), + account_key.GetAsBytes().end()); + for (auto& byte : salt_values_) data.push_back(byte); + + // We need to try account keys with different first bytes in case + // the peripheral is SASS per + // https://developers.google.com/nearby/fast-pair/early-access/specifications/extensions/sass#SassAdvertisingPayload + if (AccountKeyFilterChecker(data, bit_sets_)) { + return true; + } + data[0] = kRecentlyUsedByte; + if (AccountKeyFilterChecker(data, bit_sets_)) { + return true; + } + data[0] = kInUseByte; + return AccountKeyFilterChecker(data, bit_sets_); +} + +} // namespace fastpair +} // namespace nearby diff --git a/fastpair/common/account_key_filter.h b/fastpair/common/account_key_filter.h new file mode 100644 index 00000000..40eaace6 --- /dev/null +++ b/fastpair/common/account_key_filter.h @@ -0,0 +1,49 @@ +// 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_FASTPAIR_COMMON_ACCOUNT_KEY_FILTER_H_ +#define THIRD_PARTY_NEARBY_FASTPAIR_COMMON_ACCOUNT_KEY_FILTER_H_ + +#include +#include + +#include "fastpair/common/account_key.h" +#include "fastpair/common/non_discoverable_advertisement.h" + +namespace nearby { +namespace fastpair { + +// Class which represents a Fast Pair Account Key account_key_filter +// https://developers.google.com/nearby/fast-pair/specifications/service/provider#AccountKeyFilter +class AccountKeyFilter { + public: + explicit AccountKeyFilter(const NonDiscoverableAdvertisement& advertisement); + AccountKeyFilter(const std::vector& account_key_filter_bytes, + const std::vector& salt_values); + ~AccountKeyFilter() = default; + + // Returns true if the `account_key` is possibly in the account key set + // defined by the filter. + // Return false if `account_key` is definitely not in set. + bool IsPossiblyInSet(const AccountKey& account_key); + + private: + std::vector bit_sets_; + std::vector salt_values_; +}; + +} // namespace fastpair +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_FASTPAIR_COMMON_ACCOUNT_KEY_FILTER_H_ diff --git a/fastpair/common/account_key_filter_test.cc b/fastpair/common/account_key_filter_test.cc new file mode 100644 index 00000000..31101d0e --- /dev/null +++ b/fastpair/common/account_key_filter_test.cc @@ -0,0 +1,135 @@ +// 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 "fastpair/common/account_key_filter.h" + +#include +#include + +#include "gtest/gtest.h" +#include "fastpair/common/battery_notification.h" +#include "fastpair/common/non_discoverable_advertisement.h" + +namespace nearby { +namespace fastpair { +namespace { +// Test data comes from: +// https://developers.google.com/nearby/fast-pair/specifications/appendix/testcases#test_cases + +class AccountKeyFilterTest : public testing::Test { + protected: + const std::vector salt_{0xC7, 0xC8}; + const std::vector account_key_1_{0x11, 0x22, 0x33, 0x44, 0x55, 0x66, + 0x77, 0x88, 0x99, 0x00, 0xAA, 0xBB, + 0xCC, 0xDD, 0xEE, 0xFF}; + const std::vector account_key_2_{0x11, 0x11, 0x22, 0x22, 0x33, 0x33, + 0x44, 0x44, 0x55, 0x55, 0x66, 0x66, + 0x77, 0x77, 0x88, 0x88}; + + const std::vector filter_1_{0x02, 0x0C, 0x80, 0x2A}; + const std::vector filter_2_{0x84, 0x4A, 0x62, 0x20, 0x8B}; + const std::vector filter_1_and_2_{0x84, 0x4A, 0x62, 0x20, 0x8B}; + + const std::vector battery_data_{0b00110011, 0b01000000, 0b01000000, + 0b01000000}; + + const std::vector filter_1_with_battery_{0x01, 0x01, 0x46, 0x0A}; + const std::vector filter_2_with_battery_{0x46, 0x15, 0x24, 0xD0, + 0x08}; +}; + +TEST_F(AccountKeyFilterTest, ConstrutorWithNonDiscoverableAdvertisement) { + NonDiscoverableAdvertisement non_discoverable_advertisement( + filter_1_, NonDiscoverableAdvertisement::Type::kShowUi, salt_, {}); + EXPECT_TRUE(AccountKeyFilter(non_discoverable_advertisement) + .IsPossiblyInSet(AccountKey(account_key_1_))); + NonDiscoverableAdvertisement non_discoverable_advertisement_with_battery( + filter_1_with_battery_, NonDiscoverableAdvertisement::Type::kShowUi, + salt_, + BatteryNotification::FromBytes({0b01000000, 0b01000000, 0b01000000}, + BatteryNotification::Type::kShowUi)); + EXPECT_TRUE(AccountKeyFilter(non_discoverable_advertisement_with_battery) + .IsPossiblyInSet(AccountKey(account_key_1_))); +} + +TEST_F(AccountKeyFilterTest, EmptyAccountKeyFilter) { + AccountKeyFilter filter({}, {}); + + EXPECT_FALSE(filter.IsPossiblyInSet(AccountKey(account_key_1_))); + EXPECT_FALSE(filter.IsPossiblyInSet(AccountKey(account_key_2_))); +} + +TEST_F(AccountKeyFilterTest, EmptyAccountKey) { + EXPECT_FALSE( + AccountKeyFilter(filter_1_, salt_).IsPossiblyInSet(AccountKey(""))); +} + +TEST_F(AccountKeyFilterTest, SingleAccountKey) { + EXPECT_TRUE(AccountKeyFilter(filter_1_, salt_) + .IsPossiblyInSet(AccountKey(AccountKey(account_key_1_)))); + EXPECT_TRUE(AccountKeyFilter(filter_2_, salt_) + .IsPossiblyInSet(AccountKey(account_key_2_))); +} + +TEST_F(AccountKeyFilterTest, TwoAccountKeys) { + AccountKeyFilter filter(filter_1_and_2_, salt_); + EXPECT_TRUE(filter.IsPossiblyInSet(AccountKey(account_key_1_))); + EXPECT_TRUE(filter.IsPossiblyInSet(AccountKey(account_key_2_))); +} + +TEST_F(AccountKeyFilterTest, MissingAccountKey) { + const std::vector bytes{0x12, 0x22, 0x33, 0x44, 0x55, 0x66, + 0x77, 0x88, 0x99, 0x00, 0xAA, 0xBB, + 0xCC, 0xDD, 0xEE, 0xFF}; + AccountKey account_key(bytes); + EXPECT_FALSE(AccountKeyFilter(filter_1_, salt_).IsPossiblyInSet(account_key)); + EXPECT_FALSE( + AccountKeyFilter(filter_1_and_2_, salt_).IsPossiblyInSet(account_key)); +} + +TEST_F(AccountKeyFilterTest, AccountKeyWithBatteryData) { + std::vector salt_1 = salt_; + for (auto& byte : battery_data_) salt_1.push_back(byte); + + EXPECT_TRUE(AccountKeyFilter(filter_1_with_battery_, salt_1) + .IsPossiblyInSet(AccountKey(account_key_1_))); + + std::vector salt_2 = salt_; + for (auto& byte : battery_data_) salt_2.push_back(byte); + + EXPECT_TRUE(AccountKeyFilter(filter_2_with_battery_, salt_2) + .IsPossiblyInSet(AccountKey(account_key_2_))); +} + +TEST_F(AccountKeyFilterTest, SassEnabledPeripheral) { + // Value source: b/243855406#comment24 + const std::vector bytes{0x06, 0x3F, 0xC1, 0x8C, 0x63, 0xDC, + 0x75, 0x1A, 0xE8, 0x1A, 0xCF, 0x65, + 0x10, 0x15, 0x1D, 0xB0}; + AccountKey account_key_3(bytes); + const std::vector filter4{0x19, 0x23, 0x50, 0xE8, 0x37, + 0x68, 0xF0, 0x65, 0x22}; + const std::vector salt4{0xD7, 0xDE}; + const std::vector batteryData4{0x33, 0xE4, 0xE4, 0x64}; + std::vector salt_values{}; + salt_values.insert(salt_values.end(), salt4.begin(), salt4.end()); + salt_values.insert(salt_values.end(), batteryData4.begin(), + batteryData4.end()); + EXPECT_TRUE( + AccountKeyFilter(filter4, salt_values).IsPossiblyInSet(account_key_3)); +} + +} // namespace +} // namespace fastpair +} // namespace nearby