[bt][l2cap] Add API to upgrade security of a link

The L2CAP system now has a sense of "link security properties". This
will allow the channel creation state machine and service-level users to
request security upgrades. Channel users can now also query the link
security properties directly using the channel.

The actual link security upgrade is not performed by the L2CAP layer but
rather delegated to the layer responsible for registering the link. This
will be done by the GAP layer once this is exposed via data::Domain. It
is also the responsibility of the registering layer (i.e. GAP) to keep
the link security level stored by L2CAP up-to-date.

NET-1151
NET-881 #done
Test: bt-host-unittests

Change-Id: If975da88457919c999fe798211378bdad6dd6c62
diff --git a/drivers/bluetooth/lib/data/domain.cc b/drivers/bluetooth/lib/data/domain.cc
index 7632dfc..b02b4f6 100644
--- a/drivers/bluetooth/lib/data/domain.cc
+++ b/drivers/bluetooth/lib/data/domain.cc
@@ -69,7 +69,13 @@
     PostMessage([this, handle, role, lec = std::move(link_error_callback),
                  dispatcher]() mutable {
       if (l2cap_) {
-        l2cap_->RegisterACL(handle, role, std::move(lec), dispatcher);
+        // TODO(NET-1151): Pass security upgrade callback from an argument.
+        l2cap_->RegisterACL(
+            handle, role, std::move(lec),
+            [](auto, auto, auto) {
+              bt_log(TRACE, "data-domain", "not implemented: L2CAP security");
+            },
+            dispatcher);
       }
     });
   }
@@ -84,8 +90,13 @@
                  link_err_cb = std::move(link_error_callback),
                  chan_cb = std::move(channel_callback), dispatcher]() mutable {
       if (l2cap_) {
-        l2cap_->RegisterLE(handle, role, std::move(cp_cb),
-                           std::move(link_err_cb), dispatcher);
+        // TODO(NET-1151): Pass security upgrade callback from an argument.
+        l2cap_->RegisterLE(
+            handle, role, std::move(cp_cb), std::move(link_err_cb),
+            [](auto, auto, auto) {
+              bt_log(TRACE, "data-domain", "not implemented: L2CAP security");
+            },
+            dispatcher);
 
         auto att = l2cap_->OpenFixedChannel(handle, l2cap::kATTChannelId);
         auto smp = l2cap_->OpenFixedChannel(handle, l2cap::kLESMPChannelId);
diff --git a/drivers/bluetooth/lib/hci/BUILD.gn b/drivers/bluetooth/lib/hci/BUILD.gn
index 2f44959..67226a9 100644
--- a/drivers/bluetooth/lib/hci/BUILD.gn
+++ b/drivers/bluetooth/lib/hci/BUILD.gn
@@ -10,6 +10,8 @@
     "connection_parameters.h",
     "hci.h",
     "hci_constants.h",
+    "link_key.cc",
+    "link_key.h",
     "status.cc",
     "status.h",
     "util.cc",
@@ -42,8 +44,6 @@
     "legacy_low_energy_advertiser.h",
     "legacy_low_energy_scanner.cc",
     "legacy_low_energy_scanner.h",
-    "link_key.cc",
-    "link_key.h",
     "low_energy_advertiser.h",
     "low_energy_connector.cc",
     "low_energy_connector.h",
diff --git a/drivers/bluetooth/lib/l2cap/BUILD.gn b/drivers/bluetooth/lib/l2cap/BUILD.gn
index 73b3623..7fb2091 100644
--- a/drivers/bluetooth/lib/l2cap/BUILD.gn
+++ b/drivers/bluetooth/lib/l2cap/BUILD.gn
@@ -9,8 +9,9 @@
     "l2cap.h",
   ]
 
-  deps = [
+  public_deps = [
     "//garnet/drivers/bluetooth/lib/hci:definitions",
+    "//garnet/drivers/bluetooth/lib/sm:definitions",
   ]
 }
 
diff --git a/drivers/bluetooth/lib/l2cap/channel.cc b/drivers/bluetooth/lib/l2cap/channel.cc
index 9f474d5..e2586a3 100644
--- a/drivers/bluetooth/lib/l2cap/channel.cc
+++ b/drivers/bluetooth/lib/l2cap/channel.cc
@@ -47,6 +47,14 @@
   ZX_DEBUG_ASSERT(link_);
 }
 
+const sm::SecurityProperties ChannelImpl::security() {
+  std::lock_guard<std::mutex> lock(mtx_);
+  if (link_) {
+    return link_->security();
+  }
+  return sm::SecurityProperties();
+}
+
 bool ChannelImpl::Activate(RxCallback rx_callback,
                            ClosedCallback closed_callback,
                            async_dispatcher_t* dispatcher) {
@@ -159,6 +167,25 @@
   return true;
 }
 
