// 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 #include #include #include #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{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(); fake_chan_ = CreateFakeChannel(options); sm_chan_ = std::make_unique(fake_chan_->GetWeakPtr()); auto role = phase_args.features.initiator ? Role::kInitiator : Role::kResponder; StaticByteBuffer()> preq, pres; preq.WriteObj(phase_args.preq); pres.WriteObj(phase_args.pres); phase_2_legacy_ = std::make_unique(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()> buffer; PacketWriter writer(cmd_code, &buffer); *writer.mutable_payload() = value; return DynamicByteBuffer(buffer); } UInt128 GenerateConfirmValue(const UInt128& random, uint32_t tk = 0) const { tk = pw::bytes::ConvertOrderTo(cpp20::endian::little, tk); UInt128 tk128; tk128.fill(0); std::memcpy(tk128.data(), &tk, sizeof(tk)); StaticByteBuffer()> 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(pair.random.data()), pair.random.size()}); pair.confirm = GenerateConfirmValue(pair.random, tk); return pair; } static std::pair ExtractCodeAnd128BitCmd(ByteBufferPtr sdu) { PW_CHECK(sdu, "Tried to ExtractCodeAnd128BitCmd from nullptr in test"); auto maybe_reader = ValidPacketReader::ParseSdu(sdu); PW_CHECK(maybe_reader.is_ok(), "Tried to ExtractCodeAnd128BitCmd from invalid SMP packet"); return {maybe_reader.value().code(), maybe_reader.value().payload()}; } 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 listener_; std::unique_ptr fake_chan_; std::unique_ptr sm_chan_; std::unique_ptr 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 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 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 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()> kExpectedFailure{ kPairingFailed, ErrorCode::kUnspecifiedReason}; ASSERT_TRUE(ReceiveAndExpect(kPairingConfirmCmd, kExpectedFailure)); } TEST_F(Phase2LegacyTest, InvalidConfirmValueFails) { Code sent_code = kInvalidCode; std::optional 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()> 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()> 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()> 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()> 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()> 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()> 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()>{ 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()> 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() - 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()> 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 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 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 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 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()> 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()> kExpectedFailure{ kPairingFailed, ErrorCode::kUnspecifiedReason}; ASSERT_TRUE(ReceiveAndExpect(kPairingRandomCmd, kExpectedFailure)); ASSERT_EQ(1, listener()->pairing_error_count()); } } // namespace } // namespace bt::sm