| // Copyright 2023 The Pigweed Authors |
| // |
| // 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. |
| |
| // inclusive-language: disable |
| |
| #include "pw_bluetooth_sapphire/internal/host/sm/phase_2_legacy.h" |
| |
| #include <cstdint> |
| #include <memory> |
| |
| #include "pw_bluetooth_sapphire/internal/host/common/byte_buffer.h" |
| #include "pw_bluetooth_sapphire/internal/host/common/device_address.h" |
| #include "pw_bluetooth_sapphire/internal/host/common/random.h" |
| #include "pw_bluetooth_sapphire/internal/host/common/uint128.h" |
| #include "pw_bluetooth_sapphire/internal/host/hci/connection.h" |
| #include "pw_bluetooth_sapphire/internal/host/l2cap/fake_channel_test.h" |
| #include "pw_bluetooth_sapphire/internal/host/sm/fake_phase_listener.h" |
| #include "pw_bluetooth_sapphire/internal/host/sm/packet.h" |
| #include "pw_bluetooth_sapphire/internal/host/sm/smp.h" |
| #include "pw_bluetooth_sapphire/internal/host/sm/types.h" |
| #include "pw_bluetooth_sapphire/internal/host/sm/util.h" |
| #include "pw_bluetooth_sapphire/internal/host/testing/test_helpers.h" |
| #include "pw_unit_test/framework.h" |
| |
| namespace bt::sm { |
| namespace { |
| |
| const PairingFeatures kDefaultFeatures = { |
| .initiator = true, |
| .secure_connections = false, |
| .will_bond = true, |
| .generate_ct_key = std::optional<CrossTransportKeyAlgo>{std::nullopt}, |
| .method = PairingMethod::kJustWorks, |
| .encryption_key_size = kMaxEncryptionKeySize, |
| .local_key_distribution = KeyDistGen::kIdKey, |
| .remote_key_distribution = KeyDistGen::kIdKey | KeyDistGen::kEncKey}; |
| |
| const PairingRequestParams kDefaultPreq{ |
| .io_capability = IOCapability::kNoInputNoOutput, |
| .oob_data_flag = OOBDataFlag::kNotPresent, |
| .auth_req = AuthReq::kBondingFlag, |
| .max_encryption_key_size = kMaxEncryptionKeySize, |
| .initiator_key_dist_gen = KeyDistGen::kIdKey, |
| .responder_key_dist_gen = KeyDistGen::kIdKey | KeyDistGen::kEncKey}; |
| |
| const PairingResponseParams kDefaultPres{ |
| .io_capability = IOCapability::kNoInputNoOutput, |
| .oob_data_flag = OOBDataFlag::kNotPresent, |
| .auth_req = AuthReq::kBondingFlag, |
| .max_encryption_key_size = kMaxEncryptionKeySize, |
| .initiator_key_dist_gen = KeyDistGen::kIdKey, |
| .responder_key_dist_gen = KeyDistGen::kIdKey | KeyDistGen::kEncKey}; |
| |
| const DeviceAddress kAddr1(DeviceAddress::Type::kLEPublic, |
| {0x00, 0x00, 0x00, 0x00, 0x00, 0x01}); |
| const DeviceAddress kAddr2(DeviceAddress::Type::kLEPublic, |
| {0x00, 0x00, 0x00, 0x00, 0x00, 0x02}); |
| struct Phase2LegacyArgs { |
| PairingFeatures features = kDefaultFeatures; |
| PairingRequestParams preq = kDefaultPreq; |
| PairingResponseParams pres = kDefaultPres; |
| const DeviceAddress* initiator_addr = &kAddr1; |
| const DeviceAddress* responder_addr = &kAddr2; |
| }; |
| |
| using util::PacketSize; |
| |
| class Phase2LegacyTest : public l2cap::testing::FakeChannelTest { |
| public: |
| Phase2LegacyTest() = default; |
| ~Phase2LegacyTest() override = default; |
| |
| pw::async::HeapDispatcher& heap_dispatcher() { return heap_dispatcher_; } |
| |
| protected: |
| void SetUp() override { NewPhase2Legacy(); } |
| |
| void TearDown() override { phase_2_legacy_ = nullptr; } |
| |
| void NewPhase2Legacy(Phase2LegacyArgs phase_args = Phase2LegacyArgs(), |
| bt::LinkType ll_type = bt::LinkType::kLE) { |
| l2cap::ChannelId cid = ll_type == bt::LinkType::kLE ? l2cap::kLESMPChannelId |
| : l2cap::kSMPChannelId; |
| ChannelOptions options(cid); |
| options.link_type = ll_type; |
| |
| phase_args_ = phase_args; |
| |
| listener_ = std::make_unique<FakeListener>(); |
| fake_chan_ = CreateFakeChannel(options); |
| sm_chan_ = std::make_unique<PairingChannel>(fake_chan_->GetWeakPtr()); |
| auto role = |
| phase_args.features.initiator ? Role::kInitiator : Role::kResponder; |
| StaticByteBuffer<PacketSize<PairingRequestParams>()> preq, pres; |
| preq.WriteObj(phase_args.preq); |
| pres.WriteObj(phase_args.pres); |
| phase_2_legacy_ = |
| std::make_unique<Phase2Legacy>(sm_chan_->GetWeakPtr(), |
| listener_->as_weak_ptr(), |
| role, |
| phase_args.features, |
| preq, |
| pres, |
| *phase_args.initiator_addr, |
| *phase_args.responder_addr, |
| [this](const UInt128& stk) { |
| phase_2_complete_count_++; |
| stk_ = stk; |
| }); |
| } |
| |
| void Receive128BitCmd(Code cmd_code, const UInt128& value) { |
| fake_chan()->Receive(Make128BitCmd(cmd_code, value)); |
| } |
| |
| DynamicByteBuffer Make128BitCmd(Code cmd_code, const UInt128& value) { |
| StaticByteBuffer<PacketSize<UInt128>()> buffer; |
| PacketWriter writer(cmd_code, &buffer); |
| *writer.mutable_payload<UInt128>() = value; |
| return DynamicByteBuffer(buffer); |
| } |
| |
| UInt128 GenerateConfirmValue(const UInt128& random, uint32_t tk = 0) const { |
| tk = htole32(tk); |
| UInt128 tk128; |
| tk128.fill(0); |
| std::memcpy(tk128.data(), &tk, sizeof(tk)); |
| StaticByteBuffer<PacketSize<PairingRequestParams>()> preq, pres; |
| preq.WriteObj(phase_args_.preq); |
| pres.WriteObj(phase_args_.pres); |
| |
| UInt128 out_value; |
| util::C1(tk128, |
| random, |
| preq, |
| pres, |
| *phase_args_.initiator_addr, |
| *phase_args_.responder_addr, |
| &out_value); |
| return out_value; |
| } |
| |
| struct MatchingPair { |
| UInt128 confirm; |
| UInt128 random; |
| }; |
| MatchingPair GenerateMatchingConfirmAndRandom(uint32_t tk) { |
| MatchingPair pair; |
| random_generator()->Get( |
| {reinterpret_cast<std::byte*>(pair.random.data()), pair.random.size()}); |
| pair.confirm = GenerateConfirmValue(pair.random, tk); |
| return pair; |
| } |
| |
| static std::pair<Code, UInt128> ExtractCodeAnd128BitCmd(ByteBufferPtr sdu) { |
| BT_ASSERT_MSG(sdu, "Tried to ExtractCodeAnd128BitCmd from nullptr in test"); |
| auto maybe_reader = ValidPacketReader::ParseSdu(sdu); |
| BT_ASSERT_MSG(maybe_reader.is_ok(), |
| "Tried to ExtractCodeAnd128BitCmd from invalid SMP packet"); |
| return {maybe_reader.value().code(), |
| maybe_reader.value().payload<UInt128>()}; |
| } |
| |
| void DestroyPhase2() { phase_2_legacy_.reset(nullptr); } |
| l2cap::testing::FakeChannel* fake_chan() const { return fake_chan_.get(); } |
| Phase2Legacy* phase_2_legacy() { return phase_2_legacy_.get(); } |
| FakeListener* listener() { return listener_.get(); } |
| |
| int phase_2_complete_count() const { return phase_2_complete_count_; } |
| UInt128 stk() const { return stk_; } |
| |
| private: |
| std::unique_ptr<FakeListener> listener_; |
| std::unique_ptr<l2cap::testing::FakeChannel> fake_chan_; |
| std::unique_ptr<PairingChannel> sm_chan_; |
| std::unique_ptr<Phase2Legacy> phase_2_legacy_; |
| Phase2LegacyArgs phase_args_; |
| int phase_2_complete_count_ = 0; |
| UInt128 stk_; |
| pw::async::HeapDispatcher heap_dispatcher_{dispatcher()}; |
| |
| BT_DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(Phase2LegacyTest); |
| }; |
| |
| using Phase2LegacyDeathTest = Phase2LegacyTest; |
| |
| TEST_F(Phase2LegacyDeathTest, InvalidPairingMethodDies) { |
| Phase2LegacyArgs args; |
| // Legacy Pairing does not permit Numeric Comparison (V5.0, Vol. 3, Part H, |
| // Section 2.3.5.1) |
| args.features.method = PairingMethod::kNumericComparison; |
| ASSERT_DEATH_IF_SUPPORTED(NewPhase2Legacy(args), "method"); |
| } |
| |
| TEST_F(Phase2LegacyTest, InitiatorJustWorksStkSucceeds) { |
| Phase2LegacyArgs args; |
| args.features.initiator = true; |
| args.features.method = PairingMethod::kJustWorks; |
| NewPhase2Legacy(args); |
| // Using Just Works, pairing should request user confirmation |
| FakeListener::ConfirmCallback confirm_cb = nullptr; |
| listener()->set_confirm_delegate( |
| [&](FakeListener::ConfirmCallback cb) { confirm_cb = std::move(cb); }); |
| |
| Code sent_code = kInvalidCode; |
| std::optional<UInt128> sent_payload = std::nullopt; |
| fake_chan()->SetSendCallback( |
| [&](ByteBufferPtr sdu) { |
| std::tie(sent_code, sent_payload) = |
| ExtractCodeAnd128BitCmd(std::move(sdu)); |
| }, |
| dispatcher()); |
| phase_2_legacy()->Start(); |
| // We should request user confirmation, but not send a message until we |
| // receive it. |
| ASSERT_EQ(kInvalidCode, sent_code); |
| ASSERT_TRUE(confirm_cb); |
| confirm_cb(true); |
| RunUntilIdle(); |
| ASSERT_EQ(kPairingConfirm, sent_code); |
| |
| // Reset |sent_payload| to be able to detect that the FakeChannel's |
| // |send_callback| is notified. |
| sent_payload = std::nullopt; |
| MatchingPair values = |
| GenerateMatchingConfirmAndRandom(0); // Just Works TK is 0 |
| Receive128BitCmd(kPairingConfirm, values.confirm); |
| RunUntilIdle(); |
| ASSERT_EQ(kPairingRandom, sent_code); |
| ASSERT_TRUE(sent_payload.has_value()); |
| |
| // Receive the peer pairing random & verify pairing completes successfully |
| Receive128BitCmd(kPairingRandom, values.random); |
| RunUntilIdle(); |
| ASSERT_EQ(1, phase_2_complete_count()); |
| UInt128 generated_stk; |
| util::S1({0}, values.random, *sent_payload, &generated_stk); |
| ASSERT_EQ(generated_stk, stk()); |
| } |
| |
| TEST_F(Phase2LegacyTest, InitiatorPasskeyInputStkSucceeds) { |
| Phase2LegacyArgs args; |
| args.features.initiator = true; |
| args.features.method = PairingMethod::kPasskeyEntryInput; |
| // preq & pres are set for consistency w/ args.features - not necessary for |
| // the test to pass. |
| args.preq.io_capability = IOCapability::kKeyboardOnly; |
| args.preq.auth_req |= AuthReq::kMITM; |
| args.pres.io_capability = IOCapability::kDisplayOnly; |
| NewPhase2Legacy(args); |
| FakeListener::PasskeyResponseCallback passkey_responder = nullptr; |
| listener()->set_request_passkey_delegate( |
| [&](FakeListener::PasskeyResponseCallback cb) { |
| passkey_responder = std::move(cb); |
| }); |
| |
| Code sent_code = kInvalidCode; |
| std::optional<UInt128> sent_payload = std::nullopt; |
| fake_chan()->SetSendCallback( |
| [&](ByteBufferPtr sdu) { |
| std::tie(sent_code, sent_payload) = |
| ExtractCodeAnd128BitCmd(std::move(sdu)); |
| }, |
| dispatcher()); |
| phase_2_legacy()->Start(); |
| |
| // We should request user confirmation, but not send a message until we |
| // receive it. |
| ASSERT_EQ(kInvalidCode, sent_code); |
| ASSERT_TRUE(passkey_responder); |
| const int32_t kTk = 0x1234; |
| const UInt128 kTk128 = {0x34, 0x12}; |
| passkey_responder(kTk); |
| RunUntilIdle(); |
| ASSERT_EQ(kPairingConfirm, sent_code); |
| |
| // Reset |sent_payload| to be able to detect that the FakeChannel's |
| // |send_callback| is notified. |
| sent_payload = std::nullopt; |
| MatchingPair values = GenerateMatchingConfirmAndRandom(kTk); |
| Receive128BitCmd(kPairingConfirm, values.confirm); |
| RunUntilIdle(); |
| ASSERT_EQ(kPairingRandom, sent_code); |
| ASSERT_TRUE(sent_payload.has_value()); |
| |
| // Receive the peer pairing random & verify pairing completes successfully |
| Receive128BitCmd(kPairingRandom, values.random); |
| RunUntilIdle(); |
| ASSERT_EQ(1, phase_2_complete_count()); |
| UInt128 generated_stk; |
| util::S1(kTk128, values.random, *sent_payload, &generated_stk); |
| ASSERT_EQ(generated_stk, stk()); |
| } |
| |
| // This test is shorter than InitiatorPasskeyInputStkSucceeds because it only |
| // tests the code paths that differ for PasskeyDisplay, which all take place |
| // before sending the Confirm value. |
| TEST_F(Phase2LegacyTest, InitiatorPasskeyDisplaySucceeds) { |
| Phase2LegacyArgs args; |
| args.features.initiator = true; |
| args.features.method = PairingMethod::kPasskeyEntryDisplay; |
| // preq & pres are set for consistency w/ args.features - not necessary for |
| // the test to pass. |
| args.preq.io_capability = IOCapability::kDisplayOnly; |
| args.preq.auth_req |= AuthReq::kMITM; |
| args.pres.io_capability = IOCapability::kKeyboardOnly; |
| NewPhase2Legacy(args); |
| |
| FakeListener::ConfirmCallback display_confirmer = nullptr; |
| listener()->set_display_delegate( |
| [&](uint32_t, bool, FakeListener::ConfirmCallback cb) { |
| display_confirmer = std::move(cb); |
| }); |
| |
| Code sent_code = kInvalidCode; |
| std::optional<UInt128> sent_payload = std::nullopt; |
| fake_chan()->SetSendCallback( |
| [&](ByteBufferPtr sdu) { |
| std::tie(sent_code, sent_payload) = |
| ExtractCodeAnd128BitCmd(std::move(sdu)); |
| }, |
| dispatcher()); |
| phase_2_legacy()->Start(); |
| // We should request user confirmation, but not send a message until we |
| // receive it. |
| ASSERT_EQ(kInvalidCode, sent_code); |
| ASSERT_TRUE(display_confirmer); |
| display_confirmer(true); |
| RunUntilIdle(); |
| ASSERT_EQ(kPairingConfirm, sent_code); |
| ASSERT_TRUE(sent_payload.has_value()); |
| // After sending Pairing Confirm, the behavior is the same as |
| // InitiatorPasskeyInputStkSucceeds |
| } |
| |
| TEST_F(Phase2LegacyTest, InitiatorReceivesConfirmBeforeTkFails) { |
| Phase2LegacyArgs args; |
| args.features.initiator = true; |
| NewPhase2Legacy(args); |
| FakeListener::ConfirmCallback confirm_cb = nullptr; |
| listener()->set_confirm_delegate( |
| [&](FakeListener::ConfirmCallback cb) { confirm_cb = std::move(cb); }); |
| |
| ByteBufferPtr sent_sdu = nullptr; |
| fake_chan()->SetSendCallback( |
| [&](ByteBufferPtr sdu) { sent_sdu = std::move(sdu); }, dispatcher()); |
| phase_2_legacy()->Start(); |
| ASSERT_TRUE(confirm_cb); |
| ASSERT_FALSE(sent_sdu); |
| |
| // Receive peer confirm (generated from arbitrary peer rand {0}) before |
| // |confirm_cb| is notified |
| const auto kPairingConfirmCmd = |
| Make128BitCmd(kPairingConfirm, GenerateConfirmValue({0})); |
| const StaticByteBuffer<PacketSize<ErrorCode>()> kExpectedFailure{ |
| kPairingFailed, ErrorCode::kUnspecifiedReason}; |
| ASSERT_TRUE(ReceiveAndExpect(kPairingConfirmCmd, kExpectedFailure)); |
| } |
| |
| TEST_F(Phase2LegacyTest, InvalidConfirmValueFails) { |
| Code sent_code = kInvalidCode; |
| std::optional<UInt128> sent_payload = std::nullopt; |
| fake_chan()->SetSendCallback( |
| [&](ByteBufferPtr sdu) { |
| std::tie(sent_code, sent_payload) = |
| ExtractCodeAnd128BitCmd(std::move(sdu)); |
| }, |
| dispatcher()); |
| phase_2_legacy()->Start(); |
| RunUntilIdle(); |
| ASSERT_EQ(kPairingConfirm, sent_code); |
| // Reset |sent_payload| to be able to detect that the FakeChannel's |
| // |send_callback| is notified. |
| sent_payload = std::nullopt; |
| MatchingPair values = |
| GenerateMatchingConfirmAndRandom(0); // Just Works TK is 0 |
| Receive128BitCmd(kPairingConfirm, values.confirm); |
| RunUntilIdle(); |
| ASSERT_EQ(kPairingRandom, sent_code); |
| // Change the peer random so that the confirm value we sent does not match the |
| // random value. |
| UInt128 mismatched_peer_rand = values.random; |
| mismatched_peer_rand[0] += 1; |
| const auto kPairingRandomCmd = |
| Make128BitCmd(kPairingRandom, mismatched_peer_rand); |
| const StaticByteBuffer<PacketSize<ErrorCode>()> kExpectedFailure{ |
| kPairingFailed, ErrorCode::kConfirmValueFailed}; |
| ASSERT_TRUE(ReceiveAndExpect(kPairingRandomCmd, kExpectedFailure)); |
| ASSERT_EQ(1, listener()->pairing_error_count()); |
| } |
| |
| TEST_F(Phase2LegacyTest, JustWorksUserConfirmationRejectedPairingFails) { |
| Phase2LegacyArgs args; |
| args.features.method = PairingMethod::kJustWorks; |
| NewPhase2Legacy(args); |
| // Reject the TK request |
| bool confirmation_requested = false; |
| listener()->set_confirm_delegate([&](FakeListener::ConfirmCallback cb) { |
| confirmation_requested = true; |
| cb(false); |
| }); |
| (void)heap_dispatcher().Post( |
| [this](pw::async::Context /*ctx*/, pw::Status status) { |
| if (status.ok()) { |
| phase_2_legacy()->Start(); |
| } |
| }); |
| const StaticByteBuffer<PacketSize<ErrorCode>()> kExpectedFailure{ |
| kPairingFailed, ErrorCode::kUnspecifiedReason}; |
| ASSERT_TRUE(Expect(kExpectedFailure)); |
| ASSERT_TRUE(confirmation_requested); |
| ASSERT_EQ(1, listener()->pairing_error_count()); |
| } |
| |
| TEST_F(Phase2LegacyTest, PasskeyInputRejectedPairingFails) { |
| Phase2LegacyArgs args; |
| args.features.method = PairingMethod::kPasskeyEntryInput; |
| NewPhase2Legacy(args); |
| // Reject the TK request |
| bool confirmation_requested = false; |
| listener()->set_request_passkey_delegate( |
| [&](FakeListener::PasskeyResponseCallback cb) { |
| confirmation_requested = true; |
| const int64_t kGenericNegativeInt = -12; |
| cb(kGenericNegativeInt); |
| }); |
| (void)heap_dispatcher().Post( |
| [this](pw::async::Context /*ctx*/, pw::Status status) { |
| if (status.ok()) { |
| phase_2_legacy()->Start(); |
| } |
| }); |
| const StaticByteBuffer<PacketSize<ErrorCode>()> kExpectedFailure{ |
| kPairingFailed, ErrorCode::kPasskeyEntryFailed}; |
| ASSERT_TRUE(Expect(kExpectedFailure)); |
| ASSERT_TRUE(confirmation_requested); |
| ASSERT_EQ(1, listener()->pairing_error_count()); |
| } |
| |
| TEST_F(Phase2LegacyTest, PasskeyDisplayRejectedPairingFails) { |
| Phase2LegacyArgs args; |
| args.features.method = PairingMethod::kPasskeyEntryDisplay; |
| NewPhase2Legacy(args); |
| // Reject the TK request |
| bool confirmation_requested = false; |
| listener()->set_display_delegate( |
| [&](uint32_t /*ignore*/, bool, FakeListener::ConfirmCallback cb) { |
| confirmation_requested = true; |
| cb(false); |
| }); |
| (void)heap_dispatcher().Post( |
| [this](pw::async::Context /*ctx*/, pw::Status status) { |
| if (status.ok()) { |
| phase_2_legacy()->Start(); |
| } |
| }); |
| const StaticByteBuffer<PacketSize<ErrorCode>()> kExpectedFailure{ |
| kPairingFailed, ErrorCode::kUnspecifiedReason}; |
| ASSERT_TRUE(Expect(kExpectedFailure)); |
| ASSERT_TRUE(confirmation_requested); |
| ASSERT_EQ(1, listener()->pairing_error_count()); |
| } |
| |
| // Each of the pairing methods has its own user input callback, and thus correct |
| // behavior under destruction of the Phase needs to be checked for each method. |
| TEST_F(Phase2LegacyTest, PhaseDestroyedWhileWaitingForJustWorksTk) { |
| Phase2LegacyArgs args; |
| args.features.method = PairingMethod::kJustWorks; |
| NewPhase2Legacy(args); |
| FakeListener::ConfirmCallback respond = nullptr; |
| listener()->set_confirm_delegate([&](auto rsp) { respond = std::move(rsp); }); |
| phase_2_legacy()->Start(); |
| |
| ASSERT_TRUE(respond); |
| |
| DestroyPhase2(); |
| respond(true); |
| RunUntilIdle(); |
| SUCCEED(); |
| } |
| |
| TEST_F(Phase2LegacyTest, PhaseDestroyedWhileWaitingForPasskeyInputTk) { |
| Phase2LegacyArgs args; |
| args.features.method = PairingMethod::kPasskeyEntryInput; |
| NewPhase2Legacy(args); |
| FakeListener::PasskeyResponseCallback respond = nullptr; |
| listener()->set_request_passkey_delegate( |
| [&](auto rsp) { respond = std::move(rsp); }); |
| phase_2_legacy()->Start(); |
| |
| ASSERT_TRUE(respond); |
| |
| DestroyPhase2(); |
| respond(1234); |
| RunUntilIdle(); |
| SUCCEED(); |
| } |
| |
| TEST_F(Phase2LegacyTest, PhaseDestroyedWaitingForPasskeyDisplayTk) { |
| Phase2LegacyArgs args; |
| args.features.method = PairingMethod::kPasskeyEntryDisplay; |
| NewPhase2Legacy(args); |
| FakeListener::ConfirmCallback respond = nullptr; |
| listener()->set_display_delegate( |
| [&](uint32_t /*unused*/, bool, FakeListener::ConfirmCallback rsp) { |
| respond = std::move(rsp); |
| }); |
| phase_2_legacy()->Start(); |
| |
| ASSERT_TRUE(respond); |
| |
| DestroyPhase2(); |
| respond(true); |
| RunUntilIdle(); |
| SUCCEED(); |
| } |
| |
| TEST_F(Phase2LegacyTest, ReceiveRandomBeforeTkFails) { |
| // This test assumes initiator flow, but the behavior verified is the same for |
| // responder flow. |
| FakeListener::ConfirmCallback confirm_cb = nullptr; |
| listener()->set_confirm_delegate( |
| [&](FakeListener::ConfirmCallback cb) { confirm_cb = std::move(cb); }); |
| |
| phase_2_legacy()->Start(); |
| // We should have made the pairing delegate request, which will not be |
| // responded to. |
| ASSERT_TRUE(confirm_cb); |
| |
| MatchingPair values = |
| GenerateMatchingConfirmAndRandom(0); // Just Works TK is 0 |
| const auto kPairingRandomCmd = Make128BitCmd(kPairingRandom, values.random); |
| const StaticByteBuffer<PacketSize<ErrorCode>()> kExpectedFailure{ |
| kPairingFailed, ErrorCode::kUnspecifiedReason}; |
| ASSERT_TRUE(ReceiveAndExpect(kPairingRandomCmd, kExpectedFailure)); |
| ASSERT_EQ(1, listener()->pairing_error_count()); |
| } |
| |
| TEST_F(Phase2LegacyTest, ReceiveRandomBeforeConfirmFails) { |
| // This test assumes initiator flow, but the behavior verified is the same for |
| // responder flow. |
| bool requested_confirmation = false; |
| // We automatically confirm the TK to check the case where we have a TK, but |
| // no peer confirm. |
| listener()->set_confirm_delegate([&](FakeListener::ConfirmCallback cb) { |
| requested_confirmation = true; |
| cb(true); |
| }); |
| phase_2_legacy()->Start(); |
| ASSERT_TRUE(requested_confirmation); |
| MatchingPair values = |
| GenerateMatchingConfirmAndRandom(0); // Just Works TK is 0 |
| const auto kPairingRandomCmd = Make128BitCmd(kPairingRandom, values.random); |
| const StaticByteBuffer<PacketSize<ErrorCode>()> kExpectedFailure{ |
| kPairingFailed, ErrorCode::kUnspecifiedReason}; |
| ASSERT_TRUE(ReceiveAndExpect(kPairingRandomCmd, kExpectedFailure)); |
| ASSERT_EQ(1, listener()->pairing_error_count()); |
| } |
| |
| TEST_F(Phase2LegacyTest, ReceivePairingFailed) { |
| phase_2_legacy()->Start(); |
| fake_chan()->Receive(StaticByteBuffer<PacketSize<ErrorCode>()>{ |
| kPairingFailed, ErrorCode::kPairingNotSupported}); |
| RunUntilIdle(); |
| |
| EXPECT_EQ(1, listener()->pairing_error_count()); |
| EXPECT_EQ(Error(ErrorCode::kPairingNotSupported), listener()->last_error()); |
| } |
| |
| TEST_F(Phase2LegacyTest, UnsupportedCommandDuringPairing) { |
| // Don't confirm the TK so that the confirm value is not sent; |
| listener()->set_confirm_delegate([](auto) {}); |
| phase_2_legacy()->Start(); |
| |
| const StaticByteBuffer<PacketSize<ErrorCode>()> kExpected{ |
| kPairingFailed, ErrorCode::kCommandNotSupported}; |
| ASSERT_TRUE(ReceiveAndExpect(StaticByteBuffer<1>(0xFF), |
| kExpected)); // 0xFF is not an SMP code. |
| EXPECT_EQ(1, listener()->pairing_error_count()); |
| EXPECT_EQ(Error(ErrorCode::kCommandNotSupported), listener()->last_error()); |
| } |
| |
| TEST_F(Phase2LegacyTest, ReceiveMalformedPacket) { |
| phase_2_legacy()->Start(); |
| // clang-format off |
| const StaticByteBuffer<PacketSize<PairingRandomValue>() - 1> kMalformedPairingRandom { |
| kPairingRandom, |
| // Random value (1 octet too short) |
| 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 |
| }; |
| const StaticByteBuffer<PacketSize<ErrorCode>()> kExpectedFailure { |
| kPairingFailed, ErrorCode::kInvalidParameters |
| }; |
| // clang-format on |
| |
| EXPECT_TRUE(ReceiveAndExpect(kMalformedPairingRandom, kExpectedFailure)); |
| } |
| |
| TEST_F(Phase2LegacyTest, ResponderJustWorksStkSucceeds) { |
| Phase2LegacyArgs args; |
| args.features.initiator = false; |
| args.features.method = PairingMethod::kJustWorks; |
| NewPhase2Legacy(args); |
| // Using Just Works, pairing should request user confirmation |
| FakeListener::ConfirmCallback confirm_cb = nullptr; |
| listener()->set_confirm_delegate( |
| [&](FakeListener::ConfirmCallback cb) { confirm_cb = std::move(cb); }); |
| |
| Code sent_code = kInvalidCode; |
| std::optional<UInt128> sent_payload = std::nullopt; |
| fake_chan()->SetSendCallback( |
| [&](ByteBufferPtr sdu) { |
| std::tie(sent_code, sent_payload) = |
| ExtractCodeAnd128BitCmd(std::move(sdu)); |
| }, |
| dispatcher()); |
| phase_2_legacy()->Start(); |
| // We should not send a message until we receive the requested user input AND |
| // the peer confirm. |
| ASSERT_TRUE(confirm_cb); |
| ASSERT_EQ(kInvalidCode, sent_code); |
| confirm_cb(true); |
| RunUntilIdle(); |
| ASSERT_EQ(kInvalidCode, sent_code); |
| |
| // Now we receive the peer confirm & should send ours. |
| MatchingPair values = |
| GenerateMatchingConfirmAndRandom(0); // Just Works TK is 0 |
| Receive128BitCmd(kPairingConfirm, values.confirm); |
| RunUntilIdle(); |
| ASSERT_EQ(kPairingConfirm, sent_code); |
| |
| // Reset |sent_payload| to be able to detect that the FakeChannel's |
| // |send_callback| is notified. |
| sent_payload = std::nullopt; |
| // Receive the peer pairing random & verify we send our random & pairing |
| // completes. |
| Receive128BitCmd(kPairingRandom, values.random); |
| RunUntilIdle(); |
| ASSERT_EQ(kPairingRandom, sent_code); |
| ASSERT_TRUE(sent_payload.has_value()); |
| ASSERT_EQ(1, phase_2_complete_count()); |
| UInt128 generated_stk; |
| util::S1({0}, *sent_payload, values.random, &generated_stk); |
| ASSERT_EQ(generated_stk, stk()); |
| } |
| |
| TEST_F(Phase2LegacyTest, ResponderPasskeyInputStkSucceeds) { |
| Phase2LegacyArgs args; |
| args.features.initiator = false; |
| args.features.method = PairingMethod::kPasskeyEntryInput; |
| // preq & pres are set for consistency w/ args.features - not necessary for |
| // the test to pass. |
| args.preq.io_capability = IOCapability::kDisplayOnly; |
| args.preq.auth_req |= AuthReq::kMITM; |
| args.pres.io_capability = IOCapability::kKeyboardOnly; |
| NewPhase2Legacy(args); |
| FakeListener::PasskeyResponseCallback passkey_responder = nullptr; |
| listener()->set_request_passkey_delegate( |
| [&](FakeListener::PasskeyResponseCallback cb) { |
| passkey_responder = std::move(cb); |
| }); |
| |
| Code sent_code = kInvalidCode; |
| std::optional<UInt128> sent_payload = std::nullopt; |
| fake_chan()->SetSendCallback( |
| [&](ByteBufferPtr sdu) { |
| std::tie(sent_code, sent_payload) = |
| ExtractCodeAnd128BitCmd(std::move(sdu)); |
| }, |
| dispatcher()); |
| |
| phase_2_legacy()->Start(); |
| |
| // We should not send a message until we receive the requested user input AND |
| // the peer confirm. |
| ASSERT_TRUE(passkey_responder); |
| ASSERT_EQ(kInvalidCode, sent_code); |
| const int32_t kTk = 0x1234; |
| const UInt128 kTk128 = {0x34, 0x12}; |
| passkey_responder(kTk); |
| RunUntilIdle(); |
| ASSERT_EQ(kInvalidCode, sent_code); |
| // Now we receive the peer confirm & should send ours. |
| MatchingPair values = GenerateMatchingConfirmAndRandom(kTk); |
| Receive128BitCmd(kPairingConfirm, values.confirm); |
| RunUntilIdle(); |
| ASSERT_EQ(kPairingConfirm, sent_code); |
| |
| // Reset |sent_payload| to be able to detect that the FakeChannel's |
| // |send_callback| is notified. |
| sent_payload = std::nullopt; |
| // Receive the peer pairing random & verify we send our random & pairing |
| // completes. |
| Receive128BitCmd(kPairingRandom, values.random); |
| RunUntilIdle(); |
| ASSERT_EQ(kPairingRandom, sent_code); |
| ASSERT_TRUE(sent_payload.has_value()); |
| ASSERT_EQ(1, phase_2_complete_count()); |
| UInt128 generated_stk; |
| util::S1(kTk128, *sent_payload, values.random, &generated_stk); |
| ASSERT_EQ(generated_stk, stk()); |
| } |
| |
| // This test is shorter than ResponderPasskeyInputStkSucceeds because it only |
| // tests the code paths that differ for PasskeyDisplay, which all take place |
| // before sending the Confirm value. |
| TEST_F(Phase2LegacyTest, ResponderPasskeyDisplaySucceeds) { |
| Phase2LegacyArgs args; |
| args.features.initiator = false; |
| args.features.method = PairingMethod::kPasskeyEntryDisplay; |
| // preq & pres are set for consistency w/ args.features - not necessary for |
| // the test to pass. |
| args.preq.io_capability = IOCapability::kKeyboardOnly; |
| args.preq.auth_req |= AuthReq::kMITM; |
| args.pres.io_capability = IOCapability::kDisplayOnly; |
| NewPhase2Legacy(args); |
| |
| FakeListener::ConfirmCallback display_confirmer = nullptr; |
| uint32_t passkey = 0; |
| listener()->set_display_delegate( |
| [&](uint32_t key, bool, FakeListener::ConfirmCallback cb) { |
| passkey = key; |
| display_confirmer = std::move(cb); |
| }); |
| |
| Code sent_code = kInvalidCode; |
| std::optional<UInt128> sent_payload = std::nullopt; |
| fake_chan()->SetSendCallback( |
| [&](ByteBufferPtr sdu) { |
| std::tie(sent_code, sent_payload) = |
| ExtractCodeAnd128BitCmd(std::move(sdu)); |
| }, |
| dispatcher()); |
| |
| phase_2_legacy()->Start(); |
| // We should not send a message until we receive the requested user input AND |
| // the peer confirm. |
| ASSERT_TRUE(display_confirmer); |
| ASSERT_EQ(kInvalidCode, sent_code); |
| display_confirmer(true); |
| RunUntilIdle(); |
| ASSERT_EQ(kInvalidCode, sent_code); |
| |
| // Now we receive the peer confirm & should send ours. |
| MatchingPair values = GenerateMatchingConfirmAndRandom(passkey); |
| Receive128BitCmd(kPairingConfirm, values.confirm); |
| RunUntilIdle(); |
| ASSERT_EQ(kPairingConfirm, sent_code); |
| ASSERT_TRUE(sent_payload.has_value()); |
| } |
| |
| TEST_F(Phase2LegacyTest, ResponderReceivesConfirmBeforeTkSucceeds) { |
| Phase2LegacyArgs args; |
| args.features.initiator = false; |
| NewPhase2Legacy(args); |
| // Using Just Works, pairing should request user confirmation |
| FakeListener::ConfirmCallback confirm_cb = nullptr; |
| listener()->set_confirm_delegate( |
| [&](FakeListener::ConfirmCallback cb) { confirm_cb = std::move(cb); }); |
| |
| Code sent_code = kInvalidCode; |
| std::optional<UInt128> sent_payload = std::nullopt; |
| fake_chan()->SetSendCallback( |
| [&](ByteBufferPtr sdu) { |
| std::tie(sent_code, sent_payload) = |
| ExtractCodeAnd128BitCmd(std::move(sdu)); |
| }, |
| dispatcher()); |
| phase_2_legacy()->Start(); |
| |
| // We should not send a message until we receive the requested user input AND |
| // the peer confirm. |
| ASSERT_TRUE(confirm_cb); |
| ASSERT_EQ(kInvalidCode, sent_code); |
| MatchingPair values = |
| GenerateMatchingConfirmAndRandom(0); // Just Works TK is 0 |
| Receive128BitCmd(kPairingConfirm, values.confirm); |
| RunUntilIdle(); |
| ASSERT_EQ(kInvalidCode, sent_code); |
| |
| // Now we received the user input & should send the peer our confirmation. |
| confirm_cb(true); |
| RunUntilIdle(); |
| ASSERT_EQ(kPairingConfirm, sent_code); |
| // Reset |sent_payload| to be able to detect that the FakeChannel's |
| // |send_callback| is notified. |
| sent_payload = std::nullopt; |
| // Receive the peer pairing random & verify we send our random & pairing |
| // completes. |
| Receive128BitCmd(kPairingRandom, values.random); |
| RunUntilIdle(); |
| ASSERT_EQ(kPairingRandom, sent_code); |
| ASSERT_TRUE(sent_payload.has_value()); |
| ASSERT_EQ(1, phase_2_complete_count()); |
| UInt128 generated_stk; |
| util::S1({0}, *sent_payload, values.random, &generated_stk); |
| ASSERT_EQ(generated_stk, stk()); |
| } |
| |
| TEST_F(Phase2LegacyTest, ReceiveConfirmValueTwiceFails) { |
| // This test uses the responder flow, but the behavior verified is the same |
| // for initiator flow. |
| Phase2LegacyArgs args; |
| args.features.initiator = false; |
| NewPhase2Legacy(args); |
| |
| Code code = kInvalidCode; |
| fake_chan()->SetSendCallback( |
| [&](ByteBufferPtr sdu) { |
| std::tie(code, std::ignore) = ExtractCodeAnd128BitCmd(std::move(sdu)); |
| }, |
| dispatcher()); |
| phase_2_legacy()->Start(); |
| |
| MatchingPair values = |
| GenerateMatchingConfirmAndRandom(0); // Just Works TK is 0 |
| Receive128BitCmd(kPairingConfirm, values.confirm); |
| RunUntilIdle(); |
| ASSERT_EQ(kPairingConfirm, code); |
| const auto kPairingConfirmCmd = |
| Make128BitCmd(kPairingConfirm, values.confirm); |
| // Pairing should fail after receiving 2 confirm values with |
| // kUnspecifiedReason |
| const StaticByteBuffer<PacketSize<ErrorCode>()> kExpectedFailure{ |
| kPairingFailed, ErrorCode::kUnspecifiedReason}; |
| ASSERT_TRUE(ReceiveAndExpect(kPairingConfirmCmd, kExpectedFailure)); |
| ASSERT_EQ(1, listener()->pairing_error_count()); |
| } |
| |
| // Phase 2 ends after receiving the second random value & subsequently sending |
| // its own, but if the Phase 2 object is kept around and receives a second |
| // random value, pairing will fail. |
| TEST_F(Phase2LegacyTest, ReceiveRandomValueTwiceFails) { |
| // This test uses the responder flow, but the behavior verified is the same |
| // for initiator flow. |
| Phase2LegacyArgs args; |
| args.features.initiator = false; |
| NewPhase2Legacy(args); |
| |
| phase_2_legacy()->Start(); |
| |
| MatchingPair values = |
| GenerateMatchingConfirmAndRandom(0); // Just Works TK is 0 |
| Receive128BitCmd(kPairingConfirm, values.confirm); |
| RunUntilIdle(); |
| Receive128BitCmd(kPairingRandom, values.random); |
| RunUntilIdle(); |
| // We've completed Phase 2, and should've notified the callback |
| ASSERT_EQ(1, phase_2_complete_count()); |
| const auto kPairingRandomCmd = Make128BitCmd(kPairingRandom, values.random); |
| // Pairing should fail after receiving a second random value with |
| // kUnspecifiedReason |
| const StaticByteBuffer<PacketSize<ErrorCode>()> kExpectedFailure{ |
| kPairingFailed, ErrorCode::kUnspecifiedReason}; |
| ASSERT_TRUE(ReceiveAndExpect(kPairingRandomCmd, kExpectedFailure)); |
| ASSERT_EQ(1, listener()->pairing_error_count()); |
| } |
| } // namespace |
| } // namespace bt::sm |