+void ChannelImpl::UpgradeSecurity(sm::SecurityLevel level,
+                                  sm::StatusCallback callback) {
+  ZX_DEBUG_ASSERT(callback);
+
+  std::lock_guard<std::mutex> lock(mtx_);
+
+  if (!link_ || !active_) {
+    bt_log(TRACE, "l2cap", "Ignoring security request on inactive channel");
+    return;
+  }
+
+  ZX_DEBUG_ASSERT(dispatcher_);
+  async::PostTask(
+      link_->dispatcher(), [link = link_, level, callback = std::move(callback),
+                            dispatcher = dispatcher_]() mutable {
+        link->UpgradeSecurity(level, std::move(callback), dispatcher);
+      });
+}
+
 void ChannelImpl::OnClosed() {
   async_dispatcher_t* dispatcher;
   fit::closure task;
diff --git a/drivers/bluetooth/lib/l2cap/channel.h b/drivers/bluetooth/lib/l2cap/channel.h
index 9acd1d5..022b7ad 100644
--- a/drivers/bluetooth/lib/l2cap/channel.h
+++ b/drivers/bluetooth/lib/l2cap/channel.h
@@ -20,6 +20,8 @@
 
 #include "garnet/drivers/bluetooth/lib/hci/connection.h"
 #include "garnet/drivers/bluetooth/lib/l2cap/sdu.h"
+#include "garnet/drivers/bluetooth/lib/sm/status.h"
+#include "garnet/drivers/bluetooth/lib/sm/types.h"
 #include "lib/fxl/macros.h"
 #include "lib/fxl/synchronization/thread_checker.h"
 
@@ -65,6 +67,30 @@
   // |id()| for fixed channels and allocated by the remote for dynamic channels.
   ChannelId remote_id() const { return remote_id_; }
 
+  // The type of the logical link this channel operates on.
+  hci::Connection::LinkType link_type() const { return link_type_; }
+
+  // The connection handle of the underlying logical link.
+  hci::ConnectionHandle link_handle() const { return link_handle_; }
+
+  // Returns a value that's unique for any channel connected to this device.
+  // If two channels have different unique_ids, they represent different
+  // channels even if their ids match.
+  using UniqueId = uint64_t;
+  UniqueId unique_id() const {
+    static_assert(
+        sizeof(UniqueId) >= sizeof(hci::ConnectionHandle) + sizeof(ChannelId),
+        "UniqueId needs to be large enough to make unique IDs");
+    return (link_handle() << (sizeof(ChannelId) * CHAR_BIT)) | id();
+  }
+
+  uint16_t tx_mtu() const { return tx_mtu_; }
+  uint16_t rx_mtu() const { return rx_mtu_; }
+
+  // Returns the current link security properties of the underlying link.
+  // Returns the lowest security level if the link is closed.
+  virtual const sm::SecurityProperties security() = 0;
+
   // Callback invoked when this channel has been closed without an explicit
   // request from the owner of this instance. For example, this can happen when
   // the remote end closes a dynamically configured channel or when the
@@ -105,31 +131,18 @@
   // close when the link gets removed later.
   virtual void SignalLinkError() = 0;
 
+  // Requests to upgrade the security properties of the underlying link to the
+  // requested |level| and reports the result via |callback|. |callback| will be
+  // run on the dispatcher that the channel was activated on. Has no effect if
+  // the channel is not active.
+  virtual void UpgradeSecurity(sm::SecurityLevel level,
+                               sm::StatusCallback callback) = 0;
+
   // Sends the given SDU payload over this channel. This takes ownership of
   // |sdu|. Returns false if the SDU is rejected, for example because it exceeds
   // the channel's MTU or because the link has been closed.
   virtual bool Send(common::ByteBufferPtr sdu) = 0;
 
-  // The type of the logical link this channel operates on.
-  hci::Connection::LinkType link_type() const { return link_type_; }
-
-  // The connection handle of the underlying logical link.
-  hci::ConnectionHandle link_handle() const { return link_handle_; }
-
-  // Returns a value that's unique for any channel connected to this device.
-  // If two channels have different unique_ids, they represent different
-  // channels even if their ids match.
-  using UniqueId = uint64_t;
-  UniqueId unique_id() const {
-    static_assert(
-        sizeof(UniqueId) >= sizeof(hci::ConnectionHandle) + sizeof(ChannelId),
-        "UniqueId needs to be large enough to make unique IDs");
-    return (link_handle() << (sizeof(ChannelId) * CHAR_BIT)) | id();
-  }
-
-  uint16_t tx_mtu() const { return tx_mtu_; }
-  uint16_t rx_mtu() const { return rx_mtu_; }
-
  protected:
   friend class fbl::RefPtr<Channel>;
   Channel(ChannelId id, ChannelId remote_id,
@@ -157,11 +170,14 @@
 class ChannelImpl : public Channel {
  public:
   // Channel overrides:
+  const sm::SecurityProperties security() override;
   bool Activate(RxCallback rx_callback, ClosedCallback closed_callback,
                 async_dispatcher_t* dispatcher) override;
   void Deactivate() override;
   void SignalLinkError() override;
   bool Send(common::ByteBufferPtr sdu) override;
+  void UpgradeSecurity(sm::SecurityLevel level,
+                       sm::StatusCallback callback) override;
 
  private:
   friend class fbl::RefPtr<ChannelImpl>;
diff --git a/drivers/bluetooth/lib/l2cap/channel_manager.cc b/drivers/bluetooth/lib/l2cap/channel_manager.cc
index 76586cd..a2f10d8 100644
--- a/drivers/bluetooth/lib/l2cap/channel_manager.cc
+++ b/drivers/bluetooth/lib/l2cap/channel_manager.cc
@@ -43,29 +43,30 @@
   }
 }
 
-void ChannelManager::RegisterACL(
-    hci::ConnectionHandle handle,
-    hci::Connection::Role role,
-    LinkErrorCallback link_error_cb,
-    async_dispatcher_t* dispatcher) {
+void ChannelManager::RegisterACL(hci::ConnectionHandle handle,
+                                 hci::Connection::Role role,
+                                 LinkErrorCallback link_error_cb,
+                                 SecurityUpgradeCallback security_cb,
+                                 async_dispatcher_t* dispatcher) {
   ZX_DEBUG_ASSERT(thread_checker_.IsCreationThreadCurrent());
   bt_log(TRACE, "l2cap", "register ACL link (handle: %#.4x)", handle);
 
   auto* ll = RegisterInternal(handle, hci::Connection::LinkType::kACL, role);
   ll->set_error_callback(std::move(link_error_cb), dispatcher);
+  ll->set_security_upgrade_callback(std::move(security_cb), dispatcher);
 }
 
 void ChannelManager::RegisterLE(
-    hci::ConnectionHandle handle,
-    hci::Connection::Role role,
+    hci::ConnectionHandle handle, hci::Connection::Role role,
     LEConnectionParameterUpdateCallback conn_param_cb,
-    LinkErrorCallback link_error_cb,
+    LinkErrorCallback link_error_cb, SecurityUpgradeCallback security_cb,
     async_dispatcher_t* dispatcher) {
   ZX_DEBUG_ASSERT(thread_checker_.IsCreationThreadCurrent());
   bt_log(TRACE, "l2cap", "register LE link (handle: %#.4x)", handle);
 
   auto* ll = RegisterInternal(handle, hci::Connection::LinkType::kLE, role);
   ll->set_error_callback(std::move(link_error_cb), dispatcher);
+  ll->set_security_upgrade_callback(std::move(security_cb), dispatcher);
   ll->le_signaling_channel()->set_conn_param_update_callback(std::move(conn_param_cb),
                                                              dispatcher);
 }
@@ -87,6 +88,21 @@
   ll_map_.erase(iter);
 }
 
+void ChannelManager::AssignLinkSecurityProperties(
+    hci::ConnectionHandle handle, sm::SecurityProperties security) {
+  ZX_DEBUG_ASSERT(thread_checker_.IsCreationThreadCurrent());
+
+  bt_log(TRACE, "l2cap", "update security properties (handle: %#.4x)", handle);
+
+  auto iter = ll_map_.find(handle);
+  if (iter == ll_map_.end()) {
+    bt_log(TRACE, "l2cap", "ignoring security update on unknown link");
+    return;
+  }
+
+  iter->second->AssignSecurityProperties(security);
+}
+
 fbl::RefPtr<Channel> ChannelManager::OpenFixedChannel(
     hci::ConnectionHandle handle,
     ChannelId channel_id) {
diff --git a/drivers/bluetooth/lib/l2cap/channel_manager.h b/drivers/bluetooth/lib/l2cap/channel_manager.h
index c3c09f4..c9b4923 100644
--- a/drivers/bluetooth/lib/l2cap/channel_manager.h
+++ b/drivers/bluetooth/lib/l2cap/channel_manager.h
@@ -64,11 +64,17 @@
   // |link_error_callback| will be used to notify when a channel signals a link
   // error. It will be posted onto |dispatcher|.
   //
+  // |security_callback| will be used to request an upgrade to the link security
+  // level. This can be triggered by dynamic L2CAP channel creation or by a
+  // service-level client via Channel::UpgradeSecurity().
+  //
+  // All callbacks will be posted onto |dispatcher|.
+  //
   // It is an error to register the same |handle| value more than once as either
   // kind of channel without first unregistering it (asserted in debug builds).
-  void RegisterACL(hci::ConnectionHandle handle,
-                   hci::Connection::Role role,
+  void RegisterACL(hci::ConnectionHandle handle, hci::Connection::Role role,
                    LinkErrorCallback link_error_callback,
+                   SecurityUpgradeCallback security_callback,
                    async_dispatcher_t* dispatcher);
 
   // Registers a LE connection with the L2CAP layer. L2CAP channels can be
@@ -81,16 +87,20 @@
   // |link_error_callback| will be used to notify when a channel signals a link
   // error.
   //
-  // Both callbacks will be posted onto |dispatcher|.
+  // |security_callback| will be used to request an upgrade to the link security
+  // level. This can be triggered by dynamic L2CAP channel creation or by a
+  // service-level client via Channel::UpgradeSecurity().
+  //
+  // All callbacks will be posted onto |dispatcher|.
   //
   // It is an error to register the same |handle| value more than once as either
   // kind of channel without first unregistering it (asserted in debug builds).
   using LEConnectionParameterUpdateCallback =
       internal::LESignalingChannel::ConnectionParameterUpdateCallback;
-  void RegisterLE(hci::ConnectionHandle handle,
-                  hci::Connection::Role role,
+  void RegisterLE(hci::ConnectionHandle handle, hci::Connection::Role role,
                   LEConnectionParameterUpdateCallback conn_param_callback,
                   LinkErrorCallback link_error_callback,
+                  SecurityUpgradeCallback security_callback,
                   async_dispatcher_t* dispatcher);
 
   // Removes a previously registered connection. All corresponding Channels will
@@ -102,6 +112,11 @@
   // more packets to send after removing the link entry.
   void Unregister(hci::ConnectionHandle handle);
 
+  // Assigns the security level of a logical link. Has no effect if |handle| has
+  // not been previously registered or the link is closed.
+  void AssignLinkSecurityProperties(hci::ConnectionHandle handle,
+                                    sm::SecurityProperties security);
+
   // Opens the L2CAP fixed channel with |channel_id| over the logical link
   // identified by |connection_handle| and starts routing packets. Returns
   // nullptr if the channel is already open.
diff --git a/drivers/bluetooth/lib/l2cap/channel_manager_unittest.cc b/drivers/bluetooth/lib/l2cap/channel_manager_unittest.cc
index 0625158..5ce3fea 100644
--- a/drivers/bluetooth/lib/l2cap/channel_manager_unittest.cc
+++ b/drivers/bluetooth/lib/l2cap/channel_manager_unittest.cc
@@ -16,16 +16,20 @@
 namespace l2cap {
 namespace {
 
+using ::btlib::testing::TestController;
+using TestingBase = ::btlib::testing::FakeControllerTest<TestController>;
+
+using common::HostError;
+
 constexpr hci::ConnectionHandle kTestHandle1 = 0x0001;
 constexpr hci::ConnectionHandle kTestHandle2 = 0x0002;
 constexpr PSM kTestPsm = 0x0001;
 
-using ::btlib::testing::TestController;
-
-using TestingBase = ::btlib::testing::FakeControllerTest<TestController>;
-
 void DoNothing() {}
 void NopRxCallback(const SDU&) {}
+void NopLeConnParamCallback(const hci::LEPreferredConnectionParameters&) {}
+void NopSecurityCallback(hci::ConnectionHandle, sm::SecurityLevel,
+                         sm::StatusCallback) {}
 
 class L2CAP_ChannelManagerTest : public TestingBase {
  public:
@@ -56,6 +60,23 @@
     TestingBase::TearDown();
   }
 
+  // Helper functions for registering logical links with default arguments.
+  void RegisterLE(
+      hci::ConnectionHandle handle, hci::Connection::Role role,
+      LinkErrorCallback lec = DoNothing,
+      LEConnectionParameterUpdateCallback cpuc = NopLeConnParamCallback,
+      SecurityUpgradeCallback suc = NopSecurityCallback) {
+    chanmgr()->RegisterLE(handle, role, std::move(cpuc), std::move(lec),
+                          std::move(suc), dispatcher());
+  }
+
+  void RegisterACL(hci::ConnectionHandle handle, hci::Connection::Role role,
+                   LinkErrorCallback lec = DoNothing,
+                   SecurityUpgradeCallback suc = NopSecurityCallback) {
+    chanmgr()->RegisterACL(handle, role, std::move(lec), std::move(suc),
+                           dispatcher());
+  }
+
   fbl::RefPtr<Channel> ActivateNewFixedChannel(
       ChannelId id, hci::ConnectionHandle conn_handle = kTestHandle1,
       Channel::ClosedCallback closed_cb = DoNothing,
@@ -100,8 +121,7 @@
   // This should fail as the ChannelManager has no entry for |kTestHandle1|.
   EXPECT_EQ(nullptr, ActivateNewFixedChannel(kATTChannelId));
 
-  chanmgr()->RegisterLE(kTestHandle1, hci::Connection::Role::kMaster,
-                        [](auto) {}, DoNothing, dispatcher());
+  RegisterLE(kTestHandle1, hci::Connection::Role::kMaster);
 
   // This should fail as the ChannelManager has no entry for |kTestHandle2|.
   EXPECT_EQ(nullptr, ActivateNewFixedChannel(kATTChannelId, kTestHandle2));
@@ -109,12 +129,10 @@
 
 TEST_F(L2CAP_ChannelManagerTest, OpenFixedChannelErrorDisallowedId) {
   // LE-U link
-  chanmgr()->RegisterLE(kTestHandle1, hci::Connection::Role::kMaster,
-                        [](auto) {}, DoNothing, dispatcher());
+  RegisterLE(kTestHandle1, hci::Connection::Role::kMaster);
 
   // ACL-U link
-  chanmgr()->RegisterACL(kTestHandle2, hci::Connection::Role::kMaster,
-                         DoNothing, dispatcher());
+  RegisterACL(kTestHandle2, hci::Connection::Role::kMaster);
 
   // This should fail as kSMPChannelId is ACL-U only.
   EXPECT_EQ(nullptr, ActivateNewFixedChannel(kSMPChannelId, kTestHandle1));
@@ -124,8 +142,7 @@
 }
 
 TEST_F(L2CAP_ChannelManagerTest, ActivateFailsAfterDeactivate) {
-  chanmgr()->RegisterLE(kTestHandle1, hci::Connection::Role::kMaster,
-                        [](auto) {}, DoNothing, dispatcher());
+  RegisterLE(kTestHandle1, hci::Connection::Role::kMaster);
   auto chan = ActivateNewFixedChannel(kATTChannelId, kTestHandle1);
   ASSERT_TRUE(chan);
 
@@ -137,8 +154,7 @@
 
 TEST_F(L2CAP_ChannelManagerTest, OpenFixedChannelAndUnregisterLink) {
   // LE-U link
-  chanmgr()->RegisterLE(kTestHandle1, hci::Connection::Role::kMaster,
-                        [](auto) {}, DoNothing, dispatcher());
+  RegisterLE(kTestHandle1, hci::Connection::Role::kMaster);
 
   bool closed_called = false;
   auto closed_cb = [&closed_called] { closed_called = true; };
@@ -159,8 +175,7 @@
 
 TEST_F(L2CAP_ChannelManagerTest, OpenFixedChannelAndCloseChannel) {
   // LE-U link
-  chanmgr()->RegisterLE(kTestHandle1, hci::Connection::Role::kMaster,
-                        [](auto) {}, DoNothing, dispatcher());
+  RegisterLE(kTestHandle1, hci::Connection::Role::kMaster);
 
   bool closed_called = false;
   auto closed_cb = [&closed_called] { closed_called = true; };
@@ -180,8 +195,7 @@
 
 TEST_F(L2CAP_ChannelManagerTest, OpenAndCloseWithLinkMultipleFixedChannels) {
   // LE-U link
-  chanmgr()->RegisterLE(kTestHandle1, hci::Connection::Role::kMaster,
-                        [](auto) {}, DoNothing, dispatcher());
+  RegisterLE(kTestHandle1, hci::Connection::Role::kMaster);
 
   bool att_closed = false;
   auto att_closed_cb = [&att_closed] { att_closed = true; };
@@ -208,9 +222,7 @@
 
 TEST_F(L2CAP_ChannelManagerTest, SendingPacketDuringCleanUpHasNoEffect) {
   // LE-U link
-  chanmgr()->RegisterLE(
-      kTestHandle1, hci::Connection::Role::kMaster, [](auto) {}, DoNothing,
-      dispatcher());
+  RegisterLE(kTestHandle1, hci::Connection::Role::kMaster);
 
   bool data_sent = false;
   auto data_cb = [&](const auto& bytes) { data_sent = true; };
@@ -238,9 +250,7 @@
 // Tests that destroying the ChannelManager cleanly shuts down all channels.
 TEST_F(L2CAP_ChannelManagerTest, DestroyingChannelManagerCleansUpChannels) {
   // LE-U link
-  chanmgr()->RegisterLE(
-      kTestHandle1, hci::Connection::Role::kMaster, [](auto) {}, DoNothing,
-      dispatcher());
+  RegisterLE(kTestHandle1, hci::Connection::Role::kMaster);
 
   bool data_sent = false;
   auto data_cb = [&](const auto& bytes) { data_sent = true; };
@@ -268,8 +278,7 @@
 TEST_F(L2CAP_ChannelManagerTest, DeactivateDoesNotCrashOrHang) {
   // Tests that the clean up task posted to the LogicalLink does not crash when
   // a dynamic registry is not present (which is the case for LE links).
-  chanmgr()->RegisterLE(kTestHandle1, hci::Connection::Role::kMaster,
-                        [](auto) {}, DoNothing, dispatcher());
+  RegisterLE(kTestHandle1, hci::Connection::Role::kMaster);
   auto chan = ActivateNewFixedChannel(kATTChannelId, kTestHandle1);
   ASSERT_TRUE(chan);
 
@@ -281,8 +290,7 @@
 
 TEST_F(L2CAP_ChannelManagerTest,
        CallingDeactivateFromClosedCallbackDoesNotCrashOrHang) {
-  chanmgr()->RegisterACL(kTestHandle1, hci::Connection::Role::kMaster,
-                         DoNothing, dispatcher());
+  RegisterACL(kTestHandle1, hci::Connection::Role::kMaster);
   auto chan = chanmgr()->OpenFixedChannel(kTestHandle1, kSMPChannelId);
   chan->Activate(NopRxCallback, [chan] { chan->Deactivate(); }, dispatcher());
   chanmgr()->Unregister(kTestHandle1);  // Triggers ClosedCallback.
@@ -291,8 +299,7 @@
 
 TEST_F(L2CAP_ChannelManagerTest, ReceiveData) {
   // LE-U link
-  chanmgr()->RegisterLE(kTestHandle1, hci::Connection::Role::kMaster,
-                        [](auto) {}, DoNothing, dispatcher());
+  RegisterLE(kTestHandle1, hci::Connection::Role::kMaster);
 
   common::StaticByteBuffer<255> buffer;
 
@@ -392,8 +399,7 @@
   // Run the loop so all packets are received.
   RunLoopUntilIdle();
 
-  chanmgr()->RegisterLE(kTestHandle1, hci::Connection::Role::kMaster,
-                        [](auto) {}, DoNothing, dispatcher());
+  RegisterLE(kTestHandle1, hci::Connection::Role::kMaster);
 
   att_chan =
       ActivateNewFixedChannel(kATTChannelId, kTestHandle1, [] {}, att_rx_cb);
@@ -412,8 +418,7 @@
 TEST_F(L2CAP_ChannelManagerTest, ReceiveDataBeforeCreatingChannel) {
   constexpr size_t kPacketCount = 10;
 
-  chanmgr()->RegisterLE(kTestHandle1, hci::Connection::Role::kMaster,
-                        [](auto) {}, DoNothing, dispatcher());
+  RegisterLE(kTestHandle1, hci::Connection::Role::kMaster);
 
   common::StaticByteBuffer<255> buffer;
 
@@ -470,8 +475,7 @@
 TEST_F(L2CAP_ChannelManagerTest, ReceiveDataBeforeSettingRxHandler) {
   constexpr size_t kPacketCount = 10;
 
-  chanmgr()->RegisterLE(kTestHandle1, hci::Connection::Role::kMaster,
-                        [](auto) {}, DoNothing, dispatcher());
+  RegisterLE(kTestHandle1, hci::Connection::Role::kMaster);
   auto att_chan = chanmgr()->OpenFixedChannel(kTestHandle1, kATTChannelId);
   ZX_DEBUG_ASSERT(att_chan);
 
@@ -522,8 +526,7 @@
 }
 
 TEST_F(L2CAP_ChannelManagerTest, SendOnClosedLink) {
-  chanmgr()->RegisterLE(kTestHandle1, hci::Connection::Role::kMaster,
-                        [](auto) {}, DoNothing, dispatcher());
+  RegisterLE(kTestHandle1, hci::Connection::Role::kMaster);
   auto att_chan = ActivateNewFixedChannel(kATTChannelId, kTestHandle1);
   ZX_DEBUG_ASSERT(att_chan);
 
@@ -533,8 +536,7 @@
 }
 
 TEST_F(L2CAP_ChannelManagerTest, SendBasicSdu) {
-  chanmgr()->RegisterLE(kTestHandle1, hci::Connection::Role::kMaster,
-                        [](auto) {}, DoNothing, dispatcher());
+  RegisterLE(kTestHandle1, hci::Connection::Role::kMaster);
   auto att_chan = ActivateNewFixedChannel(kATTChannelId, kTestHandle1);
   ZX_DEBUG_ASSERT(att_chan);
 
@@ -591,10 +593,8 @@
   };
   test_device()->SetDataCallback(data_cb, dispatcher());
 
-  chanmgr()->RegisterLE(kTestHandle1, hci::Connection::Role::kMaster,
-                        [](auto) {}, DoNothing, dispatcher());
-  chanmgr()->RegisterACL(kTestHandle2, hci::Connection::Role::kMaster,
-                         DoNothing, dispatcher());
+  RegisterLE(kTestHandle1, hci::Connection::Role::kMaster);
+  RegisterACL(kTestHandle2, hci::Connection::Role::kMaster);
 
   // We use the ATT fixed-channel for LE and the SM fixed-channel for ACL.
   auto att_chan = ActivateNewFixedChannel(kATTChannelId, kTestHandle1);
@@ -691,10 +691,8 @@
   };
   test_device()->SetDataCallback(data_cb, dispatcher());
 
-  chanmgr()->RegisterLE(kTestHandle1, hci::Connection::Role::kMaster,
-                        [](auto) {}, DoNothing, dispatcher());
-  chanmgr()->RegisterACL(kTestHandle2, hci::Connection::Role::kMaster,
-                         DoNothing, dispatcher());
+  RegisterLE(kTestHandle1, hci::Connection::Role::kMaster);
+  RegisterACL(kTestHandle2, hci::Connection::Role::kMaster);
 
   // We use the ATT fixed-channel for LE and the SM fixed-channel for ACL.
   auto att_chan = ActivateNewFixedChannel(kATTChannelId, kTestHandle1);
@@ -746,8 +744,7 @@
 TEST_F(L2CAP_ChannelManagerTest, LEChannelSignalLinkError) {
   bool link_error = false;
   auto link_error_cb = [&link_error, this] { link_error = true; };
-  chanmgr()->RegisterLE(kTestHandle1, hci::Connection::Role::kMaster,
-                        [](auto) {}, link_error_cb, dispatcher());
+  RegisterLE(kTestHandle1, hci::Connection::Role::kMaster, link_error_cb);
 
   // Activate a new Attribute channel to signal the error.
   auto chan = ActivateNewFixedChannel(kATTChannelId, kTestHandle1);
@@ -763,8 +760,7 @@
 TEST_F(L2CAP_ChannelManagerTest, ACLChannelSignalLinkError) {
   bool link_error = false;
   auto link_error_cb = [&link_error, this] { link_error = true; };
-  chanmgr()->RegisterACL(kTestHandle1, hci::Connection::Role::kMaster,
-                         link_error_cb, dispatcher());
+  RegisterACL(kTestHandle1, hci::Connection::Role::kMaster, link_error_cb);
 
   // Activate a new Security Manager channel to signal the error.
   auto chan = ActivateNewFixedChannel(kSMPChannelId, kTestHandle1);
@@ -788,8 +784,8 @@
     conn_param_cb_called = true;
   };
 
-  chanmgr()->RegisterLE(kTestHandle1, hci::Connection::Role::kMaster,
-                        conn_param_cb, DoNothing, dispatcher());
+  RegisterLE(kTestHandle1, hci::Connection::Role::kMaster, DoNothing,
+             conn_param_cb);
 
   // clang-format off
   test_device()->SendACLDataChannelPacket(common::CreateStaticByteBuffer(
@@ -819,8 +815,7 @@
   constexpr ChannelId kLocalId = 0x0040;
   constexpr ChannelId kRemoteId = 0x9042;
 
-  chanmgr()->RegisterACL(kTestHandle1, hci::Connection::Role::kMaster, [] {},
-                         dispatcher());
+  RegisterACL(kTestHandle1, hci::Connection::Role::kMaster);
 
   fbl::RefPtr<Channel> channel;
   auto channel_cb = [&channel](fbl::RefPtr<l2cap::Channel> activated_chan) {
@@ -927,8 +922,7 @@
 }
 
 TEST_F(L2CAP_ChannelManagerTest, ACLOutboundDynamicChannelRemoteDisconnect) {
-  chanmgr()->RegisterACL(kTestHandle1, hci::Connection::Role::kMaster, [] {},
-                         dispatcher());
+  RegisterACL(kTestHandle1, hci::Connection::Role::kMaster);
 
   fbl::RefPtr<Channel> channel;
   auto channel_cb = [&channel](fbl::RefPtr<l2cap::Channel> activated_chan) {
@@ -1028,8 +1022,7 @@
 }
 
 TEST_F(L2CAP_ChannelManagerTest, ACLOutboundDynamicChannelDataNotBuffered) {
-  chanmgr()->RegisterACL(kTestHandle1, hci::Connection::Role::kMaster, [] {},
-                         dispatcher());
+  RegisterACL(kTestHandle1, hci::Connection::Role::kMaster);
 
   fbl::RefPtr<Channel> channel;
   auto channel_cb = [&channel](fbl::RefPtr<l2cap::Channel> activated_chan) {
@@ -1128,8 +1121,7 @@
 }
 
 TEST_F(L2CAP_ChannelManagerTest, ACLOutboundDynamicChannelRemoteRefused) {
-  chanmgr()->RegisterACL(kTestHandle1, hci::Connection::Role::kMaster, [] {},
-                         dispatcher());
+  RegisterACL(kTestHandle1, hci::Connection::Role::kMaster);
 
   bool channel_cb_called = false;
   auto channel_cb = [&channel_cb_called](fbl::RefPtr<l2cap::Channel> channel) {
@@ -1161,8 +1153,7 @@
 }
 
 TEST_F(L2CAP_ChannelManagerTest, ACLOutboundDynamicChannelFailedConfiguration) {
-  chanmgr()->RegisterACL(kTestHandle1, hci::Connection::Role::kMaster, [] {},
-                         dispatcher());
+  RegisterACL(kTestHandle1, hci::Connection::Role::kMaster);
 
   bool channel_cb_called = false;
   auto channel_cb = [&channel_cb_called](fbl::RefPtr<l2cap::Channel> channel) {
@@ -1236,8 +1227,7 @@
   constexpr ChannelId kLocalId = 0x0040;
   constexpr ChannelId kRemoteId = 0x9042;
 
-  chanmgr()->RegisterACL(kTestHandle1, hci::Connection::Role::kMaster, [] {},
-                         dispatcher());
+  RegisterACL(kTestHandle1, hci::Connection::Role::kMaster);
 
   bool closed_cb_called = false;
   auto closed_cb = [&closed_cb_called] { closed_cb_called = true; };
@@ -1346,6 +1336,103 @@
   EXPECT_FALSE(closed_cb_called);
 }
 
+TEST_F(L2CAP_ChannelManagerTest, LinkSecurityProperties) {
+  sm::SecurityProperties security(sm::SecurityLevel::kEncrypted, 16, false);
+
+  // Has no effect.
+  chanmgr()->AssignLinkSecurityProperties(kTestHandle1, security);
+
+  // Register a link and open a channel. The security properties should be
+  // accessible using the channel.
+  RegisterLE(kTestHandle1, hci::Connection::Role::kMaster);
+  auto chan = ActivateNewFixedChannel(kATTChannelId, kTestHandle1);
+  ASSERT_TRUE(chan);
+
+  // The channel should start out at the lowest level of security.
+  EXPECT_EQ(sm::SecurityProperties(), chan->security());
+
+  // Assign a new security level.
+  chanmgr()->AssignLinkSecurityProperties(kTestHandle1, security);
+
+  // Channel should return the new security level.
+  EXPECT_EQ(security, chan->security());
+}
+
+// Tests that assigning a new security level on a closed link does nothing.
+TEST_F(L2CAP_ChannelManagerTest, AssignLinkSecurityPropertiesOnClosedLink) {
+  // Register a link and open a channel. The security properties should be
+  // accessible using the channel.
+  RegisterLE(kTestHandle1, hci::Connection::Role::kMaster);
+  auto chan = ActivateNewFixedChannel(kATTChannelId, kTestHandle1);
+  ASSERT_TRUE(chan);
+
+  chanmgr()->Unregister(kTestHandle1);
+  RunLoopUntilIdle();
+
+  // Assign a new security level.
+  sm::SecurityProperties security(sm::SecurityLevel::kEncrypted, 16, false);
+  chanmgr()->AssignLinkSecurityProperties(kTestHandle1, security);
+
+  // Channel should return the old security level.
+  EXPECT_EQ(sm::SecurityProperties(), chan->security());
+}
+
+TEST_F(L2CAP_ChannelManagerTest, UpgradeSecurity) {
+  // The callback passed to to Channel::UpgradeSecurity().
+  sm::Status received_status;
+  int security_status_count = 0;
+  auto status_callback = [&](sm::Status status) {
+    received_status = status;
+    security_status_count++;
+  };
+
+  // The security handler callback assigned when registering a link.
+  sm::Status delivered_status;
+  sm::SecurityLevel last_requested_level = sm::SecurityLevel::kNoSecurity;
+  int security_request_count = 0;
+  auto security_handler = [&](hci::ConnectionHandle handle,
+                              sm::SecurityLevel level, auto callback) {
+    EXPECT_EQ(kTestHandle1, handle);
+    last_requested_level = level;
+    security_request_count++;
+
+    callback(delivered_status);
+  };
+
+  RegisterLE(kTestHandle1, hci::Connection::Role::kMaster, DoNothing,
+             NopLeConnParamCallback, std::move(security_handler));
+  auto chan = ActivateNewFixedChannel(kATTChannelId, kTestHandle1);
+  ASSERT_TRUE(chan);
+
+  // Requesting security at or below the current level should succeed without
+  // doing anything.
+  chan->UpgradeSecurity(sm::SecurityLevel::kNoSecurity, status_callback);
+  RunLoopUntilIdle();
+  EXPECT_EQ(0, security_request_count);
+  EXPECT_EQ(1, security_status_count);
+  EXPECT_TRUE(received_status);
+
+  // Test reporting an error.
+  delivered_status = sm::Status(HostError::kNotSupported);
+  chan->UpgradeSecurity(sm::SecurityLevel::kEncrypted, status_callback);
+  RunLoopUntilIdle();
+  EXPECT_EQ(1, security_request_count);
+  EXPECT_EQ(2, security_status_count);
+  EXPECT_EQ(delivered_status, received_status);
+  EXPECT_EQ(sm::SecurityLevel::kEncrypted, last_requested_level);
+
+  // Close the link. Future security requests should have no effect.
+  chanmgr()->Unregister(kTestHandle1);
+  RunLoopUntilIdle();
+
+  chan->UpgradeSecurity(sm::SecurityLevel::kAuthenticated, status_callback);
+  chan->UpgradeSecurity(sm::SecurityLevel::kAuthenticated, status_callback);
+  chan->UpgradeSecurity(sm::SecurityLevel::kAuthenticated, status_callback);
+  RunLoopUntilIdle();
+  EXPECT_EQ(1, security_request_count);
+  EXPECT_EQ(2, security_status_count);
+}
+
 }  // namespace
 }  // namespace l2cap
 }  // namespace btlib
diff --git a/drivers/bluetooth/lib/l2cap/fake_channel.cc b/drivers/bluetooth/lib/l2cap/fake_channel.cc
index 3cbfe7f..cd0a3e5 100644
--- a/drivers/bluetooth/lib/l2cap/fake_channel.cc
+++ b/drivers/bluetooth/lib/l2cap/fake_channel.cc
@@ -121,6 +121,11 @@
   return true;
 }
 
+void FakeChannel::UpgradeSecurity(sm::SecurityLevel level,
+                                  sm::StatusCallback callback) {
+  // TODO(armansito): implement
+}
+
 }  // namespace testing
 }  // namespace l2cap
 }  // namespace btlib
diff --git a/drivers/bluetooth/lib/l2cap/fake_channel.h b/drivers/bluetooth/lib/l2cap/fake_channel.h
index 366ebfe..a7d1c5e 100644
--- a/drivers/bluetooth/lib/l2cap/fake_channel.h
+++ b/drivers/bluetooth/lib/l2cap/fake_channel.h
@@ -59,17 +59,27 @@
   // True if Deactivate has yet not been called after Activate.
   bool activated() const { return static_cast<bool>(rx_cb_); }
 
+  // Assigns a link security level.
+  void set_security(const sm::SecurityProperties& sec_props) {
+    security_ = sec_props;
+  }
+
   // Channel overrides:
+  const sm::SecurityProperties security() override { return security_; }
   bool Activate(RxCallback rx_callback, ClosedCallback closed_callback,
                 async_dispatcher_t* dispatcher) override;
   void Deactivate() override;
   void SignalLinkError() override;
   bool Send(common::ByteBufferPtr sdu) override;
+  void UpgradeSecurity(sm::SecurityLevel level,
+                       sm::StatusCallback callback) override;
 
  private:
   hci::ConnectionHandle handle_;
   Fragmenter fragmenter_;
 
+  sm::SecurityProperties security_;
+
   ClosedCallback closed_cb_;
   RxCallback rx_cb_;
   async_dispatcher_t* dispatcher_;
diff --git a/drivers/bluetooth/lib/l2cap/l2cap.h b/drivers/bluetooth/lib/l2cap/l2cap.h
index 18b5d0d..fbf8d46 100644
--- a/drivers/bluetooth/lib/l2cap/l2cap.h
+++ b/drivers/bluetooth/lib/l2cap/l2cap.h
@@ -16,6 +16,9 @@
 #include "lib/fxl/macros.h"
 
 #include "garnet/drivers/bluetooth/lib/hci/connection_parameters.h"
+#include "garnet/drivers/bluetooth/lib/hci/hci.h"
+#include "garnet/drivers/bluetooth/lib/sm/status.h"
+#include "garnet/drivers/bluetooth/lib/sm/types.h"
 
 namespace btlib {
 namespace l2cap {
@@ -36,6 +39,11 @@
 using LEFixedChannelsCallback =
     fit::function<void(fbl::RefPtr<Channel> att, fbl::RefPtr<Channel> smp)>;
 
+// Callback used to request a security upgrade for an active logical link.
+// Invokes its |callback| argument with the result of the operation.
+using SecurityUpgradeCallback =
+    fit::function<void(hci::ConnectionHandle ll_handle, sm::SecurityLevel level, sm::StatusCallback callback)>;
+
 // L2CAP channel identifier uniquely identifies fixed and connection-oriented
 // channels over a logical link.
 // (see Core Spec v5.0, Vol 3, Part A, Section 2.1)
diff --git a/drivers/bluetooth/lib/l2cap/logical_link.cc b/drivers/bluetooth/lib/l2cap/logical_link.cc
index 12ed526..84145c7 100644
--- a/drivers/bluetooth/lib/l2cap/logical_link.cc
+++ b/drivers/bluetooth/lib/l2cap/logical_link.cc
@@ -224,6 +224,54 @@
   iter->second->HandleRxPdu(std::move(pdu));
 }
 
+void LogicalLink::UpgradeSecurity(sm::SecurityLevel level,
+                                  sm::StatusCallback callback,
+                                  async_dispatcher_t* dispatcher) {
+  ZX_DEBUG_ASSERT(thread_checker_.IsCreationThreadCurrent());
+  ZX_DEBUG_ASSERT(security_callback_);
+  ZX_DEBUG_ASSERT(dispatcher);
+
+  if (closed_) {
+    bt_log(TRACE, "l2cap", "Ignoring security request on closed link");
+    return;
+  }
+
+  auto status_cb = [dispatcher,
+                    f = std::move(callback)](sm::Status status) mutable {
+    async::PostTask(dispatcher, [f = std::move(f), status] { f(status); });
+  };
+
+  // Report success If the link already has the expected security level.
+  if (level <= security().level()) {
+    status_cb(sm::Status());
+    return;
+  }
+
+  bt_log(TRACE, "l2cap", "Security upgrade requested (level = %s)",
+         sm::LevelToString(level));
+  async::PostTask(security_dispatcher_,
+                  [handle = handle_, level, f = security_callback_.share(),
+                   status_cb = std::move(status_cb)]() mutable {
+                    f(handle, level, std::move(status_cb));
+                  });
+}
+
+void LogicalLink::AssignSecurityProperties(
+    const sm::SecurityProperties& security) {
+  ZX_DEBUG_ASSERT(thread_checker_.IsCreationThreadCurrent());
+
+  if (closed_) {
+    bt_log(TRACE, "l2cap", "Ignoring security request on closed link");
+    return;
+  }
+
+  bt_log(TRACE, "l2cap", "Link security updated (handle: %#.4x): %s", handle_,
+         security.ToString().c_str());
+
+  std::lock_guard<std::mutex> lock(mtx_);
+  security_ = security;
+}
+
 void LogicalLink::SendBasicFrame(ChannelId id,
                                  const common::ByteBuffer& payload) {
   ZX_DEBUG_ASSERT(thread_checker_.IsCreationThreadCurrent());
@@ -252,6 +300,15 @@
   link_error_dispatcher_ = dispatcher;
 }
 
+void LogicalLink::set_security_upgrade_callback(
+    SecurityUpgradeCallback callback, async_dispatcher_t* dispatcher) {
+  ZX_DEBUG_ASSERT(thread_checker_.IsCreationThreadCurrent());
+  ZX_DEBUG_ASSERT(static_cast<bool>(callback) == static_cast<bool>(dispatcher));
+
+  security_callback_ = std::move(callback);
+  security_dispatcher_ = dispatcher;
+}
+
 LESignalingChannel* LogicalLink::le_signaling_channel() const {
   return (type_ == hci::Connection::LinkType::kLE)
              ? static_cast<LESignalingChannel*>(signaling_channel_.get())
diff --git a/drivers/bluetooth/lib/l2cap/logical_link.h b/drivers/bluetooth/lib/l2cap/logical_link.h
index 551b742..e9073e5 100644
--- a/drivers/bluetooth/lib/l2cap/logical_link.h
+++ b/drivers/bluetooth/lib/l2cap/logical_link.h
@@ -89,11 +89,27 @@
   // It is safe to call this function on a closed link; it will have no effect.
   void SendBasicFrame(ChannelId remote_id, const common::ByteBuffer& payload);
 
+  // Requests a security upgrade using the registered security upgrade callback.
+  // Invokes the |callback| argument with the result of the operation.
+  // |callback| will be run by the requested |dispatcher|.
+  //
+  // Has no effect if the link is closed.
+  void UpgradeSecurity(sm::SecurityLevel level, sm::StatusCallback callback,
+                       async_dispatcher_t* dispatcher);
+
+  // Assigns the security level of this link and resolves pending security
+  // upgrade requests. Has no effect if the link is closed.
+  void AssignSecurityProperties(const sm::SecurityProperties& security);
+
   // Assigns the link error callback to be invoked when a channel signals a link
   // error.
   void set_error_callback(fit::closure callback,
                           async_dispatcher_t* dispatcher);
 
+  // Assigns the security upgrade delegate for this link.
+  void set_security_upgrade_callback(SecurityUpgradeCallback callback,
+                                     async_dispatcher_t* dispatcher);
+
   // Returns the dispatcher that this LogicalLink operates on.
   async_dispatcher_t* dispatcher() const { return dispatcher_; }
 
@@ -101,6 +117,11 @@
   hci::Connection::Role role() const { return role_; }
   hci::ConnectionHandle handle() const { return handle_; }
 
+  const sm::SecurityProperties security() {
+    std::lock_guard<std::mutex> lock(mtx_);
+    return security_;
+  }
+
   // Returns the LE signaling channel implementation or nullptr if this is not a
   // LE-U link.
   LESignalingChannel* le_signaling_channel() const;
@@ -159,6 +180,11 @@
                            ChannelCallback open_cb,
                            async_dispatcher_t* dispatcher);
 
+  // Members that can be accessed from any thread.
+  std::mutex mtx_;
+  sm::SecurityProperties security_ __TA_GUARDED(mtx_);
+
+  // All members below must be accessed on the L2CAP dispatcher thread.
   fxl::RefPtr<hci::Transport> hci_;
   async_dispatcher_t* dispatcher_;
 
@@ -170,6 +196,9 @@
   fit::closure link_error_cb_;
   async_dispatcher_t* link_error_dispatcher_;
 
+  SecurityUpgradeCallback security_callback_;
+  async_dispatcher_t* security_dispatcher_;
+
   // No data packets are processed once this gets set to true.
   bool closed_;
 
diff --git a/drivers/bluetooth/lib/sm/BUILD.gn b/drivers/bluetooth/lib/sm/BUILD.gn
index 75e503d..06fbda0 100644
--- a/drivers/bluetooth/lib/sm/BUILD.gn
+++ b/drivers/bluetooth/lib/sm/BUILD.gn
@@ -7,10 +7,15 @@
     "packet.cc",
     "packet.h",
     "smp.h",
+    "status.cc",
+    "status.h",
+    "types.cc",
+    "types.h",
   ]
 
   public_deps = [
     "//garnet/drivers/bluetooth/lib/common",
+    "//garnet/drivers/bluetooth/lib/hci:definitions", # for hci::LinkKey
   ]
 }
 
@@ -20,10 +25,6 @@
     "bearer.h",
     "pairing_state.cc",
     "pairing_state.h",
-    "status.cc",
-    "status.h",
-    "types.cc",
-    "types.h",
     "util.cc",
     "util.h",
   ]
diff --git a/drivers/bluetooth/lib/sm/types.cc b/drivers/bluetooth/lib/sm/types.cc
index 2b685dc..78199bc 100644
--- a/drivers/bluetooth/lib/sm/types.cc
+++ b/drivers/bluetooth/lib/sm/types.cc
@@ -8,21 +8,6 @@
 
 namespace btlib {
 namespace sm {
-namespace {
-
-std::string LevelToString(SecurityLevel level) {
-  switch (level) {
-    case SecurityLevel::kEncrypted:
-      return "encrypted";
-    case SecurityLevel::kAuthenticated:
-      return "encrypted (MITM)";
-    default:
-      break;
-  }
-  return "insecure";
-}
-
-}  // namespace
 
 PairingFeatures::PairingFeatures() { std::memset(this, 0, sizeof(*this)); }
 
@@ -36,6 +21,18 @@
       local_key_distribution(local_kd),
       remote_key_distribution(remote_kd) {}
 
+const char* LevelToString(SecurityLevel level) {
+  switch (level) {
+    case SecurityLevel::kEncrypted:
+      return "encrypted";
+    case SecurityLevel::kAuthenticated:
+      return "encrypted (MITM)";
+    default:
+      break;
+  }
+  return "not secure";
+}
+
 SecurityProperties::SecurityProperties()
     : level_(SecurityLevel::kNoSecurity), enc_key_size_(0u), sc_(false) {}
 
@@ -45,8 +42,9 @@
 
 std::string SecurityProperties::ToString() const {
   return fxl::StringPrintf(
-      "[security: %s, key size: %lu, %s]", LevelToString(level()).c_str(),
-      enc_key_size(), secure_connections() ? "secure conn." : "legacy pairing");
+      "[security: %s, key size: %lu, %s]", LevelToString(level()),
+      enc_key_size(),
+      secure_connections() ? "secure connections" : "legacy authentication");
 }
 
 LTK::LTK(const SecurityProperties& security, const hci::LinkKey& key)
diff --git a/drivers/bluetooth/lib/sm/types.h b/drivers/bluetooth/lib/sm/types.h
index 3c82a51..0332ee8 100644
--- a/drivers/bluetooth/lib/sm/types.h
+++ b/drivers/bluetooth/lib/sm/types.h
@@ -51,6 +51,9 @@
   kAuthenticated = 2,
 };
 
+// Returns a string representation of |level| for debug messages.
+const char* LevelToString(SecurityLevel level);
+
 // Represents the security properties of a key. The security properties of a
 // connection's LTK defines the security properties of the connection.
 class SecurityProperties final {