From 319e5abea448dccff02541d83f1816e0e5a8a8f5 Mon Sep 17 00:00:00 2001 From: jsobczak Date: Wed, 3 Aug 2022 14:16:33 -0700 Subject: [PATCH] Add advertisement decoder The decoder allows us to parse incoming Nearby Presence advertisements. PiperOrigin-RevId: 465146528 --- presence/implementation/BUILD | 21 ++- .../implementation/advertisement_decoder.cc | 123 +++++++++++++++ .../implementation/advertisement_decoder.h | 50 ++++++ .../advertisement_decoder_test.cc | 145 ++++++++++++++++++ 4 files changed, 338 insertions(+), 1 deletion(-) create mode 100644 presence/implementation/advertisement_decoder.cc create mode 100644 presence/implementation/advertisement_decoder.h create mode 100644 presence/implementation/advertisement_decoder_test.cc diff --git a/presence/implementation/BUILD b/presence/implementation/BUILD index c7cc69c9..048295ac 100644 --- a/presence/implementation/BUILD +++ b/presence/implementation/BUILD @@ -15,8 +15,9 @@ licenses(["notice"]) cc_library( name = "internal", - srcs = [], + srcs = ["advertisement_decoder.cc"], hdrs = [ + "advertisement_decoder.h", "broadcast_manager.h", "credential_manager.h", "credential_manager_impl.h", @@ -30,12 +31,30 @@ cc_library( ], deps = [ "//internal/platform:comm", + "//internal/platform:logging", "//internal/platform/implementation:comm", + "//third_party/nearby/presence:advertisement_factory", "//third_party/nearby/presence:credential", + "//third_party/nearby/presence:types", "//third_party/nearby/presence/implementation/mediums", "//third_party/nearby/presence/proto:credential_cc_proto", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings", + "@com_google_absl//absl/strings:str_format", + ], +) + +cc_test( + name = "advertisement_decoder_test", + size = "small", + srcs = ["advertisement_decoder_test.cc"], + deps = [ + ":internal", + "//internal/platform/implementation/g3", # build_cleaner: keep + "//third_party/nearby/presence:types", + "@com_github_protobuf_matchers//protobuf-matchers", + "@com_google_absl//absl/strings", + "@com_google_googletest//:gtest_main", ], ) diff --git a/presence/implementation/advertisement_decoder.cc b/presence/implementation/advertisement_decoder.cc new file mode 100644 index 00000000..dd51e875 --- /dev/null +++ b/presence/implementation/advertisement_decoder.cc @@ -0,0 +1,123 @@ +// 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 "third_party/nearby/presence/implementation/advertisement_decoder.h" + +#include +#include +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/escaping.h" +#include "absl/strings/str_format.h" +#include "absl/strings/string_view.h" +#include "internal/platform/logging.h" +#include "third_party/nearby/presence/data_element.h" + +namespace nearby { +namespace presence { + +constexpr uint8_t kDataTypeMask = + (1 << DataElement::kDataElementLengthShift) - 1; + +constexpr int kAdvertisementVersion = 0; + +namespace { + +absl::StatusOr ParseDataElement(const absl::string_view input, + size_t& index) { + if (index >= input.size()) { + return absl::OutOfRangeError(absl::StrFormat( + "Data element (%s) is %d bytes long. Expected more than %d", + absl::BytesToHexString(input), input.size(), index)); + } + uint8_t header = input[index]; + uint8_t data_type = header & kDataTypeMask; + size_t length = header >> DataElement::kDataElementLengthShift; + index++; + size_t start = index; + index += length; + if (index > input.size()) { + return absl::OutOfRangeError(absl::StrFormat( + "Data element (%s) is %d bytes long. Expected at least %d", + absl::BytesToHexString(input), input.size(), index)); + } + NEARBY_LOGS(VERBOSE) << "Type: " << static_cast(data_type) + << " length: " << static_cast(length) << " DE: " + << absl::BytesToHexString(input.substr(start, length)); + return DataElement(data_type, input.substr(start, length)); +} + +bool IsIdentity(int data_type) { + return data_type >= DataElement::kPrivateIdentityFieldType && + data_type <= DataElement::kProvisionedIdentityFieldType; +} + +} // namespace + +absl::StatusOr> +AdvertisementDecoder::DecodeAdvertisement(absl::string_view advertisement) { + std::vector result; + NEARBY_LOGS(VERBOSE) << "Advertisement: " + << absl::BytesToHexString(advertisement); + if (advertisement.empty()) { + return absl::OutOfRangeError("Empty advertisement"); + } + int version = advertisement[0]; + NEARBY_LOGS(VERBOSE) << "Version: " << version; + if (version != kAdvertisementVersion) { + return absl::UnimplementedError( + absl::StrFormat("Advertisment version (%d) is not supported", version)); + } + size_t index = 1; + std::string salt; + absl::StatusOr decrypted; + while (index < advertisement.size()) { + absl::StatusOr elem = ParseDataElement(advertisement, index); + if (!elem.ok()) { + NEARBY_LOGS(WARNING) << "Failed to read data element, status: " + << elem.status(); + return elem.status(); + } + if (elem->GetType() == DataElement::kSaltFieldType) { + salt = elem->GetValue(); + } + bool need_decryption = + IsIdentity(elem->GetType()) && !elem->GetValue().empty(); + if (need_decryption && index < advertisement.size()) { + NEARBY_LOGS(VERBOSE) << "Metadata: " + << absl::BytesToHexString(elem->GetValue()) + << "Salt: " << absl::BytesToHexString(salt) + << "Encrypted: " + << absl::BytesToHexString( + advertisement.substr(index)); + decrypted = credential_manager_.DecryptDataElements( + elem->GetValue(), salt, advertisement.substr(index)); + if (!decrypted.ok()) { + NEARBY_LOGS(WARNING) << "Failed to decrypt advertisement, status: " + << decrypted.status(); + return decrypted.status(); + } + // Restart the loop and iterate over the decrypted content + advertisement = *decrypted; + index = 0; + } + result.push_back(*std::move(elem)); + } + return result; +} + +} // namespace presence +} // namespace nearby diff --git a/presence/implementation/advertisement_decoder.h b/presence/implementation/advertisement_decoder.h new file mode 100644 index 00000000..f574ed81 --- /dev/null +++ b/presence/implementation/advertisement_decoder.h @@ -0,0 +1,50 @@ +// 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_PRESENCE_ADVERTISEMENT_DECODER_H_ +#define THIRD_PARTY_NEARBY_PRESENCE_ADVERTISEMENT_DECODER_H_ + +#include +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "third_party/nearby/presence/advertisement_factory.h" +#include "third_party/nearby/presence/data_element.h" +#include "third_party/nearby/presence/implementation/credential_manager.h" +#include "third_party/nearby/presence/presence_identity.h" + +namespace nearby { +namespace presence { + +// Decodes BLE NP advertisements +class AdvertisementDecoder { + public: + explicit AdvertisementDecoder(CredentialManager* credential_manager) + : credential_manager_(*ABSL_DIE_IF_NULL(credential_manager)) {} + + // Returns a list of Data Elements decoded from the advertisement. + // Returns an error if the advertisement is misformatted or if it couldn't be + // decrypted. + absl::StatusOr> DecodeAdvertisement( + absl::string_view advertisement); + + private: + CredentialManager& credential_manager_; +}; + +} // namespace presence +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_PRESENCE_ADVERTISEMENT_DECODER_H_ diff --git a/presence/implementation/advertisement_decoder_test.cc b/presence/implementation/advertisement_decoder_test.cc new file mode 100644 index 00000000..160037c4 --- /dev/null +++ b/presence/implementation/advertisement_decoder_test.cc @@ -0,0 +1,145 @@ +// 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 "third_party/nearby/presence/implementation/advertisement_decoder.h" + +#include +#include + +#include "gmock/gmock.h" +#include "protobuf-matchers/protocol-buffer-matchers.h" +#include "gtest/gtest.h" +#include "absl/strings/escaping.h" +#include "third_party/nearby/presence/data_element.h" +#include "third_party/nearby/presence/implementation/credential_manager_impl.h" + +namespace nearby { +namespace presence { + +namespace { +using ::testing::ElementsAre; +using ::testing::Return; +using ::testing::status::StatusIs; + +class MockCredentialManager : public CredentialManagerImpl { + public: + MOCK_METHOD(absl::StatusOr, DecryptDataElements, + (absl::string_view metadata_key, absl::string_view salt, + absl::string_view data_elements), + (override)); +}; + +TEST(AdvertisementDecoder, DecodeBaseNpPrivateAdvertisement) { + const std::string salt = "AB"; + const std::string metadata = + absl::HexStringToBytes("1011121314151617181920212223"); + MockCredentialManager credential_manager; + EXPECT_CALL( + credential_manager, + DecryptDataElements(metadata, salt, absl::HexStringToBytes("5051525354"))) + .WillOnce(Return(absl::HexStringToBytes("18CD29EEFF"))); + + AdvertisementDecoder decoder(&credential_manager); + + const absl::StatusOr> result = + decoder.DecodeAdvertisement(absl::HexStringToBytes( + "00204142e110111213141516171819202122235051525354")); + + ASSERT_OK(result); + EXPECT_THAT( + *result, + ElementsAre(DataElement(DataElement::kSaltFieldType, salt), + DataElement(DataElement::kPrivateIdentityFieldType, metadata), + DataElement(8, absl::HexStringToBytes("CD")), + DataElement(9, absl::HexStringToBytes("EEFF")))); +} + +TEST(AdvertisementDecoder, DecodeBaseNpPublicAdvertisement) { + const std::string salt = "AB"; + MockCredentialManager credential_manager; + AdvertisementDecoder decoder(&credential_manager); + + const absl::StatusOr> result = + decoder.DecodeAdvertisement( + absl::HexStringToBytes("002041420318CD29EEFF")); + + ASSERT_OK(result); + EXPECT_THAT( + *result, + ElementsAre(DataElement(DataElement::kSaltFieldType, salt), + DataElement(DataElement::kPublicIdentityFieldType, ""), + DataElement(8, absl::HexStringToBytes("CD")), + DataElement(9, absl::HexStringToBytes("EEFF")))); +} + +TEST(AdvertisementDecoder, InvalidAdvertisementFieldTooShort) { + MockCredentialManager credential_manager; + AdvertisementDecoder decoder(&credential_manager); + + // 0x50 header means 5 bytes long salt but only 4 bytes follow. + EXPECT_THAT( + decoder.DecodeAdvertisement(absl::HexStringToBytes("0050A0A1A2A3")), + StatusIs(absl::StatusCode::kOutOfRange)); +} + +TEST(AdvertisementDecoder, ZeroLengthPayload) { + MockCredentialManager credential_manager; + AdvertisementDecoder decoder(&credential_manager); + + // A action with type 0xA and no payload + const absl::StatusOr> result = + decoder.DecodeAdvertisement(absl::HexStringToBytes("000A")); + + ASSERT_OK(result); + EXPECT_THAT(*result, ElementsAre(DataElement(0xA, ""))); +} + +TEST(AdvertisementDecoder, EmptyAdvertisement) { + MockCredentialManager credential_manager; + AdvertisementDecoder decoder(&credential_manager); + + EXPECT_THAT(decoder.DecodeAdvertisement(""), + StatusIs(absl::StatusCode::kOutOfRange)); +} + +TEST(AdvertisementDecoder, InvalidEncryptedContent) { + const std::string salt = "AB"; + const std::string metadata = + absl::HexStringToBytes("1011121314151617181920212223"); + MockCredentialManager credential_manager; + // 0x28CD is an invalid DE, 0x28 means a 2 byte payload with type 8 alas only + // one byte is given (0xCD) + EXPECT_CALL( + credential_manager, + DecryptDataElements(metadata, salt, absl::HexStringToBytes("5051525354"))) + .WillOnce(Return(absl::HexStringToBytes("28CD"))); + AdvertisementDecoder decoder(&credential_manager); + + EXPECT_THAT(decoder.DecodeAdvertisement(absl::HexStringToBytes( + "00204142e110111213141516171819202122235051525354")), + StatusIs(absl::StatusCode::kOutOfRange)); +} + +TEST(AdvertisementDecoder, UnsupportedAdvertisementVersion) { + MockCredentialManager credential_manager; + AdvertisementDecoder decoder(&credential_manager); + + EXPECT_THAT(decoder.DecodeAdvertisement( + absl::HexStringToBytes("012041420318CD29EEFF")), + StatusIs(absl::StatusCode::kUnimplemented)); +} + +} // namespace +} // namespace presence +} // namespace nearby