blob: 2adc659a13c7ee814cfde0cbf00ac53ed7c8d236 [file] [log] [blame]
// Copyright 2020 the Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "phase_2_secure_connections.h"
#include <zircon/assert.h>
#include <memory>
#include <optional>
#include <type_traits>
#include "src/connectivity/bluetooth/core/bt-host/common/byte_buffer.h"
#include "src/connectivity/bluetooth/core/bt-host/common/log.h"
#include "src/connectivity/bluetooth/core/bt-host/common/status.h"
#include "src/connectivity/bluetooth/core/bt-host/common/uint256.h"
#include "src/connectivity/bluetooth/core/bt-host/hci/connection.h"
#include "src/connectivity/bluetooth/core/bt-host/sm/ecdh_key.h"
#include "src/connectivity/bluetooth/core/bt-host/sm/packet.h"
#include "src/connectivity/bluetooth/core/bt-host/sm/pairing_phase.h"
#include "src/connectivity/bluetooth/core/bt-host/sm/sc_stage_1_just_works_numeric_comparison.h"
#include "src/connectivity/bluetooth/core/bt-host/sm/sc_stage_1_passkey.h"
#include "src/connectivity/bluetooth/core/bt-host/sm/smp.h"
#include "src/connectivity/bluetooth/core/bt-host/sm/status.h"
#include "src/connectivity/bluetooth/core/bt-host/sm/types.h"
#include "src/connectivity/bluetooth/core/bt-host/sm/util.h"
#include "src/lib/fxl/memory/weak_ptr.h"
namespace bt::sm {
Phase2SecureConnections::Phase2SecureConnections(
fxl::WeakPtr<PairingChannel> chan, fxl::WeakPtr<Listener> listener, Role role,
PairingFeatures features, PairingRequestParams preq, PairingResponseParams pres,
const DeviceAddress& initiator_addr, const DeviceAddress& responder_addr,
OnPhase2KeyGeneratedCallback cb)
: PairingPhase(std::move(chan), std::move(listener), role),
sent_local_ecdh_(false),
local_ecdh_(),
peer_ecdh_(),
stage_1_results_(),
sent_local_dhkey_check_(false),
features_(features),
preq_(preq),
pres_(pres),
initiator_addr_(initiator_addr),
responder_addr_(responder_addr),
weak_ptr_factory_(this),
on_ltk_ready_(std::move(cb)) {
ZX_ASSERT(features_.secure_connections);
local_ecdh_ = LocalEcdhKey::Create();
ZX_ASSERT_MSG(local_ecdh_.has_value(), "failed to generate ecdh key");
ZX_ASSERT(sm_chan().SupportsSecureConnections());
sm_chan().SetChannelHandler(weak_ptr_factory_.GetWeakPtr());
}
void Phase2SecureConnections::Start() {
ZX_ASSERT(!has_failed());
if (role() == Role::kInitiator) {
SendLocalPublicKey();
}
}
void Phase2SecureConnections::SendLocalPublicKey() {
ZX_ASSERT(!sent_local_ecdh_);
// If in the responder role (i.e. not in the initiator role), attempting to send our Public Key
// before we've received the peer's is a programmer error.
ZX_ASSERT(role() == Role::kInitiator || peer_ecdh_.has_value());
sm_chan().SendMessage(kPairingPublicKey, local_ecdh_->GetSerializedPublicKey());
sent_local_ecdh_ = true;
bt_log(DEBUG, "sm", "sent ecdh public key to peer");
if (role() == Role::kResponder) {
ZX_ASSERT(ecdh_exchange_complete());
StartAuthenticationStage1();
}
}
ErrorCode Phase2SecureConnections::CanReceivePeerPublicKey() const {
// Only allowed on the LE transport.
if (sm_chan().link_type() != hci::Connection::LinkType::kLE) {
bt_log(DEBUG, "sm", "cannot accept peer ecdh key value over BR/EDR");
return ErrorCode::kCommandNotSupported;
}
if (peer_ecdh_.has_value()) {
bt_log(WARN, "sm", "received peer ecdh key twice!");
return ErrorCode::kUnspecifiedReason;
}
if (role() == Role::kInitiator && !sent_local_ecdh_) {
bt_log(WARN, "sm", "received peer ecdh key before sending local key as initiator!");
return ErrorCode::kUnspecifiedReason;
}
return ErrorCode::kNoError;
}
void Phase2SecureConnections::OnPeerPublicKey(PairingPublicKeyParams peer_pub_key) {
ErrorCode ecode = CanReceivePeerPublicKey();
if (ecode != ErrorCode::kNoError) {
Abort(ecode);
return;
}
std::optional<EcdhKey> maybe_peer_key = EcdhKey::ParseFromPublicKey(peer_pub_key);
if (!maybe_peer_key.has_value()) {
bt_log(WARN, "sm", "unable to validate peer public ECDH key");
Abort(ErrorCode::kInvalidParameters);
return;
}
EcdhKey peer_key = std::move(*maybe_peer_key);
peer_ecdh_ = std::move(peer_key);
if (role() == Role::kResponder) {
SendLocalPublicKey();
} else {
ZX_ASSERT(ecdh_exchange_complete());
StartAuthenticationStage1();
}
}
void Phase2SecureConnections::StartAuthenticationStage1() {
ZX_ASSERT(peer_ecdh_);
auto self = weak_ptr_factory_.GetWeakPtr();
auto complete_cb = [self](fit::result<ScStage1::Output, ErrorCode> res) {
if (self) {
self->OnAuthenticationStage1Complete(res);
}
};
if (is_just_works_or_numeric_comparison()) {
bt_log(DEBUG, "sm", "Starting SC Stage 1 Numeric Comparison/Just Works");
stage_1_ = std::make_unique<ScStage1JustWorksNumericComparison>(
listener(), role(), local_ecdh_->GetPublicKeyX(), peer_ecdh_->GetPublicKeyX(),
features_.method, sm_chan().GetWeakPtr(), std::move(complete_cb));
} else if (is_passkey_entry()) {
bt_log(DEBUG, "sm", "Starting SC Stage 1 Passkey Entry");
stage_1_ = std::make_unique<ScStage1Passkey>(listener(), role(), local_ecdh_->GetPublicKeyX(),
peer_ecdh_->GetPublicKeyX(), features_.method,
sm_chan().GetWeakPtr(), std::move(complete_cb));
} else { // method == kOutOfBand
// TODO(fxbug.dev/601): OOB would require significant extra plumbing & add security exposure not
// necessary for current goals. This is not spec-compliant but should allow us to pass PTS.
bt_log(WARN, "sm", "Received unsupported request for OOB pairing");
Abort(ErrorCode::kCommandNotSupported);
return;
}
stage_1_->Run();
}
void Phase2SecureConnections::OnAuthenticationStage1Complete(
fit::result<ScStage1::Output, ErrorCode> result) {
ZX_ASSERT(peer_ecdh_.has_value());
ZX_ASSERT(stage_1_);
ZX_ASSERT(!ltk_.has_value());
ZX_ASSERT(!expected_peer_dhkey_check_.has_value());
ZX_ASSERT(!local_dhkey_check_.has_value());
// The presence of Stage 1 determines whether to accept PairingConfirm/Random packets, so as it
// is now over, it should be reset.
stage_1_ = nullptr;
if (result.is_error()) {
Abort(result.error());
return;
}
stage_1_results_ = result.value();
StartAuthenticationStage2();
}
void Phase2SecureConnections::StartAuthenticationStage2() {
ZX_ASSERT(stage_1_results_.has_value());
std::optional<util::F5Results> maybe_f5 =
util::F5(local_ecdh_->CalculateDhKey(peer_ecdh_.value()), stage_1_results_->initiator_rand,
stage_1_results_->responder_rand, initiator_addr_, responder_addr_);
if (!maybe_f5.has_value()) {
bt_log(WARN, "sm", "unable to calculate local LTK/MacKey");
Abort(ErrorCode::kUnspecifiedReason);
return;
}
// Ea & Eb are the DHKey Check values used/defined in V5.0 Vol. 3 Part H Section 2.3.5.6.5/3.5.7.
// (Ea/Eb) is sent by the (initiator/responder) to be verified by the (responder/initiator). This
// exchange verifies that each device knows the private key associated with its public key.
std::optional<PairingDHKeyCheckValueE> ea =
util::F6(maybe_f5->mac_key, stage_1_results_->initiator_rand,
stage_1_results_->responder_rand, stage_1_results_->responder_r, preq_.auth_req,
preq_.oob_data_flag, preq_.io_capability, initiator_addr_, responder_addr_);
std::optional<PairingDHKeyCheckValueE> eb =
util::F6(maybe_f5->mac_key, stage_1_results_->responder_rand,
stage_1_results_->initiator_rand, stage_1_results_->initiator_r, pres_.auth_req,
pres_.oob_data_flag, pres_.io_capability, responder_addr_, initiator_addr_);
if (!eb.has_value() || !ea.has_value()) {
bt_log(WARN, "sm", "unable to calculate dhkey check \"E\"");
Abort(ErrorCode::kUnspecifiedReason);
return;
}
local_dhkey_check_ = ea;
expected_peer_dhkey_check_ = eb;
if (role() == Role::kResponder) {
std::swap(local_dhkey_check_, expected_peer_dhkey_check_);
}
ltk_ = maybe_f5->ltk;
if (role() == Role::kInitiator) {
SendDhKeyCheckE();
} else if (actual_peer_dhkey_check_.has_value()) {
// As responder, it's possible the initiator sent us the DHKey check while we waited for user
// input. In that case, check it now instead of when we receive it.
ValidatePeerDhKeyCheck();
}
}
void Phase2SecureConnections::SendDhKeyCheckE() {
ZX_ASSERT(stage_1_results_.has_value());
ZX_ASSERT(!sent_local_dhkey_check_);
ZX_ASSERT(ltk_.has_value());
ZX_ASSERT(local_dhkey_check_.has_value());
// Send local DHKey Check
sm_chan().SendMessage(kPairingDHKeyCheck, *local_dhkey_check_);
sent_local_dhkey_check_ = true;
if (role() == Role::kResponder) {
// As responder, we should only send the local DHKey check after receiving and validating the
// peer's. The presence of `peer_dhkey_check` verifies this invariant.
ZX_ASSERT(actual_peer_dhkey_check_.has_value());
on_ltk_ready_(ltk_.value());
}
}
ErrorCode Phase2SecureConnections::CanReceiveDhKeyCheck() const {
// Only allowed on the LE transport.
if (sm_chan().link_type() != hci::Connection::LinkType::kLE) {
bt_log(WARN, "sm", "cannot accept peer ecdh key check over BR/EDR (SC)");
return ErrorCode::kCommandNotSupported;
}
if (!stage_1_results_.has_value() && !stage_1_) {
bt_log(WARN, "sm", "received peer ecdh check too early! (before stage 1 started)");
return ErrorCode::kUnspecifiedReason;
}
if (actual_peer_dhkey_check_.has_value()) {
bt_log(WARN, "sm", "received peer ecdh key check twice (SC)");
return ErrorCode::kUnspecifiedReason;
}
if (role() == Role::kInitiator && !sent_local_dhkey_check_) {
bt_log(WARN, "sm",
"received peer ecdh key check as initiator before sending local ecdh key check (SC)");
return ErrorCode::kUnspecifiedReason;
}
return ErrorCode::kNoError;
}
void Phase2SecureConnections::OnDhKeyCheck(PairingDHKeyCheckValueE check) {
ErrorCode ecode = CanReceiveDhKeyCheck();
if (ecode != ErrorCode::kNoError) {
Abort(ecode);
return;
}
actual_peer_dhkey_check_ = check;
// As responder, it's possible to receive the DHKey check from the peer while waiting for user
// input in Stage 1 - if that happens, we validate the peer DhKey check when Stage 1 completes.
if (!stage_1_results_.has_value()) {
ZX_ASSERT(role() == Role::kResponder);
ZX_ASSERT(stage_1_);
return;
}
ValidatePeerDhKeyCheck();
}
void Phase2SecureConnections::ValidatePeerDhKeyCheck() {
ZX_ASSERT(actual_peer_dhkey_check_.has_value());
ZX_ASSERT(expected_peer_dhkey_check_.has_value());
if (*expected_peer_dhkey_check_ != *actual_peer_dhkey_check_) {
bt_log(WARN, "sm", "DHKey check value failed - possible attempt to hijack pairing!");
Abort(ErrorCode::kDHKeyCheckFailed);
return;
}
if (role() == Role::kInitiator) {
bt_log(INFO, "sm", "completed secure connections Phase 2 of pairing");
on_ltk_ready_(ltk_.value());
} else {
SendDhKeyCheckE();
}
}
void Phase2SecureConnections::OnPairingConfirm(PairingConfirmValue confirm) {
if (!stage_1_) {
bt_log(WARN, "sm", "received pairing confirm in SC outside of authentication stage 1");
Abort(ErrorCode::kUnspecifiedReason);
return;
}
stage_1_->OnPairingConfirm(confirm);
}
void Phase2SecureConnections::OnPairingRandom(PairingRandomValue rand) {
if (!stage_1_) {
bt_log(WARN, "sm", "received pairing random in SC outside of authentication stage 1");
Abort(ErrorCode::kUnspecifiedReason);
return;
}
stage_1_->OnPairingRandom(rand);
}
void Phase2SecureConnections::OnRxBFrame(ByteBufferPtr sdu) {
fit::result<ValidPacketReader, ErrorCode> maybe_reader = ValidPacketReader::ParseSdu(sdu);
if (maybe_reader.is_error()) {
Abort(maybe_reader.error());
return;
}
ValidPacketReader reader = maybe_reader.value();
Code smp_code = reader.code();
if (smp_code == kPairingFailed) {
OnFailure(Status(reader.payload<ErrorCode>()));
} else if (smp_code == kPairingPublicKey) {
OnPeerPublicKey(reader.payload<PairingPublicKeyParams>());
} else if (smp_code == kPairingConfirm) {
OnPairingConfirm(reader.payload<PairingConfirmValue>());
} else if (smp_code == kPairingRandom) {
OnPairingRandom(reader.payload<PairingRandomValue>());
} else if (smp_code == kPairingDHKeyCheck) {
OnDhKeyCheck(reader.payload<PairingDHKeyCheckValueE>());
} else {
bt_log(INFO, "sm", "received unexpected code %d when in Pairing SecureConnections Phase 2",
smp_code);
Abort(ErrorCode::kUnspecifiedReason);
}
}
} // namespace bt::sm