diff --git a/.github/workflows/build_samples.yml b/.github/workflows/build_samples.yml
index 2d4cfc69e3..fe6cf6e3f1 100644
--- a/.github/workflows/build_samples.yml
+++ b/.github/workflows/build_samples.yml
@@ -9,9 +9,7 @@ on:
jobs:
build_samples_job:
name: Build all samples within the project
- runs-on:
- - runs-on=${{ github.run_id }}
- - runner=64cpu-linux-x64
+ runs-on: ubuntu-22.04
# Keep aligned with target NCS version
container: ghcr.io/nrfconnect/sdk-nrf-toolchain:v2.9.1
defaults:
@@ -45,7 +43,7 @@ jobs:
- name: Build samples
working-directory: nrf-bm
run: |
- west twister -T samples --build-only -v --inline-logs --integration
+ west twister -T samples/bluetooth/ble_hrs --build-only -v --inline-logs --integration
- name: upload-logs
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
diff --git a/.github/workflows/twister.yml b/.github/workflows/twister.yml
index ce872824f7..94a6316489 100644
--- a/.github/workflows/twister.yml
+++ b/.github/workflows/twister.yml
@@ -9,9 +9,7 @@ on:
jobs:
twister_job:
name: Run Twister patch series (PR)
- runs-on:
- - runs-on=${{ github.run_id }}
- - runner=4cpu-linux-x64
+ runs-on: ubuntu-22.04
# Keep aligned with target NCS version
container: ghcr.io/nrfconnect/sdk-nrf-toolchain:v2.9.1
defaults:
diff --git a/CODEOWNERS b/CODEOWNERS
index ea8a3dc8ec..84e7a5ce06 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -46,6 +46,7 @@
/lib/bm_zms/ @nrfconnect/ncs-bm @rghaddab
/lib/ble_adv/ @nrfconnect/ncs-bm
/lib/ble_conn_params/ @nrfconnect/ncs-bm
+/lib/ble_conn_state/ @nrfconnect/ncs-bm
/lib/ble_gq/ @nrfconnect/ncs-bm
/lib/ble_qwr/ @nrfconnect/ncs-bm
/lib/ble_racp/ @nrfconnect/ncs-bm
@@ -54,6 +55,7 @@
/lib/bm_timer/ @nrfconnect/ncs-bm
/lib/boot_banner/ @nrfconnect/ncs-bm
/lib/event_scheduler/ @nrfconnect/ncs-bm
+/lib/peer_manager/ @nrfconnect/ncs-bm
/lib/sensorsim/ @nrfconnect/ncs-bm
/lib/zephyr_queue/ @nrfconnect/ncs-pluto
diff --git a/include/ble_conn_state.h b/include/ble_conn_state.h
new file mode 100644
index 0000000000..6963097de2
--- /dev/null
+++ b/include/ble_conn_state.h
@@ -0,0 +1,298 @@
+/*
+ * Copyright (c) 2015 - 2025 Nordic Semiconductor ASA
+ *
+ * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
+ */
+
+/**
+ * @file
+ *
+ * @defgroup ble_conn_state Connection state
+ * @ingroup ble_sdk_lib
+ * @{
+ * @brief Module for storing data on BLE connections.
+ *
+ * @details This module stores certain states for each connection, which can be queried by
+ * connection handle. The module uses BLE events to keep the states updated.
+ *
+ * In addition to the preprogrammed states, this module can also keep track of a number of
+ * binary user states, or user flags. These are reset to 0 for new connections, but
+ * otherwise not touched by this module.
+ *
+ * This module uses atomics to make the flag operations thread-safe.
+ *
+ * @note A connection handle is not immediately invalidated when it is disconnected. Certain states,
+ * such as the role, can still be queried until the next time a new connection is established
+ * to any device.
+ *
+ */
+
+#ifndef BLE_CONN_STATE_H__
+#define BLE_CONN_STATE_H__
+
+#include
+#include
+#include
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** @brief Connection handle statuses. */
+enum ble_conn_state_status {
+ /** The connection handle is invalid. */
+ BLE_CONN_STATUS_INVALID,
+ /** The connection handle refers to a connection that has been disconnected,
+ * but not yet invalidated.
+ */
+ BLE_CONN_STATUS_DISCONNECTED,
+ /** The connection handle refers to an active connection. */
+ BLE_CONN_STATUS_CONNECTED,
+};
+
+/** The maximum number of connections supported. */
+#define BLE_CONN_STATE_MAX_CONNECTIONS BLE_GAP_ROLE_COUNT_COMBINED_MAX
+
+/** @brief Type used to present a list of conn_handles. */
+struct ble_conn_state_conn_handle_list {
+ /** The length of the list. */
+ uint32_t len;
+ /** The list of handles. */
+ uint16_t conn_handles[BLE_CONN_STATE_MAX_CONNECTIONS];
+};
+
+/**
+ * @brief Function to be called when a flag ID is set.
+ *
+ * See @ref ble_conn_state_for_each_set_user_flag.
+ *
+ * @param[in] conn_handle The connection the flag is set for.
+ * @param[in] ctx Arbitrary pointer provided by the caller of
+ * @ref ble_conn_state_for_each_set_user_flag.
+ */
+typedef void (*ble_conn_state_user_function_t)(uint16_t conn_handle, void *ctx);
+
+/**
+ * @defgroup ble_conn_state_functions BLE connection state functions
+ * @{
+ */
+
+/**
+ * @brief Initialize or reset the module.
+ *
+ * @details This function sets all states to their default,
+ * removing all records of connection handles.
+ */
+void ble_conn_state_init(void);
+
+/**
+ * @brief Check whether a connection handle represents a valid connection.
+ *
+ * @details A connection might be valid and have a BLE_CONN_STATUS_DISCONNECTED status.
+ * Those connections are invalidated after a new connection occurs.
+ *
+ * @param[in] conn_handle Handle of the connection.
+ *
+ * @retval true If @c conn_handle represents a valid connection, thus a connection for which
+ we have a record.
+ * @retval false If @c conn_handle is @ref BLE_GAP_ROLE_INVALID, or if it has never been recorded.
+ */
+bool ble_conn_state_valid(uint16_t conn_handle);
+
+/**
+ * @brief Get the role of the local device in a connection.
+ *
+ * @param[in] conn_handle Handle of the connection to get the role for.
+ *
+ * @return The role of the local device in the connection (see @ref BLE_GAP_ROLES).
+ * If conn_handle is not valid, the function returns @ref BLE_GAP_ROLE_INVALID.
+ */
+uint8_t ble_conn_state_role(uint16_t conn_handle);
+
+/**
+ * @brief Get the status of a connection.
+ *
+ * @param[in] conn_handle Handle of the connection.
+ *
+ * @return The status of the connection.
+ * If conn_handle is not valid, the function returns @ref BLE_CONN_STATE_INVALID.
+ */
+enum ble_conn_state_status ble_conn_state_status(uint16_t conn_handle);
+
+/**
+ * @brief Check whether a connection is encrypted.
+ *
+ * @param[in] conn_handle Handle of connection to get the encryption state for.
+ *
+ * @retval true If the connection is encrypted.
+ * @retval false If the connection is not encrypted or conn_handle is invalid.
+ */
+bool ble_conn_state_encrypted(uint16_t conn_handle);
+
+/**
+ * @brief Check whether a connection encryption is protected from Man in the Middle
+ * (MITM) attacks.
+ *
+ * @param[in] conn_handle Handle of connection to get the MITM state for.
+ *
+ * @retval true If the connection is encrypted with MITM protection.
+ * @retval false If the connection is not encrypted, or encryption is not MITM protected, or
+ * conn_handle is invalid.
+ */
+bool ble_conn_state_mitm_protected(uint16_t conn_handle);
+
+/**
+ * @brief Check whether a connection was bonded using LE Secure Connections (LESC).
+ *
+ * The connection must currently be encrypted.
+ *
+ * @note This function will report false if bonded, and the LESC bonding was unauthenticated
+ * ("Just Works") and happened in a previous connection. To detect such cases as well, check
+ * the stored bonding key, e.g. in Peer Manager, which has a LESC flag associated with it.
+ *
+ * @param[in] conn_handle Handle of connection to get the LESC state for.
+ *
+ * @retval true If the connection was bonded using LESC.
+ * @retval false If the connection has not been bonded using LESC, or @c conn_handle is invalid.
+ */
+bool ble_conn_state_lesc(uint16_t conn_handle);
+
+/**
+ * @brief Get the total number of connections.
+ *
+ * @return The total number of valid connections for which the module has a record.
+ */
+uint32_t ble_conn_state_conn_count(void);
+
+/**
+ * @brief Get the total number of connections in which the role of the local
+ * device is @ref BLE_GAP_ROLE_CENTRAL.
+ *
+ * @return The number of connections in which the role of the local device is
+ * @ref BLE_GAP_ROLE_CENTRAL.
+ */
+uint32_t ble_conn_state_central_conn_count(void);
+
+/**
+ * @brief Get the total number of connections in which the role of the local
+ * device is @ref BLE_GAP_ROLE_PERIPH.
+ *
+ * @return The number of connections in which the role of the local device is
+ * @ref BLE_GAP_ROLE_PERIPH.
+ */
+uint32_t ble_conn_state_peripheral_conn_count(void);
+
+/**
+ * @brief Get a list of all connection handles for which the module has a record.
+ *
+ * @details This function takes into account connections whose state is
+ * @ref BLE_CONN_STATUS_DISCONNECTED.
+ *
+ * @return A list of all valid connection handles for which the module has a record.
+ */
+struct ble_conn_state_conn_handle_list ble_conn_state_conn_handles(void);
+
+/**
+ * @brief Get a list of connection handles in which the role of the local
+ * device is @ref BLE_GAP_ROLE_CENTRAL.
+ *
+ * @details This function takes into account connections whose state is
+ * @ref BLE_CONN_STATUS_DISCONNECTED.
+ *
+ * @return A list of all valid connection handles for which the module has a record and in which
+ * the role of local device is @ref BLE_GAP_ROLE_CENTRAL.
+ */
+struct ble_conn_state_conn_handle_list ble_conn_state_central_handles(void);
+
+/**
+ * @brief Get the handle for the connection in which the role of the local device
+ * is @ref BLE_GAP_ROLE_PERIPH.
+ *
+ * @details This function takes into account connections whose state is
+ * @ref BLE_CONN_STATUS_DISCONNECTED.
+ *
+ * @return A list of all valid connection handles for which the module has a record and in which
+ * the role of local device is @ref BLE_GAP_ROLE_PERIPH.
+ */
+struct ble_conn_state_conn_handle_list ble_conn_state_periph_handles(void);
+
+/**
+ * @brief Translate a connection handle to a value that can be used as an array index.
+ *
+ * @details Function for mapping connection handles onto the range <0 - MAX_CONNECTIONS>.
+ *
+ * @note The index will be the same as long as a connection is invalid. A subsequent connection with
+ * the same connection handle might have a different index.
+ *
+ * @param[in] conn_handle The connection for which to retrieve an index.
+ *
+ * @return An index unique to this connection. Or @ref BLE_CONN_STATE_MAX_CONNECTIONS if
+ * @c conn_handle refers to an invalid connection.
+ */
+uint16_t ble_conn_state_conn_idx(uint16_t conn_handle);
+
+/**
+ * @brief Obtain exclusive access to one of the user flag collections.
+ *
+ * @details The acquired collection contains one flag for each connection. These flags can be set
+ * and read individually for each connection.
+ *
+ * The state of user flags will not be modified by the connection state module, except to
+ * set it to 0 for a connection when that connection is invalidated.
+ *
+ * @return The index of the acquired flag, or -1 if none are available.
+ */
+int ble_conn_state_user_flag_acquire(void);
+
+/**
+ * @brief Read the value of a user flag.
+ *
+ * @param[in] conn_handle Handle of connection to get the flag state for.
+ * @param[in] flag_index Which flag to get the state for.
+ *
+ * @return The state of the flag. If @c conn_handle is invalid, the function returns false.
+ */
+bool ble_conn_state_user_flag_get(uint16_t conn_handle, uint16_t flag_index);
+
+/**
+ * @brief Set the value of a user flag.
+ *
+ * @param[in] conn_handle Handle of connection to set the flag state for.
+ * @param[in] flag_id Which flag to set the state for.
+ * @param[in] value Value to set the flag state to.
+ */
+void ble_conn_state_user_flag_set(uint16_t conn_handle, uint16_t flag_index, bool value);
+
+/**
+ * @brief Run a function for each active connection.
+ *
+ * @param[in] user_function The function to run for each connection.
+ * @param[in] ctx Arbitrary context to be passed to @p user_function.
+ *
+ * @return The number of times @p user_function was run.
+ */
+uint32_t ble_conn_state_for_each_connected(ble_conn_state_user_function_t user_function,
+ void *ctx);
+
+/**
+ * @brief Run a function for each connection that has a user flag set.
+ *
+ * @param[in] flag_index Which flag to check.
+ * @param[in] user_function The function to run when a flag is set.
+ * @param[in] ctx Arbitrary context to be passed to @p user_function.
+ *
+ * @return The number of times @p user_function was run.
+ */
+uint32_t ble_conn_state_for_each_set_user_flag(uint16_t flag_index,
+ ble_conn_state_user_function_t user_function,
+ void *ctx);
+
+/** @} */
+/** @} */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* BLE_CONN_STATE_H__ */
diff --git a/include/ble_gatt_db.h b/include/ble_gatt_db.h
new file mode 100644
index 0000000000..0e4787776b
--- /dev/null
+++ b/include/ble_gatt_db.h
@@ -0,0 +1,83 @@
+/*
+ * Copyright (c) 2015-2025 Nordic Semiconductor ASA
+ *
+ * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
+ */
+
+/**
+ * @file
+ *
+ * @defgroup ble_sdk_lib_gatt_db GATT Database Service Structure
+ * @{
+ * @ingroup ble_sdk_lib
+ */
+
+#ifndef BLE_GATT_DB_H__
+#define BLE_GATT_DB_H__
+
+#include
+#include
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** @brief The maximum number of characteristics present in a service record. */
+#define BLE_GATT_DB_MAX_CHARS 6
+
+/**@brief Structure for holding the characteristic and the handle of its CCCD present on a server.
+ */
+typedef struct {
+ /** @brief Structure containing information about the characteristic. */
+ ble_gattc_char_t characteristic;
+ /**
+ * @brief CCCD Handle value for this characteristic. This will be set to
+ * BLE_GATT_HANDLE_INVALID if a CCCD is not present at the server.
+ */
+ uint16_t cccd_handle;
+ /**
+ * @brief Extended Properties Handle value for this characteristic.
+ * This will be set to BLE_GATT_HANDLE_INVALID if an Extended
+ * Properties descriptor is not present at the server.
+ */
+ uint16_t ext_prop_handle;
+ /**
+ * @brief User Description Handle value for this characteristic. This
+ * will be set to BLE_GATT_HANDLE_INVALID if a User Description
+ * descriptor is not present at the server.
+ */
+ uint16_t user_desc_handle;
+ /**
+ * @brief Report Reference Handle value for this characteristic. This
+ * will be set to BLE_GATT_HANDLE_INVALID if a Report Reference
+ * descriptor is not present at the server.
+ */
+ uint16_t report_ref_handle;
+} ble_gatt_db_char_t;
+
+/**
+ * @brief Structure for holding information about the service and the characteristics present on a
+ * server.
+ */
+typedef struct {
+ /** @brief UUID of the service. */
+ ble_uuid_t srv_uuid;
+ /** @brief Number of characteristics present in the service. */
+ uint8_t char_count;
+ /** @brief Service Handle Range. */
+ ble_gattc_handle_range_t handle_range;
+ /**
+ * @brief Array of information related to the characteristics present in the service.
+ * This list can extend further than one.
+ */
+ ble_gatt_db_char_t charateristics[BLE_GATT_DB_MAX_CHARS];
+} ble_gatt_db_srv_t;
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* BLE_GATT_DB_H__ */
+
+/** @} */
diff --git a/include/bluetooth/peer_manager/peer_manager.h b/include/bluetooth/peer_manager/peer_manager.h
new file mode 100644
index 0000000000..fbf39a9ec5
--- /dev/null
+++ b/include/bluetooth/peer_manager/peer_manager.h
@@ -0,0 +1,825 @@
+/*
+ * Copyright (c) 2015-2025 Nordic Semiconductor ASA
+ *
+ * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
+ */
+
+/**
+ * @file peer_manager.h
+ *
+ * @defgroup peer_manager Peer Manager
+ * @ingroup ble_sdk_lib
+ * @{
+ * @brief Module for managing BLE bonding, which includes controlling encryption and pairing
+ * procedures as well as persistently storing different pieces of data that must be stored
+ * when bonded.
+ *
+ * @details The API consists of functions for configuring the pairing and encryption behavior of the
+ * device and functions for manipulating the stored data.
+ *
+ * This module uses ZMS to interface with persistent storage. (todo) The
+ * Peer Manager needs exclusive use of certain FDS file IDs and record keys. See
+ * @ref lib_fds_functionality_keys for more information.
+ */
+
+#ifndef PEER_MANAGER_H__
+#define PEER_MANAGER_H__
+
+#include
+#include
+#include
+#include
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** @brief Peer list filtrations. They determine which peer ID will be added to list. */
+typedef enum {
+ /** @brief Add all peers. */
+ PM_PEER_ID_LIST_ALL_ID,
+ /** @brief Add only peers with an ID address (static address). */
+ PM_PEER_ID_LIST_SKIP_NO_ID_ADDR = 1 << 0,
+ /**
+ * @brief Add only peers with a valid IRK. This implies @ref
+ * PM_PEER_ID_LIST_SKIP_NO_ID_ADDR, since all peers
+ * with IRKs have ID addresses.
+ */
+ PM_PEER_ID_LIST_SKIP_NO_IRK = 1 << 1,
+ /** @brief Add only peers with Central Address Resolution characteristic set to 0. */
+ PM_PEER_ID_LIST_SKIP_NO_CAR = 1 << 2,
+ /** @brief All above filters applied. */
+ PM_PEER_ID_LIST_SKIP_ALL = PM_PEER_ID_LIST_SKIP_NO_IRK | PM_PEER_ID_LIST_SKIP_NO_CAR
+} pm_peer_id_list_skip_t;
+
+/**
+ * @brief Function for initializing the Peer Manager.
+ *
+ * @details You must initialize the Peer Manager before you can call any other Peer Manager
+ * functions.
+ *
+ * @retval NRF_SUCCESS If initialization was successful.
+ * @retval NRF_ERROR_INTERNAL If an internal error occurred.
+ */
+uint32_t pm_init(void);
+
+/**
+ * @brief Function for registering an event handler with the Peer Manager.
+ *
+ * @param[in] event_handler Callback for events from the @ref peer_manager module. @p event_handler
+ * is called for every event that the Peer Manager sends after this
+ * function is called.
+ *
+ * @retval NRF_SUCCESS If initialization was successful.
+ * @retval NRF_ERROR_NULL If @p event_handler was NULL.
+ * @retval NRF_ERROR_NO_MEM If no more registrations can happen.
+ * @retval NRF_ERROR_INVALID_STATE If the Peer Manager is not initialized.
+ */
+uint32_t pm_register(pm_evt_handler_t event_handler);
+
+/**
+ * @brief Function for providing pairing and bonding parameters to use for pairing procedures.
+ *
+ * @details Until this function is called, all bonding procedures that are initiated by the
+ * peer are rejected.
+ *
+ * This function can be called multiple times with different parameters, even with NULL as
+ * @p p_sec_params, in which case the Peer Manager starts rejecting all procedures again.
+ *
+ * @param[in] p_sec_params Security parameters to be used for subsequent security procedures.
+ *
+ * @retval NRF_SUCCESS If the parameters were set successfully.
+ * @retval NRF_ERROR_INVALID_PARAM If the combination of parameters is invalid.
+ * @retval NRF_ERROR_INVALID_STATE If the Peer Manager is not initialized.
+ * @retval NRF_ERROR_INTERNAL If an internal error occurred.
+ */
+uint32_t pm_sec_params_set(ble_gap_sec_params_t *p_sec_params);
+
+/**
+ * @brief Function for establishing encryption on a connection, and optionally establishing a bond.
+ *
+ * @details This function attempts to secure the link that is specified by @p conn_handle. It uses
+ * the parameters that were previously provided in a call to @ref pm_sec_params_set.
+ *
+ * If the connection is a master connection, calling this function starts a security
+ * procedure on the link. If we have keys from a previous bonding procedure with this peer
+ * and the keys meet the security requirements in the currently active security parameters,
+ * the function attempts to establish encryption with the existing keys. If no key exists,
+ * the function attempts to perform pairing and bonding according to the currently active
+ * security parameters.
+ *
+ * If the function completes successfully, a @ref PM_EVT_CONN_SEC_START event is sent.
+ * The procedure might be queued, in which case the @ref PM_EVT_CONN_SEC_START event is
+ * delayed until the procedure is initiated in the SoftDevice.
+ *
+ * If the connection is a slave connection, the function sends a security request to
+ * the peer (master). It is up to the peer then to initiate pairing or encryption.
+ * If the peer ignores the request, a @ref BLE_GAP_EVT_AUTH_STATUS event occurs
+ * with the status @ref BLE_GAP_SEC_STATUS_TIMEOUT. Otherwise, the peer initiates
+ * security, in which case things happen as if the peer had initiated security itself.
+ * See @ref PM_EVT_CONN_SEC_START for information about peer-initiated security.
+ *
+ * @param[in] conn_handle Connection handle of the link as provided by the SoftDevice.
+ * @param[in] force_repairing Whether to force a pairing procedure even if there is an existing
+ * encryption key. This argument is relevant only for
+ * the central role. Recommended value: false.
+ *
+ * @retval NRF_SUCCESS If the operation completed successfully.
+ * @retval NRF_ERROR_BUSY If a security procedure is already in progress on the
+ * link, or if the link is disconnecting or disconnected.
+ * @retval NRF_ERROR_TIMEOUT If there was an SMP time-out, so that no more security
+ * operations can be performed on this link.
+ * @retval BLE_ERROR_INVALID_CONN_HANDLE If the connection handle is invalid.
+ * @retval NRF_ERROR_NOT_FOUND If the security parameters have not been set, either by
+ * @ref pm_sec_params_set or by @ref
+ * pm_conn_sec_params_reply.
+ * @retval NRF_ERROR_INVALID_DATA If the peer is bonded, but no LTK was found in the stored
+ * bonding data. Repairing was not requested.
+ * @retval NRF_ERROR_INVALID_STATE If the Peer Manager is not initialized.
+ * @retval NRF_ERROR_INTERNAL If an internal error occurred.
+ */
+uint32_t pm_conn_secure(uint16_t conn_handle, bool force_repairing);
+
+/**
+ * @brief Function for excluding a connection from the BLE event flow that is handled inside
+ * the Peer Manager.
+ *
+ * @details This function is optional, and must be called in reply to a @ref PM_EVT_CONN_CONFIG_REQ
+ * event, before the Peer Manager event handler returns. If it is not called in time,
+ * BLE events for a connection handle passed in the @ref PM_EVT_CONN_CONFIG_REQ event will
+ * be normally handled by the Peer Manager.
+ *
+ * @param[in] conn_handle The connection to be excluded.
+ * @param[in] p_context The context found in the request event that this function replies to.
+ *
+ * @retval NRF_SUCCESS Successful reply.
+ * @retval NRF_ERROR_NULL p_context was null.
+ */
+uint32_t pm_conn_exclude(uint16_t conn_handle, void const *p_context);
+
+/**
+ * @brief Function for providing security configuration for a link.
+ *
+ * @details This function is optional, and must be called in reply to a @ref
+ * PM_EVT_CONN_SEC_CONFIG_REQ event, before the Peer Manager event handler returns. If it
+ * is not called in time, a default configuration is used. See @ref pm_conn_sec_config_t
+ * for the value of the default.
+ *
+ * @param[in] conn_handle The connection to set the configuration for.
+ * @param[in] p_conn_sec_config The configuration.
+ */
+void pm_conn_sec_config_reply(uint16_t conn_handle, pm_conn_sec_config_t *p_conn_sec_config);
+
+/**
+ * @brief Function for providing security parameters for a link.
+ *
+ * @details This function is optional, and must be called in reply to a @ref
+ * PM_EVT_CONN_SEC_PARAMS_REQ event, before the Peer Manager event handler returns. If it
+ * is not called in time, the parameters given in @ref pm_sec_params_set are used.
+ *
+ * @param[in] conn_handle The connection to set the parameters for.
+ * @param[in] p_sec_params The parameters. If NULL, the security procedure is rejected.
+ * @param[in] p_context The context found in the request event that this function replies to.
+ *
+ * @retval NRF_SUCCESS Successful reply.
+ * @retval NRF_ERROR_NULL p_sec_params or p_context was null.
+ * @retval NRF_ERROR_INVALID_PARAM Value of p_sec_params was invalid.
+ * @retval NRF_ERROR_INVALID_STATE This module is not initialized.
+ */
+uint32_t pm_conn_sec_params_reply(uint16_t conn_handle, ble_gap_sec_params_t *p_sec_params,
+ void const *p_context);
+
+/**
+ * @brief Function for manually informing that the local database has changed.
+ *
+ * @details This function sends a service changed indication to all bonded and/or connected peers
+ * that subscribe to this indication. If a bonded peer is not connected, the indication is
+ * sent when it reconnects. Every time an indication is sent, a @ref
+ * PM_EVT_SERVICE_CHANGED_IND_SENT event occurs, followed by a @ref
+ * PM_EVT_SERVICE_CHANGED_IND_CONFIRMED when the peer sends its confirmation. Peers that
+ * are not subscribed to the service changed indication when this function is called do not
+ * receive an indication, and no events are sent to the user. Likewise, if the service
+ * changed characteristic is not present in the local database, or if the @ref
+ * PM_SERVICE_CHANGED_ENABLED is set to 0, no indications are sent peers, and no events are
+ * sent to the user.
+ */
+void pm_local_database_has_changed(void);
+
+/**
+ * @brief Function for getting the security status of a connection.
+ *
+ * @param[in] conn_handle Connection handle of the link as provided by the SoftDevice.
+ * @param[out] p_conn_sec_status Security status of the link.
+ *
+ * @retval NRF_SUCCESS If pairing was initiated successfully.
+ * @retval BLE_ERROR_INVALID_CONN_HANDLE If the connection handle is invalid.
+ * @retval NRF_ERROR_NULL If @p p_conn_sec_status was NULL.
+ * @retval NRF_ERROR_INVALID_STATE If the Peer Manager is not initialized.
+ */
+uint32_t pm_conn_sec_status_get(uint16_t conn_handle, pm_conn_sec_status_t *p_conn_sec_status);
+
+/**
+ * @brief Function for comparing the security status of a connection against a baseline.
+ *
+ * @param[in] conn_handle Connection handle of the link as provided by the SoftDevice.
+ * @param[out] p_sec_status_req Target baseline security status to compare against.
+ *
+ * @retval true If the security status of the connection matches or exceeds the baseline on all
+ * points.
+ * @retval false If the security status of the connection does not fulfil the baseline, or could
+ * not be retrieved.
+ */
+bool pm_sec_is_sufficient(uint16_t conn_handle, pm_conn_sec_status_t *p_sec_status_req);
+
+/**
+ * @brief Experimental function for specifying the public key to use for LESC operations.
+ *
+ * @details This function can be called multiple times. The specified public key will be used for
+ * all subsequent LESC (LE Secure Connections) operations until the next time this function
+ * is called.
+ *
+ * @note The key must continue to reside in application memory as it is not copied by Peer Manager.
+ *
+ * @note This function is deprecated. LESC keys are now handled internally if @ref PM_LESC_ENABLED
+ * is true. If @ref PM_LESC_ENABLED is false, this function works as before.
+ *
+ * @param[in] p_public_key The public key to use for all subsequent LESC operations.
+ *
+ * @retval NRF_SUCCESS If pairing was initiated successfully.
+ * @retval NRF_ERROR_INVALID_STATE If the Peer Manager is not initialized.
+ * @retval NRF_ERROR_FORBIDDEN If LESC module support is enabled (see @ref
+ * PM_LESC_ENABLED).
+ */
+uint32_t pm_lesc_public_key_set(ble_gap_lesc_p256_pk_t *p_public_key);
+
+/**
+ * @brief Function for setting or clearing the whitelist.
+ *
+ * When using the S13x SoftDevice v3.x, this function sets or clears the whitelist.
+ * When using the S13x SoftDevice v2.x, this function caches a list of
+ * peers that can be retrieved later by @ref pm_whitelist_get to pass to the @ref
+ * lib_ble_advertising.
+ *
+ * To clear the current whitelist, pass either NULL as @p p_peers or zero as @p peer_cnt.
+ *
+ * @param[in] p_peers The peers to add to the whitelist. Pass NULL to clear the current whitelist.
+ * @param[in] peer_cnt The number of peers to add to the whitelist. The number must not be greater
+ * than
+ * @ref BLE_GAP_WHITELIST_ADDR_MAX_COUNT. Pass zero to clear the current
+ * whitelist.
+ *
+ * @retval NRF_SUCCESS If the whitelist was successfully set or cleared.
+ * @retval BLE_GAP_ERROR_WHITELIST_IN_USE If a whitelist is already in use and cannot be set.
+ * @retval BLE_ERROR_GAP_INVALID_BLE_ADDR If a peer in @p p_peers has an address that cannot
+ * be used for whitelisting.
+ * @retval NRF_ERROR_NOT_FOUND If any of the peers in @p p_peers cannot be found.
+ * @retval NRF_ERROR_DATA_SIZE If @p peer_cnt is greater than
+ * @ref BLE_GAP_WHITELIST_ADDR_MAX_COUNT.
+ * @retval NRF_ERROR_INVALID_STATE If the Peer Manager is not initialized.
+ */
+uint32_t pm_whitelist_set(pm_peer_id_t const *p_peers, uint32_t peer_cnt);
+
+/**
+ * @brief Function for retrieving the previously set whitelist.
+ *
+ * The function retrieves the whitelist of GAP addresses and IRKs that was
+ * previously set by @ref pm_whitelist_set.
+ *
+ * To retrieve only GAP addresses or only IRKs, provide only one of the
+ * buffers. If a buffer is provided, its size must be specified.
+ *
+ * @param[out] p_addrs The buffer where to store GAP addresses. Pass NULL to retrieve
+ * only IRKs (in that case, @p p_irks must not be NULL).
+ * @param[in,out] p_addr_cnt In: The size of the @p p_addrs buffer.
+ * May be NULL if and only if @p p_addrs is NULL.
+ * Out: The number of GAP addresses copied into the buffer.
+ * If @p p_addrs is NULL, this parameter remains unchanged.
+ * @param[out] p_irks The buffer where to store IRKs. Pass NULL to retrieve
+ * only GAP addresses (in that case, @p p_addrs must not NULL).
+ * @param[in,out] p_irk_cnt In: The size of the @p p_irks buffer.
+ * May be NULL if and only if @p p_irks is NULL.
+ * Out: The number of IRKs copied into the buffer.
+ * If @p p_irks is NULL, this parameter remains unchanged.
+ *
+ * @retval NRF_SUCCESS If the whitelist was successfully retrieved.
+ * @retval BLE_ERROR_GAP_INVALID_BLE_ADDR If a peer has an address that cannot be used for
+ * whitelisting (this error can occur only
+ * when using the S13x SoftDevice v2.x).
+ * @retval NRF_ERROR_NULL If a required parameter is NULL.
+ * @retval NRF_ERROR_NO_MEM If the provided buffers are too small.
+ * @retval NRF_ERROR_NOT_FOUND If the data for any of the cached whitelisted peers
+ * cannot be found. It might have been deleted.
+ * @retval NRF_ERROR_INVALID_STATE If the Peer Manager is not initialized.
+ */
+uint32_t pm_whitelist_get(ble_gap_addr_t *p_addrs, uint32_t *p_addr_cnt, ble_gap_irk_t *p_irks,
+ uint32_t *p_irk_cnt);
+
+/**
+ * @brief Function for setting and clearing the device identities list.
+ *
+ * @note When entering directed advertising, make sure the active identities list does not contain
+ * peers that have no Central Address Resolution. See @ref pm_peer_id_list with skip_id
+ * @ref PM_PEER_ID_LIST_SKIP_NO_CAR.
+ *
+ * @param[in] p_peers The peers to add to the device identities list. Pass NULL to clear
+ * the device identities list.
+ * @param[in] peer_cnt The number of peers. Pass zero to clear the device identities list.
+ *
+ * @retval NRF_SUCCESS If the device identities list was successfully
+ * set or cleared.
+ * @retval NRF_ERROR_NOT_FOUND If a peer is invalid or its data could not
+ * be found in flash.
+ * @retval BLE_ERROR_GAP_INVALID_BLE_ADDR If a peer has an address that cannot be
+ * used for whitelisting.
+ * @retval BLE_ERROR_GAP_DEVICE_IDENTITIES_IN_USE If the device identities list is in use and
+ * cannot be set.
+ * @retval NRF_ERROR_INVALID_STATE If the Peer Manager is not initialized.
+ * @retval NRF_ERROR_NOT_SUPPORTED If using a SoftDevice that does not support
+ * device identities, e.g. S130 v2.0.
+ */
+uint32_t pm_device_identities_list_set(pm_peer_id_t const *p_peers, uint32_t peer_cnt);
+
+/**
+ * @brief Function for setting the local Bluetooth identity address.
+ *
+ * @details The local Bluetooth identity address is the address that identifies the device
+ * to other peers. The address type must be either @ref BLE_GAP_ADDR_TYPE_PUBLIC or @ref
+ * BLE_GAP_ADDR_TYPE_RANDOM_STATIC. The identity address cannot be changed while roles are running.
+ *
+ * The SoftDevice sets a default address of type @ref BLE_GAP_ADDR_TYPE_RANDOM_STATIC when it is
+ * enabled. This default address is a random number that is populated during the IC manufacturing
+ * process. It remains unchanged for the lifetime of each IC, but the application can use this
+ * function to assign a different identity address.
+ *
+ * The identity address is distributed to the peer during bonding. Changing the identity address
+ * means bonded devices might not recognize us.
+ *
+ * @note The SoftDevice functions @ref sd_ble_gap_addr_set and @ref sd_ble_gap_privacy_set must not
+ * be called when using the Peer Manager. Use the Peer Manager equivalents instead.
+ *
+ * @param[in] p_addr The GAP address to be set.
+ *
+ * @retval NRF_SUCCESS If the identity address was set successfully.
+ * @retval NRF_ERROR_NULL If @p p_addr is NULL.
+ * @retval NRF_ERROR_INVALID_ADDR If the @p p_addr pointer is invalid.
+ * @retval BLE_ERROR_GAP_INVALID_BLE_ADDR If the BLE address is invalid.
+ * @retval NRF_ERROR_BUSY If the SoftDevice was busy. Process SoftDevice events
+ * and retry.
+ * @retval NRF_ERROR_INVALID_STATE If the Peer Manager is not initialized or if this
+ * function was called while advertising, scanning, or while connected.
+ * @retval NRF_ERROR_INTERNAL If an internal error occurred.
+ */
+uint32_t pm_id_addr_set(ble_gap_addr_t const *p_addr);
+
+/**
+ * @brief Function for retrieving the local Bluetooth identity address.
+ *
+ * This function always returns the identity address, irrespective of the privacy settings.
+ * This means that the address type will always be either @ref BLE_GAP_ADDR_TYPE_PUBLIC or @ref
+ * BLE_GAP_ADDR_TYPE_RANDOM_STATIC.
+ *
+ * @param[out] p_addr Pointer to the address structure to be filled in.
+ *
+ * @retval NRF_SUCCESS If the address was retrieved successfully.
+ * @retval NRF_ERROR_NULL If @p p_addr is NULL.
+ * @retval NRF_ERROR_INVALID_STATE If the Peer Manager is not initialized.
+ */
+uint32_t pm_id_addr_get(ble_gap_addr_t *p_addr);
+
+/**
+ * @brief Function for configuring privacy settings.
+ *
+ * The privacy settings cannot be configured while advertising, scanning, or while in a connection.
+ *
+ * @note The SoftDevice functions @ref sd_ble_gap_addr_set
+ * and @ref sd_ble_gap_privacy_set must not be called when using the Peer Manager.
+ * Use this function instead.
+ *
+ * @param[in] p_privacy_params Privacy settings.
+ *
+ * @retval NRF_SUCCESS If the privacy settings were configured successfully.
+ * @retval NRF_ERROR_NULL If @p p_privacy_params is NULL.
+ * @retval NRF_ERROR_BUSY If the operation could not be performed at this time.
+ * Process SoftDevice events and retry.
+ * @retval NRF_ERROR_INVALID_PARAM If the address type is invalid.
+ * @retval NRF_ERROR_INVALID_STATE If this function is called while BLE roles using
+ * privacy are enabled.
+ * @retval NRF_ERROR_INVALID_STATE If the Peer Manager is not initialized.
+ */
+uint32_t pm_privacy_set(pm_privacy_params_t const *p_privacy_params);
+
+/**
+ * @brief Function for retrieving privacy settings.
+ *
+ * The privacy settings that are returned include the current IRK as well.
+ *
+ * @param[out] p_privacy_params Privacy settings.
+ *
+ * @retval NRF_SUCCESS If the privacy settings were retrieved successfully.
+ * @retval NRF_ERROR_NULL If @p p_privacy_params or @p p_privacy_params->p_device_irk is
+ * NULL.
+ * @retval NRF_ERROR_INVALID_STATE If the Peer Manager is not initialized.
+ */
+uint32_t pm_privacy_get(pm_privacy_params_t *p_privacy_params);
+
+/**
+ * @brief Function for resolving a resolvable address with an identity resolution key (IRK).
+ *
+ * @param[in] p_addr A private random resolvable address.
+ * @param[in] p_irk An identity resolution key (IRK).
+ *
+ * @retval true The IRK used matched the one used to create the address.
+ * @retval false The IRK used did not match the one used to create the address, or an argument was
+ * NULL or invalid.
+ */
+bool pm_address_resolve(ble_gap_addr_t const *p_addr, ble_gap_irk_t const *p_irk);
+
+/**
+ * @brief Function for getting the connection handle of the connection with a bonded peer.
+ *
+ * @param[in] peer_id The peer ID of the bonded peer.
+ * @param[out] p_conn_handle Connection handle, or @ref BLE_CONN_HANDLE_INVALID if none could be
+ * resolved. The conn_handle can refer to a recently disconnected
+ * connection.
+ *
+ * @retval NRF_SUCCESS If the connection handle was retrieved successfully.
+ * @retval NRF_ERROR_NULL If @p p_conn_handle was NULL.
+ * @retval NRF_ERROR_INVALID_STATE If the Peer Manager is not initialized.
+ */
+uint32_t pm_conn_handle_get(pm_peer_id_t peer_id, uint16_t *p_conn_handle);
+
+/**
+ * @brief Function for retrieving the ID of a peer, given its connection handle.
+ *
+ * @param[in] conn_handle The connection handle of the peer.
+ * @param[out] p_peer_id The peer ID, or @ref PM_PEER_ID_INVALID if the peer is not bonded or
+ * @p conn_handle does not refer to a valid connection.
+ *
+ * @retval NRF_SUCCESS If the peer ID was retrieved successfully.
+ * @retval NRF_ERROR_NULL If @p p_peer_id was NULL.
+ * @retval NRF_ERROR_INVALID_STATE If the Peer Manager is not initialized.
+ */
+uint32_t pm_peer_id_get(uint16_t conn_handle, pm_peer_id_t *p_peer_id);
+
+/**
+ * @brief Function for retrieving a filtered list of peer IDs.
+ *
+ * @details This function starts searching from @p first_peer_id. IDs ordering
+ * is the same as for @ref pm_next_peer_id_get(). If the first_peer_id
+ * is @ref PM_PEER_ID_INVALID, the function starts searching from the first ID.
+ * The function looks for the ID's number specified by @p p_list_size. Only those IDs that
+ * match @p skip_id are added to the list. The number of returned elements is determined
+ * by @p p_list_size.
+ *
+ * @warning The size of the @p p_peer_list buffer must be equal or greater than @p p_list_size.
+ *
+ * @param[out] p_peer_list Pointer to peer IDs list buffer.
+ * @param[in,out] p_list_size The amount of IDs to return / The number of returned IDs.
+ * @param[in] first_peer_id The first ID from which the search begins. IDs ordering
+ * is the same as for @ref pm_next_peer_id_get()
+ * @param[in] skip_id It determines which peer ID will be added to list.
+ *
+ * @retval NRF_SUCCESS If the ID list has been filled out.
+ * @retval NRF_ERROR_INVALID_PARAM If @p skip_id was invalid.
+ * @retval NRF_ERROR_NULL If peer_list or list_size was NULL.
+ * @retval NRF_ERROR_INVALID_STATE If the Peer Manager is not initialized.
+ */
+uint32_t pm_peer_id_list(pm_peer_id_t *p_peer_list, uint32_t *const p_list_size,
+ pm_peer_id_t first_peer_id, pm_peer_id_list_skip_t skip_id);
+
+/**
+ * @brief Function for getting the next peer ID in the sequence of all used peer IDs.
+ *
+ * @details This function can be used to loop through all used peer IDs. The order in which
+ * peer IDs are returned should be considered unpredictable. @ref PM_PEER_ID_INVALID
+ * is considered to be before the first and after the last used peer ID.
+ *
+ * @details To loop through all peer IDs exactly once, use the following constuct:
+ * @code{c}
+ * pm_peer_id_t current_peer_id = pm_next_peer_id_get(PM_PEER_ID_INVALID);
+ * while (current_peer_id != PM_PEER_ID_INVALID)
+ * {
+ * // Do something with current_peer_id.
+ * current_peer_id = pm_next_peer_id_get(current_peer_id)
+ * }
+ * @endcode
+ *
+ * @note This function does not report peer IDs that are pending deletion.
+ *
+ * @param[in] prev_peer_id The previous peer ID.
+ *
+ * @return The next peer ID. If @p prev_peer_id was @ref PM_PEER_ID_INVALID, the
+ * next peer ID is the first used peer ID. If @p prev_peer_id was the last
+ * used peer ID, the function returns @ref PM_PEER_ID_INVALID.
+ */
+pm_peer_id_t pm_next_peer_id_get(pm_peer_id_t prev_peer_id);
+
+/**
+ * @brief Function for querying the number of valid peer IDs that are available.
+ *
+ * @details This function returns the number of peers for which there is data in persistent storage.
+ *
+ * @return The number of valid peer IDs.
+ */
+uint32_t pm_peer_count(void);
+
+/**
+ * @anchor PM_PEER_DATA_FUNCTIONS
+ * @name Functions (Peer Data)
+ * Functions for manipulating peer data.
+ * @{
+ */
+
+/**
+ * @{
+ */
+
+/**
+ * @brief Function for retrieving stored data of a peer.
+ *
+ * @note The length of the provided buffer must be a multiple of 4.
+ *
+ * @param[in] peer_id Peer ID to get data for.
+ * @param[in] data_id Which type of data to read.
+ * @param[out] p_data Where to put the retrieved data. The documentation for
+ * @ref pm_peer_data_id_t specifies what data type each data ID is stored
+ * as.
+ * @param[in,out] p_len In: The length in bytes of @p p_data.
+ * Out: The length in bytes of the read data, if the read was successful.
+ *
+ * @retval NRF_SUCCESS If the data was read successfully.
+ * @retval NRF_ERROR_INVALID_PARAM If the data type or the peer ID was invalid or unallocated.
+ * @retval NRF_ERROR_NULL If a pointer parameter was NULL.
+ * @retval NRF_ERROR_NOT_FOUND If no stored data was found for this peer ID/data ID
+ * combination.
+ * @retval NRF_ERROR_DATA_SIZE If the provided buffer was not large enough. The data is still
+ * copied, filling the provided buffer. Note that this error can
+ * occur even if loading the same size as was stored, because the
+ * underlying layers round the length up to the nearest word (4
+ * bytes) when storing.
+ * @retval NRF_ERROR_INVALID_STATE If the Peer Manager is not initialized.
+ */
+uint32_t pm_peer_data_load(pm_peer_id_t peer_id, pm_peer_data_id_t data_id, void *p_data,
+ uint32_t *p_len);
+
+/**
+ * @brief Function for reading a peer's bonding data (@ref PM_PEER_DATA_ID_BONDING).
+ * @details See @ref pm_peer_data_load for parameters and return values.
+ */
+uint32_t pm_peer_data_bonding_load(pm_peer_id_t peer_id, pm_peer_data_bonding_t *p_data);
+
+/**
+ * @brief Function for reading a peer's remote DB values. (@ref PM_PEER_DATA_ID_GATT_REMOTE).
+ * @details See @ref pm_peer_data_load for parameters and return values.
+ */
+uint32_t pm_peer_data_remote_db_load(pm_peer_id_t peer_id, ble_gatt_db_srv_t *p_data,
+ uint32_t *p_len);
+
+/**
+ * @brief Function for reading a peer's application data. (@ref PM_PEER_DATA_ID_APPLICATION).
+ * @details See @ref pm_peer_data_load for parameters and return values.
+ */
+uint32_t pm_peer_data_app_data_load(pm_peer_id_t peer_id, void *p_data, uint32_t *p_len);
+/** @}*/
+
+/**
+ * @{
+ */
+
+/**
+ * @brief Function for setting or updating stored data of a peer.
+ *
+ * @note Writing the data to persistent storage happens asynchronously. Therefore, the buffer
+ * that contains the data must be kept alive until the operation has completed.
+ *
+ * @note The data written using this function might later be overwritten as a result of internal
+ * operations in the Peer Manager. A Peer Manager event is sent each time data is updated,
+ * regardless of whether the operation originated internally or from action by the user.
+ * Data with @p data_id @ref PM_PEER_DATA_ID_GATT_REMOTE @ref PM_PEER_DATA_ID_APPLICATION is
+ * never (over)written internally.
+ *
+ * @param[in] peer_id Peer ID to set data for.
+ * @param[in] data_id Which type of data to set.
+ * @param[in] p_data New value to set. The documentation for @ref pm_peer_data_id_t specifies
+ * what data type each data ID should be stored as.
+ * @param[in] len The length in bytes of @p p_data.
+ * @param[out] p_token A token that identifies this particular store operation. The token can be
+ * used to identify events that pertain to this operation. This parameter can
+ * be NULL.
+ *
+ * @retval NRF_SUCCESS If the data is scheduled to be written to persistent storage.
+ * @retval NRF_ERROR_NULL If @p p_data is NULL.
+ * @retval NRF_ERROR_NOT_FOUND If no peer was found for the peer ID.
+ * @retval NRF_ERROR_INVALID_ADDR If @p p_data is not word-aligned (4 bytes).
+ * @retval NRF_ERROR_BUSY If the underlying flash handler is busy with other flash
+ * operations. Try again after receiving a Peer Manager event.
+ * @retval NRF_ERROR_RESOURCES If there is not enough space in persistent storage.
+ * @retval NRF_ERROR_FORBIDDEN If data ID is @ref PM_PEER_DATA_ID_BONDING and the new bonding
+ * data also corresponds to another bonded peer. No data is written
+ * so duplicate entries are avoided.
+ * @retval NRF_ERROR_INVALID_STATE If the Peer Manager is not initialized.
+ */
+uint32_t pm_peer_data_store(pm_peer_id_t peer_id, pm_peer_data_id_t data_id, void const *p_data,
+ uint32_t len, pm_store_token_t *p_token);
+
+/**
+ * @brief Function for setting or updating a peer's bonding data (@ref PM_PEER_DATA_ID_BONDING).
+ * @details See @ref pm_peer_data_store for parameters and return values.
+ */
+uint32_t pm_peer_data_bonding_store(pm_peer_id_t peer_id, pm_peer_data_bonding_t const *p_data,
+ pm_store_token_t *p_token);
+
+/**
+ * @brief Function for setting or updating a peer's remote DB values. (@ref
+ * PM_PEER_DATA_ID_GATT_REMOTE).
+ * @details See @ref pm_peer_data_store for parameters and return values.
+ */
+uint32_t pm_peer_data_remote_db_store(pm_peer_id_t peer_id, ble_gatt_db_srv_t const *p_data,
+ uint32_t len, pm_store_token_t *p_token);
+
+/**
+ * @brief Function for setting or updating a peer's application data.
+ * (@ref PM_PEER_DATA_ID_APPLICATION).
+ * @details See @ref pm_peer_data_store for parameters and return values.
+ */
+uint32_t pm_peer_data_app_data_store(pm_peer_id_t peer_id, void const *p_data, uint32_t len,
+ pm_store_token_t *p_token);
+/** @}*/
+
+/**
+ * @{
+ */
+
+/**
+ * @brief Function for deleting a peer's stored pieces of data.
+ *
+ * @details This function deletes specific data that is stored for a peer. Note that bonding data
+ * cannot be cleared separately.
+ *
+ * To delete all data for a peer (including bonding data), use @ref pm_peer_delete.
+ *
+ * @note Clearing data in persistent storage happens asynchronously.
+ *
+ * @param[in] peer_id Peer ID to clear data for.
+ * @param[in] data_id Which data to clear.
+ *
+ * @retval NRF_SUCCESS If the clear procedure was initiated successfully.
+ * @retval NRF_ERROR_INVALID_PARAM If @p data_id was PM_PEER_DATA_ID_BONDING or invalid, or
+ * @p peer_id was invalid.
+ * @retval NRF_ERROR_NOT_FOUND If there was no data to clear for this peer ID/data ID
+ * combination.
+ * @retval NRF_ERROR_BUSY If the underlying flash handler is busy with other flash
+ * operations. Try again after receiving a Peer Manager event.
+ * @retval NRF_ERROR_INVALID_STATE If the Peer Manager is not initialized.
+ * @retval NRF_ERROR_INTERNAL If an internal error occurred.
+ */
+uint32_t pm_peer_data_delete(pm_peer_id_t peer_id, pm_peer_data_id_t data_id);
+
+/**
+ * @brief Function for manually adding a peer to the persistent storage.
+ *
+ * @details This function allocates a new peer ID and stores bonding data for the new peer. The
+ * bonding data is necessary to prevent ambiguity/inconsistency in peer data.
+ *
+ * @param[in] p_bonding_data The bonding data of the new peer (must contain a public/static
+ * address or a non-zero IRK).
+ * @param[out] p_new_peer_id Peer ID for the new peer, or an existing peer if a match was found.
+ * @param[out] p_token A token that identifies this particular store operation (storing the
+ * bonding data). The token can be used to identify events that pertain
+ * to this operation. This parameter can be NULL.
+ *
+ * @retval NRF_SUCCESS If the store operation for bonding data was initiated
+ * successfully.
+ * @retval NRF_ERROR_NULL If @p p_bonding_data or @p p_new_peer_id is NULL.
+ * @retval NRF_ERROR_INVALID_ADDR If @p p_bonding_data is not word-aligned (4 bytes).
+ * @retval NRF_ERROR_RESOURCES If there is not enough space in persistent storage.
+ * @retval NRF_ERROR_NO_MEM If there are no more available peer IDs.
+ * @retval NRF_ERROR_BUSY If the underlying flash filesystem is busy with other flash
+ * operations. Try again after receiving a Peer Manager event.
+ * @retval NRF_ERROR_INVALID_STATE If the Peer Manager is not initialized.
+ * @retval NRF_ERROR_INTERNAL If an internal error occurred.
+ */
+uint32_t pm_peer_new(pm_peer_id_t *p_new_peer_id, pm_peer_data_bonding_t *p_bonding_data,
+ pm_store_token_t *p_token);
+
+/**
+ * @brief Function for freeing persistent storage for a peer.
+ *
+ * @details This function deletes every piece of data that is associated with the specified peer and
+ * frees the peer ID to be used for another peer. The deletion happens asynchronously, and
+ * the peer ID is not freed until the data is deleted. When the operation finishes, a @ref
+ * PM_EVT_PEER_DELETE_SUCCEEDED or @ref PM_EVT_PEER_DELETE_FAILED event is sent.
+ *
+ * @warning Use this function only when not connected to or connectable for the peer that is being
+ * deleted. If the peer is or becomes connected or data is manually written in flash during
+ * this procedure (until the success or failure event happens), the behavior is undefined.
+ *
+ * @param[in] peer_id Peer ID to be freed and have all associated data deleted.
+ *
+ * @retval NRF_SUCCESS If the operation was initiated successfully.
+ * @retval NRF_ERROR_INVALID_PARAM If the peer ID was not valid.
+ * @retval NRF_ERROR_INVALID_STATE If the Peer Manager is not initialized.
+ */
+uint32_t pm_peer_delete(pm_peer_id_t peer_id);
+
+/**
+ * @brief Function for deleting all data stored for all peers.
+ *
+ * @details This function sends either a @ref PM_EVT_PEERS_DELETE_SUCCEEDED or a @ref
+ * PM_EVT_PEERS_DELETE_FAILED event. In addition, a @ref PM_EVT_PEER_DELETE_SUCCEEDED or
+ * @ref PM_EVT_PEER_DELETE_FAILED event is sent for each deleted peer.
+ *
+ * @note When there is no peer data in flash the @ref PM_EVT_PEER_DELETE_SUCCEEDED event is sent
+ * synchronously.
+ *
+ * @warning Use this function only when not connected or connectable. If a peer is or becomes
+ * connected or a @ref PM_PEER_DATA_FUNCTIONS function is used during this procedure (until
+ * the success or failure event happens), the behavior is undefined.
+ *
+ * @retval NRF_SUCCESS If the deletion process was initiated successfully.
+ * @retval NRF_ERROR_INVALID_STATE If the Peer Manager is not initialized.
+ * @retval NRF_ERROR_INTERNAL If an internal error occurred.
+ */
+uint32_t pm_peers_delete(void);
+/** @}*/
+
+/**
+ * @{
+ */
+
+/**
+ * @brief Function for finding the highest and lowest ranked peers.
+ *
+ * @details The rank is saved in persistent storage under the data ID @ref
+ * PM_PEER_DATA_ID_PEER_RANK.
+ *
+ * @details The interpretation of rank is up to the user, because the rank is only updated by
+ * calling @ref pm_peer_rank_highest or by manipulating the value using a @ref
+ * PM_PEER_DATA_FUNCTIONS function.
+ *
+ * @note Peers with no stored rank are not considered.
+ * @note Any argument that is NULL is ignored.
+ *
+ * @param[out] p_highest_ranked_peer The peer ID with the highest rank of all peers, for example,
+ * the most recently used peer.
+ * @param[out] p_highest_rank The highest rank.
+ * @param[out] p_lowest_ranked_peer The peer ID with the lowest rank of all peers, for example,
+ * the least recently used peer.
+ * @param[out] p_lowest_rank The lowest rank.
+ *
+ * @retval NRF_SUCCESS If the operation completed successfully.
+ * @retval NRF_ERROR_NOT_FOUND If no peer with stored peer rank was found.
+ * @retval NRF_ERROR_INVALID_STATE If the Peer Manager is not initialized.
+ * @retval NRF_ERROR_INTERNAL If an internal error occurred.
+ * @retval NRF_ERROR_NOT_SUPPORTED If peer rank functionality has been disabled via the @ref
+ * PM_PEER_RANKS_ENABLED configuration option.
+ */
+uint32_t pm_peer_ranks_get(pm_peer_id_t *p_highest_ranked_peer, uint32_t *p_highest_rank,
+ pm_peer_id_t *p_lowest_ranked_peer, uint32_t *p_lowest_rank);
+
+/**
+ * @brief Function for updating the rank of a peer to be highest among all stored peers.
+ *
+ * @details If this function returns @ref NRF_SUCCESS, either a @ref
+ * PM_EVT_PEER_DATA_UPDATE_SUCCEEDED or a
+ * @ref PM_EVT_PEER_DATA_UPDATE_FAILED event is sent with a @ref
+ * PM_STORE_TOKEN_INVALID store token when the operation is complete. Until the operation
+ * is complete, this function returns @ref NRF_ERROR_BUSY.
+ *
+ * When the operation is complete, the peer is the highest ranked peer as reported by
+ * @ref pm_peer_ranks_get.
+ *
+ * @note The @ref PM_EVT_PEER_DATA_UPDATE_SUCCEEDED event can arrive before the function returns if
+ * the peer is already ranked highest. In this case, the @ref
+ * pm_peer_data_update_succeeded_evt_t::flash_changed flag in the event will be false.
+ *
+ * @param[in] peer_id The peer to rank highest.
+ *
+ * @retval NRF_SUCCESS If the peer's rank is, or will be updated to be highest.
+ * @retval NRF_ERROR_INVALID_PARAM If @p peer_id is invalid, or doesn't exist in flash.
+ * @retval NRF_ERROR_RESOURCES If there is not enough space in persistent storage.
+ * @retval NRF_ERROR_BUSY If the underlying flash handler is busy with other flash
+ * operations, or if a previous call to this function has not
+ * completed. Try again after receiving a Peer Manager event.
+ * @retval NRF_ERROR_INVALID_STATE If the Peer Manager is not initialized.
+ * @retval NRF_ERROR_DATA_SIZE If the highest rank is UINT32_MAX, so the new rank would wrap
+ * around to 0. To fix this, manually update all ranks to smaller
+ * values, while still keeping their order.
+ * @retval NRF_ERROR_INTERNAL If an internal error occurred.
+ * @retval NRF_ERROR_NOT_SUPPORTED If peer rank functionality has been disabled via the @ref
+ * PM_PEER_RANKS_ENABLED configuration option.
+ */
+uint32_t pm_peer_rank_highest(pm_peer_id_t peer_id);
+
+/** @}*/
+
+/** @} */
+
+/** @} */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PEER_MANAGER_H__ */
diff --git a/include/bluetooth/peer_manager/peer_manager_types.h b/include/bluetooth/peer_manager/peer_manager_types.h
new file mode 100644
index 0000000000..155f7f98d3
--- /dev/null
+++ b/include/bluetooth/peer_manager/peer_manager_types.h
@@ -0,0 +1,568 @@
+/*
+ * Copyright (c) 2015-2025 Nordic Semiconductor ASA
+ *
+ * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
+ */
+
+/**
+ * @file peer_manager_types.h
+ *
+ * @addtogroup peer_manager
+ * @{
+ */
+
+#ifndef PEER_MANAGER_TYPES_H__
+#define PEER_MANAGER_TYPES_H__
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** @brief Handle to uniquely identify a peer for which we have persistently stored data. */
+typedef uint16_t pm_peer_id_t;
+
+/** @brief Type that is used to hold a reference to a stored item in flash. */
+typedef uint32_t pm_store_token_t;
+
+/**
+ * @brief Errors from security procedures in Peer Manager.
+ *
+ * @details Possible values are defined in @ref PM_SEC_ERRORS and @ref BLE_GAP_SEC_STATUS.
+ */
+typedef uint16_t pm_sec_error_code_t;
+
+/** @brief Invalid value for Peer ID. */
+#define PM_PEER_ID_INVALID 0xFFFF
+/** @brief Invalid value for store token. */
+#define PM_STORE_TOKEN_INVALID 0
+/** @brief The number of available peer IDs. */
+#define PM_PEER_ID_N_AVAILABLE_IDS 256
+/** @brief The static-length part of the local GATT data struct. */
+#define PM_LOCAL_DB_LEN_OVERHEAD_BYTES offsetof(pm_peer_data_local_gatt_db_t, data)
+/**
+ * @brief The base for Peer Manager defined errors. See @ref PM_SEC_ERRORS and
+ * @ref pm_sec_error_code_t.
+ */
+#define PM_CONN_SEC_ERROR_BASE 0x1000
+
+/**
+ * @defgroup PM_SEC_ERRORS Peer Manager defined security errors
+ *
+ * @details The first 256 numbers, from PM_CONN_SEC_ERROR_BASE to (PM_CONN_SEC_ERROR_BASE + 0xFF),
+ * correspond to the status codes in @ref BLE_HCI_STATUS_CODES.
+ * @{
+ */
+/**
+ * @brief Encryption failed because the peripheral has lost the LTK for this
+ * bond. See also @ref BLE_HCI_STATUS_CODE_PIN_OR_KEY_MISSING and Table 3.7
+ * ("Pairing Failed Reason Codes") in the Bluetooth Core Specification 4.2,
+ * section 3.H.3.5.5 (@linkBLEcore).
+ */
+#define PM_CONN_SEC_ERROR_PIN_OR_KEY_MISSING (PM_CONN_SEC_ERROR_BASE + 0x06)
+/**
+ * @brief Encryption ended with disconnection because of mismatching keys or a
+ * stray packet during a procedure. See the SoftDevice GAP Message Sequence Charts
+ * on encryption (@linkBLEMSCgap), the Bluetooth Core Specification 4.2,
+ * sections 6.B.5.1.3.1 and 3.H.3.5.5 (@linkBLEcore), and
+ * @ref BLE_HCI_CONN_TERMINATED_DUE_TO_MIC_FAILURE.
+ */
+#define PM_CONN_SEC_ERROR_MIC_FAILURE (PM_CONN_SEC_ERROR_BASE + 0x3D)
+/**
+ * @brief Pairing or encryption did not finish before
+ * the link disconnected for an unrelated reason.
+ */
+#define PM_CONN_SEC_ERROR_DISCONNECT (PM_CONN_SEC_ERROR_BASE + 0x100)
+/**
+ * @brief Pairing/bonding could not start because an SMP time-out has already
+ * happened on this link. This means that no more pairing or bonding can happen on
+ * this link. To be able to pair or bond, the link must be disconnected and then
+ * reconnected. See Bluetooth Core Specification 4.2 section 3.H.3.4 (@linkBLEcore).
+ */
+#define PM_CONN_SEC_ERROR_SMP_TIMEOUT (PM_CONN_SEC_ERROR_BASE + 0x101)
+/** @} */
+
+/**
+ * @defgroup PM_PEER_ID_VERSIONS All versions of Peer IDs.
+ * @brief The data ID for each iteration of the data formats in flash.
+ * @details Each time the format (in flash) of a piece of peer data changes, the data ID will also
+ * be updated. This list of defines is a record of each data ID that has ever existed, and
+ * code that caters to legacy formats can find the relevant IDs here.
+ * @{
+ */
+/** @brief The smallest data ID. */
+#define PM_PEER_DATA_ID_FIRST_VX 0
+/** @brief The data ID of the first version of bonding data. */
+#define PM_PEER_DATA_ID_BONDING_V1 0
+/** @brief The data ID of the second version of bonding data. */
+#define PM_PEER_DATA_ID_BONDING_V2 7
+/** @brief The data ID of the first version of the service changed pending flag. */
+#define PM_PEER_DATA_ID_SERVICE_CHANGED_PENDING_V1 1
+/** @brief The data ID of the first version of local GATT data. */
+#define PM_PEER_DATA_ID_GATT_LOCAL_V1 2
+/** @brief The data ID of the second version of local GATT data. */
+#define PM_PEER_DATA_ID_GATT_LOCAL_V2 8
+/** @brief The data ID of the first version of remote GATT data. */
+#define PM_PEER_DATA_ID_GATT_REMOTE_V1 3
+/** @brief The data ID of the first version of application data. */
+#define PM_PEER_DATA_ID_APPLICATION_V1 4
+/** @brief The data ID of the second version of remote GATT data. */
+#define PM_PEER_DATA_ID_GATT_REMOTE_V2 5
+/** @brief The data ID of the first version of the rank. */
+#define PM_PEER_DATA_ID_PEER_RANK_V1 6
+/** @brief The data ID of the first version of central address resolution. */
+#define PM_PEER_DATA_ID_CENTRAL_ADDR_RES_V1 9
+/** @brief The data ID after the last valid one. */
+#define PM_PEER_DATA_ID_LAST_VX 10
+/** @brief A data ID guaranteed to be invalid. */
+#define PM_PEER_DATA_ID_INVALID_VX 0xFF
+/**@}*/
+
+/**
+ * @brief The different types of data associated with a peer.
+ */
+typedef enum {
+ /** @brief The smallest data ID. */
+ PM_PEER_DATA_ID_FIRST = PM_PEER_DATA_ID_FIRST_VX,
+ /** @brief The data ID for bonding data. Type: @ref pm_peer_data_bonding_t. */
+ PM_PEER_DATA_ID_BONDING = PM_PEER_DATA_ID_BONDING_V2,
+ /** @brief The data ID for service changed state. Type: bool. */
+ PM_PEER_DATA_ID_SERVICE_CHANGED_PENDING = PM_PEER_DATA_ID_SERVICE_CHANGED_PENDING_V1,
+ /**
+ * @brief The data ID for local GATT data (sys attributes).
+ * Type: @ref pm_peer_data_local_gatt_db_t.
+ */
+ PM_PEER_DATA_ID_GATT_LOCAL = PM_PEER_DATA_ID_GATT_LOCAL_V2,
+ /** @brief The data ID for remote GATT data. Type: uint8_t array. */
+ PM_PEER_DATA_ID_GATT_REMOTE = PM_PEER_DATA_ID_GATT_REMOTE_V2,
+ /** @brief The data ID for peer rank. See @ref pm_peer_rank_highest. Type: uint32_t. */
+ PM_PEER_DATA_ID_PEER_RANK = PM_PEER_DATA_ID_PEER_RANK_V1,
+ /**
+ * @brief The data ID for central address resolution. See @ref pm_peer_id_list.
+ * Type: uint32_t.
+ */
+ PM_PEER_DATA_ID_CENTRAL_ADDR_RES = PM_PEER_DATA_ID_CENTRAL_ADDR_RES_V1,
+ /** @brief The data ID for application data. Type: uint8_t array. */
+ PM_PEER_DATA_ID_APPLICATION = PM_PEER_DATA_ID_APPLICATION_V1,
+ /** @brief One more than the highest data ID. */
+ PM_PEER_DATA_ID_LAST = PM_PEER_DATA_ID_LAST_VX,
+ /** @brief A data ID guaranteed to be invalid. */
+ PM_PEER_DATA_ID_INVALID = PM_PEER_DATA_ID_INVALID_VX,
+} pm_peer_data_id_t;
+
+/** @brief Different procedures that can lead to an encrypted link. */
+typedef enum {
+ /**
+ * @brief Using an LTK that was shared during a previous bonding procedure to encrypt the
+ * link.
+ */
+ PM_CONN_SEC_PROCEDURE_ENCRYPTION,
+ /** @brief A pairing procedure, followed by a bonding procedure. */
+ PM_CONN_SEC_PROCEDURE_BONDING,
+ /** @brief A pairing procedure with no bonding. */
+ PM_CONN_SEC_PROCEDURE_PAIRING,
+} pm_conn_sec_procedure_t;
+
+/** @brief Configuration of a security procedure. */
+typedef struct {
+ /**
+ * @brief Whether to allow the peer to pair if it wants to, but is
+ * already bonded. If this is false, the procedure is rejected, and no
+ * more events are sent. Default: false.
+ */
+ bool allow_repairing;
+} pm_conn_sec_config_t;
+
+/** @brief Data associated with a bond to a peer. */
+typedef struct {
+ /** @brief The BLE role of the local device during bonding. See @ref BLE_GAP_ROLES. */
+ uint8_t own_role;
+ /** @brief The peer's Bluetooth address and identity resolution key (IRK). */
+ ble_gap_id_key_t peer_ble_id;
+ /** @brief The peer's long-term encryption key (LTK) and master ID. */
+ ble_gap_enc_key_t peer_ltk;
+ /**
+ * @brief Locally generated long-term encryption key (LTK) and master ID, distributed to
+ * the peer.
+ */
+ ble_gap_enc_key_t own_ltk;
+} pm_peer_data_bonding_t;
+
+/** @brief Data on a local GATT database. */
+typedef struct {
+ /** @brief Flags that describe the database attributes. */
+ uint32_t flags;
+ /** @brief Size of the attribute array. */
+ uint16_t len;
+ /** @brief Array to hold the database attributes. */
+ uint8_t data[1];
+} pm_peer_data_local_gatt_db_t;
+
+/**
+ * @brief Device Privacy.
+ *
+ * The privacy feature provides a way for the device to avoid being tracked over a period of
+ * time. The privacy feature, when enabled, hides the local device identity and replaces it
+ * with a private address that is automatically refreshed at a specified interval.
+ *
+ * If a device still wants to be recognized by other peers, it needs to share it's Identity
+ * Resolving Key (IRK). With this key, a device can generate a random private address that
+ * can only be recognized by peers in possession of that key, and devices can establish
+ * connections without revealing their real identities.
+ *
+ * @note If the device IRK is updated, the new IRK becomes the one to be distributed in all
+ * bonding procedures performed after @ref sd_ble_gap_privacy_set returns.
+ * The IRK distributed during bonding procedure is the device IRK that is active when @ref
+ * sd_ble_gap_sec_params_reply is called.
+ */
+typedef ble_gap_privacy_params_t pm_privacy_params_t;
+
+/** @brief Security status of a connection. */
+typedef struct {
+ /** @brief The connection is active (not disconnected). */
+ uint8_t connected: 1;
+ /** @brief The communication on this link is encrypted. */
+ uint8_t encrypted: 1;
+ /**
+ * @brief The encrypted communication is also protected against man-in-the-middle
+ * attacks.
+ */
+ uint8_t mitm_protected: 1;
+ /** @brief The peer is bonded. */
+ uint8_t bonded: 1;
+ /** @brief The peer is paired using LESC. */
+ uint8_t lesc: 1;
+ /** @brief Reserved for future use. */
+ uint8_t reserved: 3;
+} pm_conn_sec_status_t;
+
+/** @brief Types of events that can come from the @ref peer_manager module. */
+typedef enum {
+ /**
+ * @brief A connected peer has been identified as one with
+ * which we have a bond. When performing bonding with a peer
+ * for the first time, this event will not be sent until a new
+ * connection is established with the peer. When we are
+ * central, this event is always sent when the Peer Manager
+ * receives the @ref BLE_GAP_EVT_CONNECTED event. When we are
+ * peripheral, this event might in rare cases arrive later.
+ */
+ PM_EVT_BONDED_PEER_CONNECTED,
+ /**
+ * @brief A new connection has been established. This event is a
+ * wrapper for @ref BLE_GAP_EVT_CONNECTED event and contains its
+ * parameters. Reply with @ref pm_conn_exclude before the event
+ * handler returns to exclude BLE events targeting this connection
+ * from being handled by the Peer Manager
+ */
+ PM_EVT_CONN_CONFIG_REQ,
+ /**
+ * @brief A security procedure has started on a link, initiated
+ * either locally or remotely. The security procedure is using the
+ * last parameters provided via @ref pm_sec_params_set. This event is
+ * always followed by either a @ref PM_EVT_CONN_SEC_SUCCEEDED or a
+ * @ref PM_EVT_CONN_SEC_FAILED event. This is an informational event;
+ * no action is needed for the procedure to proceed.
+ */
+ PM_EVT_CONN_SEC_START,
+ /**
+ * @brief A link has been encrypted, either as a result of a
+ * call to @ref pm_conn_secure or a result of an action by th
+ * peer. The event structure contains more information about the
+ * circumstances. This event might contain a peer ID with the
+ * value @ref PM_PEER_ID_INVALID, which means that the peer
+ * (central) used an address that could not be identified, but it
+ * used an encryption key (LTK) that is present in the database.
+ */
+ PM_EVT_CONN_SEC_SUCCEEDED,
+ /**
+ * @brief A pairing or encryption procedure has failed. In some
+ * cases, this means that security is not possible on this link
+ * (temporarily or permanently). How to handle this error depends on
+ * the application.
+ */
+ PM_EVT_CONN_SEC_FAILED,
+ /**
+ * @brief The peer (central) has requested pairing, but a bond
+ * already exists with that peer. Reply by calling @ref
+ * pm_conn_sec_config_reply before the event handler returns. If
+ * no reply is sent, a default is used.
+ */
+ PM_EVT_CONN_SEC_CONFIG_REQ,
+ /**
+ * @brief Security parameters (@ref ble_gap_sec_params_t) are
+ * needed for an ongoing security procedure. Reply with @ref
+ * pm_conn_sec_params_reply before the event handler returns. If
+ * no reply is sent, the parameters given in @ref
+ * pm_sec_params_set are used. If a peripheral connection, the
+ * central's sec_params will be available in the event.
+ */
+ PM_EVT_CONN_SEC_PARAMS_REQ,
+ /**
+ * @brief There is no more room for peer data in flash storage. To
+ * solve this problem, delete data that is not needed anymore and run a
+ * garbage collection procedure in FDS.
+ */
+ PM_EVT_STORAGE_FULL,
+ /**
+ * @brief An unrecoverable error happened inside Peer Manager. An
+ * operation failed with the provided error.
+ */
+ PM_EVT_ERROR_UNEXPECTED,
+ /**
+ * @brief A piece of peer data was stored, updated, or
+ * cleared in flash storage. This event is sent for all
+ * successful changes to peer data, also those initiated
+ * internally in Peer Manager. To identify an operation,
+ * compare the store token in the event with the store
+ * token received during the initiating function call.
+ * Events from internally initiated changes might have
+ * invalid store tokens.
+ */
+ PM_EVT_PEER_DATA_UPDATE_SUCCEEDED,
+ /**
+ * @brief A piece of peer data could not be stored,
+ * updated, or cleared in flash storage. This event is sent
+ * instead of @ref PM_EVT_PEER_DATA_UPDATE_SUCCEEDED for the
+ * failed operation.
+ */
+ PM_EVT_PEER_DATA_UPDATE_FAILED,
+ /**
+ * @brief A peer was cleared from flash storage, for example
+ * because a call to @ref pm_peer_delete succeeded. This event
+ * can also be sent as part of a call to @ref pm_peers_delete
+ * or internal cleanup.
+ */
+ PM_EVT_PEER_DELETE_SUCCEEDED,
+ /**
+ * @brief A peer could not be cleared from flash storage. This
+ * event is sent instead of @ref PM_EVT_PEER_DELETE_SUCCEEDED for
+ * the failed operation.
+ */
+ PM_EVT_PEER_DELETE_FAILED,
+ /**
+ * @brief A call to @ref pm_peers_delete has completed
+ * successfully. Flash storage now contains no peer data.
+ */
+ PM_EVT_PEERS_DELETE_SUCCEEDED,
+ /**
+ * @brief A call to @ref pm_peers_delete has failed, which
+ * means that at least one of the peers could not be deleted.
+ * Other peers might have been deleted, or might still be queued
+ * to be deleted. No more @ref PM_EVT_PEERS_DELETE_SUCCEEDED or
+ * @ref PM_EVT_PEERS_DELETE_FAILED events are sent until the
+ * next time @ref pm_peers_delete is called.
+ */
+ PM_EVT_PEERS_DELETE_FAILED,
+ /**
+ * @brief Local database values for a peer (taken from
+ * flash storage) have been provided to the SoftDevice.
+ */
+ PM_EVT_LOCAL_DB_CACHE_APPLIED,
+ /**
+ * @brief Local database values for a peer (taken from
+ * flash storage) were rejected by the SoftDevice, which
+ * means that either the database has changed or the
+ * user has manually set the local database to an
+ * invalid value (using @ref pm_peer_data_store).
+ */
+ PM_EVT_LOCAL_DB_CACHE_APPLY_FAILED,
+ /**
+ * @brief A service changed indication has been sent to a
+ * peer, as a result of a call to @ref
+ * pm_local_database_has_changed. This event will be
+ * followed by a @ref PM_EVT_SERVICE_CHANGED_IND_CONFIRMED
+ * event if the peer acknowledges the indication.
+ */
+ PM_EVT_SERVICE_CHANGED_IND_SENT,
+ /**
+ * @brief A service changed indication that was sent
+ * has been confirmed by a peer. The peer can now be
+ * considered aware that the local database has
+ * changed.
+ */
+ PM_EVT_SERVICE_CHANGED_IND_CONFIRMED,
+ /** @brief The peer (peripheral) has requested link encryption, which has been enabled. */
+ PM_EVT_SLAVE_SECURITY_REQ,
+ /** @brief The flash has been garbage collected (By FDS), possibly freeing up space. */
+ PM_EVT_FLASH_GARBAGE_COLLECTED,
+ /** @brief Garbage collection was attempted but failed. */
+ PM_EVT_FLASH_GARBAGE_COLLECTION_FAILED,
+} pm_evt_id_t;
+
+/** @brief Parameters specific to the @ref PM_EVT_CONN_CONFIG_REQ event. */
+typedef struct {
+ /** @brief Connected Event parameters. */
+ ble_gap_evt_connected_t const *p_peer_params;
+ /**
+ * @brief This pointer must be provided in the reply if the reply function takes a
+ * p_context argument.
+ */
+ void const *p_context;
+} pm_conn_config_req_evt_t;
+
+/** @brief Events parameters specific to the @ref PM_EVT_CONN_SEC_START event. */
+typedef struct {
+ /** @brief The procedure that has started. */
+ pm_conn_sec_procedure_t procedure;
+} pm_conn_sec_start_evt_t;
+
+/** @brief Parameters specific to the @ref PM_EVT_CONN_SEC_SUCCEEDED event. */
+typedef struct {
+ /** @brief The procedure that led to securing the link. */
+ pm_conn_sec_procedure_t procedure;
+ /**
+ * @brief Whether bonding data was successfully requested to be stored.
+ * This is false if: No bonding happened, or an internal error occurred
+ * when trying to store the data, or if the data was rejected via @ref
+ * pm_conn_sec_config_reply.
+ */
+ bool data_stored;
+} pm_conn_secured_evt_t;
+
+/** @brief Parameters specific to the @ref PM_EVT_CONN_SEC_FAILED event. */
+typedef struct {
+ /** @brief The procedure that failed. */
+ pm_conn_sec_procedure_t procedure;
+ /** @brief An error code that describes the failure. */
+ pm_sec_error_code_t error;
+ /** @brief The party that raised the error, see @ref BLE_GAP_SEC_STATUS_SOURCES. */
+ uint8_t error_src;
+} pm_conn_secure_failed_evt_t;
+
+/** @brief Parameters specific to the @ref PM_EVT_CONN_SEC_PARAMS_REQ event. */
+typedef struct {
+ /** @brief Peer security parameters, if role is peripheral. Otherwise, this is NULL. */
+ ble_gap_sec_params_t const *p_peer_params;
+ /**
+ * @brief This pointer must be provided in the reply if the reply function takes a
+ * p_context argument.
+ */
+ void const *p_context;
+} pm_conn_sec_params_req_evt_t;
+
+/** @brief Actions that can be performed to peer data in persistent storage. */
+typedef enum {
+ /** @brief Writing or overwriting the data. */
+ PM_PEER_DATA_OP_UPDATE,
+ /** @brief Removing the data. */
+ PM_PEER_DATA_OP_DELETE,
+} pm_peer_data_op_t;
+
+/** @brief Parameters specific to the @ref PM_EVT_PEER_DATA_UPDATE_SUCCEEDED event. */
+typedef struct {
+ /** @brief The type of the data that was changed. */
+ pm_peer_data_id_t data_id;
+ /** @brief What happened to the data. */
+ pm_peer_data_op_t action;
+ /**
+ * @brief Token that identifies the operation. For @ref
+ * PM_PEER_DATA_OP_DELETE actions, this token can be disregarded. For @ref
+ * PM_PEER_DATA_OP_UPDATE actions, compare this token with the token that is
+ * received from a call to a @ref PM_PEER_DATA_FUNCTIONS function.
+ */
+ uint32_t token;
+ /**
+ * @brief If this is false, no operation was done in flash,
+ * because the value was already what it should be. Please note
+ * that in certain scenarios, this flag will be true even if the
+ * new value is the same as the old.
+ */
+ uint8_t flash_changed: 1;
+} pm_peer_data_update_succeeded_evt_t;
+
+/** @brief Parameters specific to the @ref PM_EVT_PEER_DATA_UPDATE_FAILED event. */
+typedef struct {
+ /** @brief The type of the data that was supposed to be changed. */
+ pm_peer_data_id_t data_id;
+ /** @brief The action that failed. */
+ pm_peer_data_op_t action;
+ /**
+ * @brief Token that identifies the operation. For @ref
+ * PM_PEER_DATA_OP_DELETE actions, this token can be disregarded. For @ref
+ * PM_PEER_DATA_OP_UPDATE actions, compare this token with the token that is
+ * received from a call to a @ref PM_PEER_DATA_FUNCTIONS function.
+ */
+ uint32_t token;
+ /** @brief An error code that describes the failure. */
+ uint32_t error;
+} pm_peer_data_update_failed_t;
+
+/** @brief Standard parameters for failure events. */
+typedef struct {
+ /** @brief The error that occurred. */
+ uint32_t error;
+} pm_failure_evt_t;
+
+/**
+ * @brief An event from the @ref peer_manager module.
+ *
+ * @details The structure contains both standard parameters and parameters that are specific to some
+ * events.
+ */
+typedef struct {
+ /** @brief The type of the event. */
+ pm_evt_id_t evt_id;
+ /** @brief The connection that this event pertains to, or @ref BLE_CONN_HANDLE_INVALID. */
+ uint16_t conn_handle;
+ /** @brief The bonded peer that this event pertains to, or @ref PM_PEER_ID_INVALID. */
+ uint16_t peer_id;
+ union {
+ /** @brief Parameters specific to the @ref PM_EVT_CONN_CONFIG_REQ event. */
+ pm_conn_config_req_evt_t conn_config_req;
+ /** @brief Parameters specific to the @ref PM_EVT_CONN_SEC_START event. */
+ pm_conn_sec_start_evt_t conn_sec_start;
+ /** @brief Parameters specific to the @ref PM_EVT_CONN_SEC_SUCCEEDED event. */
+ pm_conn_secured_evt_t conn_sec_succeeded;
+ /** @brief Parameters specific to the @ref PM_EVT_CONN_SEC_FAILED event. */
+ pm_conn_secure_failed_evt_t conn_sec_failed;
+ /** @brief Parameters specific to the @ref PM_EVT_CONN_SEC_PARAMS_REQ event. */
+ pm_conn_sec_params_req_evt_t conn_sec_params_req;
+ /**
+ * @brief Parameters specific to the @ref PM_EVT_PEER_DATA_UPDATE_SUCCEEDED
+ * event.
+ */
+ pm_peer_data_update_succeeded_evt_t peer_data_update_succeeded;
+ /** @brief Parameters specific to the @ref PM_EVT_PEER_DATA_UPDATE_FAILED event. */
+ pm_peer_data_update_failed_t peer_data_update_failed;
+ /** @brief Parameters specific to the @ref PM_EVT_PEER_DELETE_FAILED event. */
+ pm_failure_evt_t peer_delete_failed;
+ /** @brief Parameters specific to the @ref PM_EVT_PEERS_DELETE_FAILED event. */
+ pm_failure_evt_t peers_delete_failed_evt;
+ /** @brief Parameters specific to the @ref PM_EVT_ERROR_UNEXPECTED event. */
+ pm_failure_evt_t error_unexpected;
+#ifdef CONFIG_SOFTDEVICE_CENTRAL
+ /** @brief Parameters specific to the @ref PM_EVT_SLAVE_SECURITY_REQ event. */
+ ble_gap_evt_sec_request_t slave_security_req;
+ /**
+ * @brief Parameters specific to the @ref PM_EVT_FLASH_GARBAGE_COLLECTION_FAILED
+ * event.
+ */
+#endif
+ pm_failure_evt_t garbage_collection_failed;
+ } params;
+} pm_evt_t;
+
+/**
+ * @brief Event handler for events from the @ref peer_manager module.
+ *
+ * @sa pm_register
+ *
+ * @param[in] p_event The event that has occurred.
+ */
+typedef void (*pm_evt_handler_t)(pm_evt_t const *p_event);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PEER_MANAGER_TYPES_H__ */
+
+/** @} */
diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt
index 7cd28e8ee8..f330201142 100644
--- a/lib/CMakeLists.txt
+++ b/lib/CMakeLists.txt
@@ -8,6 +8,7 @@ add_subdirectory_ifdef(CONFIG_BLE_ADV ble_adv)
add_subdirectory_ifdef(CONFIG_BLE_CONN_PARAMS ble_conn_params)
add_subdirectory_ifdef(CONFIG_BLE_GATT_QUEUE ble_gq)
add_subdirectory_ifdef(CONFIG_BLE_RACP ble_racp)
+add_subdirectory_ifdef(CONFIG_BLE_CONN_STATE ble_conn_state)
add_subdirectory_ifdef(CONFIG_EVENT_SCHEDULER event_scheduler)
add_subdirectory_ifdef(CONFIG_BM_BUTTONS bm_buttons)
add_subdirectory_ifdef(CONFIG_BM_STORAGE bm_storage)
@@ -17,3 +18,4 @@ add_subdirectory_ifdef(CONFIG_SENSORSIM sensorsim)
add_subdirectory_ifdef(CONFIG_NCS_BARE_METAL_BOOT_BANNER boot_banner)
add_subdirectory_ifdef(CONFIG_ZEPHYR_QUEUE zephyr_queue)
add_subdirectory_ifdef(CONFIG_BM_ZMS bm_zms)
+add_subdirectory_ifdef(CONFIG_PEER_MANAGER peer_manager)
diff --git a/lib/Kconfig b/lib/Kconfig
index a1fe1029d0..10317a89f2 100644
--- a/lib/Kconfig
+++ b/lib/Kconfig
@@ -9,6 +9,7 @@ rsource "ble_adv/Kconfig"
rsource "ble_conn_params/Kconfig"
rsource "ble_gq/Kconfig"
rsource "ble_racp/Kconfig"
+rsource "ble_conn_state/Kconfig"
rsource "event_scheduler/Kconfig"
rsource "bm_buttons/Kconfig"
rsource "bm_storage/Kconfig"
@@ -18,5 +19,6 @@ rsource "sensorsim/Kconfig"
rsource "boot_banner/Kconfig"
rsource "zephyr_queue/Kconfig"
rsource "bm_zms/Kconfig"
+rsource "peer_manager/Kconfig"
endmenu
diff --git a/lib/ble_conn_state/CMakeLists.txt b/lib/ble_conn_state/CMakeLists.txt
new file mode 100644
index 0000000000..26089e31a5
--- /dev/null
+++ b/lib/ble_conn_state/CMakeLists.txt
@@ -0,0 +1,7 @@
+#
+# Copyright (c) 2025 Nordic Semiconductor ASA
+#
+# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
+#
+zephyr_library()
+zephyr_library_sources(ble_conn_state.c)
diff --git a/lib/ble_conn_state/Kconfig b/lib/ble_conn_state/Kconfig
new file mode 100644
index 0000000000..a613463689
--- /dev/null
+++ b/lib/ble_conn_state/Kconfig
@@ -0,0 +1,31 @@
+#
+# Copyright (c) 2025 Nordic Semiconductor
+#
+# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
+#
+menuconfig BLE_CONN_STATE
+ bool "Event scheduler"
+ help
+ A library to defer event processing to main().
+
+if BLE_CONN_STATE
+
+config BLE_CONN_STATE_USER_FLAG_COUNT
+ int "User flag count"
+ range 0 24
+ default 24
+ help
+ The number of available user flags.
+
+config BLE_CONN_STATE_DEFAULT_FLAG_COLLECTION_COUNT
+ int "Default flag collection count"
+ default 6
+ help
+ The number of flags kept for each connection, excluding user flags.
+
+module=BLE_CONN_STATE
+module-dep=LOG
+module-str=BLE Connection state
+source "${ZEPHYR_BASE}/subsys/logging/Kconfig.template.log_config"
+
+endif
diff --git a/lib/ble_conn_state/ble_conn_state.c b/lib/ble_conn_state/ble_conn_state.c
new file mode 100644
index 0000000000..c4acb721a7
--- /dev/null
+++ b/lib/ble_conn_state/ble_conn_state.c
@@ -0,0 +1,463 @@
+/*
+ * Copyright (c) 2015 - 2025 Nordic Semiconductor ASA
+ *
+ * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
+ */
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+LOG_MODULE_REGISTER(ble_bms, CONFIG_BLE_CONN_STATE_LOG_LEVEL);
+
+/** The number of flags kept for each connection, including user flags. */
+#define TOTAL_FLAG_COLLECTION_COUNT (CONFIG_BLE_CONN_STATE_DEFAULT_FLAG_COLLECTION_COUNT + \
+ CONFIG_BLE_CONN_STATE_USER_FLAG_COUNT)
+
+/** Connection handles in the same order as they are indexed in the atomics */
+struct ble_conn_state_conn_handle_list connections;
+
+/**
+ * @brief Structure containing all the flag collections maintained by the Connection State module.
+ */
+struct ble_conn_state_flag_collections {
+ /** Flags indicating which connection handles are valid. */
+ atomic_t valid_flags;
+ /** Flags indicating which connections are connected, since disconnected connection handles
+ * will not immediately be invalidated.
+ */
+ atomic_t connected_flags;
+ /** Flags indicating in which connections the local device is the central. */
+ atomic_t central_flags;
+ /** Flags indicating which connections are encrypted. */
+ atomic_t encrypted_flags;
+ /** Flags indicating which connections have encryption with protection from
+ * man-in-the-middle attacks.
+ */
+ atomic_t mitm_protected_flags;
+ /** Flags indicating which connections have bonded using LE Secure Connections (LESC). */
+ atomic_t lesc_flags;
+ /** Flags that can be reserved by the user. The flags will be cleared when a connection is
+ * invalidated, otherwise, the user is wholly responsible for the flag states.
+ */
+ atomic_t user_flags[CONFIG_BLE_CONN_STATE_USER_FLAG_COUNT];
+};
+
+/** @brief Structure containing the internal state of the Connection State module. */
+struct ble_conn_state {
+ /** Bitmap for keeping track of which user flags have been acquired. */
+ atomic_t acquired_flags;
+ union {
+ /** Flag collection kept by the Connections State module. */
+ struct ble_conn_state_flag_collections flags;
+ /** Flag collections as array to allow iterating over all flag collections. */
+ atomic_t flag_array[TOTAL_FLAG_COLLECTION_COUNT];
+ };
+};
+
+/** Instantiation of the internal state. */
+static struct ble_conn_state bcs = {0};
+
+static int bcs_atomic_find_and_set_flag(atomic_t *flags)
+{
+ for (int i = 0; i < (sizeof(atomic_t) * 8); i++) {
+ if (atomic_test_and_set_bit(flags, i) == false) {
+ /* Bit was not set before */
+ return i;
+ }
+ }
+
+ return -1;
+}
+
+/** @brief Function for resetting all internal memory to the values it had at initialization. */
+static inline void bcs_internal_state_reset(void)
+{
+ memset(&bcs, 0, sizeof(struct ble_conn_state));
+}
+
+static struct ble_conn_state_conn_handle_list conn_handle_list_get(atomic_t flags)
+{
+ struct ble_conn_state_conn_handle_list conn_handle_list = {
+ .len = 0
+ };
+
+ if (flags) {
+ for (uint32_t i = 0; i < BLE_CONN_STATE_MAX_CONNECTIONS; i++) {
+ if (atomic_test_bit(&flags, i)) {
+ conn_handle_list.conn_handles[conn_handle_list.len++] = i;
+ }
+ }
+ }
+
+ return conn_handle_list;
+}
+
+static uint32_t active_flag_count(atomic_t flags)
+{
+ uint32_t set_flag_count = 0;
+
+ for (uint32_t i = 0; i < BLE_CONN_STATE_MAX_CONNECTIONS; i++) {
+ if (atomic_test_bit(&flags, i)) {
+ set_flag_count += 1;
+ }
+ }
+
+ return set_flag_count;
+}
+
+/**
+ * @brief Function for activating a connection record.
+ *
+ * @param conn_handle The connection handle to copy into the record.
+ *
+ * @return whether the record was activated successfully.
+ */
+static bool record_activate(uint16_t conn_handle)
+{
+ if (conn_handle >= BLE_CONN_STATE_MAX_CONNECTIONS) {
+ return false;
+ }
+
+ atomic_set_bit(&bcs.flags.connected_flags, conn_handle);
+ atomic_set_bit(&bcs.flags.valid_flags, conn_handle);
+ return true;
+}
+
+/**
+ * @brief Function for marking a connection record as invalid and resetting the values.
+ *
+ * @param conn_handle The connection to invalidate.
+ */
+static void record_invalidate(uint16_t conn_handle)
+{
+ for (uint32_t i = 0; i < TOTAL_FLAG_COLLECTION_COUNT; i++) {
+ atomic_clear_bit(&bcs.flag_array[i], conn_handle);
+ }
+}
+
+/**
+ * @brief Function for marking a connection as disconnected. See @ref BLE_CONN_STATUS_DISCONNECTED.
+ *
+ * @param conn_handle The connection to set as disconnected.
+ */
+static void record_set_disconnected(uint16_t conn_handle)
+{
+ atomic_clear_bit(&bcs.flags.connected_flags, conn_handle);
+}
+
+/**
+ * @brief Function for invalidating records with a @ref BLE_CONN_STATUS_DISCONNECTED
+ * connection status
+ */
+static void record_purge_disconnected(void)
+{
+ atomic_t disconnected_flags = ~bcs.flags.connected_flags;
+ struct ble_conn_state_conn_handle_list disconnected_list;
+
+ (void)atomic_and(&disconnected_flags, bcs.flags.valid_flags);
+ disconnected_list = conn_handle_list_get(disconnected_flags);
+
+ for (uint32_t i = 0; i < disconnected_list.len; i++) {
+ record_invalidate(disconnected_list.conn_handles[i]);
+ }
+}
+
+/**
+ * @brief Function for checking if a user flag has been acquired.
+ *
+ * @param[in] flag_index Which flag to check.
+ *
+ * @return Whether the flag has been acquired.
+ */
+static bool user_flag_is_acquired(uint16_t flag_index)
+{
+ return atomic_test_bit(&bcs.acquired_flags, flag_index);
+}
+
+void ble_conn_state_init(void)
+{
+ bcs_internal_state_reset();
+}
+
+static void flag_toggle(atomic_t *flags, uint16_t conn_handle, bool value)
+{
+ if (value) {
+ atomic_set_bit(flags, conn_handle);
+ } else {
+ atomic_clear_bit(flags, conn_handle);
+ }
+}
+
+int conn_id_get(uint8_t conn_idx)
+{
+ return connections.conn_handles[conn_idx];
+}
+
+int conn_idx_get(uint32_t conn_id)
+{
+ for (int i = 0; i < connections.len; i++) {
+ if (connections.conn_handles[i] == conn_id) {
+ return i;
+ }
+ }
+
+ /* conn_id not found in list */
+
+ for (int i = 0; i < BLE_CONN_STATE_MAX_CONNECTIONS; i++) {
+ if (atomic_test_bit(&bcs.flags.connected_flags, i) == false) {
+ record_invalidate(i);
+ connections.conn_handles[i] = conn_id;
+ return i;
+ }
+ }
+
+ return -1;
+}
+
+/**
+ * @brief Function for handling BLE events.
+ *
+ * @param[in] ble_evt Event received from the BLE stack.
+ * @param[in] ctx Context.
+ */
+static void ble_evt_handler(ble_evt_t const *ble_evt, void *ctx)
+{
+ uint16_t conn_handle = ble_evt->evt.gap_evt.conn_handle;
+
+ switch (ble_evt->header.evt_id) {
+ case BLE_GAP_EVT_CONNECTED:
+ record_purge_disconnected();
+
+ if (!record_activate(conn_handle)) {
+ /* No more records available. Should not happen. */
+ LOG_ERR("No more records available");
+ __ASSERT(false, "No more records available");
+#ifdef CONFIG_BLE_GAP_ROLE_CENTRAL
+ } else if (ble_evt->evt.gap_evt.params.connected.role == BLE_GAP_ROLE_CENTRAL) {
+ /* Central */
+ atomic_set_bit(&bcs.flags.central_flags, conn_handle);
+#endif
+ } else {
+ /* No implementation required. */
+ }
+
+ break;
+ case BLE_GAP_EVT_DISCONNECTED:
+ record_set_disconnected(conn_handle);
+ break;
+ case BLE_GAP_EVT_CONN_SEC_UPDATE:
+ uint8_t sec_lv = ble_evt->evt.gap_evt.params.conn_sec_update.conn_sec.sec_mode.lv;
+
+ /* Set/unset flags based on security level. */
+ flag_toggle(&bcs.flags.lesc_flags, conn_handle, sec_lv >= 4);
+ flag_toggle(&bcs.flags.mitm_protected_flags, conn_handle, sec_lv >= 3);
+ flag_toggle(&bcs.flags.encrypted_flags, conn_handle, sec_lv >= 2);
+ break;
+ case BLE_GAP_EVT_AUTH_STATUS:
+ if (ble_evt->evt.gap_evt.params.auth_status.auth_status ==
+ BLE_GAP_SEC_STATUS_SUCCESS) {
+ bool lesc = ble_evt->evt.gap_evt.params.auth_status.lesc;
+
+ flag_toggle(&bcs.flags.lesc_flags, conn_handle, lesc);
+ }
+
+ break;
+ }
+}
+
+NRF_SDH_BLE_OBSERVER(ble_evt_observer, ble_evt_handler, NULL, BLE_CONN_STATE_BLE_OBSERVER_PRIO);
+
+bool ble_conn_state_valid(uint16_t conn_handle)
+{
+ if (conn_handle >= BLE_CONN_STATE_MAX_CONNECTIONS) {
+ return false;
+ }
+
+ return atomic_test_bit(&bcs.flags.valid_flags, conn_handle);
+}
+
+uint8_t ble_conn_state_role(uint16_t conn_handle)
+{
+ uint8_t role = BLE_GAP_ROLE_INVALID;
+
+ if (ble_conn_state_valid(conn_handle)) {
+#if defined(CONFIG_SOFTDEVICE_PERIPHERAL) && defined(CONFIG_SOFTDEVICE_CENTRAL)
+ bool central;
+
+ central = atomic_test_bit(&bcs.flags.central_flags, conn_handle);
+ role = central ? BLE_GAP_ROLE_CENTRAL : BLE_GAP_ROLE_PERIPH;
+#elif defined(CONFIG_SOFTDEVICE_CENTRAL)
+ role = BLE_GAP_ROLE_CENTRAL;
+#else
+ role = BLE_GAP_ROLE_PERIPH;
+#endif
+ }
+
+ return role;
+}
+
+enum ble_conn_state_status ble_conn_state_status(uint16_t conn_handle)
+{
+ bool connected;
+ enum ble_conn_state_status conn_status = BLE_CONN_STATUS_INVALID;
+
+ if (ble_conn_state_valid(conn_handle)) {
+ connected = atomic_test_bit(&bcs.flags.connected_flags, conn_handle);
+ conn_status = connected ? BLE_CONN_STATUS_CONNECTED : BLE_CONN_STATUS_DISCONNECTED;
+ }
+
+ return conn_status;
+}
+
+bool ble_conn_state_encrypted(uint16_t conn_handle)
+{
+ if (ble_conn_state_valid(conn_handle)) {
+ return atomic_test_bit(&bcs.flags.encrypted_flags, conn_handle);
+ }
+
+ return false;
+}
+
+bool ble_conn_state_mitm_protected(uint16_t conn_handle)
+{
+ if (ble_conn_state_valid(conn_handle)) {
+ return atomic_test_bit(&bcs.flags.mitm_protected_flags, conn_handle);
+ }
+
+ return false;
+}
+
+bool ble_conn_state_lesc(uint16_t conn_handle)
+{
+ if (ble_conn_state_valid(conn_handle)) {
+ return atomic_test_bit(&bcs.flags.lesc_flags, conn_handle);
+ }
+
+ return false;
+}
+
+uint32_t ble_conn_state_conn_count(void)
+{
+ return active_flag_count(bcs.flags.connected_flags);
+}
+
+uint32_t ble_conn_state_central_conn_count(void)
+{
+ atomic_t central_conn_flags;
+
+ central_conn_flags = bcs.flags.central_flags;
+ (void)atomic_and(¢ral_conn_flags, bcs.flags.connected_flags);
+
+ return active_flag_count(central_conn_flags);
+}
+
+uint32_t ble_conn_state_peripheral_conn_count(void)
+{
+ atomic_t peripheral_conn_flags;
+
+ peripheral_conn_flags = ~bcs.flags.central_flags;
+ (void)atomic_and(&peripheral_conn_flags, bcs.flags.connected_flags);
+
+ return active_flag_count(peripheral_conn_flags);
+}
+
+struct ble_conn_state_conn_handle_list ble_conn_state_conn_handles(void)
+{
+ return conn_handle_list_get(bcs.flags.valid_flags);
+}
+
+struct ble_conn_state_conn_handle_list ble_conn_state_central_handles(void)
+{
+ atomic_t central_conn_flags;
+
+ central_conn_flags = bcs.flags.central_flags;
+ (void)atomic_and(¢ral_conn_flags, bcs.flags.connected_flags);
+
+ return conn_handle_list_get(central_conn_flags);
+}
+
+struct ble_conn_state_conn_handle_list ble_conn_state_periph_handles(void)
+{
+ atomic_t peripheral_conn_flags;
+
+ peripheral_conn_flags = ~bcs.flags.central_flags;
+ (void)atomic_and(&peripheral_conn_flags, bcs.flags.connected_flags);
+
+ return conn_handle_list_get(peripheral_conn_flags);
+}
+
+uint16_t ble_conn_state_conn_idx(uint16_t conn_handle)
+{
+ if (ble_conn_state_valid(conn_handle)) {
+ return conn_handle;
+ } else {
+ return BLE_CONN_STATE_MAX_CONNECTIONS;
+ }
+}
+
+int ble_conn_state_user_flag_acquire(void)
+{
+ return bcs_atomic_find_and_set_flag(&bcs.acquired_flags);
+}
+
+bool ble_conn_state_user_flag_get(uint16_t conn_handle, uint16_t flag_index)
+{
+ if (user_flag_is_acquired(flag_index) && ble_conn_state_valid(conn_handle)) {
+ return atomic_test_bit(&bcs.flags.user_flags[flag_index], conn_handle);
+ } else {
+ return false;
+ }
+}
+
+
+void ble_conn_state_user_flag_set(uint16_t conn_handle, uint16_t flag_index, bool value)
+{
+ if (user_flag_is_acquired(flag_index) && ble_conn_state_valid(conn_handle)) {
+ flag_toggle(&bcs.flags.user_flags[flag_index], conn_handle, value);
+ }
+}
+
+static uint32_t for_each_set_flag(atomic_t flags,
+ ble_conn_state_user_function_t user_function, void *ctx)
+{
+ uint32_t call_count = 0;
+
+ if (!user_function) {
+ return 0;
+ }
+
+ if (flags) {
+ for (uint32_t i = 0; i < BLE_CONN_STATE_MAX_CONNECTIONS; i++) {
+ if (atomic_test_bit(&flags, i)) {
+ user_function(i, ctx);
+ call_count += 1;
+ }
+ }
+ }
+
+ return call_count;
+}
+
+uint32_t ble_conn_state_for_each_connected(ble_conn_state_user_function_t user_function, void *ctx)
+{
+ return for_each_set_flag(bcs.flags.connected_flags, user_function, ctx);
+}
+
+
+uint32_t ble_conn_state_for_each_set_user_flag(uint16_t flag_index,
+ ble_conn_state_user_function_t user_function,
+ void *ctx)
+{
+ if (!user_flag_is_acquired(flag_index)) {
+ return 0;
+ }
+
+ return for_each_set_flag(bcs.flags.user_flags[flag_index], user_function, ctx);
+}
diff --git a/lib/peer_manager/CMakeLists.txt b/lib/peer_manager/CMakeLists.txt
new file mode 100644
index 0000000000..4aa6973140
--- /dev/null
+++ b/lib/peer_manager/CMakeLists.txt
@@ -0,0 +1,12 @@
+#
+# Copyright (c) 2025 Nordic Semiconductor ASA
+#
+# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
+#
+
+zephyr_library()
+zephyr_library_include_directories(include)
+zephyr_library_sources(peer_manager.c)
+zephyr_library_sources(nrf_strerror.c)
+
+add_subdirectory(modules)
diff --git a/lib/peer_manager/Kconfig b/lib/peer_manager/Kconfig
new file mode 100644
index 0000000000..913b9bb10a
--- /dev/null
+++ b/lib/peer_manager/Kconfig
@@ -0,0 +1,120 @@
+#
+# Copyright (c) 2025 Nordic Semiconductor
+#
+# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
+#
+
+menuconfig PEER_MANAGER
+ bool "Peer Manager"
+ help
+ Peer Manager
+
+if PEER_MANAGER
+
+config PM_BLE_OBSERVER_PRIO
+ int "Priority with which BLE events are dispatched to the Peer Manager module."
+ default 1
+ help
+ Priority with which BLE events are dispatched to the Peer Manager module.
+
+config PM_MAX_REGISTRANTS
+ int "Number of event handlers that can be registered."
+ default 3
+ help
+ Number of event handlers that can be registered.
+
+config PM_FLASH_BUFFERS
+ int "Number of internal buffers for flash operations."
+ default 4
+ help
+ Number of internal buffers for flash operations.
+ Decrease this value to lower RAM usage.
+
+config PM_SERVICE_CHANGED_ENABLED
+ bool "Enable/disable the service changed management for GATT server in Peer Manager."
+ default y
+ help
+ Enable/disable the service changed management for GATT server in Peer Manager.
+ If not using a GATT server, or using a server without a service changed characteristic,
+ disable this to save code space.
+
+config PM_PEER_RANKS_ENABLED
+ bool "Enable/disable the peer rank management in Peer Manager."
+ default y
+ help
+ Enable/disable the peer rank management in Peer Manager.
+ Set this to false to save code space if not using the peer rank API.
+
+config PM_LESC_ENABLED
+ bool "Enable/disable LESC support in Peer Manager."
+ default n
+ depends on PSA_WANT_ALG_ECDH
+ depends on PSA_WANT_GENERATE_RANDOM
+ depends on PSA_WANT_KEY_TYPE_ECC_KEY_PAIR_GENERATE
+ depends on PSA_WANT_KEY_TYPE_ECC_KEY_PAIR_IMPORT
+ depends on PSA_WANT_KEY_TYPE_ECC_KEY_PAIR_EXPORT
+ depends on PSA_WANT_ECC_SECP_R1_256
+ depends on (MBEDTLS_PSA_STATIC_KEY_SLOTS && MBEDTLS_PSA_KEY_SLOT_COUNT >= 1) || \
+ MBEDTLS_ENABLE_HEAP
+
+ help
+ Enable/disable LESC support in Peer Manager.
+ If set to true, you need to call nrf_ble_lesc_request_handler() in the main loop to
+ respond to LESC-related BLE events. If LESC support is not required, set this to false to
+ save code space.
+
+if PM_LESC_ENABLED
+
+config PM_LESC_GENERATE_NEW_KEYS
+ bool "Generate new LESC key pair after every pairing attempt"
+ default n
+ help
+ New LESC keys are generated on the auth status event.
+
+endif # PM_LESC_ENABLED
+
+config PM_RA_PROTECTION_ENABLED
+ bool "Enable/disable protection against repeated pairing attempts in Peer Manager."
+ default n
+ help
+ Enable/disable protection against repeated pairing attempts in Peer Manager.
+
+config PM_RA_PROTECTION_TRACKED_PEERS_NUM
+ int "Maximum number of peers whose authorization status can be tracked."
+ default 8
+ help
+ Maximum number of peers whose authorization status can be tracked.
+
+config PM_RA_PROTECTION_MIN_WAIT_INTERVAL
+ int "Minimum waiting interval (in ms) before a new pairing attempt can be initiated."
+ default 4000
+ help
+ Minimum waiting interval (in ms) before a new pairing attempt can be initiated.
+
+config PM_RA_PROTECTION_MAX_WAIT_INTERVAL
+ int "Maximum waiting interval (in ms) before a new pairing attempt can be initiated."
+ default 64000
+ help
+ Maximum waiting interval (in ms) before a new pairing attempt can be initiated.
+
+config PM_RA_PROTECTION_REWARD_PERIOD
+ int "Reward period (in ms)."
+ default 10000
+ help
+ Reward period (in ms).
+ The waiting interval is gradually decreased when no new failed pairing attempts are made
+ during reward period.
+
+config PM_HANDLER_SEC_DELAY_MS
+ int "Delay before starting security."
+ default 0
+ help
+ Delay before starting security.
+ This might be necessary for interoperability reasons, especially as peripheral.
+
+module=PEER_MANAGER
+module-dep=LOG
+module-str=Peer Manager
+source "${ZEPHYR_BASE}/subsys/logging/Kconfig.template.log_config"
+
+endif # PEER_MANAGER
diff --git a/lib/peer_manager/include/modules/auth_status_tracker.h b/lib/peer_manager/include/modules/auth_status_tracker.h
new file mode 100644
index 0000000000..30072e8dfe
--- /dev/null
+++ b/lib/peer_manager/include/modules/auth_status_tracker.h
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2018-2025 Nordic Semiconductor ASA
+ *
+ * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
+ */
+
+/**
+ * @defgroup auth_status_tracker Authorization Status Tracker
+ * @ingroup peer_manager
+ * @{
+ * @brief An internal module of @ref peer_manager. A module for tracking peers with failed
+ * authorization attempts. It uses tracking policy, which is described in Bluetooth
+ * Core Specification v5.0, Vol 3, Part H, Section 2.3.6.
+ */
+
+#ifndef AUTH_STATUS_TRACKER_H__
+#define AUTH_STATUS_TRACKER_H__
+
+#include
+#include
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief Function for initializing the Authorization Status Tracker module.
+ *
+ * @retval NRF_SUCCESS Initialization was successful.
+ * @retval Other Other error codes might be returned by the @ref app_timer_create function.
+ */
+uint32_t ast_init(void);
+
+/**
+ * @brief Function for notifying about failed authorization attempts.
+ *
+ * @param[in] conn_handle Connection handle on which authorization attempt has failed.
+ */
+void ast_auth_error_notify(uint16_t conn_handle);
+
+/**
+ * @brief Function for checking if pairing request must be rejected.
+ *
+ * @param[in] conn_handle Connection handle on which this check must be performed.
+ *
+ * @retval true If the connected peer is blacklisted.
+ * @retval false If the connected peer is not blacklisted.
+ */
+bool ast_peer_blacklisted(uint16_t conn_handle);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* AUTH_STATUS_TRACKER_H__ */
+
+/** @} */
diff --git a/lib/peer_manager/include/modules/gatt_cache_manager.h b/lib/peer_manager/include/modules/gatt_cache_manager.h
new file mode 100644
index 0000000000..a2964d3036
--- /dev/null
+++ b/lib/peer_manager/include/modules/gatt_cache_manager.h
@@ -0,0 +1,73 @@
+/**
+ * Copyright (c) 2015-2025 Nordic Semiconductor ASA
+ *
+ * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
+ */
+
+/**
+ * @defgroup gatt_cache_manager GATT Cache Manager
+ * @ingroup peer_manager
+ * @{
+ * @brief An internal module of @ref peer_manager. A module for managing persistent storing of GATT
+ * attributes.
+ */
+
+#ifndef GATT_CACHE_MANAGER_H__
+#define GATT_CACHE_MANAGER_H__
+
+#include
+#include
+#include
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief Function for initializing the GATT Cache Manager module.
+ *
+ * @retval NRF_SUCCESS Initialization was successful.
+ * @retval NRF_ERROR_INTERNAL If an internal error occurred.
+ */
+uint32_t gcm_init(void);
+
+/**
+ * @brief Function for dispatching SoftDevice events to the GATT Cache Manager module.
+ *
+ * @param[in] p_ble_evt The SoftDevice event.
+ */
+void gcm_ble_evt_handler(ble_evt_t const *p_ble_evt);
+
+/**
+ * @brief Function for triggering local GATT database data to be stored persistently.
+ *
+ * @details Values are retrieved from SoftDevice and written to persistent storage.
+ *
+ * @note This operation happens asynchronously, so any errors are reported as events.
+ *
+ * @note This function is only needed when you want to override the regular functionality of the
+ * module, e.g. to immediately store to flash instead of waiting for the native logic to
+ * perform the update.
+ *
+ * @param[in] conn_handle Connection handle to perform update on.
+ *
+ * @retval NRF_SUCCESS Store operation started.
+ */
+uint32_t gcm_local_db_cache_update(uint16_t conn_handle);
+
+/**
+ * @brief Function for manually informing that the local database has changed.
+ *
+ * @details This causes a service changed notification to be sent to all bonded peers that
+ * subscribe to it.
+ */
+void gcm_local_database_has_changed(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* GATT_CACHE_MANAGER_H__ */
+
+/** @} */
diff --git a/lib/peer_manager/include/modules/gatts_cache_manager.h b/lib/peer_manager/include/modules/gatts_cache_manager.h
new file mode 100644
index 0000000000..891d54c0c3
--- /dev/null
+++ b/lib/peer_manager/include/modules/gatts_cache_manager.h
@@ -0,0 +1,126 @@
+/*
+ * Copyright (c) 2015-2025 Nordic Semiconductor ASA
+ *
+ * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
+ */
+
+/**
+ * @defgroup gatts_cache_manager GATT Server Cache Manager
+ * @ingroup peer_manager
+ * @{
+ * @brief An internal module of @ref peer_manager. A module for managing persistent storing of GATT
+ * attributes pertaining to the GATT server role of the local device.
+ */
+
+#ifndef GATTS_CACHE_MANAGER_H__
+#define GATTS_CACHE_MANAGER_H__
+
+#include
+#include
+#include
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief Function for initializing the GATT Server Cache Manager module.
+ *
+ * @retval NRF_SUCCESS Initialization was successful.
+ * @retval NRF_ERROR_INTERNAL If an internal error occurred.
+ */
+uint32_t gscm_init(void);
+
+/**
+ * @brief Function for triggering local GATT database data to be stored persistently. Values are
+ * retrieved from the SoftDevice and written to persistent storage.
+ *
+ * @param[in] conn_handle Connection handle to perform update on.
+ *
+ * @retval NRF_SUCCESS Store operation started.
+ * @retval BLE_ERROR_INVALID_CONN_HANDLE conn_handle does not refer to an active connection with a
+ * bonded peer.
+ * @retval NRF_ERROR_INVALID_DATA Refused to update because the GATT database is already up
+ * to date.
+ * @retval NRF_ERROR_BUSY Unable to perform operation at this time. Reattempt later.
+ * @retval NRF_ERROR_DATA_SIZE Write buffer not large enough. Call will never work with
+ * this GATT database.
+ * @retval NRF_ERROR_RESOURCES No room in persistent_storage. Free up space; the
+ * operation will be automatically reattempted after the
+ * next FDS garbage collection procedure.
+ */
+uint32_t gscm_local_db_cache_update(uint16_t conn_handle);
+
+/**
+ * @brief Function for applying stored local GATT database data to the SoftDevice. Values are
+ * retrieved from persistent storage and given to the SoftDevice.
+ *
+ * @param[in] conn_handle Connection handle to apply values to.
+ *
+ * @retval NRF_SUCCESS Store operation started.
+ * @retval BLE_ERROR_INVALID_CONN_HANDLE conn_handle does not refer to an active connection with a
+ * bonded peer.
+ * @retval NRF_ERROR_INVALID_DATA The stored data was rejected by the SoftDevice, which
+ * probably means that the local database has changed. The
+ * system part of the sys_attributes was attempted applied,
+ * so service changed indications can be sent to subscribers.
+ * @retval NRF_ERROR_BUSY Unable to perform operation at this time. Reattempt later.
+ * @retval NRF_ERROR_INTERNAL An unexpected error happened.
+ */
+uint32_t gscm_local_db_cache_apply(uint16_t conn_handle);
+
+/**
+ * @brief Function for storing the fact that the local database has changed, for all currently
+ * bonded peers.
+ *
+ * @note This will cause a later call to @ref gscm_service_changed_ind_needed to return true for
+ * a connection with a currently bonded peer.
+ */
+void gscm_local_database_has_changed(void);
+
+/**
+ * @brief Function for checking if a service changed indication should be sent.
+ *
+ * @param[in] conn_handle The connection to check.
+ *
+ * @return true if a service changed indication should be sent, false if not.
+ */
+bool gscm_service_changed_ind_needed(uint16_t conn_handle);
+
+/**
+ * @brief Function for sending a service changed indication to a connected peer.
+ *
+ * @param[in] conn_handle The connection to send the indication on.
+ *
+ * @retval NRF_SUCCESS Indication sent or not needed.
+ * @retval BLE_ERROR_INVALID_CONN_HANDLE conn_handle does not refer to an active connection.
+ * @retval NRF_ERROR_BUSY Unable to send indication at this time. Reattempt
+ * later.
+ * @retval BLE_ERROR_GATTS_SYS_ATTR_MISSING Information missing. Apply local cache, then reattempt.
+ * @retval NRF_ERROR_INVALID_PARAM From @ref sd_ble_gatts_service_changed. Unexpected.
+ * @retval NRF_ERROR_NOT_SUPPORTED Service changed characteristic is not present.
+ * @retval NRF_ERROR_INVALID_STATE Service changed cannot be indicated to this peer
+ * because the peer has not subscribed to it.
+ * @retval NRF_ERROR_INTERNAL An unexpected error happened.
+ */
+uint32_t gscm_service_changed_ind_send(uint16_t conn_handle);
+
+/**
+ * @brief Function for specifying that a peer has been made aware of the latest local database
+ * change.
+ *
+ * @note After calling this, a later call to @ref gscm_service_changed_ind_needed will to return
+ * false for this peer unless @ref gscm_local_database_has_changed is called again.
+ *
+ * @param[in] peer_id The connection to send the indication on.
+ */
+void gscm_db_change_notification_done(pm_peer_id_t peer_id);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* GATTS_CACHE_MANAGER_H__ */
+
+/** @} */
diff --git a/lib/peer_manager/include/modules/id_manager.h b/lib/peer_manager/include/modules/id_manager.h
new file mode 100644
index 0000000000..3b52030d26
--- /dev/null
+++ b/lib/peer_manager/include/modules/id_manager.h
@@ -0,0 +1,272 @@
+/*
+ * Copyright (c) 2015-2025 Nordic Semiconductor ASA
+ *
+ * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
+ */
+
+/**
+ * @defgroup id_manager ID Manager
+ * @ingroup peer_manager
+ * @{
+ * @brief An internal module of @ref peer_manager. A module for keeping track of peer identities
+ * (IRK and peer address).
+ */
+
+#ifndef PEER_ID_MANAGER_H__
+#define PEER_ID_MANAGER_H__
+
+#include
+#include
+#include
+#include
+#include "peer_manager_internal.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief Function for dispatching SoftDevice events to the ID Manager module.
+ *
+ * @param[in] p_ble_evt The SoftDevice event.
+ */
+void im_ble_evt_handler(ble_evt_t const *p_ble_evt);
+
+/**
+ * @brief Function for getting the corresponding peer ID from a connection handle.
+ *
+ * @param[in] conn_handle The connection handle.
+ *
+ * @return The corresponding peer ID, or @ref PM_PEER_ID_INVALID if none could be resolved.
+ */
+pm_peer_id_t im_peer_id_get_by_conn_handle(uint16_t conn_handle);
+
+/**
+ * @brief Function for getting the corresponding peer ID from a master ID (EDIV and rand).
+ *
+ * @param[in] p_master_id The master ID.
+ *
+ * @return The corresponding peer ID, or @ref PM_PEER_ID_INVALID if none could be resolved.
+ */
+pm_peer_id_t im_peer_id_get_by_master_id(ble_gap_master_id_t const *p_master_id);
+
+/**
+ * @brief Function for getting the corresponding connection handle from a peer ID.
+ *
+ * @param[in] peer_id The peer ID.
+ *
+ * @return The corresponding connection handle, or @ref BLE_CONN_HANDLE_INVALID if none could be
+ * resolved. The conn_handle can refer to a recently disconnected connection.
+ */
+uint16_t im_conn_handle_get(pm_peer_id_t peer_id);
+
+/**
+ * @brief Function for comparing two master ids
+ * @note Two invalid master IDs will not match.
+ *
+ * @param[in] p_master_id1 First master id for comparison
+ * @param[in] p_master_id2 Second master id for comparison
+ *
+ * @return True if the input matches, false if it does not.
+ */
+bool im_master_ids_compare(ble_gap_master_id_t const *p_master_id1,
+ ble_gap_master_id_t const *p_master_id2);
+
+/**
+ * @brief Function for getting the BLE address used by the peer when connecting.
+ *
+ * @param[in] conn_handle The connection handle.
+ * @param[out] p_ble_addr The BLE address used by the peer when the connection specified by
+ * conn_handle was established. Cannot be NULL.
+ *
+ * @retval NRF_SUCCESS The address was found and copied.
+ * @retval BLE_ERROR_INVALID_CONN_HANDLE conn_handle does not refer to an active connection.
+ */
+uint32_t im_ble_addr_get(uint16_t conn_handle, ble_gap_addr_t *p_ble_addr);
+
+/**
+ * @brief Function for checking if a master ID is valid or invalid
+ *
+ * @param[in] p_master_id The master ID.
+ *
+ * @retval true The master id is valid.
+ * @retval false The master id is invalid (i.e. all zeros).
+ */
+bool im_master_id_is_valid(ble_gap_master_id_t const *p_master_id);
+
+/**
+ * @brief Function for checking if two pieces of bonding data correspond to the same peer.
+ *
+ * @param[in] p_bonding_data1 The first piece of bonding data to check.
+ * @param[in] p_bonding_data2 The second piece of bonding data to check.
+ *
+ * @retval true The bonding data correspond to the same peer.
+ * @retval false The bonding data do not correspond to the same peer.
+ */
+bool im_is_duplicate_bonding_data(pm_peer_data_bonding_t const *p_bonding_data1,
+ pm_peer_data_bonding_t const *p_bonding_data2);
+
+/**
+ * @brief Function for finding if we are already bonded to a peer.
+ *
+ * @param[in] p_bonding_data The bonding data to check.
+ * @param[in] peer_id_skip Optional peer to ignore when searching for duplicates.
+ *
+ * @return An existing peer ID for the peer, or PM_PEER_ID_INVALID if none was found.
+ */
+pm_peer_id_t im_find_duplicate_bonding_data(pm_peer_data_bonding_t const *p_bonding_data,
+ pm_peer_id_t peer_id_skip);
+
+/**
+ * @brief Function for reporting that a new peer ID has been allocated for a specified connection.
+ *
+ * @param[in] conn_handle The connection.
+ * @param[in] peer_id The new peer ID.
+ */
+void im_new_peer_id(uint16_t conn_handle, pm_peer_id_t peer_id);
+
+/**
+ * @brief Function for deleting all of a peer's data from flash and disassociating it from any
+ * connection handles it is associated with.
+ *
+ * @param[in] peer_id The peer to free.
+ *
+ * @return Any error code returned by @ref pdb_peer_free.
+ */
+uint32_t im_peer_free(pm_peer_id_t peer_id);
+
+/**
+ * @brief Function to set the local Bluetooth identity address.
+ *
+ * @details The local Bluetooth identity address is the address that identifies this device to other
+ * peers. The address type must be either @ref BLE_GAP_ADDR_TYPE_PUBLIC or @ref
+ * BLE_GAP_ADDR_TYPE_RANDOM_STATIC. The identity address cannot be changed while roles are
+ * running.
+ *
+ * @note This address will be distributed to the peer during bonding.
+ * If the address changes, the address stored in the peer device will not be valid and the
+ * ability to reconnect using the old address will be lost.
+ *
+ * @note By default the SoftDevice will set an address of type @ref BLE_GAP_ADDR_TYPE_RANDOM_STATIC
+ * upon being enabled. The address is a random number populated during the IC manufacturing
+ * process and remains unchanged for the lifetime of each IC.
+ *
+ * @param[in] p_addr Pointer to address structure.
+ *
+ * @retval NRF_SUCCESS Address successfully set.
+ * @retval BLE_ERROR_GAP_INVALID_BLE_ADDR If the GAP address is invalid.
+ * @retval NRF_ERROR_BUSY Could not process at this time. Process SoftDevice events
+ * and retry.
+ * @retval NRF_ERROR_INVALID_STATE The identity address cannot be changed while advertising,
+ * scanning, or while in a connection.
+ * @retval NRF_ERROR_INTERNAL If an internal error occurred.
+ */
+uint32_t im_id_addr_set(ble_gap_addr_t const *p_addr);
+
+/**
+ * @brief Function to get the local Bluetooth identity address.
+ *
+ * @note This will always return the identity address irrespective of the privacy settings,
+ * i.e. the address type will always be either @ref BLE_GAP_ADDR_TYPE_PUBLIC or @ref
+ * BLE_GAP_ADDR_TYPE_RANDOM_STATIC.
+ *
+ * @param[out] p_addr Pointer to address structure to be filled in.
+ *
+ * @retval NRF_SUCCESS If the address was successfully retrieved.
+ */
+uint32_t im_id_addr_get(ble_gap_addr_t *p_addr);
+
+/**
+ * @brief Function to set privacy settings.
+ *
+ * @details Privacy settings cannot be set while advertising, scanning, or while in a connection.
+ *
+ * @param[in] p_privacy_params Privacy settings.
+ *
+ * @retval NRF_SUCCESS If privacy options were set successfully.
+ * @retval NRF_ERROR_NULL If @p p_privacy_params is NULL.
+ * @retval NRF_ERROR_INVALID_PARAM If the address type is not valid.
+ * @retval NRF_ERROR_BUSY If the request could not be processed at this time.
+ * Process SoftDevice events and retry.
+ * @retval NRF_ERROR_INVALID_STATE Privacy settings cannot be changed while BLE roles using
+ * privacy are enabled.
+ */
+uint32_t im_privacy_set(pm_privacy_params_t const *p_privacy_params);
+
+/**
+ * @brief Function to retrieve the current privacy settings.
+ *
+ * @details The privacy settings returned include the current device irk as well.
+ *
+ * @param[in] p_privacy_params Privacy settings.
+ *
+ * @retval NRF_SUCCESS Successfully retrieved privacy settings.
+ * @retval NRF_ERROR_NULL @c p_privacy_params is NULL.
+ * @retval NRF_ERROR_INTERNAL If an internal error occurred.
+ */
+uint32_t im_privacy_get(pm_privacy_params_t *p_privacy_params);
+
+/**
+ * @brief Function for resolving a resolvable address with an identity resolution key (IRK).
+ *
+ * @details This function will use the ECB peripheral to resolve a resolvable address.
+ * This can be used to resolve the identity of a device distributing a random
+ * resolvable address based on any IRKs you have received earlier. If an address is
+ * resolved by an IRK, the device distributing the address must also know the IRK.
+ *
+ * @param[in] p_addr A random resolvable address.
+ * @param[in] p_irk An identity resolution key (IRK).
+ *
+ * @retval true The irk used matched the one used to create the address.
+ * @retval false The irk used did not match the one used to create the address, or an argument was
+ * NULL.
+ */
+bool im_address_resolve(ble_gap_addr_t const *p_addr, ble_gap_irk_t const *p_irk);
+
+/**
+ * @brief Function for setting / clearing the whitelist.
+ *
+ * @param p_peers The peers to whitelist. Pass NULL to clear the whitelist.
+ * @param peer_cnt The number of peers to whitelist. Pass zero to clear the whitelist.
+ *
+ * @retval NRF_SUCCESS If the whitelist was successfully set or cleared.
+ * @retval BLE_GAP_ERROR_WHITELIST_IN_USE If a whitelist is in use.
+ * @retval BLE_ERROR_GAP_INVALID_BLE_ADDR If any peer has an address which can not be used
+ * for whitelisting.
+ * @retval NRF_ERROR_NOT_FOUND If any peer or its data could not be found.
+ * @retval NRF_ERROR_DATA_SIZE If @p peer_cnt is greater than
+ * @ref BLE_GAP_WHITELIST_ADDR_MAX_COUNT.
+ */
+uint32_t im_whitelist_set(pm_peer_id_t const *p_peers, uint32_t const peer_cnt);
+
+/**
+ * @brief Retrieves the current whitelist, set by a previous call to @ref im_whitelist_set.
+ *
+ * @param[out] A buffer where to copy the GAP addresses.
+ * @param[inout] In: the size of the @p p_addrs buffer.
+ * Out: the number of address copied into the buffer.
+ * @param[out] A buffer where to copy the IRKs.
+ * @param[inout] In: the size of the @p p_irks buffer.
+ * Out: the number of IRKs copied into the buffer.
+ *
+ * @retval NRF_SUCCESS If the whitelist was successfully retrieved.
+ * @retval BLE_ERROR_GAP_INVALID_BLE_ADDR If any peer has an address which can not be used for
+ * whitelisting.
+ * @retval NRF_ERROR_NOT_FOUND If the data for any of the cached whitelisted peers
+ * can not be found anymore. It might have been deleted in
+ * the meanwhile.
+ * @retval NRF_ERROR_NO_MEM If the provided buffers are too small.
+ */
+uint32_t im_whitelist_get(ble_gap_addr_t *p_addrs, uint32_t *p_addr_cnt, ble_gap_irk_t *p_irks,
+ uint32_t *p_irk_cnt);
+
+/** @brief Set the device identities list. */
+uint32_t im_device_identities_list_set(pm_peer_id_t const *p_peers, uint32_t peer_cnt);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PEER_ID_MANAGER_H__ */
+
+/** @} */
diff --git a/lib/peer_manager/include/modules/nrf_ble_lesc.h b/lib/peer_manager/include/modules/nrf_ble_lesc.h
new file mode 100644
index 0000000000..a9da213fa2
--- /dev/null
+++ b/lib/peer_manager/include/modules/nrf_ble_lesc.h
@@ -0,0 +1,128 @@
+/*
+ * Copyright (c) 2018-2025 Nordic Semiconductor ASA
+ *
+ * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
+ */
+
+/** @file
+ *
+ * @defgroup nrf_ble_lesc LESC module
+ * @{
+ * @ingroup peer_manager
+ * @brief Module for handling LESC related events.
+ */
+
+#ifndef NRF_BLE_LESC_H__
+#define NRF_BLE_LESC_H__
+
+#include
+#include
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** @brief Peer OOB Data handler prototype. */
+typedef ble_gap_lesc_oob_data_t *(*nrf_ble_lesc_peer_oob_data_handler)(uint16_t conn_handle);
+
+/**
+ * @brief Function for initializing the LESC module.
+ *
+ * @details This function initializes the PSA crypto for ECC and ECDH calculations, which are
+ * required to handle LESC authentication procedures.
+ *
+ * @retval NRF_SUCCESS If the operation was successful.
+ * @retval NRF_ERROR_INTERNAL If @ref psa_crypto_init failed.
+ * @returns Other error codes might be returned by the @ref nrf_ble_lesc_keypair_generate function.
+ */
+uint32_t nrf_ble_lesc_init(void);
+
+/**
+ * @brief Function for generating ECC keypair used for the LESC procedure.
+ *
+ * @details This function generates an ECC key pair, which consists of a private and public key.
+ * Keys are generated using ECC and are used to create LESC DH key during authentication procedures.
+ *
+ * @retval NRF_SUCCESS If the operation was successful.
+ * @retval NRF_ERROR_BUSY If any pending request needs to be processed by @ref
+ * nrf_ble_lesc_request_handler.
+ * @retval NRF_ERROR_INTERNAL If @ref psa_generate_key, or @ref psa_export_public_key failed.
+ * @returns Other error codes might be returned.
+ */
+uint32_t nrf_ble_lesc_keypair_generate(void);
+
+/**
+ * @brief Function for generating LESC OOB data.
+ *
+ * @details This function generates LESC OOB data, which can be transmitted Out-Of-Band to the peer
+ * device and used during LESC procedure. It is required to generate ECC keypair with @ref
+ * nrf_ble_lesc_keypair_generate before calling this function.
+ *
+ * @retval NRF_SUCCESS If the operation was successful.
+ * @retval NRF_ERROR_INVALID_STATE If the ECC keypair hasn't been generated or is currently
+ * being generated.
+ */
+uint32_t nrf_ble_lesc_own_oob_data_generate(void);
+
+/**
+ * @brief Function for accessing the ECC public key used for LESC DH key generation.
+ *
+ * @details This function can be used to access the ECC public key, which is required to generate a
+ * LESC DH key at the peer side.
+ *
+ * @return Pointer to the generated public key or NULL if the key has not been generated yet.
+ */
+ble_gap_lesc_p256_pk_t *nrf_ble_lesc_public_key_get(void);
+
+/**
+ * @brief Function for accessing LESC OOB data.
+ *
+ * @details This function can be used to access LESC OOB data that is associated with this device.
+ * It is required to regenerate LESC OOB data with @ref nrf_ble_lesc_own_oob_data_generate,
+ * after each change of ECC keypair with @ref nrf_ble_lesc_keypair_generate.
+ *
+ * @return Pointer to the LESC OOB data or NULL if the data has not been generated yet or is no
+ * no longer valid.
+ */
+ble_gap_lesc_oob_data_t *nrf_ble_lesc_own_oob_data_get(void);
+
+/**
+ * @brief Function for setting the handler used to retrieve peer OOB data.
+ *
+ * @param[in] handler Function to retrieve peer OOB data.
+ */
+void nrf_ble_lesc_peer_oob_data_handler_set(nrf_ble_lesc_peer_oob_data_handler handler);
+
+/**
+ * @brief Function for responding to a DH key requests.
+ *
+ * @details This function calculates DH keys and supplies them to the SoftDevice if there are any
+ * pending requests for keys.
+ *
+ * @note This function should be called systematically (e.g. in the main application loop) to handle
+ * any pending DH key requests.
+ *
+ * @retval NRF_SUCCESS If the operation was successful.
+ * @retval NRF_ERROR_INTERNAL If the LESC module encountered an internal error. The only way to
+ * recover from this type of error is to reset the application.
+ * @returns Other error codes might be returned by the @ref sd_ble_gap_lesc_dhkey_reply function.
+ */
+uint32_t nrf_ble_lesc_request_handler(void);
+
+/**
+ * @brief Function for handling BLE stack events.
+ *
+ * @details This function handles events from the BLE stack that are of interest to the module.
+ *
+ * @param[in] p_ble_evt Event received from the BLE stack.
+ */
+void nrf_ble_lesc_on_ble_evt(ble_evt_t const *p_ble_evt);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* NRF_BLE_LESC_H__ */
+
+/** @} */
diff --git a/lib/peer_manager/include/modules/peer_data_storage.h b/lib/peer_manager/include/modules/peer_data_storage.h
new file mode 100644
index 0000000000..e56095bb7e
--- /dev/null
+++ b/lib/peer_manager/include/modules/peer_data_storage.h
@@ -0,0 +1,215 @@
+/*
+ * Copyright (c) 2015-2025 Nordic Semiconductor ASA
+ *
+ * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
+ */
+
+/**
+ * @defgroup peer_data_storage Peer Data Storage
+ * @ingroup peer_manager
+ * @{
+ * @brief An internal module of @ref peer_manager. This module provides a Peer Manager-specific API
+ * to the persistent storage.
+ *
+ * @details This module uses Flash Data Storage (FDS) to interface with persistent storage.
+ */
+
+#ifndef PEER_DATA_STORAGE_H__
+#define PEER_DATA_STORAGE_H__
+
+#include
+#include
+#include
+#include "peer_manager_internal.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** @brief The beginning of the range of file IDs reserved for Peer Manager. */
+#define PDS_FIRST_RESERVED_FILE_ID (0xC000)
+/** @brief The end of the range of file IDs reserved for Peer Manager. */
+#define PDS_LAST_RESERVED_FILE_ID (0xFFFE)
+/** @brief The beginning of the range of record keys reserved for Peer Manager. */
+#define PDS_FIRST_RESERVED_RECORD_KEY (0xC000)
+/** @brief The end of the range of record keys reserved for Peer Manager. */
+#define PDS_LAST_RESERVED_RECORD_KEY (0xFFFE)
+
+/** @brief Macro for converting a @ref pm_peer_id_t to an FDS file ID. */
+#define PEER_ID_TO_FILE_ID (PDS_FIRST_RESERVED_FILE_ID)
+/** @brief Macro for converting an FDS file ID to a @ref pm_peer_id_t. */
+#define FILE_ID_TO_PEER_ID (-PDS_FIRST_RESERVED_FILE_ID)
+/** @brief Macro for converting a @ref pm_peer_data_id_t to an FDS record ID. */
+#define DATA_ID_TO_RECORD_KEY (PDS_FIRST_RESERVED_RECORD_KEY)
+/** @brief Macro for converting an FDS record ID to a @ref pm_peer_data_id_t. */
+#define RECORD_KEY_TO_DATA_ID (-PDS_FIRST_RESERVED_RECORD_KEY)
+
+/**
+ * @brief Function for initializing the module.
+ *
+ * @retval NRF_SUCCESS If initialization was successful.
+ * @retval NRF_ERROR_RESOURCES If no flash pages were available for use.
+ * @retval NRF_ERROR_INTERNAL If the module couldn't register with the flash filesystem.
+ */
+uint32_t pds_init(void);
+
+/**
+ * @brief Function for reading peer data in flash.
+ *
+ * @param[in] peer_id The peer the data belongs to.
+ * @param[in] data_id The data to retrieve.
+ * @param[out] p_data The peer data. May not be @c NULL. p_data.length_words and p_data.data_id
+ * are ignored. p_data.p_all_data is ignored if @p p_buf_len is @c NULL.
+ * @param[in] p_buf_len Length of the provided buffer, in bytes. Pass @c NULL to only copy
+ * a pointer to the data in flash.
+ *
+ * @retval NRF_SUCCESS If the operation was successful.
+ * @retval NRF_ERROR_INVALID_PARAM If @p peer_id or @p data_id are invalid.
+ * @retval NRF_ERROR_NOT_FOUND If the data was not found in flash.
+ * @retval NRF_ERROR_DATA_SIZE If the provided buffer is too small. The data is still copied,
+ * filling the provided buffer.
+ */
+uint32_t pds_peer_data_read(pm_peer_id_t peer_id, pm_peer_data_id_t data_id,
+ pm_peer_data_t *const p_data, uint32_t const *const p_buf_len);
+
+/**
+ * @brief Function to prepare iterating over peer data in flash using @ref pds_peer_data_iterate.
+ * Call this function once each time before iterating using @ref pds_peer_data_iterate.
+ */
+void pds_peer_data_iterate_prepare(void);
+
+/**
+ * @brief Function for iterating peers' data in flash.
+ * Always call @ref pds_peer_data_iterate_prepare before starting iterating.
+ *
+ * @param[in] data_id The peer data to iterate over.
+ * @param[out] p_peer_id The peer the data belongs to.
+ * @param[out] p_data The peer data in flash.
+ *
+ * @retval true If the operation was successful.
+ * @retval false If the data was not found in flash, or another error occurred.
+ */
+bool pds_peer_data_iterate(pm_peer_data_id_t data_id, pm_peer_id_t *const p_peer_id,
+ pm_peer_data_flash_t *const p_data);
+
+/**
+ * @brief Function for storing peer data in flash. If the same piece of data already exists for the
+ * given peer, it will be updated. This operation is asynchronous.
+ * Expect a @ref PM_EVT_PEER_DATA_UPDATE_SUCCEEDED or @ref PM_EVT_PEER_DATA_UPDATE_FAILED
+ * event.
+ *
+ * @param[in] peer_id The peer the data belongs to.
+ * @param[in] p_peer_data The peer data. May not be @c NULL.
+ * @param[out] p_store_token A token identifying this particular store operation. The token can be
+ * used to identify events pertaining to this operation. Pass @p NULL
+ * if not used.
+ *
+ * @retval NRF_SUCCESS If the operation was initiated successfully.
+ * @retval NRF_ERROR_INVALID_PARAM If @p peer_id or the data ID in @p_peer_data are invalid.
+ * @retval NRF_ERROR_INVALID_ADDR If @p p_peer_data is not word-aligned.
+ * @retval NRF_ERROR_RESOURCES If no space is available in flash.
+ * @retval NRF_ERROR_BUSY If the flash filesystem was busy.
+ * @retval NRF_ERROR_INTERNAL If an unexpected error occurred.
+ */
+uint32_t pds_peer_data_store(pm_peer_id_t peer_id, pm_peer_data_const_t const *p_peer_data,
+ pm_store_token_t *p_store_token);
+
+/**
+ * @brief Function for deleting peer data in flash. This operation is asynchronous.
+ * Expect a @ref PM_EVT_PEER_DATA_UPDATE_SUCCEEDED or @ref PM_EVT_PEER_DATA_UPDATE_FAILED
+ * event.
+ *
+ * @param[in] peer_id The peer the data belongs to
+ * @param[in] data_id The data to delete.
+ *
+ * @retval NRF_SUCCESS If the operation was initiated successfully.
+ * @retval NRF_ERROR_INVALID_PARAM If @p peer_id or @p data_id are invalid.
+ * @retval NRF_ERROR_NOT_FOUND If data was not found in flash.
+ * @retval NRF_ERROR_BUSY If the flash filesystem was busy.
+ * @retval NRF_ERROR_INTERNAL If an unexpected error occurred.
+ */
+uint32_t pds_peer_data_delete(pm_peer_id_t peer_id, pm_peer_data_id_t data_id);
+
+/**
+ * @brief Function for claiming an unused peer ID.
+ *
+ * @retval PM_PEER_ID_INVALID If no peer ID was available.
+ */
+pm_peer_id_t pds_peer_id_allocate(void);
+
+/**
+ * @brief Function for freeing a peer ID and deleting all data associated with it in flash.
+ *
+ * @param[in] peer_id The ID of the peer to free.
+ *
+ * @retval NRF_SUCCESS The operation was initiated successfully.
+ * @retval NRF_ERROR_INVALID_PARAM If @p peer_id is invalid.
+ */
+uint32_t pds_peer_id_free(pm_peer_id_t peer_id);
+
+/**
+ * @brief Function for finding out whether a peer ID is in use.
+ *
+ * @param[in] peer_id The peer ID to inquire about.
+ *
+ * @retval true @p peer_id is in use.
+ * @retval false @p peer_id is free.
+ */
+bool pds_peer_id_is_allocated(pm_peer_id_t peer_id);
+
+/**
+ * @brief Function for finding out whether a peer ID is marked for deletion.
+ *
+ * @param[in] peer_id The peer ID to inquire about.
+ *
+ * @retval true @p peer_id is marked for deletion.
+ * @retval false @p peer_id is not marked for deletion.
+ */
+bool pds_peer_id_is_deleted(pm_peer_id_t peer_id);
+
+/**
+ * @brief Function for getting the next peer ID in the sequence of all used peer IDs. Can be
+ * used to loop through all used peer IDs.
+ *
+ * @note @ref PM_PEER_ID_INVALID is considered to be before the first and after the last ordinary
+ * peer ID.
+ *
+ * @param[in] prev_peer_id The previous peer ID.
+ *
+ * @return The first ordinary peer ID If @p prev_peer_id is @ref PM_PEER_ID_INVALID.
+ * @retval PM_PEER_ID_INVALID If @p prev_peer_id is the last ordinary peer ID or the
+ * module is not initialized.
+ */
+pm_peer_id_t pds_next_peer_id_get(pm_peer_id_t prev_peer_id);
+
+/**
+ * @brief Function for getting the next peer ID in the sequence of all peer IDs pending deletion.
+ * Can be used to loop through all used peer IDs.
+ *
+ * @note @ref PM_PEER_ID_INVALID is considered to be before the first and after the last ordinary
+ * peer ID.
+ *
+ * @param[in] prev_peer_id The previous peer ID.
+ *
+ * @return The next peer ID pending deletion.
+ * @return The first ordinary peer ID if prev_peer_id was @ref PM_PEER_ID_INVALID.
+ * @retval PM_PEER_ID_INVALID if prev_peer_id was the last ordinary peer ID or the module
+ * is not initialized.
+ */
+pm_peer_id_t pds_next_deleted_peer_id_get(pm_peer_id_t prev_peer_id);
+
+/**
+ * @brief Function for querying the number of valid peer IDs available. I.E the number of peers
+ * in persistent storage.
+ *
+ * @return The number of valid peer IDs.
+ */
+uint32_t pds_peer_count_get(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PEER_DATA_STORAGE_H__ */
+
+/** @} */
diff --git a/lib/peer_manager/include/modules/peer_database.h b/lib/peer_manager/include/modules/peer_database.h
new file mode 100644
index 0000000000..dd42311362
--- /dev/null
+++ b/lib/peer_manager/include/modules/peer_database.h
@@ -0,0 +1,162 @@
+/*
+ * Copyright (c) 2015-2025 Nordic Semiconductor ASA
+ *
+ * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
+ */
+
+/**
+ * @defgroup peer_database Peer Database
+ * @ingroup peer_manager
+ * @{
+ * @brief An internal module of @ref peer_manager. A module for simple management of reading and
+ * writing of peer data into persistent storage.
+ *
+ */
+
+#ifndef PEER_DATABASE_H__
+#define PEER_DATABASE_H__
+
+#include
+#include
+#include "peer_manager_internal.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief The size (in bytes) of each block in the internal buffer accessible via
+ * @ref pdb_write_buf_get.
+ */
+#define PDB_WRITE_BUF_SIZE (sizeof(pm_peer_data_bonding_t))
+
+/**
+ * @brief Macro for creating a peer ID value from a connection handle.
+ *
+ * Use this macro with pdb_write_buf_get() or pdb_write_buf_store(). It allows to create a write
+ * buffer even if you do not yet know the proper peer ID the data will be stored for.
+ *
+ * @return @p conn_handle + @ref PM_PEER_ID_N_AVAILABLE_IDS.
+ */
+#define PDB_TEMP_PEER_ID(conn_handle) (PM_PEER_ID_N_AVAILABLE_IDS + conn_handle)
+
+/**
+ * @brief Function for initializing the module.
+ *
+ * @retval NRF_SUCCESS If initialization was successful.
+ * @retval NRF_ERROR_INTERNAL An unexpected error happened.
+ */
+uint32_t pdb_init(void);
+
+/**
+ * @brief Function for freeing a peer's persistent bond storage.
+ *
+ * @note This function will call @ref pdb_write_buf_release on the data for this peer.
+ *
+ * @param[in] peer_id ID to be freed.
+ *
+ * @retval NRF_SUCCESS Peer ID was released and clear operation was initiated
+ * successfully.
+ * @retval NRF_ERROR_INVALID_PARAM Peer ID was invalid.
+ */
+uint32_t pdb_peer_free(pm_peer_id_t peer_id);
+
+/**
+ * @brief Function for retrieving a pointer to peer data in flash (read-only).
+ *
+ * @note Dereferencing this pointer is not the safest thing to do if interrupts are enabled,
+ * because Flash Data Storage garbage collection might move the data around. Either disable
+ * interrupts while using the data, or use @ref pdb_peer_data_load.
+ *
+ * @param[in] peer_id The peer the data belongs to.
+ * @param[in] data_id The data to read.
+ * @param[out] p_peer_data The peer data, read-only.
+ *
+ * @retval NRF_SUCCESS If the pointer to the data was retrieved successfully.
+ * @retval NRF_ERROR_INVALID_PARAM If either @p peer_id or @p data_id are invalid.
+ * @retval NRF_ERROR_NOT_FOUND If data was not found in flash.
+ */
+uint32_t pdb_peer_data_ptr_get(pm_peer_id_t peer_id, pm_peer_data_id_t data_id,
+ pm_peer_data_flash_t *const p_peer_data);
+
+/**
+ * @brief Function for retrieving pointers to a write buffer for peer data.
+ *
+ * @details This function will provide pointers to a buffer of the data. The data buffer will not be
+ * written to persistent storage until @ref pdb_write_buf_store is called. The buffer is
+ * released by calling either @ref pdb_write_buf_release, @ref pdb_write_buf_store, or
+ * @ref pdb_peer_free.
+ *
+ * When the data_id refers to a variable length data type, the available size is written
+ * to the data, both the top-level, and any internal length fields.
+ *
+ * @note Calling this function on a peer_id/data_id pair that already has a buffer created will
+ * give the same buffer, not create a new one. If n_bufs was increased since last time, the
+ * buffer might be relocated to be able to provide additional room. In this case, the data
+ * will be copied. If n_bufs was increased since last time, this function might return @ref
+ * NRF_ERROR_BUSY. In that case, the buffer is automatically released.
+ *
+ * @param[in] peer_id ID of the peer to get a write buffer for. If @p peer_id is larger than
+ * @ref PM_PEER_ID_N_AVAILABLE_IDS, it is interpreted as pertaining to
+ * the connection with connection handle peer_id -
+ * PM_PEER_ID_N_AVAILABLE_IDS. See @ref PDB_TEMP_PEER_ID.
+ * @param[in] data_id The piece of data to get.
+ * @param[in] n_bufs Number of contiguous buffers needed.
+ * @param[out] p_peer_data Pointers to mutable peer data.
+ *
+ * @retval NRF_SUCCESS Data retrieved successfully.
+ * @retval NRF_ERROR_INVALID_PARAM @p data_id was invalid, or @p n_bufs was 0 or more than the
+ * total available buffers.
+ * @retval NRF_ERROR_FORBIDDEN n_bufs was higher or lower than the existing buffer. If needed,
+ * release the existing buffer with @ref pdb_write_buf_release, and
+ * call this function again.
+ * @retval NRF_ERROR_NULL p_peer_data was NULL.
+ * @retval NRF_ERROR_BUSY Not enough buffer(s) available.
+ * @retval NRF_ERROR_INTERNAL Unexpected internal error.
+ */
+uint32_t pdb_write_buf_get(pm_peer_id_t peer_id, pm_peer_data_id_t data_id, uint32_t n_bufs,
+ pm_peer_data_t *p_peer_data);
+
+/**
+ * @brief Function for freeing a write buffer allocated with @ref pdb_write_buf_get.
+ *
+ * @note This function will not write peer data to persistent memory. Data in released buffer will
+ * be lost.
+ *
+ * @param[in] peer_id ID of peer to release buffer for.
+ * @param[in] data_id Which piece of data to release buffer for.
+ *
+ * @retval NRF_SUCCESS Successfully released buffer.
+ * @retval NRF_ERROR_NOT_FOUND No buffer was allocated for this peer ID/data ID pair.
+ */
+uint32_t pdb_write_buf_release(pm_peer_id_t peer_id, pm_peer_data_id_t data_id);
+
+/**
+ * @brief Function for writing data into persistent storage. Writing happens asynchronously.
+ *
+ * @note This will unlock the data after it has been written.
+ *
+ * @param[in] peer_id The ID used to address the write buffer.
+ * @param[in] data_id The piece of data to store.
+ * @param[in] new_peer_id The ID to put in flash. This is usually the same as peer_id, but
+ * must be valid, i.e. allocated (and smaller than @ref
+ * PM_PEER_ID_N_AVAILABLE_IDS).
+ *
+ * @retval NRF_SUCCESS Data storing was successfully started.
+ * @retval NRF_ERROR_RESOURCES No space available in persistent storage. Please clear some
+ * space, the operation will be reattempted after the next compress
+ * procedure.
+ * @retval NRF_ERROR_INVALID_PARAM @p data_id or @p new_peer_id was invalid.
+ * @retval NRF_ERROR_NOT_FOUND No buffer has been allocated for this peer ID/data ID pair.
+ * @retval NRF_ERROR_INTERNAL Unexpected internal error.
+ */
+uint32_t pdb_write_buf_store(pm_peer_id_t peer_id, pm_peer_data_id_t data_id,
+ pm_peer_id_t new_peer_id);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PEER_DATABASE_H__ */
+
+/** @} */
diff --git a/lib/peer_manager/include/modules/peer_id.h b/lib/peer_manager/include/modules/peer_id.h
new file mode 100644
index 0000000000..6353b80440
--- /dev/null
+++ b/lib/peer_manager/include/modules/peer_id.h
@@ -0,0 +1,125 @@
+/*
+ * Copyright (c) 2015-2025 Nordic Semiconductor ASA
+ *
+ * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
+ */
+
+/**
+ * @defgroup peer_id Peer IDs
+ * @ingroup peer_manager
+ * @{
+ * @brief An internal module of @ref peer_manager. This module keeps track of which peer IDs are in
+ * use and which are free.
+ */
+
+#ifndef PEER_ID_H__
+#define PEER_ID_H__
+
+#include
+#include
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** @brief Function for initializing the module. */
+void peer_id_init(void);
+
+/**
+ * @brief Function for claiming an unused peer ID.
+ *
+ * @param peer_id The peer ID to allocate. If this is @ref PM_PEER_ID_INVALID, the first available
+ * will be allocated.
+ *
+ * @return The allocated peer ID.
+ * @retval PM_PEER_ID_INVALID If no peer ID could be allocated or module is not initialized.
+ */
+pm_peer_id_t peer_id_allocate(pm_peer_id_t peer_id);
+
+/**
+ * @brief Function for marking a peer ID for deletion.
+ *
+ * @param peer_id The peer ID to delete.
+ *
+ * @retval true Deletion was successful.
+ * @retval false Peer ID already marked for deletion, peer_id was PM_PEER_ID_INVALID, or module is
+ * not initialized.
+ */
+bool peer_id_delete(pm_peer_id_t peer_id);
+
+/**
+ * @brief Function for freeing a peer ID and clearing all data associated with it in persistent
+ * storage.
+ *
+ * @param[in] peer_id Peer ID to free.
+ */
+void peer_id_free(pm_peer_id_t peer_id);
+
+/**
+ * @brief Function for finding out whether a peer ID is marked for deletion.
+ *
+ * @param[in] peer_id The peer ID to inquire about.
+ *
+ * @retval true peer_id is in marked for deletion.
+ * @retval false peer_id is not marked for deletion, or the module is not initialized.
+ */
+bool peer_id_is_deleted(pm_peer_id_t peer_id);
+
+/**
+ * @brief Function for finding out whether a peer ID is in use.
+ *
+ * @param[in] peer_id The peer ID to inquire about.
+ *
+ * @retval true peer_id is in use.
+ * @retval false peer_id is free, or the module is not initialized.
+ */
+bool peer_id_is_allocated(pm_peer_id_t peer_id);
+
+/**
+ * @brief Function for getting the next peer ID in the sequence of all used peer IDs. Can be
+ * used to loop through all used peer IDs.
+ *
+ * @note @ref PM_PEER_ID_INVALID is considered to be before the first and after the last ordinary
+ * peer ID.
+ *
+ * @param[in] prev_peer_id The previous peer ID.
+ *
+ * @return The next peer ID.
+ * @return The first used peer ID if prev_peer_id was @ref PM_PEER_ID_INVALID.
+ * @retval PM_PEER_ID_INVALID if prev_peer_id was the last ordinary peer ID or the module is
+ * not initialized.
+ */
+pm_peer_id_t peer_id_get_next_used(pm_peer_id_t prev_peer_id);
+
+/**
+ * @brief Function for getting the next peer ID in the sequence of all peer IDs marked for deletion.
+ * Can be used to loop through all peer IDs marked for deletion.
+ *
+ * @note @ref PM_PEER_ID_INVALID is considered to be before the first and after the last ordinary
+ * peer ID.
+ *
+ * @param[in] prev_peer_id The previous peer ID.
+ *
+ * @return The next peer ID.
+ * @return The first used peer ID if prev_peer_id was @ref PM_PEER_ID_INVALID.
+ * @retval PM_PEER_ID_INVALID if prev_peer_id was the last ordinary peer ID or the module is
+ * not initialized.
+ */
+pm_peer_id_t peer_id_get_next_deleted(pm_peer_id_t prev_peer_id);
+
+/**
+ * @brief Function for querying the number of valid peer IDs available. I.E the number of peers
+ * in persistent storage.
+ *
+ * @return The number of valid peer IDs, or 0 if module is not initialized.
+ */
+uint32_t peer_id_n_ids(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PEER_ID_H__ */
+
+/** @} */
diff --git a/lib/peer_manager/include/modules/peer_manager_handler.h b/lib/peer_manager/include/modules/peer_manager_handler.h
new file mode 100644
index 0000000000..fd28686fa6
--- /dev/null
+++ b/lib/peer_manager/include/modules/peer_manager_handler.h
@@ -0,0 +1,140 @@
+/*
+ * Copyright (c) 2018-2025 Nordic Semiconductor ASA
+ *
+ * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
+ */
+
+/**
+ * @file peer_manager_handler.h
+ *
+ * @defgroup pm_handler Peer Manager Standard Event Handlers
+ * @ingroup peer_manager
+ * @{
+ * @brief Standard event handlers implementing some best practices for BLE security.
+ */
+
+#ifndef PEER_MANAGER_HANDLER_H__
+#define PEER_MANAGER_HANDLER_H__
+
+#include
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief Standard function for making Peer Manager calls based on Peer Manager events.
+ *
+ * This function does the following:
+ * - Logs all PM events using @ref nrf_log, at different severity levels.
+ * - Starts encryption if connected to an already bonded peer. This is affected by @ref
+ * PM_HANDLER_SEC_DELAY_MS.
+ * - Calls @ref app_error on fatal errors.
+ *
+ * @note In normal circumstances, this function should be called for every Peer Manager event.
+ *
+ * @param[in] p_pm_evt Peer Manager event to handle.
+ */
+void pm_handler_on_pm_evt(pm_evt_t const *p_pm_evt);
+
+/**
+ * @brief Auxiliary standard function for logging Peer Manager events.
+ *
+ * This function logs all PM events using @ref nrf_log, at different severity levels. The
+ * @ref PM_LOG_ENABLED and other @c PM_LOG_* configs control these log messages.
+ *
+ * @note This function is called internally by @ref pm_handler_on_pm_evt.
+ *
+ * @param[in] p_pm_evt Peer Manager event to log.
+ */
+void pm_handler_pm_evt_log(pm_evt_t const *p_pm_evt);
+
+/**
+ * @brief Auxiliary standard function for maintaining room in flash based on Peer Manager events.
+ *
+ * This function does the following:
+ * - Ranks peers by when they last connected.
+ * - Garbage collects the flash when needed.
+ * - Deletes the lowest ranked peer(s) when garbage collection is insufficient.
+ *
+ * @note See also @ref pm_handler_flash_clean_on_return.
+ * @note In normal circumstances, this function should be called for every Peer Manager event.
+ * @note This function is a supplement to @ref pm_handler_on_pm_evt, not its replacement.
+ *
+ * @param[in] p_pm_evt Peer Manager event to handle.
+ */
+void pm_handler_flash_clean(pm_evt_t const *p_pm_evt);
+
+/**
+ * @brief Function to call when a Peer Manager function returns @ref NRF_ERROR_RESOURCES.
+ *
+ * @note This should only be used if @ref pm_handler_flash_clean is also used.
+ */
+void pm_handler_flash_clean_on_return(void);
+
+/**
+ * @brief Auxiliary standard function for disconnecting when the connection could not be secured.
+ *
+ * This function disconnects whenever connection security fails, i.e. whenever it receives a
+ * @ref PM_EVT_CONN_SEC_FAILED.
+ *
+ * @note In normal circumstances, this function should be called for every Peer Manager event.
+ * @note This function is a supplement to @ref pm_handler_on_pm_evt, not its replacement.
+ *
+ * @param[in] p_pm_evt Peer Manager event to handle.
+ */
+void pm_handler_disconnect_on_sec_failure(pm_evt_t const *p_pm_evt);
+
+/**
+ * @brief Auxiliary standard function for disconnecting on insufficient connection security.
+ *
+ * This function disconnects whenever the connection security succeeds, that is whenever it
+ * receives a @ref PM_EVT_CONN_SEC_SUCCEEDED, but the established security does not fulfill the
+ * provided criteria.
+ *
+ * @note In normal circumstances, this function should be called for every Peer Manager event.
+ * @note This function is a supplement to @ref pm_handler_on_pm_evt, not its replacement.
+ *
+ * @param[in] p_pm_evt Peer Manager event to handle.
+ * @param[in] p_min_conn_sec Minimum security status below which to disconnect the link.
+ */
+void pm_handler_disconnect_on_insufficient_sec(pm_evt_t const *p_pm_evt,
+ pm_conn_sec_status_t *p_min_conn_sec);
+
+/**
+ * @brief Function for securing a connection when it is established.
+ *
+ * This function starts security when receiving a @ref BLE_GAP_EVT_CONNECTED event. This is
+ * affected by @ref PM_HANDLER_SEC_DELAY_MS.
+ *
+ * @note In normal circumstances, this function should be called for every BLE event.
+ *
+ * @param[in] p_ble_evt BLE event to handle.
+ */
+void pm_handler_secure_on_connection(ble_evt_t const *p_ble_evt);
+
+/**
+ * @brief Function for securing a connection if a GATT read or write operation lacks security.
+ *
+ * This function starts pairing if a GATTC procedure fails with insufficient encryption
+ * or insufficient authentication. This is meant to delay performing pairing/bonding until
+ * it is actually needed to access resources. This is affected by @ref PM_HANDLER_SEC_DELAY_MS.
+ *
+ * @note When using this handler, the failed GATTC operation must be retried by the user.
+ * @note This does not work when using Write Without Response (@ref BLE_GATT_OP_WRITE_CMD) because
+ * the server does not send any response, even on error. Instead, the write will be
+ * silently dropped by the server.
+ * @note In normal circumstances, this function should be called for every BLE event.
+ *
+ * @param[in] p_ble_evt BLE event to handle.
+ */
+void pm_handler_secure_on_error(ble_evt_t const *p_ble_evt);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PEER_MANAGER_HANDLER_H__ */
+
+/** @} */
diff --git a/lib/peer_manager/include/modules/peer_manager_internal.h b/lib/peer_manager/include/modules/peer_manager_internal.h
new file mode 100644
index 0000000000..3f17b61b5e
--- /dev/null
+++ b/lib/peer_manager/include/modules/peer_manager_internal.h
@@ -0,0 +1,205 @@
+/*
+ * Copyright (c) 2015-2025 Nordic Semiconductor ASA
+ *
+ * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
+ */
+
+/**
+ * @file peer_manager_types.h
+ *
+ * @addtogroup peer_manager
+ * @brief File containing definitions used solely inside the Peer Manager's modules.
+ * @{
+ */
+
+#ifndef PEER_MANAGER_INTERNAL_H__
+#define PEER_MANAGER_INTERNAL_H__
+
+#include
+#include
+#include
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief One piece of data associated with a peer, together with its type.
+ *
+ * @note This type is deprecated.
+ */
+typedef struct {
+ /** @brief The length of the data in words. */
+ uint16_t length_words;
+ /**
+ * @brief ID that specifies the type of data (defines which member of the union is
+ * used).
+ */
+ pm_peer_data_id_t data_id;
+ /** @brief The data. */
+ union {
+ /** @brief The exchanged bond information in addition to metadata of the bonding. */
+ pm_peer_data_bonding_t *p_bonding_data;
+ /**
+ * @brief A value locally assigned to this peer. Its
+ * interpretation is up to the user. The rank is not set
+ * automatically by the Peer Manager, but it is assigned by
+ * the user using either @ref pm_peer_rank_highest or a @ref
+ * PM_PEER_DATA_FUNCTIONS function.
+ */
+ uint32_t *p_peer_rank;
+ /** @brief Value of peer's Central Address Resolution characteristic. */
+ uint32_t *p_central_addr_res;
+ /** @brief Whether a service changed indication should be sent to the peer. */
+ bool *p_service_changed_pending;
+ /** @brief Persistent information pertaining to a peer GATT client. */
+ pm_peer_data_local_gatt_db_t *p_local_gatt_db;
+ /** @brief Persistent information pertaining to a peer GATT server. */
+ ble_gatt_db_srv_t *p_remote_gatt_db;
+ /**
+ * @brief Arbitrary data to associate with the peer. This data can be freely used
+ * by the application.
+ */
+ uint8_t *p_application_data;
+ /**
+ * @brief Generic access pointer to the data. It is used only to
+ * handle the data without regard to type.
+ */
+ void *p_all_data;
+ };
+} pm_peer_data_t;
+
+/**
+ * @brief Immutable version of @ref pm_peer_data_t.
+ *
+ * @note This type is deprecated.
+ */
+typedef struct {
+ /** @brief The length of the data in words. */
+ uint16_t length_words;
+ /**
+ * @brief ID that specifies the type of data (defines which member of the union is
+ * used).
+ */
+ pm_peer_data_id_t data_id;
+ /** @brief The data. */
+ union {
+ /** @brief Immutable @ref pm_peer_data_t::p_bonding_data. */
+ pm_peer_data_bonding_t const *p_bonding_data;
+ /** @brief Immutable @ref pm_peer_data_t::p_peer_rank. */
+ uint32_t const *p_peer_rank;
+ /** @brief Immutable @ref pm_peer_data_t::p_central_addr_res. */
+ uint32_t const *p_central_addr_res;
+ /** @brief Immutable @ref pm_peer_data_t::p_service_changed_pending. */
+ bool const *p_service_changed_pending;
+ /** @brief Immutable @ref pm_peer_data_t::p_local_gatt_db. */
+ pm_peer_data_local_gatt_db_t const *p_local_gatt_db;
+ /** @brief Immutable @ref pm_peer_data_t::p_remote_gatt_db. */
+ ble_gatt_db_srv_t const *p_remote_gatt_db;
+ /** @brief Immutable @ref pm_peer_data_t::p_application_data. */
+ uint8_t const *p_application_data;
+ /** @brief Immutable @ref pm_peer_data_t::p_all_data. */
+ void const *p_all_data;
+ };
+} pm_peer_data_const_t;
+
+/**
+ * @brief Version of @ref pm_peer_data_t that reflects the structure of peer data in flash.
+ *
+ * @note This type is deprecated.
+ */
+typedef pm_peer_data_const_t pm_peer_data_flash_t;
+
+/**
+ * @brief Event handler for events from the @ref peer_manager module.
+ *
+ * @sa pm_register
+ *
+ * @param[in] p_event The event that has occurred.
+ */
+typedef void (*pm_evt_handler_internal_t)(pm_evt_t *p_event);
+
+/** @brief The number of bytes in a word. */
+#define BYTES_PER_WORD (4)
+
+/**
+ * @brief Macro for calculating the flash size of bonding data.
+ *
+ * @return The number of words that the data takes in flash.
+ */
+#define PM_BONDING_DATA_N_WORDS() BYTES_TO_WORDS(sizeof(pm_peer_data_bonding_t))
+
+/**
+ * @brief Macro for calculating the flash size of service changed pending state.
+ *
+ * @return The number of words that the data takes in flash.
+ */
+#define PM_SC_STATE_N_WORDS() BYTES_TO_WORDS(sizeof(bool))
+
+/**
+ * @brief Macro for calculating the flash size of local GATT database data.
+ *
+ * @param[in] local_db_len The length, in bytes, of the database as reported by the SoftDevice.
+ *
+ * @return The number of words that the data takes in flash.
+ */
+#define PM_LOCAL_DB_N_WORDS(local_db_len) \
+ BYTES_TO_WORDS((local_db_len) + PM_LOCAL_DB_LEN_OVERHEAD_BYTES)
+
+/**
+ * @brief Macro for calculating the length of a local GATT database attribute array.
+ *
+ * @param[in] n_words The number of words that the data takes in flash.
+ *
+ * @return The length of the database attribute array.
+ */
+#define PM_LOCAL_DB_LEN(n_words) (((n_words)*BYTES_PER_WORD) - PM_LOCAL_DB_LEN_OVERHEAD_BYTES)
+
+/**
+ * @brief Macro for calculating the flash size of remote GATT database data.
+ *
+ * @param[in] service_count The number of services in the service array.
+ *
+ * @return The number of words that the data takes in flash.
+ */
+#define PM_REMOTE_DB_N_WORDS(service_count) \
+ BYTES_TO_WORDS(sizeof(ble_gatt_db_srv_t) * (service_count))
+
+/**
+ * @brief Macro for calculating the flash size of remote GATT database data.
+ *
+ * @param[in] n_words The length in number of words.
+ *
+ * @return The number of words that the data takes in flash.
+ */
+#define PM_REMOTE_DB_N_SERVICES(n_words) (((n_words)*BYTES_PER_WORD) / sizeof(ble_gatt_db_srv_t))
+
+/**
+ * @brief Function for calculating the flash size of the usage index.
+ *
+ * @return The number of words that the data takes in flash.
+ */
+#define PM_USAGE_INDEX_N_WORDS() BYTES_TO_WORDS(sizeof(uint32_t))
+
+#ifdef NRF_PM_DEBUG
+
+#define NRF_PM_DEBUG_CHECK(condition) \
+ if (!(condition)) { \
+ __asm("bkpt #0"); \
+ }
+
+#else
+
+/* Prevent "variable set but never used" compiler warnings. */
+#define NRF_PM_DEBUG_CHECK(condition) (void)(condition)
+
+#endif
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PEER_MANAGER_INTERNAL_H__ */
+
+/** @} */
diff --git a/lib/peer_manager/include/modules/pm_buffer.h b/lib/peer_manager/include/modules/pm_buffer.h
new file mode 100644
index 0000000000..846c85c9a7
--- /dev/null
+++ b/lib/peer_manager/include/modules/pm_buffer.h
@@ -0,0 +1,111 @@
+/*
+ * Copyright (c) 2015-2025 Nordic Semiconductor ASA
+ *
+ * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
+ */
+
+/**
+ * @defgroup pm_buffer Buffer
+ * @ingroup peer_manager
+ * @{
+ * @brief An internal module of @ref peer_manager. This module provides a simple buffer.
+ */
+
+#ifndef BUFFER_H__
+#define BUFFER_H__
+
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** @brief Invalid buffer block ID. */
+#define PM_BUFFER_INVALID_ID 0xFF
+
+/**
+ * @brief Convenience macro for declaring memory and initializing a buffer instance.
+ *
+ * @param[out] p_buffer The buffer instance to initialize.
+ * @param[in] n_blocks The desired number of blocks in the buffer.
+ * @param[in] block_size The desired block size of the buffer.
+ * @param[out] err_code The return code from @ref pm_buffer_init.
+ */
+#define PM_BUFFER_INIT(p_buffer, n_blocks, block_size, err_code) \
+ do { \
+ __ALIGN(4) static uint8_t buffer_memory[(n_blocks) * (block_size)]; \
+ static atomic_t mutex_memory[(n_blocks - 1) / (sizeof(atomic_t) * 8) + 1]; \
+ err_code = pm_buffer_init((p_buffer), buffer_memory, (n_blocks) * (block_size), \
+ mutex_memory, (n_blocks), (block_size)); \
+ } while (0)
+
+typedef struct {
+ /**
+ * @brief The storage for all buffer entries. The size of the buffer must be
+ * n_blocks*block_size.
+ */
+ uint8_t *p_memory;
+ /** @brief A mutex group with one mutex for each buffer entry. */
+ atomic_t *p_mutex;
+ /** @brief The number of allocatable blocks in the buffer. */
+ uint32_t n_blocks;
+ /** @brief The size of each block in the buffer. */
+ uint32_t block_size;
+} pm_buffer_t;
+
+/**
+ * @brief Function for initializing a buffer instance.
+ *
+ * @param[out] p_buffer The buffer instance to initialize.
+ * @param[in] p_buffer_memory The memory this buffer will use.
+ * @param[in] buffer_memory_size The size of p_buffer_memory. This must be at least
+ * n_blocks*block_size.
+ * @param[in] p_mutex_memory The memory for the mutexes. This must be at least
+ * @ref NRF_ATFLAGS_ARRAY_LEN(n_blocks).
+ * @param[in] n_blocks The number of blocks in the buffer.
+ * @param[in] block_size The size of each block.
+ *
+ * @retval NRF_SUCCESS Successfully initialized buffer instance.
+ * @retval NRF_ERROR_INVALID_PARAM A parameter was 0 or NULL or a size was too small.
+ */
+uint32_t pm_buffer_init(pm_buffer_t *p_buffer, uint8_t *p_buffer_memory,
+ uint32_t buffer_memory_size, atomic_t *p_mutex_memory,
+ uint32_t n_blocks, uint32_t block_size);
+
+/**
+ * @brief Function for acquiring a buffer block in a buffer.
+ *
+ * @param[in] p_buffer The buffer instance acquire from.
+ * @param[in] n_blocks The number of contiguous blocks to acquire.
+ *
+ * @return The id of the acquired block, if successful.
+ * @retval PM_BUFFER_INVALID_ID If unsuccessful.
+ */
+uint8_t pm_buffer_block_acquire(pm_buffer_t *p_buffer, uint32_t n_blocks);
+
+/**
+ * @brief Function for getting a pointer to a specific buffer block.
+ *
+ * @param[in] p_buffer The buffer instance get from.
+ * @param[in] id The id of the buffer to get the pointer for.
+ *
+ * @return A pointer to the buffer for the specified id, if the id is valid.
+ * @retval NULL If the id is invalid.
+ */
+uint8_t *pm_buffer_ptr_get(pm_buffer_t *p_buffer, uint8_t id);
+
+/**
+ * @brief Function for releasing a buffer block.
+ *
+ * @param[in] p_buffer The buffer instance containing the block to release.
+ * @param[in] id The id of the block to release.
+ */
+void pm_buffer_release(pm_buffer_t *p_buffer, uint8_t id);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* BUFFER_H__ */
+
+/** @} */
diff --git a/lib/peer_manager/include/modules/security_dispatcher.h b/lib/peer_manager/include/modules/security_dispatcher.h
new file mode 100644
index 0000000000..b13055ca3b
--- /dev/null
+++ b/lib/peer_manager/include/modules/security_dispatcher.h
@@ -0,0 +1,123 @@
+/*
+ * Copyright (c) 2015-2025 Nordic Semiconductor ASA
+ *
+ * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
+ */
+
+/**
+ * @defgroup security_dispatcher Security Dispatcher
+ * @ingroup peer_manager
+ * @{
+ * @brief An internal module of @ref peer_manager. A module for streamlining pairing, bonding, and
+ * encryption, including flash storage of shared data.
+ *
+ */
+
+#ifndef SECURITY_DISPATCHER_H__
+#define SECURITY_DISPATCHER_H__
+
+#include
+#include
+#include
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief Function for initializing the Security Dispatcher module.
+ *
+ * @retval NRF_SUCCESS Initialization was successful.
+ * @retval NRF_ERROR_INTERNAL An unexpected fatal error occurred.
+ */
+uint32_t smd_init(void);
+
+/**
+ * @brief Function for dispatching SoftDevice events to the Security Dispatcher module.
+ *
+ * @param[in] ble_evt The SoftDevice event.
+ */
+void smd_ble_evt_handler(ble_evt_t const *ble_evt);
+
+/**
+ * @brief Function for providing security configuration for a link.
+ *
+ * @details This function is optional, and must be called in reply to a @ref
+ * PM_EVT_CONN_SEC_CONFIG_REQ event, before the Peer Manager event handler returns. If it
+ * is not called in time, a default configuration is used. See @ref pm_conn_sec_config_t
+ * for the value of the default.
+ *
+ * @param[in] conn_handle The connection to set the configuration for.
+ * @param[in] p_conn_sec_config The configuration.
+ */
+void smd_conn_sec_config_reply(uint16_t conn_handle, pm_conn_sec_config_t *p_conn_sec_config);
+
+/**
+ * @brief Function for providing pairing and bonding parameters to use for the current pairing
+ * procedure on a connection.
+ *
+ * @note If this function returns an @ref NRF_ERROR_NULL, @ref NRF_ERROR_INVALID_PARAM, @ref
+ * BLE_ERROR_INVALID_CONN_HANDLE, or @ref NRF_ERROR_RESOURCES, this function can be called
+ * again after corrective action.
+ *
+ * @note To reject a request, call this function with NULL p_sec_params.
+ *
+ * @param[in] conn_handle The connection handle of the connection the pairing is happening on.
+ * @param[in] p_sec_params The security parameters to use for this link.
+ * @param[in] p_public_key A pointer to the public key to use if using LESC, or NULL.
+ *
+ * @retval NRF_SUCCESS Success.
+ * @retval NRF_ERROR_INVALID_STATE No parameters have been requested on that conn_handle, or
+ * the link is disconnecting.
+ * @retval NRF_ERROR_INVALID_PARAM Invalid combination of parameters (not including
+ * conn_handle).
+ * @retval NRF_ERROR_TIMEOUT There has been an SMP timeout, so no more SMP operations
+ * can be performed on this link.
+ * @retval BLE_ERROR_INVALID_CONN_HANDLE Invalid connection handle.
+ * @retval NRF_ERROR_BUSY No write buffer. Reattempt later.
+ * @retval NRF_ERROR_INTERNAL A fatal error occurred.
+ */
+uint32_t smd_params_reply(uint16_t conn_handle, ble_gap_sec_params_t *p_sec_params,
+ ble_gap_lesc_p256_pk_t *p_public_key);
+
+/**
+ * @brief Function for initiating security on the link, with the specified parameters.
+ *
+ * @note If the connection is a peripheral connection, this will send a security request to the
+ * master, but the master is not obligated to initiate pairing or encryption in response.
+ * @note If the connection is a central connection and a key is available, the parameters will be
+ * used to determine whether to re-pair or to encrypt using the existing key. If no key is
+ * available, pairing will be started.
+ *
+ * @param[in] conn_handle Handle of the connection to initiate pairing on.
+ * @param[in] p_sec_params The security parameters to use for this link. As a central, this can
+ * be NULL to reject a slave security request.
+ * @param[in] force_repairing Whether to force a pairing procedure to happen regardless of whether
+ * an encryption key already exists. This argument is only relevant for
+ * the central role. Recommended value: false
+ *
+ * @retval NRF_SUCCESS Success.
+ * @retval NRF_ERROR_NULL p_sec_params was NULL (peripheral only).
+ * @retval NRF_ERROR_INVALID_STATE A security procedure is already in progress on the link,
+ * or the link is disconnecting.
+ * @retval NRF_ERROR_INVALID_PARAM Invalid combination of parameters (not including
+ * conn_handle).
+ * @retval NRF_ERROR_INVALID_DATA Peer is bonded, but no LTK was found, and repairing was
+ * not requested.
+ * @retval NRF_ERROR_BUSY Unable to initiate procedure at this time.
+ * @retval NRF_ERROR_TIMEOUT There has been an SMP timeout, so no more SMP operations
+ * can be performed on this link.
+ * @retval BLE_ERROR_INVALID_CONN_HANDLE Invalid connection handle.
+ * @retval NRF_ERROR_INTERNAL No more available peer IDs.
+ */
+uint32_t smd_link_secure(uint16_t conn_handle, ble_gap_sec_params_t *p_sec_params,
+ bool force_repairing);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* SECURITY_DISPATCHER_H__ */
+
+/** @} */
diff --git a/lib/peer_manager/include/modules/security_manager.h b/lib/peer_manager/include/modules/security_manager.h
new file mode 100644
index 0000000000..9cc41c1891
--- /dev/null
+++ b/lib/peer_manager/include/modules/security_manager.h
@@ -0,0 +1,165 @@
+/*
+ * Copyright (c) 2015-2025 Nordic Semiconductor ASA
+ *
+ * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
+ */
+
+/**
+ * @defgroup security_manager Security Manager
+ * @ingroup peer_manager
+ * @{
+ * @brief An internal module of @ref peer_manager. A module for streamlining pairing, bonding, and
+ * encryption, including flash storage of shared data.
+ */
+
+#ifndef SECURITY_MANAGER_H__
+#define SECURITY_MANAGER_H__
+
+#include
+#include
+#include
+#include
+#include "security_dispatcher.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief Function for initializing the Security Manager module.
+ *
+ * @retval NRF_SUCCESS If initialization was successful.
+ * @retval NRF_ERROR_INTERNAL If an unexpected error occurred.
+ */
+uint32_t sm_init(void);
+
+/**
+ * @brief Function for dispatching SoftDevice events to the Security Manager module.
+ *
+ * @param[in] ble_evt The SoftDevice event.
+ */
+void sm_ble_evt_handler(ble_evt_t const *ble_evt);
+
+/**
+ * @brief Function for providing pairing and bonding parameters to use for pairing procedures.
+ *
+ * @details Until this is called, all bonding procedures initiated by the peer will be rejected.
+ * This function can be called multiple times, even with NULL p_sec_params, in which case
+ * it will go back to rejecting all procedures.
+ *
+ * @param[in] p_sec_params The security parameters to use for this link. Can be NULL to reject
+ * all pairing procedures.
+ *
+ * @retval NRF_SUCCESS Success.
+ * @retval NRF_ERROR_INVALID_PARAM Invalid combination of parameters.
+ */
+uint32_t sm_sec_params_set(ble_gap_sec_params_t *p_sec_params);
+
+/**
+ * @brief Function for providing security configuration for a link.
+ *
+ * @details This function is optional, and must be called in reply to a @ref
+ * PM_EVT_CONN_SEC_CONFIG_REQ event, before the Peer Manager event handler returns. If it
+ * is not called in time, a default configuration is used. See @ref pm_conn_sec_config_t
+ * for the value of the default.
+ *
+ * @param[in] conn_handle The connection to set the configuration for.
+ * @param[in] p_conn_sec_config The configuration.
+ */
+void sm_conn_sec_config_reply(uint16_t conn_handle, pm_conn_sec_config_t *p_conn_sec_config);
+
+/**
+ * @brief Function for providing security parameters for a link.
+ *
+ * @details This function is optional, and must be called in reply to a @ref
+ * PM_EVT_CONN_SEC_PARAMS_REQ event, before the Security Manager event handler returns. If
+ * it is not called in time, the parameters given in @ref sm_sec_params_set are used. See
+ * @ref pm_conn_sec_config_t for the value of the default.
+ *
+ * @param[in] conn_handle The connection to set the parameters for.
+ * @param[in] p_sec_params The parameters. If NULL, the security procedure is rejected.
+ * @param[in] p_context The context found in the request event that this function replies to.
+ *
+ * @retval NRF_SUCCESS Successful reply.
+ * @retval NRF_ERROR_NULL p_context was null.
+ * @retval NRF_ERROR_INVALID_PARAM Value of p_sec_params was invalid.
+ * @retval NRF_ERROR_INVALID_STATE This module is not initialized.
+ */
+uint32_t sm_sec_params_reply(uint16_t conn_handle, ble_gap_sec_params_t *p_sec_params,
+ void const *p_context);
+
+/**
+ * @brief Experimental function for specifying the public key to use for LESC operations.
+ *
+ * @details This function can be called multiple times. The specified public key will be used for
+ * all subsequent LESC (LE Secure Connections) operations until the next time this function
+ * is called.
+ *
+ * @note The key must continue to reside in application memory as it is not copied by Peer Manager.
+ *
+ * @param[in] p_public_key The public key to use for all subsequent LESC operations.
+ *
+ * @retval NRF_SUCCESS Pairing initiated successfully.
+ * @retval NRF_ERROR_INVALID_STATE This module is not initialized.
+ */
+uint32_t sm_lesc_public_key_set(ble_gap_lesc_p256_pk_t *p_public_key);
+
+/**
+ * @brief Function for getting the security status of a connection.
+ *
+ * @param[in] conn_handle Connection handle of the link as provided by the SoftDevice.
+ * @param[out] p_conn_sec_status Security status of the link.
+ *
+ * @retval NRF_SUCCESS If pairing was initiated successfully.
+ * @retval BLE_ERROR_INVALID_CONN_HANDLE If the connection handle is invalid.
+ * @retval NRF_ERROR_NULL If @p p_conn_sec_status was NULL.
+ */
+uint32_t sm_conn_sec_status_get(uint16_t conn_handle, pm_conn_sec_status_t *p_conn_sec_status);
+
+/**
+ * @brief Function for comparing the security status of a connection against a baseline.
+ *
+ * @param[in] conn_handle Connection handle of the link as provided by the SoftDevice.
+ * @param[out] p_sec_status_req Target baseline security status to compare against.
+ *
+ * @retval true If the security status of the connection matches or exceeds the baseline on all
+ * points.
+ * @retval false If the security status of the connection does not fulfill the baseline, or could
+ * not be retrieved.
+ */
+bool sm_sec_is_sufficient(uint16_t conn_handle, pm_conn_sec_status_t *p_sec_status_req);
+
+/**
+ * @brief Function for initiating security on the link, with the specified parameters.
+ *
+ * @note If the connection is a peripheral connection, this will send a security request to the
+ * master, but the master is not obligated to initiate pairing or encryption in response.
+ * @note If the connection is a central connection and a key is available, the parameters will be
+ * used to determine whether to re-pair or to encrypt using the existing key. If no key is
+ * available, pairing will be started.
+ *
+ * @param[in] conn_handle Handle of the connection to initiate pairing on.
+ * @param[in] force_repairing Whether to force a pairing procedure to happen regardless of whether
+ * an encryption key already exists. This argument is only relevant for
+ * the central role. Recommended value: false
+ *
+ * @retval NRF_SUCCESS Success.
+ * @retval NRF_ERROR_TIMEOUT There has been an SMP timeout, so no more SMP operations
+ * can be performed on this link.
+ * @retval BLE_ERROR_INVALID_CONN_HANDLE Invalid connection handle.
+ * @retval NRF_ERROR_INVALID_DATA Peer is bonded, but no LTK was found, and repairing was
+ * not requested.
+ * @retval NRF_ERROR_NOT_FOUND Security parameters have not been set.
+ * @retval NRF_ERROR_INVALID_STATE A security procedure is already in progress on the link,
+ * or the link is disconnecting.
+ * @retval NRF_ERROR_INTERNAL An unexpected error occurred.
+ */
+uint32_t sm_link_secure(uint16_t conn_handle, bool force_repairing);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* SECURITY_MANAGER_H__ */
+
+/** @} */
diff --git a/lib/peer_manager/include/nordic_common.h b/lib/peer_manager/include/nordic_common.h
new file mode 100644
index 0000000000..a4db927de0
--- /dev/null
+++ b/lib/peer_manager/include/nordic_common.h
@@ -0,0 +1,185 @@
+/*
+ * Copyright (c) 2008-2025 Nordic Semiconductor ASA
+ *
+ * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
+ */
+
+/** @file
+ * @brief Common defines and macros for firmware developed by Nordic Semiconductor.
+ */
+
+#ifndef NORDIC_COMMON_H__
+#define NORDIC_COMMON_H__
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief Check if selected module is enabled
+ *
+ * This is save function for driver enable checking.
+ * Correct from Lint point of view (not using default of undefined value).
+ *
+ * Usage:
+ * @code
+ #if NRF_MODULE_ENABLED(UART)
+ ...
+ #endif
+ * @endcode
+ *
+ * @param module The module name.
+ *
+ * @retval 1 The macro _ENABLE is defined and is non-zero.
+ * @retval 0 The macro _ENABLE is not defined or it equals zero.
+ *
+ * @note
+ * This macro intentionally does not implement second expansion level.
+ * The name of the module to be checked has to be given directly as a parameter.
+ * And given parameter would be connected with @c _ENABLED postfix directly
+ * without evaluating its value.
+ */
+#ifdef NRF_MODULE_ENABLE_ALL
+#warning "Do not use NRF_MODULE_ENABLE_ALL for real builds."
+#define NRF_MODULE_ENABLED(module) 1
+#else
+#define NRF_MODULE_ENABLED(module) ((defined(module##_ENABLED) && (module##_ENABLED)) ? 1 : 0)
+#endif
+/** The upper 8 bits of a 32 bit value */
+#define MSB_32(a) (((a)&0xFF000000) >> 24)
+/** The lower 8 bits (of a 32 bit value) */
+#define LSB_32(a) ((a)&0x000000FF)
+
+/** The upper 8 bits of a 16 bit value */
+#define MSB_16(a) (((a)&0xFF00) >> 8)
+/** The lower 8 bits (of a 16 bit value) */
+#define LSB_16(a) ((a)&0x00FF)
+
+/** Leaves the minimum of the two 32-bit arguments */
+#define MIN(a, b) ((a) < (b) ? (a) : (b))
+/** Leaves the maximum of the two 32-bit arguments */
+#define MAX(a, b) ((a) < (b) ? (b) : (a))
+
+/**@brief Concatenates two parameters.
+ *
+ * It realizes two level expansion to make it sure that all the parameters
+ * are actually expanded before gluing them together.
+ *
+ * @param p1 First parameter to concatenating
+ * @param p2 Second parameter to concatenating
+ *
+ * @return Two parameters glued together.
+ * They have to create correct C mnemonic in other case
+ * preprocessor error would be generated.
+ *
+ * @sa CONCAT_3
+ */
+#define CONCAT_2(p1, p2) CONCAT_2_(p1, p2)
+/** Auxiliary macro used by @ref CONCAT_2 */
+#define CONCAT_2_(p1, p2) p1##p2
+
+/**@brief Concatenates three parameters.
+ *
+ * It realizes two level expansion to make it sure that all the parameters
+ * are actually expanded before gluing them together.
+ *
+ * @param p1 First parameter to concatenating
+ * @param p2 Second parameter to concatenating
+ * @param p3 Third parameter to concatenating
+ *
+ * @return Three parameters glued together.
+ * They have to create correct C mnemonic in other case
+ * preprocessor error would be generated.
+ *
+ * @sa CONCAT_2
+ */
+#define CONCAT_3(p1, p2, p3) CONCAT_3_(p1, p2, p3)
+/** Auxiliary macro used by @ref CONCAT_3 */
+#define CONCAT_3_(p1, p2, p3) p1##p2##p3
+
+#define STRINGIFY_(val) #val
+/** Converts a macro argument into a character constant.
+ */
+#define STRINGIFY(val) STRINGIFY_(val)
+
+/** Counts number of elements inside the array
+ */
+#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
+
+/**@brief Set a bit in the uint32 word.
+ *
+ * @param[in] W Word whose bit is being set.
+ * @param[in] B Bit number in the word to be set.
+ */
+#define SET_BIT(W, B) ((W) |= (uint32_t)(1U << (B)))
+
+/**@brief Clears a bit in the uint32 word.
+ *
+ * @param[in] W Word whose bit is to be cleared.
+ * @param[in] B Bit number in the word to be cleared.
+ */
+#define CLR_BIT(W, B) ((W) &= (~(uint32_t)(1U << (B))))
+
+/**@brief Checks if a bit is set.
+ *
+ * @param[in] W Word whose bit is to be checked.
+ * @param[in] B Bit number in the word to be checked.
+ *
+ * @retval 1 if bit is set.
+ * @retval 0 if bit is not set.
+ */
+#define IS_SET(W, B) (((W) >> (B)) & 1)
+
+#define BIT_0 0x01 /**< The value of bit 0 */
+#define BIT_1 0x02 /**< The value of bit 1 */
+#define BIT_2 0x04 /**< The value of bit 2 */
+#define BIT_3 0x08 /**< The value of bit 3 */
+#define BIT_4 0x10 /**< The value of bit 4 */
+#define BIT_5 0x20 /**< The value of bit 5 */
+#define BIT_6 0x40 /**< The value of bit 6 */
+#define BIT_7 0x80 /**< The value of bit 7 */
+#define BIT_8 0x0100 /**< The value of bit 8 */
+#define BIT_9 0x0200 /**< The value of bit 9 */
+#define BIT_10 0x0400 /**< The value of bit 10 */
+#define BIT_11 0x0800 /**< The value of bit 11 */
+#define BIT_12 0x1000 /**< The value of bit 12 */
+#define BIT_13 0x2000 /**< The value of bit 13 */
+#define BIT_14 0x4000 /**< The value of bit 14 */
+#define BIT_15 0x8000 /**< The value of bit 15 */
+#define BIT_16 0x00010000 /**< The value of bit 16 */
+#define BIT_17 0x00020000 /**< The value of bit 17 */
+#define BIT_18 0x00040000 /**< The value of bit 18 */
+#define BIT_19 0x00080000 /**< The value of bit 19 */
+#define BIT_20 0x00100000 /**< The value of bit 20 */
+#define BIT_21 0x00200000 /**< The value of bit 21 */
+#define BIT_22 0x00400000 /**< The value of bit 22 */
+#define BIT_23 0x00800000 /**< The value of bit 23 */
+#define BIT_24 0x01000000 /**< The value of bit 24 */
+#define BIT_25 0x02000000 /**< The value of bit 25 */
+#define BIT_26 0x04000000 /**< The value of bit 26 */
+#define BIT_27 0x08000000 /**< The value of bit 27 */
+#define BIT_28 0x10000000 /**< The value of bit 28 */
+#define BIT_29 0x20000000 /**< The value of bit 29 */
+#define BIT_30 0x40000000 /**< The value of bit 30 */
+#define BIT_31 0x80000000 /**< The value of bit 31 */
+
+#define UNUSED_VARIABLE(X) ((void)(X))
+#define UNUSED_PARAMETER(X) UNUSED_VARIABLE(X)
+#define UNUSED_RETURN_VALUE(X) UNUSED_VARIABLE(X)
+
+/**
+ * @brief Macro for calculating the number of words that are needed to hold a number of bytes.
+ *
+ * @details Adds 3 and divides by 4.
+ *
+ * @param[in] n_bytes The number of bytes.
+ *
+ * @return The number of words that @p n_bytes take up (rounded up).
+ */
+#define BYTES_TO_WORDS(n_bytes) (((n_bytes) + 3) >> 2)
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* NORDIC_COMMON_H__ */
diff --git a/lib/peer_manager/include/nrf_strerror.h b/lib/peer_manager/include/nrf_strerror.h
new file mode 100644
index 0000000000..de6ecaf797
--- /dev/null
+++ b/lib/peer_manager/include/nrf_strerror.h
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2017-2025 Nordic Semiconductor ASA
+ *
+ * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
+ */
+
+/**
+ * @defgroup nrf_strerror Error code to string converter
+ * @ingroup app_common
+ *
+ * @brief Module for converting error code into a printable string.
+ * @{
+ */
+#ifndef NRF_STRERROR_H__
+#define NRF_STRERROR_H__
+
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief Function for getting a printable error string.
+ *
+ * @param code Error code to convert.
+ *
+ * @note This function cannot fail.
+ * For the function that may fail with error translation, see @ref nrf_strerror_find.
+ *
+ * @return Pointer to the printable string.
+ * If the string is not found,
+ * it returns a simple string that says that the error is unknown.
+ */
+char const *nrf_strerror_get(uint32_t code);
+
+/**
+ * @brief Function for finding a printable error string.
+ *
+ * This function gets the error string in the same way as @ref nrf_strerror_get,
+ * but if the string is not found, it returns NULL.
+ *
+ * @param code Error code to convert.
+ * @return Pointer to the printable string.
+ * If the string is not found, NULL is returned.
+ */
+char const *nrf_strerror_find(uint32_t code);
+
+/** @} */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* NRF_STRERROR_H__ */
diff --git a/lib/peer_manager/include/sdk_macros.h b/lib/peer_manager/include/sdk_macros.h
new file mode 100644
index 0000000000..5631d3e6fc
--- /dev/null
+++ b/lib/peer_manager/include/sdk_macros.h
@@ -0,0 +1,178 @@
+/*
+ * Copyright (c) 2015-2025 Nordic Semiconductor ASA
+ *
+ * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
+ */
+
+/**
+ * @file
+ *
+ * @defgroup sdk_common_macros SDK Common Header
+ * @ingroup app_common
+ * @brief Macros for parameter checking and similar tasks
+ * @{
+ */
+
+#ifndef SDK_MACROS_H__
+#define SDK_MACROS_H__
+
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief Macro for parameter checking.
+ *
+ * If @p _cond evaluates to true, does nothing. Otherwise,
+ * if @p _module ## _PARAM_CHECK_DISABLED is @e not set (default), prints an error message
+ * if @p _printfn is provided, and returns from the calling function context with code @p _err.
+ * If @p _module ## _PARAM_CHECK_DISABLED is set, behaves like the ASSERT macro.
+ *
+ * Parameter checking implemented using this macro can be optionally turned off for release code.
+ * Only disable runtime parameter checks if size if a major concern.
+ *
+ * @param _module The module name.
+ * @param _cond The condition to be evaluated.
+ * @param _err The error to be returned.
+ * @param _printfn A printf-compatible function used to log the error.
+ * Leave empty if no logging is needed.
+ *
+ * @hideinitializer
+ */
+#define NRF_PARAM_CHECK(_module, _cond, _err, _printfn) \
+ do { \
+ if ((_cond)) { \
+ /* Do nothing. */ \
+ } else if (!(_module##_PARAM_CHECK_DISABLED)) { \
+ _printfn("%s check failed in %s() with value 0x%x.", #_cond, __func__, \
+ _err); \
+ return _err; \
+ } else { \
+ __ASSERT((_cond)); \
+ } \
+ } while (0)
+
+/**
+ * @brief Macro for verifying statement to be true. It will cause the exterior function to return
+ * err_code if the statement is not true.
+ *
+ * @param[in] statement Statement to test.
+ * @param[in] err_code Error value to return if test was invalid.
+ *
+ * @retval nothing, but will cause the exterior function to return @p err_code if @p statement
+ * is false.
+ */
+#define VERIFY_TRUE(statement, err_code) \
+ do { \
+ if (!(statement)) { \
+ return err_code; \
+ } \
+ } while (0)
+
+/**
+ * @brief Macro for verifying statement to be true. It will cause the exterior function to return
+ * if the statement is not true.
+ *
+ * @param[in] statement Statement to test.
+ */
+#define VERIFY_TRUE_VOID(statement) VERIFY_TRUE((statement),)
+
+/**
+ * @brief Macro for verifying statement to be false. It will cause the exterior function to return
+ * err_code if the statement is not false.
+ *
+ * @param[in] statement Statement to test.
+ * @param[in] err_code Error value to return if test was invalid.
+ *
+ * @retval nothing, but will cause the exterior function to return @p err_code if @p statement
+ * is true.
+ */
+#define VERIFY_FALSE(statement, err_code) \
+ do { \
+ if ((statement)) { \
+ return err_code; \
+ } \
+ } while (0)
+
+/**
+ * @brief Macro for verifying statement to be false. It will cause the exterior function to return
+ * if the statement is not false.
+ *
+ * @param[in] statement Statement to test.
+ */
+#define VERIFY_FALSE_VOID(statement) VERIFY_FALSE((statement),)
+
+/**
+ * @brief Macro for verifying that a function returned NRF_SUCCESS. It will cause the exterior
+ * function to return error code of statement if it is not @ref NRF_SUCCESS.
+ *
+ * @param[in] statement Statement to check against NRF_SUCCESS.
+ */
+#define VERIFY_SUCCESS(statement) \
+ do { \
+ uint32_t _err_code = (uint32_t)(statement); \
+ if (_err_code != NRF_SUCCESS) { \
+ return _err_code; \
+ } \
+ } while (0)
+
+/**
+ * @brief Macro for verifying that a function returned NRF_SUCCESS. It will cause the exterior
+ * function to return if the err_code is not @ref NRF_SUCCESS.
+ *
+ * @param[in] err_code The error code to check.
+ */
+#define VERIFY_SUCCESS_VOID(err_code) VERIFY_TRUE_VOID((err_code) == NRF_SUCCESS)
+
+/**
+ * @brief Macro for verifying that the module is initialized. It will cause the exterior function to
+ * return @ref NRF_ERROR_INVALID_STATE if not.
+ *
+ * @note MODULE_INITIALIZED must be defined in each module using this macro. MODULE_INITIALIZED
+ * should be true if the module is initialized, false if not.
+ */
+#define VERIFY_MODULE_INITIALIZED() VERIFY_TRUE((MODULE_INITIALIZED), NRF_ERROR_INVALID_STATE)
+
+/**
+ * @brief Macro for verifying that the module is initialized. It will cause the exterior function to
+ * return if not.
+ *
+ * @note MODULE_INITIALIZED must be defined in each module using this macro. MODULE_INITIALIZED
+ * should be true if the module is initialized, false if not.
+ */
+#define VERIFY_MODULE_INITIALIZED_VOID() VERIFY_TRUE_VOID((MODULE_INITIALIZED))
+
+/**
+ * @brief Macro for verifying that the module is initialized. It will cause the exterior function to
+ * return false if not.
+ *
+ * @note MODULE_INITIALIZED must be defined in each module using this macro. MODULE_INITIALIZED
+ * should be true if the module is initialized, false if not.
+ */
+#define VERIFY_MODULE_INITIALIZED_BOOL() VERIFY_TRUE((MODULE_INITIALIZED), false)
+
+/**
+ * @brief Macro for verifying that the module is initialized. It will cause the exterior function to
+ * return if not.
+ *
+ * @param[in] param The variable to check if is NULL.
+ */
+#define VERIFY_PARAM_NOT_NULL(param) VERIFY_FALSE(((param) == NULL), NRF_ERROR_NULL)
+
+/**
+ * @brief Macro for verifying that the module is initialized. It will cause the exterior function to
+ * return if not.
+ *
+ * @param[in] param The variable to check if is NULL.
+ */
+#define VERIFY_PARAM_NOT_NULL_VOID(param) VERIFY_FALSE_VOID(((param) == NULL))
+
+/** @} */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* SDK_MACROS_H__ */
diff --git a/lib/peer_manager/modules/CMakeLists.txt b/lib/peer_manager/modules/CMakeLists.txt
new file mode 100644
index 0000000000..2b437230dd
--- /dev/null
+++ b/lib/peer_manager/modules/CMakeLists.txt
@@ -0,0 +1,18 @@
+#
+# Copyright (c) 2025 Nordic Semiconductor ASA
+#
+# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
+#
+
+zephyr_library_sources(auth_status_tracker.c)
+zephyr_library_sources(gatt_cache_manager.c)
+zephyr_library_sources(gatts_cache_manager.c)
+zephyr_library_sources(id_manager.c)
+zephyr_library_sources_ifdef(CONFIG_PM_LESC_ENABLED nrf_ble_lesc.c)
+zephyr_library_sources(peer_data_storage.c)
+zephyr_library_sources(peer_database.c)
+zephyr_library_sources(peer_id.c)
+zephyr_library_sources(peer_manager_handler.c)
+zephyr_library_sources(pm_buffer.c)
+zephyr_library_sources(security_dispatcher.c)
+zephyr_library_sources(security_manager.c)
diff --git a/lib/peer_manager/modules/auth_status_tracker.c b/lib/peer_manager/modules/auth_status_tracker.c
new file mode 100644
index 0000000000..bd57e64d01
--- /dev/null
+++ b/lib/peer_manager/modules/auth_status_tracker.c
@@ -0,0 +1,278 @@
+/*
+ * Copyright (c) 2018-2025 Nordic Semiconductor ASA
+ *
+ * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
+ */
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+LOG_MODULE_DECLARE(peer_manager, CONFIG_PEER_MANAGER_LOG_LEVEL);
+
+/* Assume that waiting interval doubles with each failed authentication. */
+#define PAIR_REWARD_TICKS BM_TIMER_MS_TO_TICKS(CONFIG_PM_RA_PROTECTION_REWARD_PERIOD)
+#define PENALITY_LVL_TO_PENALITY_MS(_lvl) (CONFIG_PM_RA_PROTECTION_MIN_WAIT_INTERVAL * (1 << _lvl))
+#define PENALITY_LVL_TO_PENALITY_TICKS(_lvl) BM_TIMER_MS_TO_TICKS(PENALITY_LVL_TO_PENALITY_MS(_lvl))
+#define PENALITY_LVL_NEXT_SET(_lvl) \
+ _lvl = (PENALITY_LVL_TO_PENALITY_MS(_lvl) >= (CONFIG_PM_RA_PROTECTION_MAX_WAIT_INTERVAL)) \
+ ? (_lvl) \
+ : (_lvl + 1)
+
+/** @brief Tracked peer state. */
+typedef struct {
+ /** @brief BLE address, used to identify peer. */
+ ble_gap_addr_t peer_addr;
+ /**
+ * @brief Accumulated reward ticks, used to decrease penality level after achieving certain
+ * threshold.
+ */
+ uint32_t reward_ticks;
+ /**
+ * @brief Accumulated penality ticks, used to determine remaining time
+ * in which pairing attempts should be rejected.
+ */
+ uint32_t penality_ticks;
+ /**
+ * @brief Accumulated penality level, used to determine waiting interval
+ * after failed authorization attempt.
+ */
+ uint8_t penality_lvl;
+ /**
+ * @brief Flag indicating that the waiting interval for this peer has not
+ * passed yet.
+ */
+ bool is_active;
+ /** @brief Flag indicating that this entry is valid in the peer blacklist. */
+ bool is_valid;
+} blacklisted_peer_t;
+
+static struct bm_timer m_pairing_attempt_timer;
+static blacklisted_peer_t m_blacklisted_peers[CONFIG_PM_RA_PROTECTION_TRACKED_PEERS_NUM];
+static uint64_t m_ticks_cnt;
+
+/**
+ * @brief Function for updating the state of blacklisted peers after timer has been stopped or
+ * timed out.
+ *
+ * @param[in] ticks_passed The number of ticks since the timer has started.
+ */
+static uint32_t blacklisted_peers_state_update(uint32_t ticks_passed)
+{
+ uint32_t minimal_ticks = UINT32_MAX;
+
+ for (uint32_t id = 0; id < ARRAY_SIZE(m_blacklisted_peers); id++) {
+ blacklisted_peer_t *p_bl_peer = &m_blacklisted_peers[id];
+
+ if (p_bl_peer->is_valid) {
+ if (p_bl_peer->is_active) {
+ if (p_bl_peer->penality_ticks > ticks_passed) {
+ p_bl_peer->penality_ticks -= ticks_passed;
+ minimal_ticks =
+ MIN(minimal_ticks, p_bl_peer->penality_ticks);
+ } else {
+ p_bl_peer->is_active = false;
+
+ if (p_bl_peer->penality_lvl == 0) {
+ p_bl_peer->is_valid = false;
+ LOG_DBG("Peer has been removed from the "
+ "blacklist, its address:");
+ LOG_HEXDUMP_DBG(p_bl_peer->peer_addr.addr,
+ sizeof(p_bl_peer->peer_addr.addr), "");
+ } else {
+ minimal_ticks =
+ MIN(minimal_ticks, PAIR_REWARD_TICKS);
+ }
+
+ LOG_DBG("Pairing waiting interval has expired for:");
+ LOG_HEXDUMP_DBG(p_bl_peer->peer_addr.addr,
+ sizeof(p_bl_peer->peer_addr.addr), "");
+ }
+ } else {
+ if (p_bl_peer->penality_lvl == 0) {
+ p_bl_peer->is_valid = false;
+ LOG_DBG("Peer has been removed from the blacklist, "
+ "its address:");
+ LOG_HEXDUMP_DBG(p_bl_peer->peer_addr.addr,
+ sizeof(p_bl_peer->peer_addr.addr), "");
+ } else {
+ p_bl_peer->reward_ticks += ticks_passed;
+ if (p_bl_peer->reward_ticks >= PAIR_REWARD_TICKS) {
+ p_bl_peer->penality_lvl--;
+ p_bl_peer->reward_ticks -= PAIR_REWARD_TICKS;
+ LOG_DBG("Peer penality level has decreased "
+ "to %d for device:",
+ p_bl_peer->penality_lvl);
+ LOG_HEXDUMP_DBG(p_bl_peer->peer_addr.addr,
+ sizeof(p_bl_peer->peer_addr.addr), "");
+ }
+
+ minimal_ticks =
+ MIN(minimal_ticks,
+ (PAIR_REWARD_TICKS - p_bl_peer->reward_ticks));
+ }
+ }
+ }
+ }
+
+ return minimal_ticks;
+}
+
+/**
+ * @brief Function for handling state transition of blacklisted peers.
+ *
+ * @param[in] context Context containing the number of ticks since the timer has started.
+ */
+static void blacklisted_peers_state_transition_handle(void *context)
+{
+ int err;
+ uint32_t minimal_ticks;
+ uint32_t ticks_passed = (uint32_t)context;
+
+ minimal_ticks = blacklisted_peers_state_update(ticks_passed);
+ m_ticks_cnt = k_uptime_ticks();
+
+ if (minimal_ticks != UINT32_MAX) {
+ err = bm_timer_start(&m_pairing_attempt_timer, minimal_ticks,
+ (void *)minimal_ticks);
+ if (err) {
+ LOG_WRN("bm_timer_start() returned %d", err);
+ }
+ LOG_DBG("Restarting the timer");
+ }
+}
+
+uint32_t ast_init(void)
+{
+ int err_code = bm_timer_init(&m_pairing_attempt_timer, BM_TIMER_MODE_SINGLE_SHOT,
+ blacklisted_peers_state_transition_handle);
+ if (err_code) {
+ return NRF_ERROR_INTERNAL;
+ }
+
+ return NRF_SUCCESS;
+}
+
+void ast_auth_error_notify(uint16_t conn_handle)
+{
+ int err;
+ uint32_t err_code;
+ ble_gap_addr_t peer_addr;
+ uint32_t new_timeout;
+ uint32_t free_id = ARRAY_SIZE(m_blacklisted_peers);
+ bool new_bl_entry = true;
+
+ /* Get the peer address associated with connection handle. */
+ err_code = im_ble_addr_get(conn_handle, &peer_addr);
+ if (err_code != NRF_SUCCESS) {
+ LOG_WRN("im_ble_addr_get() returned %s. conn_handle: %d. "
+ "Link was likely disconnected.",
+ nrf_strerror_get(err_code), conn_handle);
+ return;
+ }
+
+ /* Stop the timer and update the state of all blacklisted peers. */
+ err = bm_timer_stop(&m_pairing_attempt_timer);
+ if (err) {
+ LOG_WRN("bm_timer_stop() returned %d", err);
+ return;
+ }
+
+ new_timeout = blacklisted_peers_state_update((uint32_t)(k_uptime_ticks() - m_ticks_cnt));
+ m_ticks_cnt = k_uptime_ticks();
+
+ /* Check if authorization has failed for already blacklisted peer. */
+ for (uint32_t id = 0; id < ARRAY_SIZE(m_blacklisted_peers); id++) {
+ blacklisted_peer_t *p_bl_peer = &m_blacklisted_peers[id];
+
+ if (p_bl_peer->is_valid) {
+ if (memcmp(peer_addr.addr, p_bl_peer->peer_addr.addr, BLE_GAP_ADDR_LEN) ==
+ 0) {
+ uint8_t lvl = p_bl_peer->penality_lvl;
+
+ PENALITY_LVL_NEXT_SET(lvl);
+ p_bl_peer->penality_lvl = lvl;
+ p_bl_peer->reward_ticks = 0;
+ p_bl_peer->penality_ticks = PENALITY_LVL_TO_PENALITY_TICKS(lvl);
+
+ new_timeout = MIN(new_timeout, p_bl_peer->penality_ticks);
+
+ p_bl_peer->is_active = true;
+ new_bl_entry = false;
+
+ LOG_DBG("Pairing waiting interval has been renewed. "
+ "Penality level: %d for device:",
+ lvl);
+ LOG_HEXDUMP_DBG(p_bl_peer->peer_addr.addr,
+ sizeof(p_bl_peer->peer_addr.addr), "");
+ }
+ } else {
+ free_id = id;
+ }
+ }
+
+ /* Add a new peer to the blacklist. */
+ if (new_bl_entry) {
+ if (free_id < ARRAY_SIZE(m_blacklisted_peers)) {
+ blacklisted_peer_t *p_bl_peer = &m_blacklisted_peers[free_id];
+
+ memcpy(&p_bl_peer->peer_addr, &peer_addr, sizeof(peer_addr));
+
+ p_bl_peer->penality_lvl = 0;
+ p_bl_peer->reward_ticks = 0;
+ p_bl_peer->penality_ticks =
+ PENALITY_LVL_TO_PENALITY_TICKS(p_bl_peer->penality_lvl);
+
+ new_timeout = MIN(new_timeout, p_bl_peer->penality_ticks);
+
+ p_bl_peer->is_active = true;
+ p_bl_peer->is_valid = true;
+ LOG_DBG("New peer has been added to the blacklist:");
+ LOG_HEXDUMP_DBG(p_bl_peer->peer_addr.addr,
+ sizeof(p_bl_peer->peer_addr.addr), "");
+ } else {
+ LOG_WRN("No space to blacklist another peer ID");
+ }
+ }
+
+ /* Restart the timer. */
+ if (new_timeout != UINT32_MAX) {
+ err = bm_timer_start(&m_pairing_attempt_timer, new_timeout, (void *)new_timeout);
+ if (err) {
+ LOG_WRN("bm_timer_start() returned %d", err);
+ }
+ }
+}
+
+bool ast_peer_blacklisted(uint16_t conn_handle)
+{
+ uint32_t err_code;
+ ble_gap_addr_t peer_addr;
+
+ err_code = im_ble_addr_get(conn_handle, &peer_addr);
+ if (err_code != NRF_SUCCESS) {
+ LOG_WRN("im_ble_addr_get() returned %s. conn_handle: %d. "
+ "Link was likely disconnected.",
+ nrf_strerror_get(err_code), conn_handle);
+ return true;
+ }
+
+ for (uint32_t id = 0; id < ARRAY_SIZE(m_blacklisted_peers); id++) {
+ blacklisted_peer_t *p_bl_peer = &m_blacklisted_peers[id];
+
+ if (p_bl_peer->is_valid) {
+ if ((memcmp(peer_addr.addr, p_bl_peer->peer_addr.addr, BLE_GAP_ADDR_LEN) ==
+ 0) &&
+ (p_bl_peer->is_active)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
diff --git a/lib/peer_manager/modules/gatt_cache_manager.c b/lib/peer_manager/modules/gatt_cache_manager.c
new file mode 100644
index 0000000000..f63c9cd016
--- /dev/null
+++ b/lib/peer_manager/modules/gatt_cache_manager.c
@@ -0,0 +1,807 @@
+/*
+ * Copyright (c) 2015-2025 Nordic Semiconductor ASA
+ *
+ * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
+ */
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+LOG_MODULE_DECLARE(peer_manager, CONFIG_PEER_MANAGER_LOG_LEVEL);
+
+#define NRF_MTX_LOCKED 1
+#define NRF_MTX_UNLOCKED 0
+
+typedef atomic_t nrf_mtx_t;
+
+/**
+ * @brief Initialize mutex.
+ *
+ * This function _must_ be called before nrf_mtx_trylock() and nrf_mtx_unlock() functions.
+ *
+ * @param[in, out] p_mtx The mutex to be initialized.
+ */
+__STATIC_INLINE void nrf_mtx_init(nrf_mtx_t *p_mtx)
+{
+ __ASSERT(p_mtx != NULL, "");
+
+ atomic_set(p_mtx, NRF_MTX_UNLOCKED);
+}
+
+/**
+ * @brief Try to lock a mutex.
+ *
+ * If the mutex is already held by another context, this function will return immediately.
+ *
+ * @param[in, out] p_mtx The mutex to be locked.
+ * @return true if lock was acquired, false if not
+ */
+__STATIC_INLINE bool nrf_mtx_trylock(nrf_mtx_t *p_mtx)
+{
+ __ASSERT(p_mtx != NULL, "");
+
+ return atomic_cas(p_mtx, NRF_MTX_UNLOCKED, NRF_MTX_LOCKED);
+}
+
+/**
+ * @brief Unlock a mutex.
+ *
+ * This function _must_ only be called when holding the lock. Unlocking a mutex which you do not
+ * hold will give undefined behavior.
+ *
+ * @note Unlock must happen from the same context as the one used to lock the mutex.
+ *
+ * @param[in, out] p_mtx The mutex to be unlocked.
+ */
+__STATIC_INLINE void nrf_mtx_unlock(nrf_mtx_t *p_mtx)
+{
+ __ASSERT(p_mtx != NULL, "");
+ __ASSERT(*p_mtx == NRF_MTX_LOCKED, "");
+
+ atomic_set(p_mtx, NRF_MTX_UNLOCKED);
+}
+
+/* The number of registered event handlers. */
+#define GCM_EVENT_HANDLERS_CNT ARRAY_SIZE(m_evt_handlers)
+
+/* GATT Cache Manager event handler in Peer Manager. */
+extern void pm_gcm_evt_handler(pm_evt_t *p_gcm_evt);
+
+/**
+ * @brief GATT Cache Manager events' handlers.
+ * The number of elements in this array is GCM_EVENT_HANDLERS_CNT.
+ */
+static pm_evt_handler_internal_t m_evt_handlers[] = {pm_gcm_evt_handler};
+
+static bool m_module_initialized;
+/** @brief Mutex indicating whether a local DB write operation is ongoing. */
+static nrf_mtx_t m_db_update_in_progress_mutex;
+/**
+ * @brief Flag ID for flag collection to keep track of which connections need a local DB update
+ * procedure.
+ */
+static int m_flag_local_db_update_pending;
+/**
+ * @brief Flag ID for flag collection to keep track of which connections need a local DB apply
+ * procedure.
+ */
+static int m_flag_local_db_apply_pending;
+/**
+ * @brief Flag ID for flag collection to keep track of which connections need to be sent a service
+ * changed indication.
+ */
+static int m_flag_service_changed_pending;
+/**
+ * @brief Flag ID for flag collection to keep track of which connections have been sent a service
+ * changed indication and are waiting for a handle value confirmation.
+ */
+static int m_flag_service_changed_sent;
+/**
+ * @brief Flag ID for flag collection to keep track of which connections need to have their Central
+ * Address Resolution value stored.
+ */
+static int m_flag_car_update_pending;
+/**
+ * @brief Flag ID for flag collection to keep track of which connections are pending Central
+ * Address Resolution handle reply.
+ */
+static int m_flag_car_handle_queried;
+/**
+ * @brief Flag ID for flag collection to keep track of which connections are pending Central
+ * Address Resolution value reply.
+ */
+static int m_flag_car_value_queried;
+
+#ifdef CONFIG_PM_SERVICE_CHANGED_ENABLED
+BUILD_ASSERT(IS_ENABLED(CONFIG_PM_SERVICE_CHANGED_ENABLED) ||
+ !IS_ENABLED(CONFIG_NRF_SDH_BLE_SERVICE_CHANGED),
+ "CONFIG_PM_SERVICE_CHANGED_ENABLED should be enabled "
+ "if NRF_SDH_BLE_SERVICE_CHANGED is enabled.");
+#else
+#define CONFIG_PM_SERVICE_CHANGED_ENABLED 1
+#endif
+
+/**
+ * @brief Function for resetting the module variable(s) of the GSCM module.
+ *
+ * @param[out] The instance to reset.
+ */
+static void internal_state_reset(void)
+{
+ m_module_initialized = false;
+}
+
+static void evt_send(pm_evt_t *p_gcm_evt)
+{
+ p_gcm_evt->peer_id = im_peer_id_get_by_conn_handle(p_gcm_evt->conn_handle);
+
+ for (uint32_t i = 0; i < GCM_EVENT_HANDLERS_CNT; i++) {
+ m_evt_handlers[i](p_gcm_evt);
+ }
+}
+
+/**
+ * @brief Function for checking a write event for whether a CCCD was written during the write
+ * operation.
+ *
+ * @param[in] p_write_evt The parameters of the write event.
+ *
+ * @return Whether the write was on a CCCD.
+ */
+static bool cccd_written(ble_gatts_evt_write_t const *p_write_evt)
+{
+ return ((p_write_evt->op == BLE_GATTS_OP_WRITE_REQ) &&
+ (p_write_evt->uuid.type == BLE_UUID_TYPE_BLE) &&
+ (p_write_evt->uuid.uuid == BLE_UUID_DESCRIPTOR_CLIENT_CHAR_CONFIG));
+}
+
+/**
+ * @brief Function for sending an PM_EVT_ERROR_UNEXPECTED event.
+ *
+ * @param[in] conn_handle The connection handle the event pertains to.
+ * @param[in] err_code The unexpected error that occurred.
+ */
+static void send_unexpected_error(uint16_t conn_handle, uint32_t err_code)
+{
+ pm_evt_t error_evt = {.evt_id = PM_EVT_ERROR_UNEXPECTED,
+ .conn_handle = conn_handle,
+ .params = {.error_unexpected = {.error = err_code}}};
+
+ evt_send(&error_evt);
+}
+
+/**
+ * @brief Function for performing the local DB update procedure in an event context, where no return
+ * code can be given.
+ *
+ * @details This function will do the procedure, and check the result, set a flag if needed, and
+ * send an event if needed.
+ *
+ * @param[in] conn_handle The connection to perform the procedure on.
+ */
+static void local_db_apply_in_evt(uint16_t conn_handle)
+{
+ bool set_procedure_as_pending = false;
+ uint32_t err_code;
+ pm_evt_t event = {
+ .conn_handle = conn_handle,
+ };
+
+ if (conn_handle == BLE_CONN_HANDLE_INVALID) {
+ return;
+ }
+
+ err_code = gscm_local_db_cache_apply(conn_handle);
+
+ switch (err_code) {
+ case NRF_SUCCESS:
+ event.evt_id = PM_EVT_LOCAL_DB_CACHE_APPLIED;
+
+ evt_send(&event);
+ break;
+
+ case NRF_ERROR_BUSY:
+ set_procedure_as_pending = true;
+ break;
+
+ case NRF_ERROR_INVALID_DATA:
+ event.evt_id = PM_EVT_LOCAL_DB_CACHE_APPLY_FAILED;
+
+ LOG_WRN("The local database has changed, so some subscriptions to notifications "
+ "and indications could not be restored for conn_handle %d",
+ conn_handle);
+ evt_send(&event);
+ break;
+
+ case BLE_ERROR_INVALID_CONN_HANDLE:
+ /* Do nothing */
+ break;
+
+ default:
+ LOG_ERR("gscm_local_db_cache_apply() returned %s which should not happen. "
+ "conn_handle: %d",
+ nrf_strerror_get(err_code), conn_handle);
+ send_unexpected_error(conn_handle, err_code);
+ break;
+ }
+
+ ble_conn_state_user_flag_set(conn_handle, m_flag_local_db_apply_pending,
+ set_procedure_as_pending);
+}
+
+/**
+ * @brief Function for asynchronously starting a DB update procedure.
+ *
+ * @note This procedure can only be started asynchronously.
+ *
+ * @param[in] conn_handle The connection to perform the procedure on.
+ * @param[in] update Whether to perform the procedure.
+ */
+static __INLINE void local_db_update(uint16_t conn_handle, bool update)
+{
+ ble_conn_state_user_flag_set(conn_handle, m_flag_local_db_update_pending, update);
+}
+
+/**
+ * @brief Function for performing the local DB update procedure in an event context, where no return
+ * code can be given.
+ *
+ * @details This function will do the procedure, and check the result, set a flag if needed, and
+ * send an event if needed.
+ *
+ * @param[in] conn_handle The connection to perform the procedure on.
+ */
+static bool local_db_update_in_evt(uint16_t conn_handle)
+{
+ bool set_procedure_as_pending = false;
+ bool success = false;
+ uint32_t err_code = gscm_local_db_cache_update(conn_handle);
+
+ switch (err_code) {
+ case NRF_SUCCESS:
+ success = true;
+ break;
+
+ case NRF_ERROR_INVALID_DATA:
+ /* Fallthrough */
+ case BLE_ERROR_INVALID_CONN_HANDLE:
+ /* Do nothing */
+ break;
+
+ case NRF_ERROR_BUSY:
+ set_procedure_as_pending = true;
+ break;
+
+ case NRF_ERROR_RESOURCES: {
+ pm_evt_t event = {
+ .evt_id = PM_EVT_STORAGE_FULL,
+ .conn_handle = conn_handle,
+ };
+
+ LOG_WRN("Flash full. Could not store data for conn_handle: %d", conn_handle);
+ evt_send(&event);
+ break;
+ }
+
+ default:
+ LOG_ERR("gscm_local_db_cache_update() returned %s for conn_handle: %d",
+ nrf_strerror_get(err_code), conn_handle);
+ send_unexpected_error(conn_handle, err_code);
+ break;
+ }
+
+ local_db_update(conn_handle, set_procedure_as_pending);
+
+ return success;
+}
+
+#if CONFIG_PM_SERVICE_CHANGED_ENABLED
+
+/**
+ * @brief Function for getting the value of the CCCD for the service changed characteristic.
+ *
+ * @details This function will search all system handles consecutively.
+ *
+ * @param[in] conn_handle The connection to check.
+ * @param[out] p_cccd The CCCD value of the service changed characteristic for this link.
+ *
+ * @return Any error from @ref sd_ble_gatts_value_get or @ref sd_ble_gatts_attr_get.
+ */
+static uint32_t service_changed_cccd(uint16_t conn_handle, uint16_t *p_cccd)
+{
+ bool sc_found = false;
+ uint16_t end_handle;
+
+ uint32_t err_code = sd_ble_gatts_initial_user_handle_get(&end_handle);
+
+ __ASSERT(err_code == NRF_SUCCESS, "");
+
+ for (uint16_t handle = 1; handle < end_handle; handle++) {
+ ble_uuid_t uuid;
+ ble_gatts_value_t value = {.p_value = (uint8_t *)&uuid.uuid, .len = 2, .offset = 0};
+
+ err_code = sd_ble_gatts_attr_get(handle, &uuid, NULL);
+ if (err_code != NRF_SUCCESS) {
+ return err_code;
+ } else if (!sc_found &&
+ (uuid.uuid == BLE_UUID_GATT_CHARACTERISTIC_SERVICE_CHANGED)) {
+ sc_found = true;
+ } else if (sc_found && (uuid.uuid == BLE_UUID_DESCRIPTOR_CLIENT_CHAR_CONFIG)) {
+ value.p_value = (uint8_t *)p_cccd;
+ return sd_ble_gatts_value_get(conn_handle, handle, &value);
+ }
+ }
+ return NRF_ERROR_NOT_FOUND;
+}
+
+/**
+ * @brief Function for sending a service changed indication in an event context, where no return
+ * code can be given.
+ *
+ * @details This function will do the procedure, and check the result, set a flag if needed, and
+ * send an event if needed.
+ *
+ * @param[in] conn_handle The connection to perform the procedure on.
+ */
+static void service_changed_send_in_evt(uint16_t conn_handle)
+{
+ bool sc_pending_state = true;
+ bool sc_sent_state = false;
+ uint32_t err_code = gscm_service_changed_ind_send(conn_handle);
+
+ switch (err_code) {
+ case NRF_SUCCESS: {
+ pm_evt_t event = {
+ .evt_id = PM_EVT_SERVICE_CHANGED_IND_SENT,
+ .conn_handle = conn_handle,
+ };
+
+ sc_sent_state = true;
+
+ evt_send(&event);
+ break;
+ }
+
+ case NRF_ERROR_BUSY:
+ /* Do nothing. */
+ break;
+
+ case NRF_ERROR_INVALID_STATE: {
+ uint16_t cccd;
+
+ err_code = service_changed_cccd(conn_handle, &cccd);
+ if ((err_code == NRF_SUCCESS) && cccd) {
+ /* Possible ATT_MTU exchange ongoing. */
+ /* Do nothing, treat as busy. */
+ break;
+ }
+ if (err_code != NRF_SUCCESS) {
+ LOG_DBG("Unexpected error when looking for service changed "
+ "CCCD: %s",
+ nrf_strerror_get(err_code));
+ }
+ /* CCCDs not enabled or an error happened. Drop indication. */
+ /* Fallthrough. */
+ }
+ /* Sometimes fallthrough. */
+ case NRF_ERROR_NOT_SUPPORTED:
+ /* Service changed not supported. Drop indication. */
+ sc_pending_state = false;
+ gscm_db_change_notification_done(im_peer_id_get_by_conn_handle(conn_handle));
+ break;
+
+ case BLE_ERROR_GATTS_SYS_ATTR_MISSING:
+ local_db_apply_in_evt(conn_handle);
+ break;
+
+ case BLE_ERROR_INVALID_CONN_HANDLE:
+ /* Do nothing. */
+ break;
+
+ default:
+ LOG_ERR("gscm_service_changed_ind_send() returned %s for conn_handle: %d",
+ nrf_strerror_get(err_code), conn_handle);
+ send_unexpected_error(conn_handle, err_code);
+ break;
+ }
+
+ ble_conn_state_user_flag_set(conn_handle, m_flag_service_changed_pending, sc_pending_state);
+ ble_conn_state_user_flag_set(conn_handle, m_flag_service_changed_sent, sc_sent_state);
+}
+#endif
+
+static void apply_pending_handle(uint16_t conn_handle, void *p_context)
+{
+ UNUSED_PARAMETER(p_context);
+ local_db_apply_in_evt(conn_handle);
+}
+
+static __INLINE void apply_pending_flags_check(void)
+{
+ UNUSED_RETURN_VALUE(ble_conn_state_for_each_set_user_flag(m_flag_local_db_apply_pending,
+ apply_pending_handle, NULL));
+}
+
+static void db_update_pending_handle(uint16_t conn_handle, void *p_context)
+{
+ UNUSED_PARAMETER(p_context);
+ if (nrf_mtx_trylock(&m_db_update_in_progress_mutex)) {
+ if (local_db_update_in_evt(conn_handle)) {
+ /* Successfully started writing to flash. */
+ return;
+ }
+
+ nrf_mtx_unlock(&m_db_update_in_progress_mutex);
+ }
+}
+
+#if CONFIG_PM_SERVICE_CHANGED_ENABLED
+static void sc_send_pending_handle(uint16_t conn_handle, void *p_context)
+{
+ UNUSED_PARAMETER(p_context);
+ if (!ble_conn_state_user_flag_get(conn_handle, m_flag_service_changed_sent)) {
+ service_changed_send_in_evt(conn_handle);
+ }
+}
+
+static __INLINE void service_changed_pending_flags_check(void)
+{
+ UNUSED_RETURN_VALUE(ble_conn_state_for_each_set_user_flag(m_flag_service_changed_pending,
+ sc_send_pending_handle, NULL));
+}
+
+static void service_changed_needed(uint16_t conn_handle)
+{
+ if (gscm_service_changed_ind_needed(conn_handle)) {
+ ble_conn_state_user_flag_set(conn_handle, m_flag_service_changed_pending, true);
+ }
+}
+#endif
+
+static void car_update_pending_handle(uint16_t conn_handle, void *p_context)
+{
+ UNUSED_PARAMETER(p_context);
+
+ ble_uuid_t car_uuid;
+
+ memset(&car_uuid, 0, sizeof(ble_uuid_t));
+ car_uuid.uuid = BLE_UUID_GAP_CHARACTERISTIC_CAR;
+ car_uuid.type = BLE_UUID_TYPE_BLE;
+
+ ble_gattc_handle_range_t const car_handle_range = {1, 0xFFFF};
+
+ uint32_t err_code =
+ sd_ble_gattc_char_value_by_uuid_read(conn_handle, &car_uuid, &car_handle_range);
+
+ if (err_code == NRF_SUCCESS) {
+ ble_conn_state_user_flag_set(conn_handle, m_flag_car_handle_queried, true);
+ }
+}
+
+static void car_update_needed(uint16_t conn_handle)
+{
+ pm_peer_data_t peer_data;
+
+ if (pds_peer_data_read(im_peer_id_get_by_conn_handle(conn_handle),
+ PM_PEER_DATA_ID_CENTRAL_ADDR_RES, &peer_data,
+ NULL) == NRF_ERROR_NOT_FOUND) {
+ ble_conn_state_user_flag_set(conn_handle, m_flag_car_update_pending, true);
+ }
+}
+
+static __INLINE void update_pending_flags_check(void)
+{
+ uint32_t count = ble_conn_state_for_each_set_user_flag(m_flag_local_db_update_pending,
+ db_update_pending_handle, NULL);
+ if (count == 0) {
+ count = ble_conn_state_for_each_set_user_flag(m_flag_car_update_pending,
+ car_update_pending_handle, NULL);
+ UNUSED_RETURN_VALUE(count);
+ }
+}
+
+/**
+ * @brief Callback function for events from the ID Manager module.
+ * This function is registered in the ID Manager module.
+ *
+ * @param[in] p_event The event from the ID Manager module.
+ */
+void gcm_im_evt_handler(pm_evt_t *p_event)
+{
+ switch (p_event->evt_id) {
+ case PM_EVT_BONDED_PEER_CONNECTED:
+ local_db_apply_in_evt(p_event->conn_handle);
+#if (CONFIG_PM_SERVICE_CHANGED_ENABLED == 1)
+ service_changed_needed(p_event->conn_handle);
+#endif
+ car_update_needed(p_event->conn_handle);
+ update_pending_flags_check();
+ break;
+ default:
+ break;
+ }
+}
+
+/**
+ * @brief Callback function for events from the Peer Database module.
+ * This handler is extern in Peer Database.
+ *
+ * @param[in] p_event The event from the Security Dispatcher module.
+ */
+void gcm_pdb_evt_handler(pm_evt_t *p_event)
+{
+ if (p_event->evt_id == PM_EVT_PEER_DATA_UPDATE_SUCCEEDED &&
+ p_event->params.peer_data_update_succeeded.action == PM_PEER_DATA_OP_UPDATE) {
+ switch (p_event->params.peer_data_update_succeeded.data_id) {
+ case PM_PEER_DATA_ID_BONDING: {
+ uint16_t conn_handle = im_conn_handle_get(p_event->peer_id);
+
+ if (conn_handle != BLE_CONN_HANDLE_INVALID) {
+ local_db_update(conn_handle, true);
+ car_update_needed(conn_handle);
+ }
+ break;
+ }
+
+#if CONFIG_PM_SERVICE_CHANGED_ENABLED
+ case PM_PEER_DATA_ID_SERVICE_CHANGED_PENDING: {
+ uint32_t err_code;
+ pm_peer_data_flash_t peer_data;
+
+ err_code = pdb_peer_data_ptr_get(p_event->peer_id,
+ PM_PEER_DATA_ID_SERVICE_CHANGED_PENDING,
+ &peer_data);
+
+ if (err_code == NRF_SUCCESS) {
+ if (*peer_data.p_service_changed_pending) {
+ uint16_t conn_handle = im_conn_handle_get(p_event->peer_id);
+
+ if (conn_handle != BLE_CONN_HANDLE_INVALID) {
+ ble_conn_state_user_flag_set(
+ conn_handle, m_flag_service_changed_pending,
+ true);
+ service_changed_pending_flags_check();
+ }
+ }
+ }
+ break;
+ }
+#endif
+
+ case PM_PEER_DATA_ID_GATT_LOCAL:
+ if (m_db_update_in_progress_mutex == NRF_MTX_LOCKED) {
+ nrf_mtx_unlock(&m_db_update_in_progress_mutex);
+ }
+
+ /* Expecting a call to update_pending_flags_check() immediately. */
+ break;
+
+ default:
+ /* No action */
+ break;
+ }
+ }
+
+ update_pending_flags_check();
+}
+
+uint32_t gcm_init(void)
+{
+ NRF_PM_DEBUG_CHECK(!m_module_initialized);
+
+ internal_state_reset();
+
+ m_flag_local_db_update_pending = ble_conn_state_user_flag_acquire();
+ m_flag_local_db_apply_pending = ble_conn_state_user_flag_acquire();
+ m_flag_service_changed_pending = ble_conn_state_user_flag_acquire();
+ m_flag_service_changed_sent = ble_conn_state_user_flag_acquire();
+ m_flag_car_update_pending = ble_conn_state_user_flag_acquire();
+ m_flag_car_handle_queried = ble_conn_state_user_flag_acquire();
+ m_flag_car_value_queried = ble_conn_state_user_flag_acquire();
+
+ if ((m_flag_local_db_update_pending == CONFIG_BLE_CONN_STATE_USER_FLAG_COUNT) ||
+ (m_flag_local_db_apply_pending == CONFIG_BLE_CONN_STATE_USER_FLAG_COUNT) ||
+ (m_flag_service_changed_pending == CONFIG_BLE_CONN_STATE_USER_FLAG_COUNT) ||
+ (m_flag_service_changed_sent == CONFIG_BLE_CONN_STATE_USER_FLAG_COUNT) ||
+ (m_flag_car_update_pending == CONFIG_BLE_CONN_STATE_USER_FLAG_COUNT) ||
+ (m_flag_car_handle_queried == CONFIG_BLE_CONN_STATE_USER_FLAG_COUNT) ||
+ (m_flag_car_value_queried == CONFIG_BLE_CONN_STATE_USER_FLAG_COUNT)) {
+ LOG_ERR("Could not acquire conn_state user flags. Increase "
+ "BLE_CONN_STATE_USER_FLAG_COUNT in the ble_conn_state module.");
+ return NRF_ERROR_INTERNAL;
+ }
+
+ nrf_mtx_init(&m_db_update_in_progress_mutex);
+
+ m_module_initialized = true;
+
+ return NRF_SUCCESS;
+}
+
+void store_car_value(uint16_t conn_handle, bool car_value)
+{
+ /* Use a uint32_t to enforce 4-byte alignment. */
+ static const uint32_t car_value_true = true;
+ static const uint32_t car_value_false;
+
+ pm_peer_data_const_t peer_data = {
+ .data_id = PM_PEER_DATA_ID_CENTRAL_ADDR_RES,
+ .length_words = 1,
+ };
+
+ ble_conn_state_user_flag_set(conn_handle, m_flag_car_update_pending, false);
+ peer_data.p_central_addr_res = car_value ? &car_value_true : &car_value_false;
+ uint32_t err_code =
+ pds_peer_data_store(im_peer_id_get_by_conn_handle(conn_handle), &peer_data, NULL);
+ if (err_code != NRF_SUCCESS) {
+ LOG_WRN("CAR char value couldn't be stored (error: %s). Reattempt will "
+ "happen on the next connection.",
+ nrf_strerror_get(err_code));
+ }
+}
+
+/**
+ * @brief Callback function for BLE events from the SoftDevice.
+ *
+ * @param[in] p_ble_evt The BLE event from the SoftDevice.
+ */
+void gcm_ble_evt_handler(ble_evt_t const *p_ble_evt)
+{
+ uint16_t conn_handle = p_ble_evt->evt.gatts_evt.conn_handle;
+
+ switch (p_ble_evt->header.evt_id) {
+ case BLE_GATTS_EVT_SYS_ATTR_MISSING:
+ local_db_apply_in_evt(conn_handle);
+ break;
+
+#if CONFIG_PM_SERVICE_CHANGED_ENABLED
+ case BLE_GATTS_EVT_SC_CONFIRM: {
+ pm_evt_t event = {
+ .evt_id = PM_EVT_SERVICE_CHANGED_IND_CONFIRMED,
+ .peer_id = im_peer_id_get_by_conn_handle(conn_handle),
+ .conn_handle = conn_handle,
+ };
+
+ gscm_db_change_notification_done(event.peer_id);
+
+ ble_conn_state_user_flag_set(conn_handle, m_flag_service_changed_sent, false);
+ ble_conn_state_user_flag_set(conn_handle, m_flag_service_changed_pending, false);
+ evt_send(&event);
+ break;
+ }
+#endif
+
+ case BLE_GATTS_EVT_WRITE:
+ if (cccd_written(&p_ble_evt->evt.gatts_evt.params.write)) {
+ local_db_update(conn_handle, true);
+ update_pending_flags_check();
+ }
+ break;
+
+ case BLE_GATTC_EVT_CHAR_VAL_BY_UUID_READ_RSP: {
+ bool handle_found = false;
+
+ conn_handle = p_ble_evt->evt.gattc_evt.conn_handle;
+ const ble_gattc_evt_char_val_by_uuid_read_rsp_t *p_val =
+ &p_ble_evt->evt.gattc_evt.params.char_val_by_uuid_read_rsp;
+
+ if (!ble_conn_state_user_flag_get(conn_handle, m_flag_car_handle_queried)) {
+ break;
+ }
+
+ ble_conn_state_user_flag_set(conn_handle, m_flag_car_handle_queried, false);
+
+ if (p_ble_evt->evt.gattc_evt.gatt_status ==
+ BLE_GATT_STATUS_ATTERR_ATTRIBUTE_NOT_FOUND) {
+ /* Store 0. */
+ } else if (p_ble_evt->evt.gattc_evt.gatt_status != BLE_GATT_STATUS_SUCCESS) {
+ LOG_WRN("Unexpected GATT status while getting CAR char value: 0x%x",
+ p_ble_evt->evt.gattc_evt.gatt_status);
+ /* Store 0. */
+ } else {
+ if (p_val->count != 1) {
+ LOG_WRN("Multiple (%d) CAR characteristics found, using the first.",
+ p_val->count);
+ }
+
+ if (p_val->value_len != 1) {
+ LOG_WRN("Unexpected CAR characteristic value length (%d), store 0.",
+ p_val->value_len);
+ /* Store 0. */
+ } else {
+ uint32_t err_code = sd_ble_gattc_read(
+ conn_handle, *(uint16_t *)p_val->handle_value, 0);
+
+ if (err_code == NRF_SUCCESS) {
+ handle_found = true;
+ ble_conn_state_user_flag_set(
+ conn_handle, m_flag_car_value_queried, true);
+ }
+ }
+ }
+
+ if (!handle_found) {
+ store_car_value(conn_handle, false);
+ }
+ break;
+ }
+
+ case BLE_GATTC_EVT_READ_RSP: {
+ bool car_value = false;
+
+ conn_handle = p_ble_evt->evt.gattc_evt.conn_handle;
+ const ble_gattc_evt_read_rsp_t *p_val = &p_ble_evt->evt.gattc_evt.params.read_rsp;
+
+ if (!ble_conn_state_user_flag_get(conn_handle, m_flag_car_value_queried)) {
+ break;
+ }
+
+ ble_conn_state_user_flag_set(conn_handle, m_flag_car_value_queried, false);
+
+ if (p_ble_evt->evt.gattc_evt.gatt_status != BLE_GATT_STATUS_SUCCESS) {
+ LOG_WRN("Unexpected GATT status while getting CAR char value: 0x%x",
+ p_ble_evt->evt.gattc_evt.gatt_status);
+ /* Store 0. */
+ } else {
+ if (p_val->len != 1) {
+ LOG_WRN("Unexpected CAR characteristic value length (%d), store 0.",
+ p_val->len);
+ /* Store 0. */
+ } else {
+ car_value = *p_val->data;
+ }
+ }
+
+ store_car_value(conn_handle, car_value);
+ }
+ }
+
+ apply_pending_flags_check();
+#if CONFIG_PM_SERVICE_CHANGED_ENABLED
+ service_changed_pending_flags_check();
+#endif
+}
+
+uint32_t gcm_local_db_cache_update(uint16_t conn_handle)
+{
+ NRF_PM_DEBUG_CHECK(m_module_initialized);
+
+ local_db_update(conn_handle, true);
+ update_pending_flags_check();
+
+ return NRF_SUCCESS;
+}
+
+#if CONFIG_PM_SERVICE_CHANGED_ENABLED
+void gcm_local_database_has_changed(void)
+{
+ gscm_local_database_has_changed();
+
+ struct ble_conn_state_conn_handle_list conn_handles = ble_conn_state_conn_handles();
+
+ for (uint16_t i = 0; i < conn_handles.len; i++) {
+ if (im_peer_id_get_by_conn_handle(conn_handles.conn_handles[i]) ==
+ PM_PEER_ID_INVALID) {
+ ble_conn_state_user_flag_set(conn_handles.conn_handles[i],
+ m_flag_service_changed_pending, true);
+ }
+ }
+
+ service_changed_pending_flags_check();
+}
+#endif
diff --git a/lib/peer_manager/modules/gatts_cache_manager.c b/lib/peer_manager/modules/gatts_cache_manager.c
new file mode 100644
index 0000000000..152cd2cc61
--- /dev/null
+++ b/lib/peer_manager/modules/gatts_cache_manager.c
@@ -0,0 +1,374 @@
+/*
+ * Copyright (c) 2015-2025 Nordic Semiconductor ASA
+ *
+ * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
+ */
+
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+LOG_MODULE_DECLARE(peer_manager, CONFIG_PEER_MANAGER_LOG_LEVEL);
+
+#if !defined(CONFIG_PM_SERVICE_CHANGED_ENABLED) || (CONFIG_PM_SERVICE_CHANGED_ENABLED == 1)
+
+/* The number of registered event handlers. */
+#define GSCM_EVENT_HANDLERS_CNT ARRAY_SIZE(m_evt_handlers)
+
+/* GATTS Cache Manager event handler in Peer Manager. */
+extern void pm_gscm_evt_handler(pm_evt_t *p_gcm_evt);
+
+/* GATTS Cache Manager events' handlers.
+ * The number of elements in this array is GSCM_EVENT_HANDLERS_CNT.
+ */
+static pm_evt_handler_internal_t m_evt_handlers[] = {pm_gscm_evt_handler};
+#endif
+
+/* Syntactic sugar, two spoons. */
+#define SYS_ATTR_SYS (BLE_GATTS_SYS_ATTR_FLAG_SYS_SRVCS)
+#define SYS_ATTR_USR (BLE_GATTS_SYS_ATTR_FLAG_USR_SRVCS)
+#define SYS_ATTR_BOTH (SYS_ATTR_SYS | SYS_ATTR_USR)
+
+static bool m_module_initialized;
+static pm_peer_id_t m_current_sc_store_peer_id;
+
+/** @brief Function for resetting the module variable(s) of the GSCM module. */
+static void internal_state_reset(void)
+{
+ m_module_initialized = false;
+ m_current_sc_store_peer_id = PM_PEER_ID_INVALID;
+
+ /* If CONFIG_PM_SERVICE_CHANGED_ENABLED is 0, this variable is unused. */
+ UNUSED_VARIABLE(m_current_sc_store_peer_id);
+}
+
+#if !defined(CONFIG_PM_SERVICE_CHANGED_ENABLED) || (CONFIG_PM_SERVICE_CHANGED_ENABLED == 1)
+static void evt_send(pm_evt_t *p_gscm_evt)
+{
+ p_gscm_evt->conn_handle = im_conn_handle_get(p_gscm_evt->peer_id);
+
+ for (uint32_t i = 0; i < GSCM_EVENT_HANDLERS_CNT; i++) {
+ m_evt_handlers[i](p_gscm_evt);
+ }
+}
+
+/**
+ * @brief Function for storing service_changed_pending = true to flash for all peers, in sequence.
+ *
+ * This function aborts if it gets @ref NRF_ERROR_BUSY when trying to store. A subsequent call will
+ * continue where the last call was aborted.
+ */
+static void service_changed_pending_set(void)
+{
+ NRF_PM_DEBUG_CHECK(m_module_initialized);
+
+ uint32_t err_code;
+ /* Use a uint32_t to enforce 4-byte alignment. */
+ static const uint32_t service_changed_pending = true;
+
+ pm_peer_data_const_t peer_data = {
+ .data_id = PM_PEER_DATA_ID_SERVICE_CHANGED_PENDING,
+ .length_words = PM_SC_STATE_N_WORDS(),
+ .p_service_changed_pending = (bool *)&service_changed_pending,
+ };
+
+ while (m_current_sc_store_peer_id != PM_PEER_ID_INVALID) {
+ err_code = pds_peer_data_store(m_current_sc_store_peer_id, &peer_data, NULL);
+ if (err_code != NRF_SUCCESS) {
+ pm_evt_t evt = {.peer_id = m_current_sc_store_peer_id};
+
+ if (err_code == NRF_ERROR_BUSY) {
+ /* Do nothing. */
+ } else if (err_code == NRF_ERROR_RESOURCES) {
+ evt.evt_id = PM_EVT_STORAGE_FULL;
+ evt_send(&evt);
+ } else {
+ LOG_ERR("pds_peer_data_store() returned %s while storing "
+ "service changed "
+ "state for peer id %d.",
+ nrf_strerror_get(err_code),
+ m_current_sc_store_peer_id);
+ evt.evt_id = PM_EVT_ERROR_UNEXPECTED;
+ evt.params.error_unexpected.error = err_code;
+ evt_send(&evt);
+ }
+ break;
+ }
+
+ m_current_sc_store_peer_id = pds_next_peer_id_get(m_current_sc_store_peer_id);
+ }
+}
+
+/**
+ * @brief Event handler for events from the Peer Database module.
+ * This function is extern in Peer Database.
+ *
+ * @param[in] p_event The event that has happened with peer id and flags.
+ */
+void gscm_pdb_evt_handler(pm_evt_t *p_event)
+{
+ if (m_current_sc_store_peer_id != PM_PEER_ID_INVALID) {
+ service_changed_pending_set();
+ }
+}
+#endif
+
+uint32_t gscm_init(void)
+{
+ NRF_PM_DEBUG_CHECK(!m_module_initialized);
+
+ internal_state_reset();
+ m_module_initialized = true;
+
+ return NRF_SUCCESS;
+}
+
+uint32_t gscm_local_db_cache_update(uint16_t conn_handle)
+{
+ NRF_PM_DEBUG_CHECK(m_module_initialized);
+
+ pm_peer_id_t peer_id = im_peer_id_get_by_conn_handle(conn_handle);
+ uint32_t err_code;
+
+ if (peer_id == PM_PEER_ID_INVALID) {
+ return BLE_ERROR_INVALID_CONN_HANDLE;
+ }
+
+ pm_peer_data_t peer_data;
+ uint16_t n_bufs = 1;
+ bool retry_with_bigger_buffer = false;
+
+ do {
+ retry_with_bigger_buffer = false;
+
+ err_code = pdb_write_buf_get(peer_id, PM_PEER_DATA_ID_GATT_LOCAL, n_bufs++,
+ &peer_data);
+ if (err_code == NRF_SUCCESS) {
+ pm_peer_data_local_gatt_db_t *p_local_gatt_db =
+ peer_data.p_local_gatt_db;
+
+ p_local_gatt_db->flags = SYS_ATTR_BOTH;
+
+ err_code = sd_ble_gatts_sys_attr_get(
+ conn_handle, &p_local_gatt_db->data[0],
+ &p_local_gatt_db->len, p_local_gatt_db->flags);
+
+ if (err_code == NRF_SUCCESS) {
+ pm_peer_data_flash_t curr_peer_data;
+
+ err_code = pdb_peer_data_ptr_get(peer_id,
+ PM_PEER_DATA_ID_GATT_LOCAL,
+ &curr_peer_data);
+
+ if ((err_code != NRF_SUCCESS) &&
+ (err_code != NRF_ERROR_NOT_FOUND)) {
+ LOG_ERR("pdb_peer_data_ptr_get() returned %s "
+ "for conn_handle: %d",
+ nrf_strerror_get(err_code),
+ conn_handle);
+ return NRF_ERROR_INTERNAL;
+ }
+
+ if ((err_code == NRF_ERROR_NOT_FOUND) ||
+ (p_local_gatt_db->len !=
+ curr_peer_data.p_local_gatt_db->len) ||
+ (memcmp(p_local_gatt_db->data,
+ curr_peer_data.p_local_gatt_db->data,
+ p_local_gatt_db->len) != 0)) {
+ err_code = pdb_write_buf_store(
+ peer_id, PM_PEER_DATA_ID_GATT_LOCAL,
+ peer_id);
+ } else {
+ LOG_DBG("Local db is already up to date, "
+ "skipping write.");
+ uint32_t err_code_release = pdb_write_buf_release(
+ peer_id, PM_PEER_DATA_ID_GATT_LOCAL);
+
+ if (err_code_release == NRF_SUCCESS) {
+ err_code = NRF_ERROR_INVALID_DATA;
+ } else {
+ LOG_ERR("Did another thread manipulate "
+ "PM_PEER_DATA_ID_GATT_LOCAL for "
+ "peer_id %d at the same time? "
+ "pdb_write_buf_release() returned "
+ "%s.",
+ peer_id,
+ nrf_strerror_get(err_code_release));
+ err_code = NRF_ERROR_INTERNAL;
+ }
+ }
+ } else {
+ if (err_code == NRF_ERROR_DATA_SIZE) {
+ /* The sys attributes are bigger than the requested
+ * write buffer.
+ */
+ retry_with_bigger_buffer = true;
+ } else if (err_code == NRF_ERROR_NOT_FOUND) {
+ /* There are no sys attributes in the GATT db, so
+ * nothing needs to be stored.
+ */
+ err_code = NRF_SUCCESS;
+ }
+
+ uint32_t err_code_release = pdb_write_buf_release(
+ peer_id, PM_PEER_DATA_ID_GATT_LOCAL);
+
+ if (err_code_release != NRF_SUCCESS) {
+ LOG_ERR("Did another thread manipulate "
+ "PM_PEER_DATA_ID_GATT_LOCAL for "
+ "peer_id %d at the same time? "
+ "pdb_write_buf_release() returned %s.",
+ peer_id,
+ nrf_strerror_get(err_code_release));
+ err_code = NRF_ERROR_INTERNAL;
+ }
+ }
+ } else if (err_code == NRF_ERROR_INVALID_PARAM) {
+ /* The sys attributes are bigger than the entire write buffer. */
+ err_code = NRF_ERROR_DATA_SIZE;
+ }
+ } while (retry_with_bigger_buffer);
+
+ return err_code;
+}
+
+uint32_t gscm_local_db_cache_apply(uint16_t conn_handle)
+{
+ NRF_PM_DEBUG_CHECK(m_module_initialized);
+
+ pm_peer_id_t peer_id = im_peer_id_get_by_conn_handle(conn_handle);
+ uint32_t err_code;
+ pm_peer_data_flash_t peer_data;
+ uint8_t const *p_sys_attr_data = NULL;
+ uint16_t sys_attr_len = 0;
+ uint32_t sys_attr_flags = (SYS_ATTR_BOTH);
+ bool all_attributes_applied = true;
+
+ if (peer_id != PM_PEER_ID_INVALID) {
+ err_code = pdb_peer_data_ptr_get(peer_id, PM_PEER_DATA_ID_GATT_LOCAL, &peer_data);
+ if (err_code == NRF_SUCCESS) {
+ pm_peer_data_local_gatt_db_t const *p_local_gatt_db;
+
+ p_local_gatt_db = peer_data.p_local_gatt_db;
+ p_sys_attr_data = p_local_gatt_db->data;
+ sys_attr_len = p_local_gatt_db->len;
+ sys_attr_flags = p_local_gatt_db->flags;
+ }
+ }
+
+ do {
+ err_code = sd_ble_gatts_sys_attr_set(conn_handle, p_sys_attr_data, sys_attr_len,
+ sys_attr_flags);
+
+ if (err_code == NRF_ERROR_NO_MEM) {
+ err_code = NRF_ERROR_BUSY;
+ } else if (err_code == NRF_ERROR_INVALID_STATE) {
+ err_code = NRF_SUCCESS;
+ } else if (err_code == NRF_ERROR_INVALID_DATA) {
+ all_attributes_applied = false;
+
+ if (sys_attr_flags & SYS_ATTR_USR) {
+ /* Try setting only system attributes. */
+ sys_attr_flags = SYS_ATTR_SYS;
+ } else if (p_sys_attr_data || sys_attr_len) {
+ /* Try reporting that none exist. */
+ p_sys_attr_data = NULL;
+ sys_attr_len = 0;
+ sys_attr_flags = SYS_ATTR_BOTH;
+ } else {
+ LOG_ERR("sd_ble_gatts_sys_attr_set() returned "
+ "NRF_ERROR_INVALID_DATA for NULL "
+ "pointer which should never happen. conn_handle: %d",
+ conn_handle);
+ err_code = NRF_ERROR_INTERNAL;
+ }
+ }
+ } while (err_code == NRF_ERROR_INVALID_DATA);
+
+ if (!all_attributes_applied) {
+ err_code = NRF_ERROR_INVALID_DATA;
+ }
+
+ return err_code;
+}
+
+#if !defined(CONFIG_PM_SERVICE_CHANGED_ENABLED) || (CONFIG_PM_SERVICE_CHANGED_ENABLED == 1)
+void gscm_local_database_has_changed(void)
+{
+ NRF_PM_DEBUG_CHECK(m_module_initialized);
+ m_current_sc_store_peer_id = pds_next_peer_id_get(PM_PEER_ID_INVALID);
+ service_changed_pending_set();
+}
+
+bool gscm_service_changed_ind_needed(uint16_t conn_handle)
+{
+ uint32_t err_code;
+ bool service_changed_state;
+ pm_peer_data_flash_t peer_data;
+
+ peer_data.p_service_changed_pending = &service_changed_state;
+ pm_peer_id_t peer_id = im_peer_id_get_by_conn_handle(conn_handle);
+
+ err_code =
+ pdb_peer_data_ptr_get(peer_id, PM_PEER_DATA_ID_SERVICE_CHANGED_PENDING, &peer_data);
+
+ if (err_code != NRF_SUCCESS) {
+ return false;
+ }
+
+ return *peer_data.p_service_changed_pending;
+}
+
+uint32_t gscm_service_changed_ind_send(uint16_t conn_handle)
+{
+ static uint16_t start_handle;
+ const uint16_t end_handle = 0xFFFF;
+ uint32_t err_code;
+
+ err_code = sd_ble_gatts_initial_user_handle_get(&start_handle);
+
+ if (err_code != NRF_SUCCESS) {
+ LOG_ERR("sd_ble_gatts_initial_user_handle_get() returned %s which should not "
+ "happen.",
+ nrf_strerror_get(err_code));
+ return NRF_ERROR_INTERNAL;
+ }
+
+ do {
+ err_code = sd_ble_gatts_service_changed(conn_handle, start_handle, end_handle);
+ if (err_code == BLE_ERROR_INVALID_ATTR_HANDLE) {
+ start_handle += 1;
+ }
+ } while (err_code == BLE_ERROR_INVALID_ATTR_HANDLE);
+
+ return err_code;
+}
+
+void gscm_db_change_notification_done(pm_peer_id_t peer_id)
+{
+ NRF_PM_DEBUG_CHECK(m_module_initialized);
+
+ /* Use a uint32_t to enforce 4-byte alignment. */
+ static const uint32_t service_changed_pending;
+
+ pm_peer_data_const_t peer_data = {
+ .data_id = PM_PEER_DATA_ID_SERVICE_CHANGED_PENDING,
+ .length_words = PM_SC_STATE_N_WORDS(),
+ .p_service_changed_pending = (bool *)&service_changed_pending,
+ };
+
+ /* Don't need to check return code, because all error conditions can be ignored. */
+ (void)pds_peer_data_store(peer_id, &peer_data, NULL);
+}
+#endif
diff --git a/lib/peer_manager/modules/id_manager.c b/lib/peer_manager/modules/id_manager.c
new file mode 100644
index 0000000000..14cac437e9
--- /dev/null
+++ b/lib/peer_manager/modules/id_manager.c
@@ -0,0 +1,645 @@
+/*
+ * Copyright (c) 2015-2025 Nordic Semiconductor ASA
+ *
+ * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
+ */
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+LOG_MODULE_DECLARE(peer_manager, CONFIG_PEER_MANAGER_LOG_LEVEL);
+
+#define IM_MAX_CONN_HANDLES (20)
+#define IM_NO_INVALID_CONN_HANDLES (0xFF)
+#define IM_ADDR_CLEARTEXT_LENGTH (3)
+#define IM_ADDR_CIPHERTEXT_LENGTH (3)
+
+/* The number of registered event handlers. */
+#define IM_EVENT_HANDLERS_CNT ARRAY_SIZE(m_evt_handlers)
+
+/* Identity Manager event handlers in Peer Manager and GATT Cache Manager. */
+extern void pm_im_evt_handler(pm_evt_t *p_event);
+extern void gcm_im_evt_handler(pm_evt_t *p_event);
+
+/* Identity Manager events' handlers.
+ * The number of elements in this array is IM_EVENT_HANDLERS_CNT.
+ */
+static pm_evt_handler_internal_t const m_evt_handlers[] = {pm_im_evt_handler, gcm_im_evt_handler};
+
+typedef struct {
+ pm_peer_id_t peer_id;
+ ble_gap_addr_t peer_address;
+} im_connection_t;
+
+static im_connection_t m_connections[IM_MAX_CONN_HANDLES];
+static uint8_t m_wlisted_peer_cnt;
+static pm_peer_id_t m_wlisted_peers[BLE_GAP_WHITELIST_ADDR_MAX_COUNT];
+
+/**
+ * @brief Function for sending an event to all registered event handlers.
+ *
+ * @param[in] p_event The event to distribute.
+ */
+static void evt_send(pm_evt_t *p_event)
+{
+ for (uint32_t i = 0; i < IM_EVENT_HANDLERS_CNT; i++) {
+ m_evt_handlers[i](p_event);
+ }
+}
+
+/**
+ * @brief Function checking the validity of an IRK
+ *
+ * @detail An all-zero IRK is not valid. This function will check if a given IRK is valid.
+ *
+ * @param[in] p_irk The IRK for which the validity is going to be checked.
+ *
+ * @retval true The IRK is valid.
+ * @retval false The IRK is invalid.
+ */
+bool is_valid_irk(ble_gap_irk_t const *p_irk)
+{
+ NRF_PM_DEBUG_CHECK(p_irk != NULL);
+
+ for (uint32_t i = 0; i < BLE_GAP_SEC_KEY_LEN; i++) {
+ if (p_irk->irk[i] != 0) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/**
+ * @brief Function for comparing two addresses to determine if they are identical
+ *
+ * @note The address type need to be identical, as well as every bit in the address itself.
+ *
+ * @param[in] p_addr1 The first address to be compared.
+ * @param[in] p_addr2 The second address to be compared.
+ *
+ * @retval true The addresses are identical.
+ * @retval false The addresses are not identical.
+ */
+bool addr_compare(ble_gap_addr_t const *p_addr1, ble_gap_addr_t const *p_addr2)
+{
+ if ((p_addr1 == NULL) || (p_addr2 == NULL)) {
+ return false;
+ }
+
+ /* Check that the addr type is identical, return false if it is not */
+ if (p_addr1->addr_type != p_addr2->addr_type) {
+ return false;
+ }
+
+ /* Check if the addr bytes are is identical */
+ return (memcmp(p_addr1->addr, p_addr2->addr, BLE_GAP_ADDR_LEN) == 0);
+}
+
+void im_ble_evt_handler(ble_evt_t const *ble_evt)
+{
+ ble_gap_evt_t gap_evt;
+ pm_peer_id_t bonded_matching_peer_id;
+
+ if (ble_evt->header.evt_id != BLE_GAP_EVT_CONNECTED) {
+ /* Nothing to do. */
+ return;
+ }
+
+ gap_evt = ble_evt->evt.gap_evt;
+ bonded_matching_peer_id = PM_PEER_ID_INVALID;
+
+ if (gap_evt.params.connected.peer_addr.addr_type !=
+ BLE_GAP_ADDR_TYPE_RANDOM_PRIVATE_NON_RESOLVABLE) {
+ /* Search the database for bonding data matching the one that triggered the event.
+ * Public and static addresses can be matched on address alone, while resolvable
+ * random addresses can be resolved agains known IRKs. Non-resolvable random
+ * addresses are never matching because they are not longterm form of
+ * identification.
+ */
+
+ pm_peer_id_t peer_id;
+ pm_peer_data_flash_t peer_data;
+
+ pds_peer_data_iterate_prepare();
+
+ switch (gap_evt.params.connected.peer_addr.addr_type) {
+ case BLE_GAP_ADDR_TYPE_PUBLIC:
+ case BLE_GAP_ADDR_TYPE_RANDOM_STATIC: {
+ while (pds_peer_data_iterate(PM_PEER_DATA_ID_BONDING, &peer_id,
+ &peer_data)) {
+ if (addr_compare(
+ &gap_evt.params.connected.peer_addr,
+ &peer_data.p_bonding_data->peer_ble_id.id_addr_info)) {
+ bonded_matching_peer_id = peer_id;
+ break;
+ }
+ }
+ } break;
+
+ case BLE_GAP_ADDR_TYPE_RANDOM_PRIVATE_RESOLVABLE: {
+ while (pds_peer_data_iterate(PM_PEER_DATA_ID_BONDING, &peer_id,
+ &peer_data)) {
+ if (im_address_resolve(
+ &gap_evt.params.connected.peer_addr,
+ &peer_data.p_bonding_data->peer_ble_id.id_info)) {
+ bonded_matching_peer_id = peer_id;
+ break;
+ }
+ }
+ } break;
+
+ default:
+ NRF_PM_DEBUG_CHECK(false);
+ break;
+ }
+ }
+
+ m_connections[gap_evt.conn_handle].peer_id = bonded_matching_peer_id;
+ m_connections[gap_evt.conn_handle].peer_address = gap_evt.params.connected.peer_addr;
+
+ if (bonded_matching_peer_id != PM_PEER_ID_INVALID) {
+ /* Send a bonded peer event */
+ pm_evt_t im_evt;
+
+ im_evt.conn_handle = gap_evt.conn_handle;
+ im_evt.peer_id = bonded_matching_peer_id;
+ im_evt.evt_id = PM_EVT_BONDED_PEER_CONNECTED;
+ evt_send(&im_evt);
+ }
+}
+
+/**
+ * @brief Function to compare two sets of bonding data to check if they belong to the same device.
+ * @note Invalid irks will never match even though they are identical.
+ *
+ * @param[in] p_bonding_data1 First bonding data for comparison
+ * @param[in] p_bonding_data2 Second bonding data for comparison
+ *
+ * @return True if the input matches, false if it does not.
+ */
+bool im_is_duplicate_bonding_data(pm_peer_data_bonding_t const *p_bonding_data1,
+ pm_peer_data_bonding_t const *p_bonding_data2)
+{
+ NRF_PM_DEBUG_CHECK(p_bonding_data1 != NULL);
+ NRF_PM_DEBUG_CHECK(p_bonding_data2 != NULL);
+
+ ble_gap_addr_t const *p_addr1 = &p_bonding_data1->peer_ble_id.id_addr_info;
+ ble_gap_addr_t const *p_addr2 = &p_bonding_data2->peer_ble_id.id_addr_info;
+
+ bool duplicate_irk =
+ ((memcmp(p_bonding_data1->peer_ble_id.id_info.irk,
+ p_bonding_data2->peer_ble_id.id_info.irk, BLE_GAP_SEC_KEY_LEN) == 0) &&
+ is_valid_irk(&p_bonding_data1->peer_ble_id.id_info) &&
+ is_valid_irk(&p_bonding_data2->peer_ble_id.id_info));
+
+ bool duplicate_addr = addr_compare(p_addr1, p_addr2);
+
+ bool id_addrs = ((p_addr1->addr_type != BLE_GAP_ADDR_TYPE_RANDOM_PRIVATE_RESOLVABLE) &&
+ (p_addr1->addr_type != BLE_GAP_ADDR_TYPE_RANDOM_PRIVATE_NON_RESOLVABLE) &&
+ (p_addr2->addr_type != BLE_GAP_ADDR_TYPE_RANDOM_PRIVATE_RESOLVABLE) &&
+ (p_addr2->addr_type != BLE_GAP_ADDR_TYPE_RANDOM_PRIVATE_NON_RESOLVABLE));
+
+ return (duplicate_addr && id_addrs) || (duplicate_irk && !id_addrs);
+}
+
+pm_peer_id_t im_find_duplicate_bonding_data(pm_peer_data_bonding_t const *p_bonding_data,
+ pm_peer_id_t peer_id_skip)
+{
+ pm_peer_id_t peer_id;
+ pm_peer_data_flash_t peer_data_duplicate;
+
+ NRF_PM_DEBUG_CHECK(p_bonding_data != NULL);
+
+ pds_peer_data_iterate_prepare();
+
+ while (pds_peer_data_iterate(PM_PEER_DATA_ID_BONDING, &peer_id, &peer_data_duplicate)) {
+ if ((peer_id != peer_id_skip) &&
+ im_is_duplicate_bonding_data(p_bonding_data,
+ peer_data_duplicate.p_bonding_data)) {
+ return peer_id;
+ }
+ }
+ return PM_PEER_ID_INVALID;
+}
+
+pm_peer_id_t im_peer_id_get_by_conn_handle(uint16_t conn_handle)
+{
+ if ((conn_handle >= IM_MAX_CONN_HANDLES) || !ble_conn_state_valid(conn_handle)) {
+ return PM_PEER_ID_INVALID;
+ }
+
+ return m_connections[conn_handle].peer_id;
+}
+
+uint32_t im_ble_addr_get(uint16_t conn_handle, ble_gap_addr_t *p_ble_addr)
+{
+ NRF_PM_DEBUG_CHECK(p_ble_addr != NULL);
+
+ if ((conn_handle >= IM_MAX_CONN_HANDLES) || !ble_conn_state_valid(conn_handle)) {
+ return BLE_ERROR_INVALID_CONN_HANDLE;
+ }
+
+ *p_ble_addr = m_connections[conn_handle].peer_address;
+ return NRF_SUCCESS;
+}
+
+bool im_master_ids_compare(ble_gap_master_id_t const *p_master_id1,
+ ble_gap_master_id_t const *p_master_id2)
+{
+ NRF_PM_DEBUG_CHECK(p_master_id1 != NULL);
+ NRF_PM_DEBUG_CHECK(p_master_id2 != NULL);
+
+ if (!im_master_id_is_valid(p_master_id1)) {
+ return false;
+ }
+
+ if (p_master_id1->ediv != p_master_id2->ediv) {
+ return false;
+ }
+
+ return (memcmp(p_master_id1->rand, p_master_id2->rand, BLE_GAP_SEC_RAND_LEN) == 0);
+}
+
+pm_peer_id_t im_peer_id_get_by_master_id(ble_gap_master_id_t const *p_master_id)
+{
+ pm_peer_id_t peer_id;
+ pm_peer_data_flash_t peer_data;
+
+ NRF_PM_DEBUG_CHECK(p_master_id != NULL);
+
+ pds_peer_data_iterate_prepare();
+
+ /* For each stored peer, check if the master_id matches p_master_id */
+ while (pds_peer_data_iterate(PM_PEER_DATA_ID_BONDING, &peer_id, &peer_data)) {
+ if (im_master_ids_compare(p_master_id,
+ &peer_data.p_bonding_data->own_ltk.master_id) ||
+ im_master_ids_compare(p_master_id,
+ &peer_data.p_bonding_data->peer_ltk.master_id)) {
+ /* If a matching master ID is found then return the peer ID. */
+ return peer_id;
+ }
+ }
+
+ /* If no matching master ID is found return PM_PEER_ID_INVALID. */
+ return PM_PEER_ID_INVALID;
+}
+
+uint16_t im_conn_handle_get(pm_peer_id_t peer_id)
+{
+ if (peer_id == PM_PEER_ID_INVALID) {
+ return BLE_CONN_HANDLE_INVALID;
+ }
+
+ for (uint16_t conn_handle = 0; conn_handle < IM_MAX_CONN_HANDLES; conn_handle++) {
+ if ((m_connections[conn_handle].peer_id == peer_id) &&
+ ble_conn_state_valid(conn_handle)) {
+ return conn_handle;
+ }
+ }
+ return BLE_CONN_HANDLE_INVALID;
+}
+
+bool im_master_id_is_valid(ble_gap_master_id_t const *p_master_id)
+{
+ if (p_master_id->ediv != 0) {
+ return true;
+ }
+
+ for (uint32_t i = 0; i < BLE_GAP_SEC_RAND_LEN; i++) {
+ if (p_master_id->rand[i] != 0) {
+ return true;
+ }
+ }
+ return false;
+}
+
+void im_new_peer_id(uint16_t conn_handle, pm_peer_id_t peer_id)
+{
+ if (conn_handle < IM_MAX_CONN_HANDLES) {
+ m_connections[conn_handle].peer_id = peer_id;
+ }
+}
+
+uint32_t im_peer_free(pm_peer_id_t peer_id)
+{
+ uint16_t conn_handle;
+ uint32_t ret;
+
+ conn_handle = im_conn_handle_get(peer_id);
+ ret = pdb_peer_free(peer_id);
+
+ if (ret == NRF_SUCCESS && (conn_handle < IM_MAX_CONN_HANDLES)) {
+ m_connections[conn_handle].peer_id = PM_PEER_ID_INVALID;
+ }
+ return ret;
+}
+
+/** @brief Given a list of peers, loads their GAP address and IRK into the provided buffers. */
+static uint32_t peers_id_keys_get(pm_peer_id_t const *p_peers, uint32_t peer_cnt,
+ ble_gap_addr_t *p_gap_addrs, uint32_t *p_addr_cnt,
+ ble_gap_irk_t *p_gap_irks, uint32_t *p_irk_cnt)
+{
+ uint32_t ret;
+
+ pm_peer_data_bonding_t bond_data;
+ pm_peer_data_t peer_data;
+
+ uint32_t const buf_size = sizeof(bond_data);
+
+ bool copy_addrs = false;
+ bool copy_irks = false;
+
+ NRF_PM_DEBUG_CHECK(p_peers != NULL);
+
+ /* One of these two has to be provided. */
+ NRF_PM_DEBUG_CHECK((p_gap_addrs != NULL) || (p_gap_irks != NULL));
+
+ if ((p_gap_addrs != NULL) && (p_addr_cnt != NULL)) {
+ NRF_PM_DEBUG_CHECK((*p_addr_cnt) >= peer_cnt);
+
+ copy_addrs = true;
+ *p_addr_cnt = 0;
+ }
+
+ if ((p_gap_irks != NULL) && (p_irk_cnt != NULL)) {
+ NRF_PM_DEBUG_CHECK((*p_irk_cnt) >= peer_cnt);
+
+ copy_irks = true;
+ *p_irk_cnt = 0;
+ }
+
+ memset(&peer_data, 0x00, sizeof(peer_data));
+ peer_data.p_bonding_data = &bond_data;
+
+ /* Read through flash memory and look for peers ID keys. */
+
+ for (uint32_t i = 0; i < peer_cnt; i++) {
+ memset(&bond_data, 0x00, sizeof(bond_data));
+
+ /* Read peer data from flash. */
+ ret = pds_peer_data_read(p_peers[i], PM_PEER_DATA_ID_BONDING, &peer_data,
+ &buf_size);
+
+ if ((ret == NRF_ERROR_NOT_FOUND) || (ret == NRF_ERROR_INVALID_PARAM)) {
+ /* Peer data coulnd't be found in flash or peer ID is not valid. */
+ return NRF_ERROR_NOT_FOUND;
+ }
+
+ uint8_t const addr_type = bond_data.peer_ble_id.id_addr_info.addr_type;
+
+ if ((addr_type != BLE_GAP_ADDR_TYPE_PUBLIC) &&
+ (addr_type != BLE_GAP_ADDR_TYPE_RANDOM_STATIC)) {
+ /* The address shared by the peer during bonding can't be used for
+ * whitelisting.
+ */
+ return BLE_ERROR_GAP_INVALID_BLE_ADDR;
+ }
+
+ /* Copy the GAP address. */
+ if (copy_addrs) {
+ memcpy(&p_gap_addrs[i], &bond_data.peer_ble_id.id_addr_info,
+ sizeof(ble_gap_addr_t));
+ (*p_addr_cnt)++;
+ }
+
+ /* Copy the IRK. */
+ if (copy_irks) {
+ memcpy(&p_gap_irks[i], bond_data.peer_ble_id.id_info.irk,
+ BLE_GAP_SEC_KEY_LEN);
+ (*p_irk_cnt)++;
+ }
+ }
+
+ return NRF_SUCCESS;
+}
+
+uint32_t im_device_identities_list_set(pm_peer_id_t const *p_peers, uint32_t peer_cnt)
+{
+ uint32_t ret;
+ pm_peer_data_t peer_data;
+ pm_peer_data_bonding_t bond_data;
+
+ ble_gap_id_key_t keys[BLE_GAP_DEVICE_IDENTITIES_MAX_COUNT];
+ ble_gap_id_key_t const *key_ptrs[BLE_GAP_DEVICE_IDENTITIES_MAX_COUNT];
+
+ if (peer_cnt > BLE_GAP_DEVICE_IDENTITIES_MAX_COUNT) {
+ return NRF_ERROR_INVALID_PARAM;
+ }
+
+ if ((p_peers == NULL) || (peer_cnt == 0)) {
+ /* Clear the device identities list. */
+ return sd_ble_gap_device_identities_set(NULL, NULL, 0);
+ }
+
+ peer_data.p_bonding_data = &bond_data;
+ uint32_t const buf_size = sizeof(bond_data);
+
+ memset(keys, 0x00, sizeof(keys));
+ for (uint32_t i = 0; i < BLE_GAP_DEVICE_IDENTITIES_MAX_COUNT; i++) {
+ key_ptrs[i] = &keys[i];
+ }
+
+ for (uint32_t i = 0; i < peer_cnt; i++) {
+ memset(&bond_data, 0x00, sizeof(bond_data));
+
+ /* Read peer data from flash. */
+ ret = pds_peer_data_read(p_peers[i], PM_PEER_DATA_ID_BONDING, &peer_data,
+ &buf_size);
+
+ if ((ret == NRF_ERROR_NOT_FOUND) || (ret == NRF_ERROR_INVALID_PARAM)) {
+ LOG_WRN("peer id %d: Peer data could not be found in flash. Remove "
+ "the peer ID "
+ "from the peer list and try again.",
+ p_peers[i]);
+ return NRF_ERROR_NOT_FOUND;
+ }
+
+ uint8_t const addr_type = bond_data.peer_ble_id.id_addr_info.addr_type;
+
+ if ((addr_type != BLE_GAP_ADDR_TYPE_PUBLIC) &&
+ (addr_type != BLE_GAP_ADDR_TYPE_RANDOM_STATIC)) {
+ LOG_WRN("peer id %d: The address shared by the peer during bonding cannot "
+ "be "
+ "whitelisted. Remove the peer ID from the peer list and try again.",
+ p_peers[i]);
+ return BLE_ERROR_GAP_INVALID_BLE_ADDR;
+ }
+
+ /* Copy data to the buffer. */
+ memcpy(&keys[i], &bond_data.peer_ble_id, sizeof(ble_gap_id_key_t));
+ }
+
+ return sd_ble_gap_device_identities_set(key_ptrs, NULL, peer_cnt);
+}
+
+uint32_t im_id_addr_set(ble_gap_addr_t const *p_addr)
+{
+ return sd_ble_gap_addr_set(p_addr);
+}
+
+uint32_t im_id_addr_get(ble_gap_addr_t *p_addr)
+{
+ NRF_PM_DEBUG_CHECK(p_addr != NULL);
+
+ return sd_ble_gap_addr_get(p_addr);
+}
+
+uint32_t im_privacy_set(pm_privacy_params_t const *p_privacy_params)
+{
+ return sd_ble_gap_privacy_set(p_privacy_params);
+}
+
+uint32_t im_privacy_get(pm_privacy_params_t *p_privacy_params)
+{
+ return sd_ble_gap_privacy_get(p_privacy_params);
+}
+
+/* Create a whitelist for the user using the cached list of peers.
+ * This whitelist is meant to be provided by the application to the Advertising module.
+ */
+uint32_t im_whitelist_get(ble_gap_addr_t *p_addrs, uint32_t *p_addr_cnt, ble_gap_irk_t *p_irks,
+ uint32_t *p_irk_cnt)
+{
+ /* One of the two buffers has to be provided. */
+ NRF_PM_DEBUG_CHECK((p_addrs != NULL) || (p_irks != NULL));
+ NRF_PM_DEBUG_CHECK((p_addr_cnt != NULL) || (p_irk_cnt != NULL));
+
+ if (((p_addr_cnt != NULL) && (m_wlisted_peer_cnt > *p_addr_cnt)) ||
+ ((p_irk_cnt != NULL) && (m_wlisted_peer_cnt > *p_irk_cnt))) {
+ /* The size of the cached list of peers is larger than the provided buffers. */
+ return NRF_ERROR_NO_MEM;
+ }
+
+ /* NRF_SUCCESS or
+ * NRF_ERROR_NOT_FOUND, if a peer or its data were not found.
+ * BLE_ERROR_GAP_INVALID_BLE_ADDR, if a peer address can not be used for whitelisting.
+ */
+ return peers_id_keys_get(m_wlisted_peers, m_wlisted_peer_cnt, p_addrs, p_addr_cnt, p_irks,
+ p_irk_cnt);
+}
+
+/* Copies the peers to whitelist into a local cache.
+ * The cached list will be used by im_whitelist_get() to retrieve the active whitelist.
+ * For SoftDevices 3x, also loads the peers' GAP addresses and whitelists them using
+ * sd_ble_gap_whitelist_set().
+ */
+uint32_t im_whitelist_set(pm_peer_id_t const *p_peers, uint32_t peer_cnt)
+{
+ /* Clear the cache of whitelisted peers. */
+ memset(m_wlisted_peers, 0x00, sizeof(m_wlisted_peers));
+
+ if ((p_peers == NULL) || (peer_cnt == 0)) {
+ /* Clear the current whitelist. */
+ m_wlisted_peer_cnt = 0;
+
+ /* NRF_SUCCESS, or
+ * BLE_GAP_ERROR_WHITELIST_IN_USE
+ */
+ return sd_ble_gap_whitelist_set(NULL, 0);
+ }
+
+ /* Copy the new whitelisted peers. */
+ m_wlisted_peer_cnt = peer_cnt;
+ memcpy(m_wlisted_peers, p_peers, sizeof(pm_peer_id_t) * peer_cnt);
+
+ uint32_t ret;
+ uint32_t wlist_addr_cnt = 0;
+
+ ble_gap_addr_t const *addr_ptrs[BLE_GAP_WHITELIST_ADDR_MAX_COUNT];
+ ble_gap_addr_t addrs[BLE_GAP_WHITELIST_ADDR_MAX_COUNT];
+
+ memset(addrs, 0x00, sizeof(addrs));
+
+ /* Fetch GAP addresses for these peers, but don't fetch IRKs. */
+ ret = peers_id_keys_get(p_peers, peer_cnt, addrs, &wlist_addr_cnt, NULL, NULL);
+
+ if (ret != NRF_SUCCESS) {
+ /* NRF_ERROR_NOT_FOUND, if a peer or its data were not found.
+ * BLE_ERROR_GAP_INVALID_BLE_ADDR, if a peer address can not be used for
+ * whitelisting.
+ */
+ return ret;
+ }
+
+ for (uint32_t i = 0; i < BLE_GAP_WHITELIST_ADDR_MAX_COUNT; i++) {
+ addr_ptrs[i] = &addrs[i];
+ }
+
+ /* NRF_ERROR_DATA_SIZE, if peer_cnt > BLE_GAP_WHITELIST_ADDR_MAX_COUNT.
+ * BLE_ERROR_GAP_WHITELIST_IN_USE, if a whitelist is in use.
+ */
+ return sd_ble_gap_whitelist_set(addr_ptrs, peer_cnt);
+}
+
+/**
+ * @brief Function for calculating the ah() hash function described in Bluetooth core specification
+ * 4.2 section 3.H.2.2.2.
+ *
+ * @detail BLE uses a hash function to calculate the first half of a resolvable address
+ * from the second half of the address and an irk. This function will use the ECB
+ * periferal to hash these data according to the Bluetooth core specification.
+ *
+ * @note The ECB expect little endian input and output.
+ * This function expect big endian and will reverse the data as necessary.
+ *
+ * @param[in] p_k The key used in the hash function.
+ * For address resolution this is should be the irk.
+ * The array must have a length of 16.
+ * @param[in] p_r The rand used in the hash function. For generating a new address
+ * this would be a random number. For resolving a resolvable address
+ * this would be the last half of the address being resolved.
+ * The array must have a length of 3.
+ * @param[out] p_local_hash The result of the hash operation. For address resolution this
+ * will match the first half of the address being resolved if and only
+ * if the irk used in the hash function is the same one used to generate
+ * the address.
+ * The array must have a length of 16.
+ */
+void ah(uint8_t const *p_k, uint8_t const *p_r, uint8_t *p_local_hash)
+{
+ nrf_ecb_hal_data_t ecb_hal_data;
+
+ for (uint32_t i = 0; i < SOC_ECB_KEY_LENGTH; i++) {
+ ecb_hal_data.key[i] = p_k[SOC_ECB_KEY_LENGTH - 1 - i];
+ }
+
+ memset(ecb_hal_data.cleartext, 0, SOC_ECB_KEY_LENGTH - IM_ADDR_CLEARTEXT_LENGTH);
+
+ for (uint32_t i = 0; i < IM_ADDR_CLEARTEXT_LENGTH; i++) {
+ ecb_hal_data.cleartext[SOC_ECB_KEY_LENGTH - 1 - i] = p_r[i];
+ }
+
+ /* Can only return NRF_SUCCESS. */
+ (void)sd_ecb_block_encrypt(&ecb_hal_data);
+
+ for (uint32_t i = 0; i < IM_ADDR_CIPHERTEXT_LENGTH; i++) {
+ p_local_hash[i] = ecb_hal_data.ciphertext[SOC_ECB_KEY_LENGTH - 1 - i];
+ }
+}
+
+bool im_address_resolve(ble_gap_addr_t const *p_addr, ble_gap_irk_t const *p_irk)
+{
+ uint8_t hash[IM_ADDR_CIPHERTEXT_LENGTH];
+ uint8_t local_hash[IM_ADDR_CIPHERTEXT_LENGTH];
+ uint8_t prand[IM_ADDR_CLEARTEXT_LENGTH];
+
+ if (p_addr->addr_type != BLE_GAP_ADDR_TYPE_RANDOM_PRIVATE_RESOLVABLE) {
+ return false;
+ }
+
+ memcpy(hash, p_addr->addr, IM_ADDR_CIPHERTEXT_LENGTH);
+ memcpy(prand, &p_addr->addr[IM_ADDR_CIPHERTEXT_LENGTH], IM_ADDR_CLEARTEXT_LENGTH);
+ ah(p_irk->irk, prand, local_hash);
+
+ return (memcmp(hash, local_hash, IM_ADDR_CIPHERTEXT_LENGTH) == 0);
+}
diff --git a/lib/peer_manager/modules/nrf_ble_lesc.c b/lib/peer_manager/modules/nrf_ble_lesc.c
new file mode 100644
index 0000000000..e14c504801
--- /dev/null
+++ b/lib/peer_manager/modules/nrf_ble_lesc.c
@@ -0,0 +1,430 @@
+/*
+ * Copyright (c) 2018-2025 Nordic Semiconductor ASA
+ *
+ * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
+ */
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+LOG_MODULE_DECLARE(peer_manager, CONFIG_PEER_MANAGER_LOG_LEVEL);
+
+/** @brief Descriptor of the peer public key. */
+typedef struct {
+ /** @brief Peer public key. Stored in little-endian. */
+ uint8_t value[BLE_GAP_LESC_P256_PK_LEN];
+ /** @brief Flag indicating that the public key has been requested to compute DH key. */
+ bool is_requested;
+ /** @brief Flag indicating that the public key is valid. */
+ bool is_valid;
+ /** @brief Flag indicating that the passkey key has been requested. */
+ bool passkey_requested;
+ /** @brief Flag indicating that the passkey display event has been received. */
+ bool passkey_displayed;
+} nrf_ble_lesc_peer_pub_key_t;
+
+/**
+ * @brief The maximum number of peripheral and central connections combined.
+ * This value is based on what the SoftDevice handler module uses.
+ */
+#define NRF_BLE_LESC_LINK_COUNT CONFIG_NRF_SDH_BLE_TOTAL_LINK_COUNT
+
+/** LESC ECC Public Key. */
+__ALIGN(4) static ble_gap_lesc_p256_pk_t m_lesc_public_key;
+/** LESC ECC DH Key. */
+__ALIGN(4) static ble_gap_lesc_dhkey_t m_lesc_dh_key;
+
+/** Flag indicating that the module encountered an internal error. */
+static bool m_ble_lesc_internal_error;
+/** Flag indicating that the local ECDH key pair was generated. */
+static bool m_keypair_generated;
+
+/** ID of ECC private/public key pair. */
+static psa_key_id_t m_keypair_id;
+/** Array of pointers to peer public keys, used for LESC DH generation. */
+static nrf_ble_lesc_peer_pub_key_t m_peer_keys[NRF_BLE_LESC_LINK_COUNT];
+
+static bool m_lesc_oobd_own_generated;
+/** LESC OOB data used in LESC OOB pairing mode. */
+static ble_gap_lesc_oob_data_t m_ble_lesc_oobd_own;
+static nrf_ble_lesc_peer_oob_data_handler m_lesc_oobd_peer_handler;
+
+#define ECC_PUB_KEY_UNCOMPRESSED_FORMAT_MARKER 0x04
+#define ECC_PUB_KEY_EXPORT_SIZE \
+ PSA_EXPORT_PUBLIC_KEY_OUTPUT_SIZE(PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_SECP_R1), 256)
+#define COORD_SIZE (BLE_GAP_LESC_P256_PK_LEN / 2)
+
+/* Convert an ECC (secp256r1) public key from between big-endian and little-endian.
+ * The two coordinates are converted individually.
+ */
+static void ecc_public_key_byte_order_invert(const uint8_t *raw_key_in, uint8_t *raw_key_out)
+{
+ sys_memcpy_swap(raw_key_out, raw_key_in, COORD_SIZE);
+ sys_memcpy_swap(&raw_key_out[COORD_SIZE], &raw_key_in[COORD_SIZE], COORD_SIZE);
+}
+
+uint32_t nrf_ble_lesc_init(void)
+{
+ psa_status_t status;
+
+ memset((void *)m_peer_keys, 0, sizeof(m_peer_keys));
+
+ /* Ensure that psa_crypto has been initialized. */
+ status = psa_crypto_init();
+ if (status != PSA_SUCCESS) {
+ LOG_ERR("psa_crypto_init() returned status %d", status);
+ return NRF_ERROR_INTERNAL;
+ }
+
+ LOG_DBG("Initialized nrf_ble_lesc.");
+
+ /* Reset module state. */
+ m_ble_lesc_internal_error = false;
+ m_keypair_generated = false;
+
+ /* Generate ECC key pair. Only one key pair is automatically generated by this module. */
+ return nrf_ble_lesc_keypair_generate();
+}
+
+uint32_t nrf_ble_lesc_keypair_generate(void)
+{
+ psa_status_t status;
+ uint8_t pub_key[ECC_PUB_KEY_EXPORT_SIZE];
+ size_t pub_key_len = 0;
+
+ /* Check if any DH computation is pending */
+ for (uint16_t i = 0; i < ARRAY_SIZE(m_peer_keys); i++) {
+ if (m_peer_keys[i].is_valid) {
+ return NRF_ERROR_BUSY;
+ }
+ }
+
+ /* Update flag to indicate that there is no valid private key. */
+ m_keypair_generated = false;
+ m_lesc_oobd_own_generated = false;
+
+ /* Destroy the previous key pair (if any), to free up memory. */
+ status = psa_destroy_key(m_keypair_id);
+ if (status != PSA_SUCCESS && status != PSA_ERROR_INVALID_HANDLE) {
+ LOG_ERR("psa_destroy_key returned status %d", status);
+ } else {
+ m_keypair_id = 0;
+ }
+
+ LOG_DBG("Generating ECC key pair");
+
+ psa_key_attributes_t key_attributes = PSA_KEY_ATTRIBUTES_INIT;
+
+ psa_set_key_usage_flags(&key_attributes, PSA_KEY_USAGE_DERIVE);
+ psa_set_key_lifetime(&key_attributes, PSA_KEY_LIFETIME_VOLATILE);
+ psa_set_key_algorithm(&key_attributes, PSA_ALG_ECDH);
+ psa_set_key_type(&key_attributes, PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_SECP_R1));
+ psa_set_key_bits(&key_attributes, 256);
+
+ status = psa_generate_key(&key_attributes, &m_keypair_id);
+ if (status != PSA_SUCCESS) {
+ LOG_ERR("psa_generate_key() returned status %d", status);
+ return NRF_ERROR_INTERNAL;
+ }
+
+ /* Export the raw representation of the public key. */
+ status = psa_export_public_key(m_keypair_id, pub_key, sizeof(pub_key), &pub_key_len);
+ if (status != PSA_SUCCESS) {
+ LOG_ERR("psa_export_public_key() returned status %d", status);
+ return NRF_ERROR_INTERNAL;
+ }
+ /* Convert from big-endian to little-endian.
+ * Drop the first byte indicating the serialization format.
+ */
+ ecc_public_key_byte_order_invert(&pub_key[1], m_lesc_public_key.pk);
+
+ /* Set the flag to indicate that there is a valid ECDH key pair generated. */
+ m_keypair_generated = true;
+
+ return NRF_SUCCESS;
+}
+
+uint32_t nrf_ble_lesc_own_oob_data_generate(void)
+{
+ uint32_t err_code = NRF_ERROR_INVALID_STATE;
+
+ m_lesc_oobd_own_generated = false;
+
+ if (m_keypair_generated) {
+ err_code = sd_ble_gap_lesc_oob_data_get(BLE_CONN_HANDLE_INVALID, &m_lesc_public_key,
+ &m_ble_lesc_oobd_own);
+ if (err_code == NRF_SUCCESS) {
+ m_lesc_oobd_own_generated = true;
+ }
+ }
+
+ return err_code;
+}
+
+ble_gap_lesc_p256_pk_t *nrf_ble_lesc_public_key_get(void)
+{
+ ble_gap_lesc_p256_pk_t *p_lesc_pk = NULL;
+
+ if (m_keypair_generated) {
+ p_lesc_pk = &m_lesc_public_key;
+ } else {
+ LOG_ERR("Trying to access LESC public key that has not been generated yet.");
+ }
+
+ return p_lesc_pk;
+}
+
+ble_gap_lesc_oob_data_t *nrf_ble_lesc_own_oob_data_get(void)
+{
+ ble_gap_lesc_oob_data_t *p_lesc_oobd_own = NULL;
+
+ if (m_lesc_oobd_own_generated) {
+ p_lesc_oobd_own = &m_ble_lesc_oobd_own;
+ } else {
+ LOG_ERR("Trying to access LESC OOB data that have not been generated yet.");
+ }
+
+ return p_lesc_oobd_own;
+}
+
+void nrf_ble_lesc_peer_oob_data_handler_set(nrf_ble_lesc_peer_oob_data_handler handler)
+{
+ m_lesc_oobd_peer_handler = handler;
+}
+
+/**
+ * @brief Function for calculating a DH key and responding to the DH key request on a given
+ * connection handle.
+ *
+ * @param[in] p_peer_public_key ECC peer public key, used to compute shared secret.
+ * @param[in] conn_handle Connection handle.
+ *
+ * @retval NRF_SUCCESS If the operation was successful.
+ * @retval NRF_ERROR_INTERNAL If @ref psa_raw_key_agreement, or @ref psa_generate_random failed.
+ * @returns Other error codes might be returned by @ref sd_ble_gap_lesc_dhkey_reply.
+ * functions.
+ */
+static uint32_t compute_and_give_dhkey(nrf_ble_lesc_peer_pub_key_t *p_peer_public_key,
+ uint16_t conn_handle)
+{
+ psa_status_t status = PSA_ERROR_BAD_STATE;
+ uint8_t *p_shared_secret = m_lesc_dh_key.key;
+ size_t shared_secret_size;
+ uint8_t pub_key[ECC_PUB_KEY_EXPORT_SIZE];
+
+ /* Check if there is a valid generated and set a local ECDH public key. */
+ if (!m_keypair_generated) {
+ return NRF_ERROR_INTERNAL;
+ }
+
+ /* Check if the public_key is valid */
+ if (p_peer_public_key->is_valid) {
+ /* Add the uncompressed format marker. */
+ pub_key[0] = ECC_PUB_KEY_UNCOMPRESSED_FORMAT_MARKER;
+
+ /* Convert from little-endian to big-endian. */
+ ecc_public_key_byte_order_invert(p_peer_public_key->value, &pub_key[1]);
+
+ status = psa_raw_key_agreement(PSA_ALG_ECDH, m_keypair_id,
+ pub_key, sizeof(pub_key),
+ p_shared_secret, BLE_GAP_LESC_DHKEY_LEN, &shared_secret_size);
+ }
+
+ if ((status == PSA_SUCCESS) && (shared_secret_size == BLE_GAP_LESC_DHKEY_LEN)) {
+ /* Convert secret from big-endian to little-endian. */
+ sys_mem_swap(p_shared_secret, BLE_GAP_LESC_DHKEY_LEN);
+ } else {
+ LOG_ERR("psa_raw_key_agreement() returned status %d", status);
+
+ LOG_WRN("Creating invalid shared secret to make LESC fail.");
+ status = psa_generate_random(p_shared_secret, BLE_GAP_LESC_DHKEY_LEN);
+ if (status != PSA_SUCCESS) {
+ LOG_ERR("psa_generate_random() returned status %d", status);
+ return NRF_ERROR_INTERNAL;
+ }
+ }
+
+ LOG_INF("Calling sd_ble_gap_lesc_dhkey_reply on conn_handle: %d", conn_handle);
+ return sd_ble_gap_lesc_dhkey_reply(conn_handle, &m_lesc_dh_key);
+}
+
+uint32_t nrf_ble_lesc_request_handler(void)
+{
+ uint32_t err_code = NRF_SUCCESS;
+
+ /* If the LESC module is in an invalid state, a restart is required. */
+ if (m_ble_lesc_internal_error) {
+ return NRF_ERROR_INTERNAL;
+ }
+
+ for (uint16_t i = 0; i < NRF_BLE_LESC_LINK_COUNT; i++) {
+ if (m_peer_keys[i].is_requested) {
+ err_code = compute_and_give_dhkey(&m_peer_keys[i], i);
+ m_peer_keys[i].is_requested = false;
+ m_peer_keys[i].is_valid = false;
+ m_peer_keys[i].passkey_requested = false;
+ m_peer_keys[i].passkey_displayed = false;
+
+ VERIFY_SUCCESS(err_code);
+ }
+ }
+
+ return err_code;
+}
+
+/**
+ * @brief Function for handling a DH key request event.
+ *
+ * @param[in] conn_handle Connection handle.
+ * @param[in] idx Data index assigned to the connection handle.
+ * @param[in] p_dhkey_request DH key request descriptor.
+ *
+ * @retval NRF_SUCCESS If the operation was successful.
+ * @returns Other error codes might be returned by @ref sd_ble_gap_auth_key_reply, and
+ * @ref sd_ble_gap_disconnect.
+ */
+static uint32_t on_dhkey_request(uint16_t conn_handle, int idx,
+ ble_gap_evt_lesc_dhkey_request_t const *p_dhkey_request)
+{
+ uint32_t err_code = NRF_SUCCESS;
+ const uint8_t *const p_public_raw = p_dhkey_request->p_pk_peer->pk;
+
+ /* Don't allow to pair with remote peer which uses the same public key.
+ * Compare only X cordinate of the public key, bytes from 0 to 31.
+ */
+ if (memcmp(m_lesc_public_key.pk, p_public_raw, BLE_GAP_LESC_P256_PK_LEN / 2) == 0) {
+ LOG_WRN("Remote peer is using identical public key.");
+ m_peer_keys[idx].is_valid = false;
+
+ /* In case when we have gotten passkey requested then we will respond to it with the
+ * "NONE" key type to prevent us from going through Authentication Stage 1.
+ */
+ if (m_peer_keys[idx].passkey_requested) {
+ m_peer_keys[idx].passkey_requested = false;
+
+ err_code = sd_ble_gap_auth_key_reply(conn_handle,
+ BLE_GAP_AUTH_KEY_TYPE_NONE, NULL);
+
+ return err_code;
+
+ }
+ /* In case we have gotten passkey display event then we need to disconnect a link
+ * to prevent us from going through Authentication Stage 1.
+ */
+ else if (m_peer_keys[idx].passkey_displayed) {
+ m_peer_keys[idx].passkey_displayed = false;
+
+ err_code = sd_ble_gap_disconnect(conn_handle,
+ BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION);
+ if (err_code != NRF_SUCCESS && err_code != NRF_ERROR_INVALID_STATE) {
+ return err_code;
+ }
+
+ return NRF_SUCCESS;
+ }
+ } else {
+ memcpy(m_peer_keys[idx].value, p_public_raw, BLE_GAP_LESC_P256_PK_LEN);
+ m_peer_keys[idx].is_valid = true;
+ }
+
+ m_peer_keys[idx].is_requested = true;
+
+ return NRF_SUCCESS;
+}
+
+/**
+ * @brief Function for setting LESC OOB data.
+ *
+ * @param[in] conn_handle Connection handle.
+ *
+ * @retval NRF_SUCCESS If the operation was successful.
+ * @returns Other error codes might be returned by @ref sd_ble_gap_lesc_oob_data_set.
+ */
+static uint32_t lesc_oob_data_set(uint16_t conn_handle)
+{
+ uint32_t err_code;
+ ble_gap_lesc_oob_data_t *p_lesc_oobd_own;
+ ble_gap_lesc_oob_data_t *p_lesc_oobd_peer;
+
+ p_lesc_oobd_own = (m_lesc_oobd_own_generated) ? &m_ble_lesc_oobd_own : NULL;
+ p_lesc_oobd_peer =
+ (m_lesc_oobd_peer_handler != NULL) ? m_lesc_oobd_peer_handler(conn_handle) : NULL;
+
+ err_code = sd_ble_gap_lesc_oob_data_set(conn_handle, p_lesc_oobd_own, p_lesc_oobd_peer);
+ return err_code;
+}
+
+void nrf_ble_lesc_on_ble_evt(ble_evt_t const *p_ble_evt)
+{
+ uint32_t err_code = NRF_SUCCESS;
+ const uint16_t conn_handle = p_ble_evt->evt.gap_evt.conn_handle;
+ const int idx = nrf_sdh_ble_idx_get(conn_handle);
+
+ __ASSERT(idx >= 0, "Invalid idx %d for conn_handle %#x, evt_id %#x",
+ idx, conn_handle, p_ble_evt->header.evt_id);
+
+ switch (p_ble_evt->header.evt_id) {
+ case BLE_GAP_EVT_DISCONNECTED:
+ m_peer_keys[idx].is_valid = false;
+ m_peer_keys[idx].is_requested = false;
+ m_peer_keys[idx].passkey_requested = false;
+ m_peer_keys[idx].passkey_displayed = false;
+
+ break;
+
+ case BLE_GAP_EVT_AUTH_KEY_REQUEST:
+ if (p_ble_evt->evt.gap_evt.params.auth_key_request.key_type ==
+ BLE_GAP_AUTH_KEY_TYPE_PASSKEY) {
+ m_peer_keys[idx].passkey_requested = true;
+ }
+
+ break;
+
+ case BLE_GAP_EVT_PASSKEY_DISPLAY:
+ m_peer_keys[idx].passkey_displayed = true;
+
+ break;
+
+ case BLE_GAP_EVT_LESC_DHKEY_REQUEST:
+ LOG_DBG("BLE_GAP_EVT_LESC_DHKEY_REQUEST");
+
+ if (p_ble_evt->evt.gap_evt.params.lesc_dhkey_request.oobd_req) {
+ err_code = lesc_oob_data_set(conn_handle);
+ if (err_code != NRF_SUCCESS) {
+ LOG_ERR("sd_ble_gap_lesc_oob_data_set() returned error 0x%x.",
+ err_code);
+ m_ble_lesc_internal_error = true;
+ }
+ }
+
+ err_code = on_dhkey_request(conn_handle, idx,
+ &p_ble_evt->evt.gap_evt.params.lesc_dhkey_request);
+ if (err_code != NRF_SUCCESS) {
+ m_ble_lesc_internal_error = true;
+ }
+ break;
+
+#if defined(CONFIG_PM_LESC_GENERATE_NEW_KEYS)
+ case BLE_GAP_EVT_AUTH_STATUS:
+ /* Generate new pairing keys. */
+ err_code = nrf_ble_lesc_keypair_generate();
+ if (err_code != NRF_SUCCESS) {
+ m_ble_lesc_internal_error = true;
+ }
+
+ break;
+#endif /* CONFIG_PM_LESC_GENERATE_NEW_KEYS */
+
+ default:
+ break;
+ }
+}
diff --git a/lib/peer_manager/modules/peer_data_storage.c b/lib/peer_manager/modules/peer_data_storage.c
new file mode 100644
index 0000000000..2e19f22c3c
--- /dev/null
+++ b/lib/peer_manager/modules/peer_data_storage.c
@@ -0,0 +1,541 @@
+/*
+ * Copyright (c) 2015-2025 Nordic Semiconductor ASA
+ *
+ * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
+ */
+
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+LOG_MODULE_DECLARE(peer_manager, CONFIG_PEER_MANAGER_LOG_LEVEL);
+
+/* Macro for verifying that the peer id is within a valid range. */
+#define VERIFY_PEER_ID_IN_RANGE(id) \
+ VERIFY_FALSE((id >= PM_PEER_ID_N_AVAILABLE_IDS), NRF_ERROR_INVALID_PARAM)
+
+/* Macro for verifying that the peer data id is within a valid range. */
+#define VERIFY_PEER_DATA_ID_IN_RANGE(id) \
+ VERIFY_TRUE(peer_data_id_is_valid(id), NRF_ERROR_INVALID_PARAM)
+
+/* The number of registered event handlers. */
+#define PDS_EVENT_HANDLERS_CNT ARRAY_SIZE(m_evt_handlers)
+
+/* Peer Data Storage event handler in Peer Database. */
+extern void pdb_pds_evt_handler(pm_evt_t *evt);
+
+/* Peer Data Storage events' handlers.
+ * The number of elements in this array is PDS_EVENT_HANDLERS_CNT.
+ */
+static pm_evt_handler_internal_t const m_evt_handlers[] = {
+ pdb_pds_evt_handler,
+};
+
+static bool m_module_initialized;
+static volatile bool m_peer_delete_deferred;
+
+/* A token used for Flash Data Storage searches. */
+static fds_find_token_t m_fds_ftok;
+
+/* Function for dispatching events to all registered event handlers. */
+static void pds_evt_send(pm_evt_t *p_event)
+{
+ p_event->conn_handle = BLE_CONN_HANDLE_INVALID;
+
+ for (uint32_t i = 0; i < PDS_EVENT_HANDLERS_CNT; i++) {
+ m_evt_handlers[i](p_event);
+ }
+}
+
+/* Function to convert peer IDs to file IDs. */
+static uint16_t peer_id_to_file_id(pm_peer_id_t peer_id)
+{
+ return (uint16_t)(peer_id + PEER_ID_TO_FILE_ID);
+}
+
+/* Function to convert peer data id to type id. */
+static pm_peer_id_t file_id_to_peer_id(uint16_t file_id)
+{
+ return (pm_peer_id_t)(file_id + FILE_ID_TO_PEER_ID);
+}
+
+/* Function to convert peer data IDs to record keys. */
+static uint16_t peer_data_id_to_record_key(pm_peer_data_id_t peer_data_id)
+{
+ return (uint16_t)(peer_data_id + DATA_ID_TO_RECORD_KEY);
+}
+
+/* Function to convert record keys to peer data IDs. */
+static pm_peer_data_id_t record_key_to_peer_data_id(uint16_t record_key)
+{
+ return (pm_peer_data_id_t)(record_key + RECORD_KEY_TO_DATA_ID);
+}
+
+/* Function for checking whether a file ID is relevant for the Peer Manager. */
+static bool file_id_within_pm_range(uint16_t file_id)
+{
+ return ((PDS_FIRST_RESERVED_FILE_ID <= file_id) && (file_id <= PDS_LAST_RESERVED_FILE_ID));
+}
+
+/* Function for checking whether a record key is relevant for the Peer Manager. */
+static bool record_key_within_pm_range(uint16_t record_key)
+{
+ return ((PDS_FIRST_RESERVED_RECORD_KEY <= record_key) &&
+ (record_key <= PDS_LAST_RESERVED_RECORD_KEY));
+}
+
+static bool peer_data_id_is_valid(pm_peer_data_id_t data_id)
+{
+ return ((data_id == PM_PEER_DATA_ID_BONDING) ||
+ (data_id == PM_PEER_DATA_ID_SERVICE_CHANGED_PENDING) ||
+ (data_id == PM_PEER_DATA_ID_GATT_LOCAL) ||
+ (data_id == PM_PEER_DATA_ID_GATT_REMOTE) ||
+ (data_id == PM_PEER_DATA_ID_PEER_RANK) ||
+ (data_id == PM_PEER_DATA_ID_CENTRAL_ADDR_RES) ||
+ (data_id == PM_PEER_DATA_ID_APPLICATION));
+}
+
+/**
+ * @brief Function for sending a PM_EVT_ERROR_UNEXPECTED event.
+ *
+ * @param[in] peer_id The peer the event pertains to.
+ * @param[in] err_code The unexpected error that occurred.
+ */
+static void send_unexpected_error(pm_peer_id_t peer_id, uint32_t err_code)
+{
+ pm_evt_t error_evt = {.evt_id = PM_EVT_ERROR_UNEXPECTED,
+ .peer_id = peer_id,
+ .params = {.error_unexpected = {
+ .error = err_code,
+ }}};
+ pds_evt_send(&error_evt);
+}
+
+/* Function for deleting all data beloning to a peer.
+ * These operations will be sent to FDS one at a time.
+ */
+static void peer_data_delete_process(void)
+{
+ uint32_t ret;
+ pm_peer_id_t peer_id;
+ uint16_t file_id;
+ fds_record_desc_t desc;
+ fds_find_token_t ftok;
+
+ m_peer_delete_deferred = false;
+
+ memset(&ftok, 0x00, sizeof(fds_find_token_t));
+ peer_id = peer_id_get_next_deleted(PM_PEER_ID_INVALID);
+
+ while ((peer_id != PM_PEER_ID_INVALID) &&
+ (fds_record_find_in_file(peer_id_to_file_id(peer_id), &desc, &ftok) ==
+ FDS_ERR_NOT_FOUND)) {
+ peer_id_free(peer_id);
+ peer_id = peer_id_get_next_deleted(peer_id);
+ }
+
+ if (peer_id != PM_PEER_ID_INVALID) {
+ file_id = peer_id_to_file_id(peer_id);
+ ret = fds_file_delete(file_id);
+
+ if (ret == FDS_ERR_NO_SPACE_IN_QUEUES) {
+ m_peer_delete_deferred = true;
+ } else if (ret != NRF_SUCCESS) {
+ LOG_ERR("Could not delete peer data. fds_file_delete() returned 0x%x "
+ "for peer_id: %d",
+ ret, peer_id);
+ send_unexpected_error(peer_id, ret);
+ }
+ }
+}
+
+static uint32_t peer_data_find(pm_peer_id_t peer_id, pm_peer_data_id_t data_id,
+ fds_record_desc_t *const p_desc)
+{
+ uint32_t ret;
+ fds_find_token_t ftok;
+
+ NRF_PM_DEBUG_CHECK(peer_id < PM_PEER_ID_N_AVAILABLE_IDS);
+ NRF_PM_DEBUG_CHECK(peer_data_id_is_valid(data_id));
+ NRF_PM_DEBUG_CHECK(p_desc != NULL);
+
+ memset(&ftok, 0x00, sizeof(fds_find_token_t));
+
+ uint16_t file_id = peer_id_to_file_id(peer_id);
+ uint16_t record_key = peer_data_id_to_record_key(data_id);
+
+ ret = fds_record_find(file_id, record_key, p_desc, &ftok);
+
+ if (ret != NRF_SUCCESS) {
+ return NRF_ERROR_NOT_FOUND;
+ }
+
+ return NRF_SUCCESS;
+}
+
+static void peer_ids_load(void)
+{
+ fds_record_desc_t record_desc;
+ fds_flash_record_t record;
+ fds_find_token_t ftok;
+
+ memset(&ftok, 0x00, sizeof(fds_find_token_t));
+
+ uint16_t const record_key = peer_data_id_to_record_key(PM_PEER_DATA_ID_BONDING);
+
+ while (fds_record_find_by_key(record_key, &record_desc, &ftok) == NRF_SUCCESS) {
+ pm_peer_id_t peer_id;
+
+ /* It is safe to ignore the return value since the descriptor was
+ * just obtained and also 'record' is different from NULL.
+ */
+ (void)fds_record_open(&record_desc, &record);
+ peer_id = file_id_to_peer_id(record.p_header->file_id);
+ (void)fds_record_close(&record_desc);
+
+ (void)peer_id_allocate(peer_id);
+ }
+}
+
+static void fds_evt_handler(fds_evt_t const *const p_fds_evt)
+{
+ pm_evt_t pds_evt = {.peer_id = file_id_to_peer_id(p_fds_evt->write.file_id)};
+
+ switch (p_fds_evt->id) {
+ case FDS_EVT_WRITE:
+ case FDS_EVT_UPDATE:
+ case FDS_EVT_DEL_RECORD:
+ if (file_id_within_pm_range(p_fds_evt->write.file_id) ||
+ record_key_within_pm_range(p_fds_evt->write.record_key)) {
+ pds_evt.params.peer_data_update_succeeded.data_id =
+ record_key_to_peer_data_id(p_fds_evt->write.record_key);
+ pds_evt.params.peer_data_update_succeeded.action =
+ (p_fds_evt->id == FDS_EVT_DEL_RECORD) ? PM_PEER_DATA_OP_DELETE
+ : PM_PEER_DATA_OP_UPDATE;
+ pds_evt.params.peer_data_update_succeeded.token =
+ p_fds_evt->write.record_id;
+
+ if (p_fds_evt->result == NRF_SUCCESS) {
+ pds_evt.evt_id = PM_EVT_PEER_DATA_UPDATE_SUCCEEDED;
+ pds_evt.params.peer_data_update_succeeded.flash_changed = true;
+ } else {
+ pds_evt.evt_id = PM_EVT_PEER_DATA_UPDATE_FAILED;
+ pds_evt.params.peer_data_update_failed.error = p_fds_evt->result;
+ }
+
+ pds_evt_send(&pds_evt);
+ }
+ break;
+
+ case FDS_EVT_DEL_FILE:
+ if (file_id_within_pm_range(p_fds_evt->del.file_id) &&
+ (p_fds_evt->del.record_key == FDS_RECORD_KEY_DIRTY)) {
+ if (p_fds_evt->result == NRF_SUCCESS) {
+ pds_evt.evt_id = PM_EVT_PEER_DELETE_SUCCEEDED;
+ peer_id_free(pds_evt.peer_id);
+ } else {
+ pds_evt.evt_id = PM_EVT_PEER_DELETE_FAILED;
+ pds_evt.params.peer_delete_failed.error = p_fds_evt->result;
+ }
+
+ /* Trigger remaining deletes. */
+ m_peer_delete_deferred = true;
+
+ pds_evt_send(&pds_evt);
+ }
+ break;
+
+ case FDS_EVT_GC:
+ if (p_fds_evt->result == NRF_SUCCESS) {
+ pds_evt.evt_id = PM_EVT_FLASH_GARBAGE_COLLECTED;
+ } else {
+ pds_evt.evt_id = PM_EVT_FLASH_GARBAGE_COLLECTION_FAILED;
+ pds_evt.params.garbage_collection_failed.error = p_fds_evt->result;
+ }
+ pds_evt.peer_id = PM_PEER_ID_INVALID;
+ pds_evt_send(&pds_evt);
+ break;
+
+ default:
+ /* No action. */
+ break;
+ }
+
+ if (m_peer_delete_deferred) {
+ peer_data_delete_process();
+ }
+}
+
+uint32_t pds_init(void)
+{
+ uint32_t ret;
+
+ /* Check for re-initialization if debugging. */
+ NRF_PM_DEBUG_CHECK(!m_module_initialized);
+
+ ret = fds_register(fds_evt_handler);
+ if (ret != NRF_SUCCESS) {
+ LOG_ERR("Could not initialize flash storage. fds_register() returned 0x%x.", ret);
+ return NRF_ERROR_INTERNAL;
+ }
+
+ ret = fds_init();
+ if (ret != NRF_SUCCESS) {
+ LOG_ERR("Could not initialize flash storage. fds_init() returned 0x%x.", ret);
+ return NRF_ERROR_RESOURCES;
+ }
+
+ peer_id_init();
+ peer_ids_load();
+
+ m_module_initialized = true;
+
+ return NRF_SUCCESS;
+}
+
+uint32_t pds_peer_data_read(pm_peer_id_t peer_id, pm_peer_data_id_t data_id,
+ pm_peer_data_t *const p_data, uint32_t const *const p_buf_len)
+{
+ uint32_t ret;
+ fds_record_desc_t rec_desc;
+ fds_flash_record_t rec_flash;
+
+ NRF_PM_DEBUG_CHECK(m_module_initialized);
+ NRF_PM_DEBUG_CHECK(p_data != NULL);
+
+ VERIFY_PEER_ID_IN_RANGE(peer_id);
+ VERIFY_PEER_DATA_ID_IN_RANGE(data_id);
+
+ ret = peer_data_find(peer_id, data_id, &rec_desc);
+
+ if (ret != NRF_SUCCESS) {
+ return NRF_ERROR_NOT_FOUND;
+ }
+
+ /* Shouldn't fail, unless the record was deleted in the meanwhile or the CRC check has
+ * failed.
+ */
+ ret = fds_record_open(&rec_desc, &rec_flash);
+
+ if (ret != NRF_SUCCESS) {
+ return NRF_ERROR_NOT_FOUND;
+ }
+
+ p_data->data_id = data_id;
+ p_data->length_words = rec_flash.p_header->length_words;
+
+ /* If p_buf_len is NULL, provide a pointer to data in flash, otherwise,
+ * check that the buffer is large enough and copy the data in flash into the buffer.
+ */
+ if (p_buf_len == NULL) {
+ /* The cast is necessary because if no buffer is provided, we just copy the pointer,
+ * but in that case it should be considered a pointer to const data by the caller,
+ * since it is a pointer to data in flash.
+ */
+ p_data->p_all_data = (void *)rec_flash.p_data;
+ } else {
+ uint32_t const data_len_bytes = (p_data->length_words * sizeof(uint32_t));
+ uint32_t const copy_len_bytes =
+ MIN((*p_buf_len), (p_data->length_words * sizeof(uint32_t)));
+
+ memcpy(p_data->p_all_data, rec_flash.p_data, copy_len_bytes);
+
+ if (copy_len_bytes < data_len_bytes) {
+ return NRF_ERROR_DATA_SIZE;
+ }
+ }
+
+ /* Shouldn't fail unless the record was already closed, in which case it can be ignored. */
+ (void)fds_record_close(&rec_desc);
+
+ return NRF_SUCCESS;
+}
+
+void pds_peer_data_iterate_prepare(void)
+{
+ memset(&m_fds_ftok, 0x00, sizeof(fds_find_token_t));
+}
+
+bool pds_peer_data_iterate(pm_peer_data_id_t data_id, pm_peer_id_t *const p_peer_id,
+ pm_peer_data_flash_t *const p_data)
+{
+ uint32_t ret;
+ uint16_t rec_key;
+ fds_record_desc_t rec_desc;
+ fds_flash_record_t rec_flash;
+
+ NRF_PM_DEBUG_CHECK(m_module_initialized);
+ NRF_PM_DEBUG_CHECK(p_peer_id != NULL);
+ NRF_PM_DEBUG_CHECK(p_data != NULL);
+
+ VERIFY_PEER_DATA_ID_IN_RANGE(data_id);
+
+ rec_key = peer_data_id_to_record_key(data_id);
+
+ if (fds_record_find_by_key(rec_key, &rec_desc, &m_fds_ftok) != NRF_SUCCESS) {
+ return false;
+ }
+
+ ret = fds_record_open(&rec_desc, &rec_flash);
+
+ if (ret != NRF_SUCCESS) {
+ /* It can only happen if the record was deleted after the call to
+ * fds_record_find_by_key(), before we could open it, or if CRC support was enabled
+ * in Flash Data Storage at compile time and the CRC check failed.
+ */
+ return false;
+ }
+
+ p_data->data_id = data_id;
+ p_data->length_words = rec_flash.p_header->length_words;
+ p_data->p_all_data = rec_flash.p_data;
+
+ *p_peer_id = file_id_to_peer_id(rec_flash.p_header->file_id);
+
+ (void)fds_record_close(&rec_desc);
+
+ return true;
+}
+
+uint32_t pds_peer_data_store(pm_peer_id_t peer_id, pm_peer_data_const_t const *p_peer_data,
+ pm_store_token_t *p_store_token)
+{
+ uint32_t ret;
+ fds_record_t rec;
+ fds_record_desc_t rec_desc;
+
+ NRF_PM_DEBUG_CHECK(m_module_initialized);
+ NRF_PM_DEBUG_CHECK(p_peer_data != NULL);
+
+ VERIFY_PEER_ID_IN_RANGE(peer_id);
+ VERIFY_PEER_DATA_ID_IN_RANGE(p_peer_data->data_id);
+
+ /* Prepare the record to be stored in flash. */
+ rec.file_id = peer_id_to_file_id(peer_id);
+ rec.key = peer_data_id_to_record_key(p_peer_data->data_id);
+ rec.data.p_data = (void *)p_peer_data->p_all_data;
+ rec.data.length_words = p_peer_data->length_words;
+
+ ret = peer_data_find(peer_id, p_peer_data->data_id, &rec_desc);
+
+ if (ret == NRF_ERROR_NOT_FOUND) {
+ ret = fds_record_write(&rec_desc, &rec);
+ } else {
+ /* Update existing record. */
+ ret = fds_record_update(&rec_desc, &rec);
+ }
+
+ switch (ret) {
+ case NRF_SUCCESS:
+ if (p_store_token != NULL) {
+ /* Update the store token. */
+ (void)fds_record_id_from_desc(&rec_desc, (uint32_t *)p_store_token);
+ }
+ return NRF_SUCCESS;
+
+ case FDS_ERR_BUSY:
+ case FDS_ERR_NO_SPACE_IN_QUEUES:
+ return NRF_ERROR_BUSY;
+
+ case FDS_ERR_NO_SPACE_IN_FLASH:
+ return NRF_ERROR_RESOURCES;
+
+ case FDS_ERR_UNALIGNED_ADDR:
+ return NRF_ERROR_INVALID_ADDR;
+
+ default:
+ LOG_ERR("Could not write data to flash. fds_record_{write|update}() returned 0x%x. "
+ "peer_id: %d",
+ ret, peer_id);
+ return NRF_ERROR_INTERNAL;
+ }
+}
+
+uint32_t pds_peer_data_delete(pm_peer_id_t peer_id, pm_peer_data_id_t data_id)
+{
+ uint32_t ret;
+ fds_record_desc_t record_desc;
+
+ NRF_PM_DEBUG_CHECK(m_module_initialized);
+
+ VERIFY_PEER_ID_IN_RANGE(peer_id);
+ VERIFY_PEER_DATA_ID_IN_RANGE(data_id);
+
+ ret = peer_data_find(peer_id, data_id, &record_desc);
+
+ if (ret != NRF_SUCCESS) {
+ return NRF_ERROR_NOT_FOUND;
+ }
+
+ ret = fds_record_delete(&record_desc);
+
+ switch (ret) {
+ case NRF_SUCCESS:
+ return NRF_SUCCESS;
+
+ case FDS_ERR_NO_SPACE_IN_QUEUES:
+ return NRF_ERROR_BUSY;
+
+ default:
+ LOG_ERR("Could not delete peer. fds_record_delete() returned 0x%x. peer_id: %d, "
+ "data_id: %d.",
+ ret, peer_id, data_id);
+ return NRF_ERROR_INTERNAL;
+ }
+}
+
+pm_peer_id_t pds_peer_id_allocate(void)
+{
+ NRF_PM_DEBUG_CHECK(m_module_initialized);
+ return peer_id_allocate(PM_PEER_ID_INVALID);
+}
+
+uint32_t pds_peer_id_free(pm_peer_id_t peer_id)
+{
+ NRF_PM_DEBUG_CHECK(m_module_initialized);
+ VERIFY_PEER_ID_IN_RANGE(peer_id);
+
+ (void)peer_id_delete(peer_id);
+ peer_data_delete_process();
+
+ return NRF_SUCCESS;
+}
+
+bool pds_peer_id_is_allocated(pm_peer_id_t peer_id)
+{
+ NRF_PM_DEBUG_CHECK(m_module_initialized);
+ return peer_id_is_allocated(peer_id);
+}
+
+bool pds_peer_id_is_deleted(pm_peer_id_t peer_id)
+{
+ NRF_PM_DEBUG_CHECK(m_module_initialized);
+ return peer_id_is_deleted(peer_id);
+}
+
+pm_peer_id_t pds_next_peer_id_get(pm_peer_id_t prev_peer_id)
+{
+ NRF_PM_DEBUG_CHECK(m_module_initialized);
+ return peer_id_get_next_used(prev_peer_id);
+}
+
+pm_peer_id_t pds_next_deleted_peer_id_get(pm_peer_id_t prev_peer_id)
+{
+ NRF_PM_DEBUG_CHECK(m_module_initialized);
+ return peer_id_get_next_deleted(prev_peer_id);
+}
+
+uint32_t pds_peer_count_get(void)
+{
+ NRF_PM_DEBUG_CHECK(m_module_initialized);
+ return peer_id_n_ids();
+}
diff --git a/lib/peer_manager/modules/peer_database.c b/lib/peer_manager/modules/peer_database.c
new file mode 100644
index 0000000000..cf701ce5c2
--- /dev/null
+++ b/lib/peer_manager/modules/peer_database.c
@@ -0,0 +1,647 @@
+/*
+ * Copyright (c) 2015-2025 Nordic Semiconductor ASA
+ *
+ * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
+ */
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+LOG_MODULE_DECLARE(peer_manager, CONFIG_PEER_MANAGER_LOG_LEVEL);
+
+/**
+ * @brief Macro for verifying that the data ID is among the values eligible for using the write
+ * buffer.
+ *
+ * @param[in] data_id The data ID to verify.
+ */
+#define VERIFY_DATA_ID_WRITE_BUF(data_id) \
+ do { \
+ if (((data_id) != PM_PEER_DATA_ID_BONDING) && \
+ ((data_id) != PM_PEER_DATA_ID_GATT_LOCAL)) { \
+ return NRF_ERROR_INVALID_PARAM; \
+ } \
+ } while (0)
+
+/* The number of registered event handlers. */
+#define PDB_EVENT_HANDLERS_CNT ARRAY_SIZE(m_evt_handlers)
+
+/* Peer Database event handlers in other Peer Manager submodules. */
+extern void pm_pdb_evt_handler(pm_evt_t *p_event);
+extern void sm_pdb_evt_handler(pm_evt_t *p_event);
+#if !defined(CONFIG_PM_SERVICE_CHANGED_ENABLED) || (CONFIG_PM_SERVICE_CHANGED_ENABLED == 1)
+extern void gscm_pdb_evt_handler(pm_evt_t *p_event);
+#endif
+extern void gcm_pdb_evt_handler(pm_evt_t *p_event);
+
+/**
+ * @brief Peer Database events' handlers.
+ * The number of elements in this array is PDB_EVENT_HANDLERS_CNT.
+ */
+static pm_evt_handler_internal_t const m_evt_handlers[] = {
+ pm_pdb_evt_handler,
+ sm_pdb_evt_handler,
+#if !defined(CONFIG_PM_SERVICE_CHANGED_ENABLED) || (CONFIG_PM_SERVICE_CHANGED_ENABLED == 1)
+ gscm_pdb_evt_handler,
+#endif
+ gcm_pdb_evt_handler,
+};
+
+/**
+ * @brief Struct for keeping track of one write buffer, from allocation, until it is fully written
+ * or cancelled.
+ */
+typedef struct {
+ /** @brief The peer ID this buffer belongs to. */
+ pm_peer_id_t peer_id;
+ /** @brief The data ID this buffer belongs to. */
+ pm_peer_data_id_t data_id;
+ /**
+ * @brief Token given by Peer Data Storage when a flash write has been
+ * successfully requested. This is used as the check for whether such
+ * an operation has been successfully requested.
+ */
+ pm_store_token_t store_token;
+ /** @brief The number of buffer blocks containing peer data. */
+ uint8_t n_bufs;
+ /** @brief The index of the first (or only) buffer block containing peer data. */
+ uint8_t buffer_block_id;
+ /**
+ * @brief Flag indicating that the buffer was attempted written to
+ * flash, but a flash full error was returned and the operation
+ * should be retried after room has been made.
+ */
+ uint8_t store_flash_full: 1;
+ /**
+ * @brief Flag indicating that the buffer was attempted written to flash,
+ * but a busy error was returned and the operation should be retried.
+ */
+ uint8_t store_busy: 1;
+} pdb_buffer_record_t;
+
+static bool m_module_initialized;
+/** @brief The internal states of the write buffer. */
+static pm_buffer_t m_write_buffer;
+/** @brief The available write buffer records. */
+static pdb_buffer_record_t m_write_buffer_records[CONFIG_PM_FLASH_BUFFERS];
+/**
+ * @brief Whether there are any pending (Not yet successfully requested in Peer Data
+ * Storage) store operations. This flag is for convenience only. The real bookkeeping
+ * is in the records (@ref m_write_buffer_records).
+ */
+static bool m_pending_store;
+
+/**
+ * @brief Function for invalidating a record of a write buffer allocation.
+ *
+ * @param[in] p_record The record to invalidate.
+ */
+static void write_buffer_record_invalidate(pdb_buffer_record_t *p_record)
+{
+ p_record->peer_id = PM_PEER_ID_INVALID;
+ p_record->data_id = PM_PEER_DATA_ID_INVALID;
+ p_record->buffer_block_id = PM_BUFFER_INVALID_ID;
+ p_record->store_busy = false;
+ p_record->store_flash_full = false;
+ p_record->n_bufs = 0;
+ p_record->store_token = PM_STORE_TOKEN_INVALID;
+}
+
+/**
+ * @brief Function for finding a record of a write buffer allocation.
+ *
+ * @param[in] peer_id The peer ID in the record.
+ * @param[inout] p_index In: The starting index, out: The index of the record
+ *
+ * @return A pointer to the matching record, or NULL if none was found.
+ */
+static pdb_buffer_record_t *write_buffer_record_find_next(pm_peer_id_t peer_id, uint32_t *p_index)
+{
+ for (uint32_t i = *p_index; i < CONFIG_PM_FLASH_BUFFERS; i++) {
+ if (m_write_buffer_records[i].peer_id == peer_id) {
+ *p_index = i;
+ return &m_write_buffer_records[i];
+ }
+ }
+ return NULL;
+}
+
+/**
+ * @brief Function for finding a record of a write buffer allocation.
+ *
+ * @param[in] peer_id The peer ID in the record.
+ * @param[in] data_id The data ID in the record.
+ *
+ * @return A pointer to the matching record, or NULL if none was found.
+ */
+static pdb_buffer_record_t *write_buffer_record_find(pm_peer_id_t peer_id,
+ pm_peer_data_id_t data_id)
+{
+ uint32_t index = 0;
+ pdb_buffer_record_t *p_record = write_buffer_record_find_next(peer_id, &index);
+
+ while ((p_record != NULL) && ((p_record->data_id != data_id) || (p_record->store_busy) ||
+ (p_record->store_flash_full) ||
+ (p_record->store_token != PM_STORE_TOKEN_INVALID))) {
+ index++;
+ p_record = write_buffer_record_find_next(peer_id, &index);
+ }
+
+ return p_record;
+}
+
+/**
+ * @brief Function for finding a record of a write buffer allocation that has been sent to be
+ * stored.
+ *
+ * @param[in] store_token The store token received when store was called for the record.
+ *
+ * @return A pointer to the matching record, or NULL if none was found.
+ */
+static pdb_buffer_record_t *write_buffer_record_find_stored(pm_store_token_t store_token)
+{
+ for (int i = 0; i < CONFIG_PM_FLASH_BUFFERS; i++) {
+ if (m_write_buffer_records[i].store_token == store_token) {
+ return &m_write_buffer_records[i];
+ }
+ }
+ return NULL;
+}
+
+/**
+ * @brief Function for finding an available record for write buffer allocation.
+ *
+ * @return A pointer to the available record, or NULL if none was found.
+ */
+static pdb_buffer_record_t *write_buffer_record_find_unused(void)
+{
+ return write_buffer_record_find(PM_PEER_ID_INVALID, PM_PEER_DATA_ID_INVALID);
+}
+
+/**
+ * @brief Function for gracefully deactivating a write buffer record.
+ *
+ * @details This function will first release any buffers, then invalidate the record.
+ *
+ * @param[inout] p_write_buffer_record The record to release.
+ *
+ * @return A pointer to the matching record, or NULL if none was found.
+ */
+static void write_buffer_record_release(pdb_buffer_record_t *p_write_buffer_record)
+{
+ for (uint32_t i = 0; i < p_write_buffer_record->n_bufs; i++) {
+ pm_buffer_release(&m_write_buffer, p_write_buffer_record->buffer_block_id + i);
+ }
+
+ write_buffer_record_invalidate(p_write_buffer_record);
+}
+
+/**
+ * @brief Function for claiming and activating a write buffer record.
+ *
+ * @param[out] pp_write_buffer_record The claimed record.
+ * @param[in] peer_id The peer ID this record should have.
+ * @param[in] data_id The data ID this record should have.
+ */
+static void write_buffer_record_acquire(pdb_buffer_record_t **pp_write_buffer_record,
+ pm_peer_id_t peer_id, pm_peer_data_id_t data_id)
+{
+ if (pp_write_buffer_record == NULL) {
+ return;
+ }
+ *pp_write_buffer_record = write_buffer_record_find_unused();
+ if (*pp_write_buffer_record == NULL) {
+ /* This also means the buffer is full. */
+ return;
+ }
+ (*pp_write_buffer_record)->peer_id = peer_id;
+ (*pp_write_buffer_record)->data_id = data_id;
+}
+
+/**
+ * @brief Function for dispatching outbound events to all registered event handlers.
+ *
+ * @param[in] p_event The event to dispatch.
+ */
+static void pdb_evt_send(pm_evt_t *p_event)
+{
+ for (uint32_t i = 0; i < PDB_EVENT_HANDLERS_CNT; i++) {
+ m_evt_handlers[i](p_event);
+ }
+}
+
+/**
+ * @brief Function for resetting the internal state of the Peer Database module.
+ *
+ * @param[out] p_event The event to dispatch.
+ */
+static void internal_state_reset(void)
+{
+ for (uint32_t i = 0; i < CONFIG_PM_FLASH_BUFFERS; i++) {
+ write_buffer_record_invalidate(&m_write_buffer_records[i]);
+ }
+}
+
+static void peer_data_point_to_buffer(pm_peer_data_t *p_peer_data, pm_peer_data_id_t data_id,
+ uint8_t *p_buffer_memory, uint16_t n_bufs)
+{
+ uint16_t n_bytes = n_bufs * PDB_WRITE_BUF_SIZE;
+
+ p_peer_data->data_id = data_id;
+
+ p_peer_data->p_all_data = (pm_peer_data_bonding_t *)p_buffer_memory;
+ p_peer_data->length_words = BYTES_TO_WORDS(n_bytes);
+}
+
+static void peer_data_const_point_to_buffer(pm_peer_data_const_t *p_peer_data,
+ pm_peer_data_id_t data_id, uint8_t *p_buffer_memory,
+ uint32_t n_bufs)
+{
+ peer_data_point_to_buffer((pm_peer_data_t *)p_peer_data, data_id, p_buffer_memory, n_bufs);
+}
+
+static void write_buf_length_words_set(pm_peer_data_const_t *p_peer_data)
+{
+ switch (p_peer_data->data_id) {
+ case PM_PEER_DATA_ID_BONDING:
+ p_peer_data->length_words = PM_BONDING_DATA_N_WORDS();
+ break;
+ case PM_PEER_DATA_ID_SERVICE_CHANGED_PENDING:
+ p_peer_data->length_words = PM_SC_STATE_N_WORDS();
+ break;
+ case PM_PEER_DATA_ID_PEER_RANK:
+ p_peer_data->length_words = PM_USAGE_INDEX_N_WORDS();
+ break;
+ case PM_PEER_DATA_ID_GATT_LOCAL:
+ p_peer_data->length_words = PM_LOCAL_DB_N_WORDS(p_peer_data->p_local_gatt_db->len);
+ break;
+ default:
+ /* No action needed. */
+ break;
+ }
+}
+
+/**
+ * @brief Function for writing data into persistent storage. Writing happens asynchronously.
+ *
+ * @note This will unlock the data after it has been written.
+ *
+ * @param[in] p_write_buffer_record The write buffer record to write into persistent storage.
+ *
+ * @retval NRF_SUCCESS Data storing was successfully started.
+ * @retval NRF_ERROR_RESOURCES No space available in persistent storage. Please clear some
+ * space, the operation will be reattempted after the next compress
+ * procedure.
+ * @retval NRF_ERROR_INVALID_PARAM Data ID was invalid.
+ * @retval NRF_ERROR_INVALID_STATE Module is not initialized.
+ * @retval NRF_ERROR_INTERNAL Unexpected internal error.
+ */
+uint32_t write_buf_store(pdb_buffer_record_t *p_write_buffer_record)
+{
+ uint32_t err_code = NRF_SUCCESS;
+ pm_peer_data_const_t peer_data = {.data_id = p_write_buffer_record->data_id};
+ uint8_t *p_buffer_memory;
+
+ p_buffer_memory =
+ pm_buffer_ptr_get(&m_write_buffer, p_write_buffer_record->buffer_block_id);
+ if (p_buffer_memory == NULL) {
+ LOG_ERR("pm_buffer_ptr_get() could not retrieve RAM buffer. block_id: %d",
+ p_write_buffer_record->buffer_block_id);
+ return NRF_ERROR_INTERNAL;
+ }
+
+ peer_data_const_point_to_buffer(&peer_data, p_write_buffer_record->data_id, p_buffer_memory,
+ p_write_buffer_record->n_bufs);
+ write_buf_length_words_set(&peer_data);
+
+ err_code = pds_peer_data_store(p_write_buffer_record->peer_id, &peer_data,
+ &p_write_buffer_record->store_token);
+
+ switch (err_code) {
+ case NRF_SUCCESS:
+ p_write_buffer_record->store_busy = false;
+ p_write_buffer_record->store_flash_full = false;
+ break;
+
+ case NRF_ERROR_BUSY:
+ p_write_buffer_record->store_busy = true;
+ p_write_buffer_record->store_flash_full = false;
+ m_pending_store = true;
+
+ err_code = NRF_SUCCESS;
+ break;
+
+ case NRF_ERROR_RESOURCES:
+ p_write_buffer_record->store_busy = false;
+ p_write_buffer_record->store_flash_full = true;
+ m_pending_store = true;
+ break;
+
+ case NRF_ERROR_INVALID_PARAM:
+ /* No action. */
+ break;
+
+ default:
+ LOG_ERR("pds_peer_data_store() returned %s. peer_id: %d",
+ nrf_strerror_get(err_code), p_write_buffer_record->peer_id);
+ err_code = NRF_ERROR_INTERNAL;
+ break;
+ }
+
+ return err_code;
+}
+
+/**
+ * @brief This calls @ref write_buf_store and sends events based on the return value.
+ *
+ * See @ref write_buf_store for more info.
+ *
+ * @return Whether or not the store operation succeeded.
+ */
+static bool write_buf_store_in_event(pdb_buffer_record_t *p_write_buffer_record)
+{
+ uint32_t err_code;
+ pm_evt_t event;
+
+ err_code = write_buf_store(p_write_buffer_record);
+ if (err_code != NRF_SUCCESS) {
+ event.conn_handle = BLE_CONN_HANDLE_INVALID;
+ event.peer_id = p_write_buffer_record->peer_id;
+
+ if (err_code == NRF_ERROR_RESOURCES) {
+ event.evt_id = PM_EVT_STORAGE_FULL;
+ } else {
+ event.evt_id = PM_EVT_ERROR_UNEXPECTED;
+ event.params.error_unexpected.error = err_code;
+
+ LOG_ERR("Some peer data was not properly written to flash. "
+ "write_buf_store() "
+ "returned %s for peer_id: %d",
+ nrf_strerror_get(err_code), p_write_buffer_record->peer_id);
+ }
+
+ pdb_evt_send(&event);
+
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * @brief This reattempts store operations on write buffers, that previously failed because of @ref
+ * NRF_ERROR_BUSY or @ref NRF_ERROR_RESOURCES errors.
+ *
+ * param[in] retry_flash_full Whether to retry operations that failed because of an
+ * @ref NRF_ERROR_RESOURCES error.
+ */
+static void reattempt_previous_operations(bool retry_flash_full)
+{
+ if (!m_pending_store) {
+ return;
+ }
+
+ m_pending_store = false;
+
+ for (uint32_t i = 0; i < CONFIG_PM_FLASH_BUFFERS; i++) {
+ if ((m_write_buffer_records[i].store_busy) ||
+ (m_write_buffer_records[i].store_flash_full)) {
+ m_pending_store = true;
+
+ if (m_write_buffer_records[i].store_busy || retry_flash_full) {
+ if (!write_buf_store_in_event(&m_write_buffer_records[i])) {
+ return;
+ }
+ }
+ }
+ }
+}
+
+/**
+ * @brief Function for handling events from the Peer Data Storage module.
+ * This function is extern in Peer Data Storage.
+ *
+ * @param[in] p_event The event to handle.
+ */
+void pdb_pds_evt_handler(pm_evt_t *p_event)
+{
+ pdb_buffer_record_t *p_write_buffer_record;
+ bool evt_send = true;
+ bool retry_flash_full = false;
+
+ p_write_buffer_record =
+ write_buffer_record_find_stored(p_event->params.peer_data_update_succeeded.token);
+
+ switch (p_event->evt_id) {
+ case PM_EVT_PEER_DATA_UPDATE_SUCCEEDED:
+ if ((p_event->params.peer_data_update_succeeded.action == PM_PEER_DATA_OP_UPDATE) &&
+ (p_write_buffer_record != NULL)) {
+ /* The write came from PDB. */
+ write_buffer_record_release(p_write_buffer_record);
+ }
+ break;
+
+ case PM_EVT_PEER_DATA_UPDATE_FAILED:
+ if ((p_event->params.peer_data_update_succeeded.action == PM_PEER_DATA_OP_UPDATE) &&
+ (p_write_buffer_record != NULL)) {
+ /* The write came from PDB, retry. */
+ p_write_buffer_record->store_token = PM_STORE_TOKEN_INVALID;
+ p_write_buffer_record->store_busy = true;
+ m_pending_store = true;
+ evt_send = false;
+ }
+ break;
+
+ case PM_EVT_FLASH_GARBAGE_COLLECTED:
+ retry_flash_full = true;
+ break;
+
+ default:
+ break;
+ }
+
+ if (evt_send) {
+ /* Forward the event to all registered Peer Database event handlers. */
+ pdb_evt_send(p_event);
+ }
+
+ reattempt_previous_operations(retry_flash_full);
+}
+
+uint32_t pdb_init(void)
+{
+ uint32_t err_code;
+
+ NRF_PM_DEBUG_CHECK(!m_module_initialized);
+
+ internal_state_reset();
+
+ PM_BUFFER_INIT(&m_write_buffer, CONFIG_PM_FLASH_BUFFERS, PDB_WRITE_BUF_SIZE, err_code);
+
+ if (err_code != NRF_SUCCESS) {
+ LOG_ERR("PM_BUFFER_INIT() returned %s.", nrf_strerror_get(err_code));
+ return NRF_ERROR_INTERNAL;
+ }
+
+ m_module_initialized = true;
+
+ return NRF_SUCCESS;
+}
+
+uint32_t pdb_peer_free(pm_peer_id_t peer_id)
+{
+ uint32_t err_code;
+
+ NRF_PM_DEBUG_CHECK(m_module_initialized);
+
+ uint32_t index = 0;
+ pdb_buffer_record_t *p_record = write_buffer_record_find_next(peer_id, &index);
+
+ while (p_record != NULL) {
+ err_code = pdb_write_buf_release(peer_id, p_record->data_id);
+ /* All return values are acceptable. */
+ UNUSED_VARIABLE(err_code);
+
+ if ((err_code != NRF_SUCCESS) && (err_code != NRF_ERROR_NOT_FOUND)) {
+ LOG_ERR("pdb_write_buf_release() returned %s which should not "
+ "happen. peer_id: %d, "
+ "data_id: %d",
+ nrf_strerror_get(err_code), peer_id, p_record->data_id);
+ return NRF_ERROR_INTERNAL;
+ }
+
+ index++;
+ p_record = write_buffer_record_find_next(peer_id, &index);
+ }
+
+ err_code = pds_peer_id_free(peer_id);
+
+ if ((err_code == NRF_SUCCESS) || (err_code == NRF_ERROR_INVALID_PARAM)) {
+ return err_code;
+ }
+
+ LOG_ERR("Peer ID %d was not properly released. pds_peer_id_free() returned %s. "
+ "peer_id: %d",
+ peer_id, nrf_strerror_get(err_code));
+
+ return NRF_ERROR_INTERNAL;
+}
+
+uint32_t pdb_peer_data_ptr_get(pm_peer_id_t peer_id, pm_peer_data_id_t data_id,
+ pm_peer_data_flash_t *const p_peer_data)
+{
+ NRF_PM_DEBUG_CHECK(m_module_initialized);
+ NRF_PM_DEBUG_CHECK(p_peer_data != NULL);
+
+ /* Pass NULL to only retrieve a pointer. */
+ return pds_peer_data_read(peer_id, data_id, (pm_peer_data_t *)p_peer_data, NULL);
+}
+
+uint32_t pdb_write_buf_get(pm_peer_id_t peer_id, pm_peer_data_id_t data_id, uint32_t n_bufs,
+ pm_peer_data_t *p_peer_data)
+{
+ NRF_PM_DEBUG_CHECK(m_module_initialized);
+
+ VERIFY_PARAM_NOT_NULL(p_peer_data);
+ VERIFY_DATA_ID_WRITE_BUF(data_id);
+
+ if ((n_bufs == 0) || (n_bufs > CONFIG_PM_FLASH_BUFFERS)) {
+ return NRF_ERROR_INVALID_PARAM;
+ }
+
+ pdb_buffer_record_t *p_write_buffer_record;
+ uint8_t *p_buffer_memory;
+ bool new_record = false;
+
+ p_write_buffer_record = write_buffer_record_find(peer_id, data_id);
+
+ if (p_write_buffer_record == NULL) {
+ /* No buffer exists. */
+ write_buffer_record_acquire(&p_write_buffer_record, peer_id, data_id);
+ if (p_write_buffer_record == NULL) {
+ return NRF_ERROR_BUSY;
+ }
+ } else if (p_write_buffer_record->n_bufs != n_bufs) {
+ /* Buffer exists with a different n_bufs from what was requested. */
+ return NRF_ERROR_FORBIDDEN;
+ }
+
+ if (p_write_buffer_record->buffer_block_id == PM_BUFFER_INVALID_ID) {
+ p_write_buffer_record->buffer_block_id =
+ pm_buffer_block_acquire(&m_write_buffer, n_bufs);
+
+ if (p_write_buffer_record->buffer_block_id == PM_BUFFER_INVALID_ID) {
+ write_buffer_record_invalidate(p_write_buffer_record);
+ return NRF_ERROR_BUSY;
+ }
+
+ new_record = true;
+ }
+
+ p_write_buffer_record->n_bufs = n_bufs;
+
+ p_buffer_memory =
+ pm_buffer_ptr_get(&m_write_buffer, p_write_buffer_record->buffer_block_id);
+
+ if (p_buffer_memory == NULL) {
+ LOG_ERR("Cannot store data to flash because pm_buffer_ptr_get() could not retrieve "
+ "RAM buffer. Is block_id %d not allocated?",
+ p_write_buffer_record->buffer_block_id);
+ return NRF_ERROR_INTERNAL;
+ }
+
+ peer_data_point_to_buffer(p_peer_data, data_id, p_buffer_memory, n_bufs);
+ if (new_record && (data_id == PM_PEER_DATA_ID_GATT_LOCAL)) {
+ p_peer_data->p_local_gatt_db->len = PM_LOCAL_DB_LEN(p_peer_data->length_words);
+ }
+
+ return NRF_SUCCESS;
+}
+
+uint32_t pdb_write_buf_release(pm_peer_id_t peer_id, pm_peer_data_id_t data_id)
+{
+ NRF_PM_DEBUG_CHECK(m_module_initialized);
+
+ pdb_buffer_record_t *p_write_buffer_record;
+
+ p_write_buffer_record = write_buffer_record_find(peer_id, data_id);
+
+ if (p_write_buffer_record == NULL) {
+ return NRF_ERROR_NOT_FOUND;
+ }
+
+ write_buffer_record_release(p_write_buffer_record);
+
+ return NRF_SUCCESS;
+}
+
+uint32_t pdb_write_buf_store(pm_peer_id_t peer_id, pm_peer_data_id_t data_id,
+ pm_peer_id_t new_peer_id)
+{
+ NRF_PM_DEBUG_CHECK(m_module_initialized);
+
+ VERIFY_DATA_ID_WRITE_BUF(data_id);
+
+ if (!pds_peer_id_is_allocated(new_peer_id)) {
+ return NRF_ERROR_INVALID_PARAM;
+ }
+
+ pdb_buffer_record_t *p_write_buffer_record = write_buffer_record_find(peer_id, data_id);
+
+ if (p_write_buffer_record == NULL) {
+ return NRF_ERROR_NOT_FOUND;
+ }
+
+ p_write_buffer_record->peer_id = new_peer_id;
+ p_write_buffer_record->data_id = data_id;
+ return write_buf_store(p_write_buffer_record);
+}
diff --git a/lib/peer_manager/modules/peer_id.c b/lib/peer_manager/modules/peer_id.c
new file mode 100644
index 0000000000..e11fdad555
--- /dev/null
+++ b/lib/peer_manager/modules/peer_id.c
@@ -0,0 +1,170 @@
+/*
+ * Copyright (c) 2015-2025 Nordic Semiconductor ASA
+ *
+ * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
+ */
+
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define FLAGS_PER_ELEMENT (sizeof(atomic_t) * CHAR_BIT)
+#define FLAGS_ARRAY_LEN(flag_count) (((flag_count - 1) / FLAGS_PER_ELEMENT) + 1)
+#define ATOMIC_BITMAP(name) atomic_t name[FLAGS_ARRAY_LEN((PM_PEER_ID_N_AVAILABLE_IDS))]
+
+typedef struct {
+ /** Bitmap designating which peer IDs are in use. */
+ ATOMIC_BITMAP(used_peer_ids);
+ /** Bitmap designating which peer IDs are marked for deletion. */
+ ATOMIC_BITMAP(deleted_peer_ids);
+} pi_t;
+
+static pi_t m_pi = {{0}, {0}};
+
+static void internal_state_reset(pi_t *p_pi)
+{
+ memset(p_pi, 0, sizeof(pi_t));
+}
+
+void peer_id_init(void)
+{
+ internal_state_reset(&m_pi);
+}
+
+static uint32_t find_and_set_flag(atomic_t *p_flags, uint32_t flag_count)
+{
+ for (uint32_t i = 0; i < FLAGS_ARRAY_LEN(flag_count); i++) {
+ uint32_t word = atomic_get(&p_flags[i]);
+ uint32_t inverted = ~word;
+
+ while (inverted) {
+ /* Find lowest zero bit */
+ uint32_t first_zero = NRF_CTZ(inverted);
+ uint32_t first_zero_global = first_zero + (i * 32);
+
+ if (first_zero_global >= flag_count) {
+ break;
+ }
+ if (!atomic_test_and_set_bit(p_flags, first_zero_global)) {
+ return first_zero_global;
+ }
+ inverted &= ~(1u << first_zero);
+ }
+ }
+ return flag_count;
+}
+
+static pm_peer_id_t claim(pm_peer_id_t peer_id, atomic_t *p_peer_id_flags)
+{
+ pm_peer_id_t allocated_peer_id = PM_PEER_ID_INVALID;
+
+ if (peer_id == PM_PEER_ID_INVALID) {
+ allocated_peer_id =
+ find_and_set_flag(p_peer_id_flags, PM_PEER_ID_N_AVAILABLE_IDS);
+ if (allocated_peer_id == PM_PEER_ID_N_AVAILABLE_IDS) {
+ allocated_peer_id = PM_PEER_ID_INVALID;
+ }
+ } else if (peer_id < PM_PEER_ID_N_AVAILABLE_IDS) {
+ bool lock_success = !atomic_test_and_set_bit(p_peer_id_flags, peer_id);
+
+ allocated_peer_id = lock_success ? peer_id : PM_PEER_ID_INVALID;
+ }
+ return allocated_peer_id;
+}
+
+static void release(pm_peer_id_t peer_id, atomic_t *p_peer_id_flags)
+{
+ if (peer_id < PM_PEER_ID_N_AVAILABLE_IDS) {
+ atomic_clear_bit(p_peer_id_flags, peer_id);
+ }
+}
+
+pm_peer_id_t peer_id_allocate(pm_peer_id_t peer_id)
+{
+ return claim(peer_id, m_pi.used_peer_ids);
+}
+
+bool peer_id_delete(pm_peer_id_t peer_id)
+{
+ pm_peer_id_t deleted_peer_id;
+
+ if (peer_id == PM_PEER_ID_INVALID) {
+ return false;
+ }
+
+ deleted_peer_id = claim(peer_id, m_pi.deleted_peer_ids);
+
+ return (deleted_peer_id == peer_id);
+}
+
+void peer_id_free(pm_peer_id_t peer_id)
+{
+ release(peer_id, m_pi.used_peer_ids);
+ release(peer_id, m_pi.deleted_peer_ids);
+}
+
+bool peer_id_is_allocated(pm_peer_id_t peer_id)
+{
+ if (peer_id < PM_PEER_ID_N_AVAILABLE_IDS) {
+ return atomic_test_bit(m_pi.used_peer_ids, peer_id);
+ }
+ return false;
+}
+
+bool peer_id_is_deleted(pm_peer_id_t peer_id)
+{
+ if (peer_id < PM_PEER_ID_N_AVAILABLE_IDS) {
+ return atomic_test_bit(m_pi.deleted_peer_ids, peer_id);
+ }
+ return false;
+}
+
+pm_peer_id_t next_id_get(pm_peer_id_t prev_peer_id, atomic_t *p_peer_id_flags)
+{
+ pm_peer_id_t i = (prev_peer_id == PM_PEER_ID_INVALID) ? 0 : (prev_peer_id + 1);
+
+ for (; i < PM_PEER_ID_N_AVAILABLE_IDS; i++) {
+ if (atomic_test_bit(p_peer_id_flags, i)) {
+ return i;
+ }
+ }
+
+ return PM_PEER_ID_INVALID;
+}
+
+pm_peer_id_t peer_id_get_next_used(pm_peer_id_t peer_id)
+{
+ peer_id = next_id_get(peer_id, m_pi.used_peer_ids);
+
+ while (peer_id != PM_PEER_ID_INVALID) {
+ if (!peer_id_is_deleted(peer_id)) {
+ return peer_id;
+ }
+
+ peer_id = next_id_get(peer_id, m_pi.used_peer_ids);
+ }
+
+ return peer_id;
+}
+
+pm_peer_id_t peer_id_get_next_deleted(pm_peer_id_t prev_peer_id)
+{
+ return next_id_get(prev_peer_id, m_pi.deleted_peer_ids);
+}
+
+uint32_t peer_id_n_ids(void)
+{
+ uint32_t n_ids = 0;
+
+ for (pm_peer_id_t i = 0; i < PM_PEER_ID_N_AVAILABLE_IDS; i++) {
+ n_ids += atomic_test_bit(m_pi.used_peer_ids, i);
+ }
+
+ return n_ids;
+}
diff --git a/lib/peer_manager/modules/peer_manager_handler.c b/lib/peer_manager/modules/peer_manager_handler.c
new file mode 100644
index 0000000000..24c5537174
--- /dev/null
+++ b/lib/peer_manager/modules/peer_manager_handler.c
@@ -0,0 +1,603 @@
+/*
+ * Copyright (c) 2018-2025 Nordic Semiconductor ASA
+ *
+ * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
+ */
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#if CONFIG_PM_HANDLER_SEC_DELAY_MS > 0
+#include
+#endif
+
+#include
+
+#define APP_ERROR_CHECK
+
+LOG_MODULE_DECLARE(peer_manager, CONFIG_PEER_MANAGER_LOG_LEVEL);
+
+static const char * const m_roles_str[] = {
+ "Invalid Role",
+ "Peripheral",
+ "Central",
+};
+
+static const char * const m_sec_procedure_str[] = {
+ "Encryption",
+ "Bonding",
+ "Pairing",
+};
+
+#define PM_EVT_STR(_name) [_name] = STRINGIFY(_name)
+
+static const char * const m_event_str[] = {
+ PM_EVT_STR(PM_EVT_BONDED_PEER_CONNECTED),
+ PM_EVT_STR(PM_EVT_CONN_CONFIG_REQ),
+ PM_EVT_STR(PM_EVT_CONN_SEC_START),
+ PM_EVT_STR(PM_EVT_CONN_SEC_SUCCEEDED),
+ PM_EVT_STR(PM_EVT_CONN_SEC_FAILED),
+ PM_EVT_STR(PM_EVT_CONN_SEC_CONFIG_REQ),
+ PM_EVT_STR(PM_EVT_CONN_SEC_PARAMS_REQ),
+ PM_EVT_STR(PM_EVT_STORAGE_FULL),
+ PM_EVT_STR(PM_EVT_ERROR_UNEXPECTED),
+ PM_EVT_STR(PM_EVT_PEER_DATA_UPDATE_SUCCEEDED),
+ PM_EVT_STR(PM_EVT_PEER_DATA_UPDATE_FAILED),
+ PM_EVT_STR(PM_EVT_PEER_DELETE_SUCCEEDED),
+ PM_EVT_STR(PM_EVT_PEER_DELETE_FAILED),
+ PM_EVT_STR(PM_EVT_PEERS_DELETE_SUCCEEDED),
+ PM_EVT_STR(PM_EVT_PEERS_DELETE_FAILED),
+ PM_EVT_STR(PM_EVT_LOCAL_DB_CACHE_APPLIED),
+ PM_EVT_STR(PM_EVT_LOCAL_DB_CACHE_APPLY_FAILED),
+ PM_EVT_STR(PM_EVT_SERVICE_CHANGED_IND_SENT),
+ PM_EVT_STR(PM_EVT_SERVICE_CHANGED_IND_CONFIRMED),
+ PM_EVT_STR(PM_EVT_SLAVE_SECURITY_REQ),
+ PM_EVT_STR(PM_EVT_FLASH_GARBAGE_COLLECTED),
+ PM_EVT_STR(PM_EVT_FLASH_GARBAGE_COLLECTION_FAILED),
+};
+
+static const char * const m_data_id_str[] = {
+ "Outdated (0)", "Service changed pending flag",
+ "Outdated (2)", "Outdated (3)",
+ "Application data", "Remote database",
+ "Peer rank", "Bonding data",
+ "Local database", "Central address resolution",
+};
+
+static const char * const m_data_action_str[] = {"Update", "Delete"};
+
+#define PM_SEC_ERR_STR(_name) \
+ { \
+ .error = _name, .error_str = #_name \
+ }
+
+typedef struct {
+ pm_sec_error_code_t error;
+ const char *error_str;
+} sec_err_str_t;
+
+static const sec_err_str_t m_pm_sec_error_str[] = {
+ PM_SEC_ERR_STR(PM_CONN_SEC_ERROR_PIN_OR_KEY_MISSING),
+ PM_SEC_ERR_STR(PM_CONN_SEC_ERROR_MIC_FAILURE),
+ PM_SEC_ERR_STR(PM_CONN_SEC_ERROR_DISCONNECT),
+ PM_SEC_ERR_STR(PM_CONN_SEC_ERROR_SMP_TIMEOUT),
+};
+
+static const char *sec_err_string_get(pm_sec_error_code_t error)
+{
+ static char errstr[30];
+
+ for (uint32_t i = 0; i < (sizeof(m_pm_sec_error_str) / sizeof(sec_err_str_t)); i++) {
+ if (m_pm_sec_error_str[i].error == error) {
+ return m_pm_sec_error_str[i].error_str;
+ }
+ }
+
+ int len = snprintf(errstr, sizeof(errstr), "%s 0x%hx",
+ (error < PM_CONN_SEC_ERROR_BASE) ? "BLE_GAP_SEC_STATUS"
+ : "PM_CONN_SEC_ERROR",
+ error);
+ UNUSED_VARIABLE(len);
+ return errstr;
+}
+
+static void _conn_secure(uint16_t conn_handle, bool force)
+{
+ uint32_t err_code;
+
+ if (!force) {
+ pm_conn_sec_status_t status;
+
+ err_code = pm_conn_sec_status_get(conn_handle, &status);
+ if (err_code != BLE_ERROR_INVALID_CONN_HANDLE) {
+ APP_ERROR_CHECK(err_code);
+ }
+
+ /* If the link is already secured, don't initiate security procedure. */
+ if (status.encrypted) {
+ LOG_DBG("Already encrypted, skipping security.");
+ return;
+ }
+ }
+
+ err_code = pm_conn_secure(conn_handle, false);
+
+ if ((err_code == NRF_SUCCESS) || (err_code == NRF_ERROR_BUSY)) {
+ /* Success. */
+ } else if (err_code == NRF_ERROR_TIMEOUT) {
+ LOG_WRN("pm_conn_secure() failed because an SMP timeout is preventing security on "
+ "the link. Disconnecting conn_handle %d.",
+ conn_handle);
+
+ err_code = sd_ble_gap_disconnect(conn_handle,
+ BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION);
+ if (err_code != NRF_SUCCESS) {
+ LOG_WRN("sd_ble_gap_disconnect() returned %s on conn_handle %d.",
+ nrf_strerror_get(err_code), conn_handle);
+ }
+ } else if (err_code == NRF_ERROR_INVALID_DATA) {
+ LOG_WRN("pm_conn_secure() failed because the stored data for conn_handle %d does "
+ "not have a valid key.",
+ conn_handle);
+ } else if (err_code == BLE_ERROR_INVALID_CONN_HANDLE) {
+ LOG_WRN("pm_conn_secure() failed because conn_handle %d is not a valid connection.",
+ conn_handle);
+ } else {
+ LOG_ERR("Asserting. pm_conn_secure() returned %s on conn_handle %d.",
+ nrf_strerror_get(err_code), conn_handle);
+ APP_ERROR_CHECK(err_code);
+ }
+}
+
+#if CONFIG_PM_HANDLER_SEC_DELAY_MS > 0
+static struct bm_timer secure_delay_timer;
+
+typedef union {
+ struct {
+ uint16_t conn_handle;
+ bool force;
+ } values;
+ void *p_void;
+} conn_secure_context_t;
+
+STATIC_ASSERT(sizeof(conn_secure_context_t) <= sizeof(void *),
+ "conn_secure_context_t is too large.");
+
+static void _conn_secure(uint16_t conn_handle, bool force);
+
+static void delayed_conn_secure(void *context)
+{
+ conn_secure_context_t sec_context = {.p_void = context};
+
+ _conn_secure(sec_context.values.conn_handle, sec_context.values.force);
+}
+
+static void conn_secure(uint16_t conn_handle, bool force)
+{
+ int err;
+ static bool created;
+
+ if (!created) {
+ err = bm_timer_init(&secure_delay_timer, BM_TIMER_MODE_SINGLE_SHOT,
+ delayed_conn_secure);
+ APP_ERROR_CHECK(err);
+ created = true;
+ }
+
+ conn_secure_context_t sec_context = {0};
+
+ sec_context.values.conn_handle = conn_handle;
+ sec_context.values.force = force;
+
+ err = bm_timer_start(secure_delay_timer,
+ BM_TIMER_MS_TO_TICKS(CONFIG_PM_HANDLER_SEC_DELAY_MS),
+ sec_context.p_void);
+ APP_ERROR_CHECK(err);
+}
+
+#else
+static void conn_secure(uint16_t conn_handle, bool force)
+{
+ _conn_secure(conn_handle, force);
+}
+#endif
+
+void pm_handler_on_pm_evt(pm_evt_t const *p_pm_evt)
+{
+ pm_handler_pm_evt_log(p_pm_evt);
+
+ if (p_pm_evt->evt_id == PM_EVT_BONDED_PEER_CONNECTED) {
+ conn_secure(p_pm_evt->conn_handle, false);
+ } else if (p_pm_evt->evt_id == PM_EVT_ERROR_UNEXPECTED) {
+ LOG_ERR("Asserting.");
+ APP_ERROR_CHECK(p_pm_evt->params.error_unexpected.error);
+ }
+}
+
+void pm_handler_flash_clean_on_return(void)
+{
+ /* Trigger the mechanism to make more room in flash. */
+ pm_evt_t storage_full_evt = {.evt_id = PM_EVT_STORAGE_FULL};
+
+ pm_handler_flash_clean(&storage_full_evt);
+}
+
+static void rank_highest(pm_peer_id_t peer_id)
+{
+ /* Trigger a pm_peer_rank_highest() with internal bookkeeping. */
+ pm_evt_t connected_evt = {.evt_id = PM_EVT_BONDED_PEER_CONNECTED, .peer_id = peer_id};
+
+ pm_handler_flash_clean(&connected_evt);
+}
+
+void pm_handler_flash_clean(pm_evt_t const *p_pm_evt)
+{
+ uint32_t err_code;
+ /* Indicates whether garbage collection is currently being run. */
+ static bool flash_cleaning;
+ /* Indicates whether a successful write happened after the last garbage
+ * collection. If this is false when flash is full, it means just a
+ * garbage collection won't work, so some data should be deleted.
+ */
+ static bool flash_write_after_gc = true;
+
+/* Size of rank_queue. */
+#define RANK_QUEUE_SIZE 8
+/* Initial value of rank_queue. */
+#define RANK_QUEUE_INIT PM_PEER_ID_INVALID,
+
+ /* Queue of rank_highest calls that failed because of full flash. */
+ static pm_peer_id_t rank_queue[8] = {[0 ... RANK_QUEUE_SIZE - 1] = RANK_QUEUE_INIT};
+ /* Write pointer for rank_queue. */
+ static int rank_queue_wr;
+
+ switch (p_pm_evt->evt_id) {
+ case PM_EVT_BONDED_PEER_CONNECTED:
+ err_code = pm_peer_rank_highest(p_pm_evt->peer_id);
+ if ((err_code == NRF_ERROR_RESOURCES) || (err_code == NRF_ERROR_BUSY)) {
+ /* Queue pm_peer_rank_highest() call and attempt to clean flash. */
+ rank_queue[rank_queue_wr] = p_pm_evt->peer_id;
+ rank_queue_wr = (rank_queue_wr + 1) % RANK_QUEUE_SIZE;
+ pm_handler_flash_clean_on_return();
+ } else if ((err_code != NRF_ERROR_NOT_SUPPORTED) &&
+ (err_code != NRF_ERROR_INVALID_PARAM) &&
+ (err_code != NRF_ERROR_DATA_SIZE)) {
+ APP_ERROR_CHECK(err_code);
+ } else {
+ LOG_DBG("pm_peer_rank_highest() returned %s for peer id %d",
+ nrf_strerror_get(err_code), p_pm_evt->peer_id);
+ }
+ break;
+
+ case PM_EVT_CONN_SEC_START:
+ break;
+
+ case PM_EVT_CONN_SEC_SUCCEEDED:
+ /* PM_CONN_SEC_PROCEDURE_ENCRYPTION in case peer was not recognized at connection
+ * time.
+ */
+ if ((p_pm_evt->params.conn_sec_succeeded.procedure ==
+ PM_CONN_SEC_PROCEDURE_BONDING) ||
+ (p_pm_evt->params.conn_sec_succeeded.procedure ==
+ PM_CONN_SEC_PROCEDURE_ENCRYPTION)) {
+ rank_highest(p_pm_evt->peer_id);
+ }
+ break;
+
+ case PM_EVT_CONN_SEC_FAILED:
+ case PM_EVT_CONN_SEC_CONFIG_REQ:
+ case PM_EVT_CONN_SEC_PARAMS_REQ:
+ break;
+
+ case PM_EVT_STORAGE_FULL:
+ if (!flash_cleaning) {
+ err_code = NRF_SUCCESS;
+ LOG_INF("Attempting to clean flash.");
+ if (!flash_write_after_gc) {
+ /* Check whether another user of FDS has deleted a record that can
+ * be GCed.
+ */
+ fds_stat_t fds_stats;
+
+ err_code = fds_stat(&fds_stats);
+ APP_ERROR_CHECK(err_code);
+ flash_write_after_gc = (fds_stats.dirty_records > 0);
+ }
+ if (!flash_write_after_gc) {
+ pm_peer_id_t peer_id_to_delete;
+
+ err_code = pm_peer_ranks_get(NULL, NULL, &peer_id_to_delete, NULL);
+ if (err_code == NRF_SUCCESS) {
+ LOG_INF("Deleting lowest ranked peer (peer_id: %d)",
+ peer_id_to_delete);
+ err_code = pm_peer_delete(peer_id_to_delete);
+ APP_ERROR_CHECK(err_code);
+ flash_write_after_gc = true;
+ }
+ if (err_code == NRF_ERROR_NOT_FOUND) {
+ LOG_ERR("There are no peers to delete.");
+ } else if (err_code == NRF_ERROR_NOT_SUPPORTED) {
+ LOG_WRN("Peer ranks functionality is disabled, so "
+ "no peers are deleted.");
+ } else {
+ APP_ERROR_CHECK(err_code);
+ }
+ }
+ if (err_code == NRF_SUCCESS) {
+ err_code = fds_gc();
+ if (err_code == NRF_SUCCESS) {
+ LOG_DBG("Running flash garbage collection.");
+ flash_cleaning = true;
+ } else if (err_code != FDS_ERR_NO_SPACE_IN_QUEUES) {
+ APP_ERROR_CHECK(err_code);
+ }
+ }
+ }
+ break;
+
+ case PM_EVT_ERROR_UNEXPECTED:
+ break;
+
+ case PM_EVT_PEER_DATA_UPDATE_SUCCEEDED:
+ flash_write_after_gc = true;
+ break;
+
+ case PM_EVT_PEER_DATA_UPDATE_FAILED:
+ break;
+
+ case PM_EVT_PEER_DELETE_SUCCEEDED:
+ flash_write_after_gc = true;
+ break;
+
+ case PM_EVT_PEER_DELETE_FAILED:
+ case PM_EVT_PEERS_DELETE_SUCCEEDED:
+ case PM_EVT_PEERS_DELETE_FAILED:
+ case PM_EVT_LOCAL_DB_CACHE_APPLIED:
+ case PM_EVT_LOCAL_DB_CACHE_APPLY_FAILED:
+ case PM_EVT_SERVICE_CHANGED_IND_SENT:
+ case PM_EVT_SERVICE_CHANGED_IND_CONFIRMED:
+ case PM_EVT_SLAVE_SECURITY_REQ:
+ break;
+
+ case PM_EVT_FLASH_GARBAGE_COLLECTED:
+ flash_cleaning = false;
+ flash_write_after_gc = false;
+ {
+ /* Reattempt queued pm_peer_rank_highest() calls. */
+ int rank_queue_rd = rank_queue_wr;
+
+ for (int i = 0; i < RANK_QUEUE_SIZE; i++) {
+ pm_peer_id_t peer_id =
+ rank_queue[(i + rank_queue_rd) % RANK_QUEUE_SIZE];
+ if (peer_id != PM_PEER_ID_INVALID) {
+ rank_queue[(i + rank_queue_rd) % RANK_QUEUE_SIZE] =
+ PM_PEER_ID_INVALID;
+ rank_highest(peer_id);
+ }
+ }
+ }
+ break;
+
+ case PM_EVT_FLASH_GARBAGE_COLLECTION_FAILED:
+ flash_cleaning = false;
+
+ if (p_pm_evt->params.garbage_collection_failed.error == FDS_ERR_BUSY ||
+ p_pm_evt->params.garbage_collection_failed.error == FDS_ERR_OPERATION_TIMEOUT) {
+ /* Retry immediately if error is transient. */
+ pm_handler_flash_clean_on_return();
+ }
+ break;
+
+ default:
+ break;
+ }
+}
+
+void pm_handler_pm_evt_log(pm_evt_t const *p_pm_evt)
+{
+ LOG_DBG("Event %s", m_event_str[p_pm_evt->evt_id]);
+
+ switch (p_pm_evt->evt_id) {
+ case PM_EVT_BONDED_PEER_CONNECTED:
+ LOG_DBG("Previously bonded peer connected: role: %s, conn_handle: %d, peer_id: %d",
+ m_roles_str[ble_conn_state_role(p_pm_evt->conn_handle)],
+ p_pm_evt->conn_handle, p_pm_evt->peer_id);
+ break;
+
+ case PM_EVT_CONN_CONFIG_REQ:
+ LOG_DBG("Connection configuration request");
+ break;
+
+ case PM_EVT_CONN_SEC_START:
+ LOG_DBG("Connection security procedure started: role: %s, conn_handle: %d, "
+ "procedure: %s",
+ m_roles_str[ble_conn_state_role(p_pm_evt->conn_handle)],
+ p_pm_evt->conn_handle,
+ m_sec_procedure_str[p_pm_evt->params.conn_sec_start.procedure]);
+ break;
+
+ case PM_EVT_CONN_SEC_SUCCEEDED:
+ LOG_INF("Connection secured: role: %s, conn_handle: %d, procedure: %s",
+ m_roles_str[ble_conn_state_role(p_pm_evt->conn_handle)],
+ p_pm_evt->conn_handle,
+ m_sec_procedure_str[p_pm_evt->params.conn_sec_start.procedure]);
+ break;
+
+ case PM_EVT_CONN_SEC_FAILED:
+ LOG_INF("Connection security failed: role: %s, conn_handle: 0x%x, procedure: "
+ "%s, error: %d",
+ m_roles_str[ble_conn_state_role(p_pm_evt->conn_handle)],
+ p_pm_evt->conn_handle,
+ m_sec_procedure_str[p_pm_evt->params.conn_sec_start.procedure],
+ p_pm_evt->params.conn_sec_failed.error);
+ LOG_DBG("Error (decoded): %s",
+ sec_err_string_get(p_pm_evt->params.conn_sec_failed.error));
+ break;
+
+ case PM_EVT_CONN_SEC_CONFIG_REQ:
+ LOG_DBG("Security configuration request");
+ break;
+
+ case PM_EVT_CONN_SEC_PARAMS_REQ:
+ LOG_DBG("Security parameter request");
+ break;
+
+ case PM_EVT_STORAGE_FULL:
+ LOG_WRN("Flash storage is full");
+ break;
+
+ case PM_EVT_ERROR_UNEXPECTED:
+ LOG_ERR("Unexpected fatal error occurred: error: %s",
+ nrf_strerror_get(p_pm_evt->params.error_unexpected.error));
+ break;
+
+ case PM_EVT_PEER_DATA_UPDATE_SUCCEEDED:
+ LOG_INF("Peer data updated in flash: peer_id: %d, data_id: %s, action: %s%s",
+ p_pm_evt->peer_id,
+ m_data_id_str[p_pm_evt->params.peer_data_update_succeeded.data_id],
+ m_data_action_str[p_pm_evt->params.peer_data_update_succeeded.action],
+ p_pm_evt->params.peer_data_update_succeeded.flash_changed
+ ? ""
+ : ", no change");
+ break;
+
+ case PM_EVT_PEER_DATA_UPDATE_FAILED:
+ /* This can happen if the SoftDevice is too busy with BLE operations. */
+ LOG_WRN("Peer data updated failed: peer_id: %d, data_id: %s, action: %s, error: %s",
+ p_pm_evt->peer_id,
+ m_data_id_str[p_pm_evt->params.peer_data_update_failed.data_id],
+ m_data_action_str[p_pm_evt->params.peer_data_update_succeeded.action],
+ nrf_strerror_get(p_pm_evt->params.peer_data_update_failed.error));
+ break;
+
+ case PM_EVT_PEER_DELETE_SUCCEEDED:
+ LOG_ERR("Peer deleted successfully: peer_id: %d", p_pm_evt->peer_id);
+ break;
+
+ case PM_EVT_PEER_DELETE_FAILED:
+ LOG_ERR("Peer deletion failed: peer_id: %d, error: %s", p_pm_evt->peer_id,
+ nrf_strerror_get(p_pm_evt->params.peer_delete_failed.error));
+ break;
+
+ case PM_EVT_PEERS_DELETE_SUCCEEDED:
+ LOG_INF("All peers deleted.");
+ break;
+
+ case PM_EVT_PEERS_DELETE_FAILED:
+ LOG_ERR("All peer deletion failed: error: %s",
+ nrf_strerror_get(p_pm_evt->params.peers_delete_failed_evt.error));
+ break;
+
+ case PM_EVT_LOCAL_DB_CACHE_APPLIED:
+ LOG_DBG("Previously stored local DB applied: conn_handle: %d, peer_id: %d",
+ p_pm_evt->conn_handle, p_pm_evt->peer_id);
+ break;
+
+ case PM_EVT_LOCAL_DB_CACHE_APPLY_FAILED:
+ /* This can happen when the local DB has changed. */
+ LOG_WRN("Local DB could not be applied: conn_handle: %d, peer_id: %d",
+ p_pm_evt->conn_handle, p_pm_evt->peer_id);
+ break;
+
+ case PM_EVT_SERVICE_CHANGED_IND_SENT:
+ LOG_DBG("Sending Service Changed indication.");
+ break;
+
+ case PM_EVT_SERVICE_CHANGED_IND_CONFIRMED:
+ LOG_DBG("Service Changed indication confirmed.");
+ break;
+
+ case PM_EVT_SLAVE_SECURITY_REQ:
+ LOG_DBG("Security Request received from peer.");
+ break;
+
+ case PM_EVT_FLASH_GARBAGE_COLLECTED:
+ LOG_DBG("Flash garbage collection complete.");
+ break;
+
+ case PM_EVT_FLASH_GARBAGE_COLLECTION_FAILED:
+ LOG_WRN("Flash garbage collection failed with error %s.",
+ nrf_strerror_get(p_pm_evt->params.garbage_collection_failed.error));
+ break;
+
+ default:
+ LOG_WRN("Unexpected PM event ID: 0x%x.", p_pm_evt->evt_id);
+ break;
+ }
+}
+
+void pm_handler_disconnect_on_sec_failure(pm_evt_t const *p_pm_evt)
+{
+ uint32_t err_code;
+
+ if (p_pm_evt->evt_id == PM_EVT_CONN_SEC_FAILED) {
+ LOG_WRN("Disconnecting conn_handle %d.", p_pm_evt->conn_handle);
+ err_code = sd_ble_gap_disconnect(p_pm_evt->conn_handle,
+ BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION);
+ if ((err_code != NRF_ERROR_INVALID_STATE) &&
+ (err_code != BLE_ERROR_INVALID_CONN_HANDLE)) {
+ APP_ERROR_CHECK(err_code);
+ }
+ }
+}
+
+void pm_handler_disconnect_on_insufficient_sec(pm_evt_t const *p_pm_evt,
+ pm_conn_sec_status_t *p_min_conn_sec)
+{
+ if (p_pm_evt->evt_id == PM_EVT_CONN_SEC_SUCCEEDED) {
+ if (!pm_sec_is_sufficient(p_pm_evt->conn_handle, p_min_conn_sec)) {
+ LOG_WRN("Connection security is insufficient, disconnecting.");
+ uint32_t err_code = sd_ble_gap_disconnect(
+ p_pm_evt->conn_handle, BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION);
+ APP_ERROR_CHECK(err_code);
+ }
+ }
+}
+
+void pm_handler_secure_on_connection(ble_evt_t const *p_ble_evt)
+{
+ switch (p_ble_evt->header.evt_id) {
+ case BLE_GAP_EVT_CONNECTED:
+ LOG_DBG("Connected, securing connection. conn_handle: %d",
+ p_ble_evt->evt.gap_evt.conn_handle);
+ conn_secure(p_ble_evt->evt.gap_evt.conn_handle, false);
+ break;
+
+#if CONFIG_PM_HANDLER_SEC_DELAY_MS > 0
+ case BLE_GAP_EVT_DISCONNECTED: {
+ int err = bm_timer_stop(&secure_delay_timer);
+
+ APP_ERROR_CHECK(err);
+ } break;
+#endif
+
+ default:
+ break;
+ }
+}
+
+void pm_handler_secure_on_error(ble_evt_t const *p_ble_evt)
+{
+ if ((p_ble_evt->header.evt_id >= BLE_GATTC_EVT_BASE) &&
+ (p_ble_evt->header.evt_id <= BLE_GATTC_EVT_LAST)) {
+ if ((p_ble_evt->evt.gattc_evt.gatt_status ==
+ BLE_GATT_STATUS_ATTERR_INSUF_ENCRYPTION) ||
+ (p_ble_evt->evt.gattc_evt.gatt_status ==
+ BLE_GATT_STATUS_ATTERR_INSUF_AUTHENTICATION)) {
+ LOG_INF("GATTC procedure (evt id 0x%x) failed because it needs "
+ "encryption. Bonding: conn_handle=%d",
+ p_ble_evt->header.evt_id,
+ p_ble_evt->evt.gattc_evt.conn_handle);
+ conn_secure(p_ble_evt->evt.gattc_evt.conn_handle, true);
+ }
+ }
+}
diff --git a/lib/peer_manager/modules/pm_buffer.c b/lib/peer_manager/modules/pm_buffer.c
new file mode 100644
index 0000000000..bbc642040e
--- /dev/null
+++ b/lib/peer_manager/modules/pm_buffer.c
@@ -0,0 +1,96 @@
+/*
+ * Copyright (c) 2015-2025 Nordic Semiconductor ASA
+ *
+ * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
+ */
+
+
+#include
+#include
+#include
+#include
+#include
+
+#define BUFFER_IS_VALID(p_buffer) \
+ ((p_buffer != NULL) && (p_buffer->p_memory != NULL) && (p_buffer->p_mutex != NULL))
+
+static bool mutex_lock(atomic_t *mutex, int index)
+{
+ return !atomic_test_and_set_bit(mutex, index);
+}
+
+static void mutex_unlock(atomic_t *mutex, int index)
+{
+ atomic_clear_bit(mutex, index);
+}
+
+static bool mutex_lock_status_get(atomic_t *mutex, int index)
+{
+ return atomic_test_bit(mutex, index);
+}
+
+uint32_t pm_buffer_init(pm_buffer_t *p_buffer, uint8_t *p_buffer_memory,
+ uint32_t buffer_memory_size, atomic_t *p_mutex_memory,
+ uint32_t n_blocks, uint32_t block_size)
+{
+ if ((p_buffer != NULL) && (p_buffer_memory != NULL) && (p_mutex_memory != NULL) &&
+ (buffer_memory_size >= (n_blocks * block_size)) && (n_blocks != 0) &&
+ (block_size != 0)) {
+ p_buffer->p_memory = p_buffer_memory;
+ p_buffer->p_mutex = p_mutex_memory;
+ p_buffer->n_blocks = n_blocks;
+ p_buffer->block_size = block_size;
+
+ return NRF_SUCCESS;
+ } else {
+ return NRF_ERROR_INVALID_PARAM;
+ }
+}
+
+uint8_t pm_buffer_block_acquire(pm_buffer_t *p_buffer, uint32_t n_blocks)
+{
+ if (!BUFFER_IS_VALID(p_buffer)) {
+ return PM_BUFFER_INVALID_ID;
+ }
+
+ uint8_t first_locked_mutex = PM_BUFFER_INVALID_ID;
+
+ for (uint8_t i = 0; i < p_buffer->n_blocks; i++) {
+ if (mutex_lock(p_buffer->p_mutex, i)) {
+ if (first_locked_mutex == PM_BUFFER_INVALID_ID) {
+ first_locked_mutex = i;
+ }
+ if ((i - first_locked_mutex + 1U) == n_blocks) {
+ return first_locked_mutex;
+ }
+ } else if (first_locked_mutex != PM_BUFFER_INVALID_ID) {
+ for (uint8_t j = first_locked_mutex; j < i; j++) {
+ pm_buffer_release(p_buffer, j);
+ }
+ first_locked_mutex = PM_BUFFER_INVALID_ID;
+ }
+ }
+
+ return (PM_BUFFER_INVALID_ID);
+}
+
+uint8_t *pm_buffer_ptr_get(pm_buffer_t *p_buffer, uint8_t id)
+{
+ if (!BUFFER_IS_VALID(p_buffer)) {
+ return NULL;
+ }
+
+ if ((id != PM_BUFFER_INVALID_ID) && mutex_lock_status_get(p_buffer->p_mutex, id)) {
+ return &p_buffer->p_memory[id * p_buffer->block_size];
+ } else {
+ return NULL;
+ }
+}
+
+void pm_buffer_release(pm_buffer_t *p_buffer, uint8_t id)
+{
+ if (BUFFER_IS_VALID(p_buffer) && (id != PM_BUFFER_INVALID_ID) &&
+ mutex_lock_status_get(p_buffer->p_mutex, id)) {
+ mutex_unlock(p_buffer->p_mutex, id);
+ }
+}
diff --git a/lib/peer_manager/modules/security_dispatcher.c b/lib/peer_manager/modules/security_dispatcher.c
new file mode 100644
index 0000000000..4bc5bb7065
--- /dev/null
+++ b/lib/peer_manager/modules/security_dispatcher.c
@@ -0,0 +1,917 @@
+/*
+ * Copyright (c) 2015-2025 Nordic Semiconductor ASA
+ *
+ * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
+ */
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#if CONFIG_PM_RA_PROTECTION_ENABLED
+#include
+#endif /* CONFIG_PM_RA_PROTECTION_ENABLED */
+
+#include
+
+LOG_MODULE_DECLARE(peer_manager, CONFIG_PEER_MANAGER_LOG_LEVEL);
+
+/* The number of registered event handlers. */
+#define SMD_EVENT_HANDLERS_CNT ARRAY_SIZE(m_evt_handlers)
+
+/* Security Dispacher event handlers in Security Manager and GATT Cache Manager. */
+extern void sm_smd_evt_handler(pm_evt_t *p_event);
+
+/* Security Dispatcher events' handlers.
+ * The number of elements in this array is SMD_EVENT_HANDLERS_CNT.
+ */
+static pm_evt_handler_internal_t const m_evt_handlers[] = {sm_smd_evt_handler};
+
+static bool m_module_initialized;
+
+static int m_flag_sec_proc = CONFIG_BLE_CONN_STATE_USER_FLAG_COUNT;
+static int m_flag_sec_proc_pairing = CONFIG_BLE_CONN_STATE_USER_FLAG_COUNT;
+static int m_flag_sec_proc_bonding = CONFIG_BLE_CONN_STATE_USER_FLAG_COUNT;
+static int m_flag_allow_repairing = CONFIG_BLE_CONN_STATE_USER_FLAG_COUNT;
+
+static ble_gap_lesc_p256_pk_t m_peer_pk;
+
+static __INLINE bool sec_procedure(uint16_t conn_handle)
+{
+ return ble_conn_state_user_flag_get(conn_handle, m_flag_sec_proc);
+}
+
+static __INLINE bool pairing(uint16_t conn_handle)
+{
+ return ble_conn_state_user_flag_get(conn_handle, m_flag_sec_proc_pairing);
+}
+
+static __INLINE bool bonding(uint16_t conn_handle)
+{
+ return ble_conn_state_user_flag_get(conn_handle, m_flag_sec_proc_bonding);
+}
+
+static __INLINE bool allow_repairing(uint16_t conn_handle)
+{
+ return ble_conn_state_user_flag_get(conn_handle, m_flag_allow_repairing);
+}
+
+/**
+ * @brief Function for sending an SMD event to all event handlers.
+ *
+ * @param[in] p_event The event to pass to all event handlers.
+ */
+static void evt_send(pm_evt_t *p_event)
+{
+ p_event->peer_id = im_peer_id_get_by_conn_handle(p_event->conn_handle);
+
+ for (uint32_t i = 0; i < SMD_EVENT_HANDLERS_CNT; i++) {
+ m_evt_handlers[i](p_event);
+ }
+}
+
+/**
+ * @brief Function for sending a PM_EVT_CONN_SEC_START event.
+ *
+ * @param[in] conn_handle The connection handle the event pertains to.
+ * @param[in] procedure The procedure that has started on the connection.
+ */
+static void sec_start_send(uint16_t conn_handle, pm_conn_sec_procedure_t procedure)
+{
+ pm_evt_t evt = {.evt_id = PM_EVT_CONN_SEC_START,
+ .conn_handle = conn_handle,
+ .params = {.conn_sec_start = {.procedure = procedure}}};
+
+ evt_send(&evt);
+}
+
+/**
+ * @brief Function for sending a PM_EVT_ERROR_UNEXPECTED event.
+ *
+ * @param[in] conn_handle The connection handle the event pertains to.
+ * @param[in] err_code The unexpected error that occurred.
+ */
+static void send_unexpected_error(uint16_t conn_handle, uint32_t err_code)
+{
+ pm_evt_t error_evt = {.evt_id = PM_EVT_ERROR_UNEXPECTED,
+ .conn_handle = conn_handle,
+ .params = {.error_unexpected = {
+ .error = err_code,
+ }}};
+
+ evt_send(&error_evt);
+}
+
+/**
+ * @brief Function for sending a PM_EVT_STORAGE_FULL event.
+ *
+ * @param[in] conn_handle The connection handle the event pertains to.
+ */
+static void send_storage_full_evt(uint16_t conn_handle)
+{
+ pm_evt_t evt = {.evt_id = PM_EVT_STORAGE_FULL, .conn_handle = conn_handle};
+
+ evt_send(&evt);
+}
+
+/**
+ * @brief Function for cleaning up after a failed security procedure.
+ *
+ * @param[in] conn_handle The handle of the connection the security procedure happens on.
+ * @param[in] procedure The procedure that failed.
+ * @param[in] error The error the procedure failed with.
+ * @param[in] error_src The party that raised the error. See @ref BLE_GAP_SEC_STATUS_SOURCES.
+ */
+static void conn_sec_failure(uint16_t conn_handle, pm_conn_sec_procedure_t procedure,
+ pm_sec_error_code_t error, uint8_t error_src)
+{
+ pm_evt_t evt = {.evt_id = PM_EVT_CONN_SEC_FAILED,
+ .conn_handle = conn_handle,
+ .params = {.conn_sec_failed = {
+ .procedure = procedure,
+ .error = error,
+ .error_src = error_src,
+ }}};
+
+ ble_conn_state_user_flag_set(conn_handle, m_flag_sec_proc, false);
+
+ evt_send(&evt);
+}
+
+/**
+ * @brief Function for cleaning up after a failed pairing procedure.
+ *
+ * @param[in] conn_handle The handle of the connection the pairing procedure happens on.
+ * @param[in] error The error the procedure failed with.
+ * @param[in] error_src The source of the error (local or remote). See @ref
+ * BLE_GAP_SEC_STATUS_SOURCES.
+ */
+static void pairing_failure(uint16_t conn_handle, pm_sec_error_code_t error, uint8_t error_src)
+{
+ uint32_t err_code = NRF_SUCCESS;
+ pm_conn_sec_procedure_t procedure = bonding(conn_handle) ? PM_CONN_SEC_PROCEDURE_BONDING
+ : PM_CONN_SEC_PROCEDURE_PAIRING;
+
+ err_code = pdb_write_buf_release(PDB_TEMP_PEER_ID(conn_handle), PM_PEER_DATA_ID_BONDING);
+ if ((err_code != NRF_SUCCESS) &&
+ (err_code != NRF_ERROR_NOT_FOUND /* No buffer was allocated */)) {
+ LOG_ERR("Could not clean up after failed bonding procedure. "
+ "pdb_write_buf_release() returned %s. conn_handle: %d.",
+ nrf_strerror_get(err_code), conn_handle);
+ send_unexpected_error(conn_handle, err_code);
+ }
+
+ conn_sec_failure(conn_handle, procedure, error, error_src);
+}
+
+/**
+ * @brief Function for cleaning up after a failed encryption procedure.
+ *
+ * @param[in] conn_handle The handle of the connection the encryption procedure happens on.
+ * @param[in] error The error the procedure failed with.
+ * @param[in] error_src The party that raised the error. See @ref BLE_GAP_SEC_STATUS_SOURCES.
+ */
+static __INLINE void encryption_failure(uint16_t conn_handle, pm_sec_error_code_t error,
+ uint8_t error_src)
+{
+ conn_sec_failure(conn_handle, PM_CONN_SEC_PROCEDURE_ENCRYPTION, error, error_src);
+}
+
+/**
+ * @brief Function for possibly cleaning up after a failed pairing or encryption procedure.
+ *
+ * @param[in] conn_handle The handle of the connection the pairing procedure happens on.
+ * @param[in] error The error the procedure failed with.
+ * @param[in] error_src The party that raised the error. See @ref BLE_GAP_SEC_STATUS_SOURCES.
+ */
+static void link_secure_failure(uint16_t conn_handle, pm_sec_error_code_t error, uint8_t error_src)
+{
+ if (sec_procedure(conn_handle)) {
+ if (pairing(conn_handle)) {
+ pairing_failure(conn_handle, error, error_src);
+ } else {
+ encryption_failure(conn_handle, error, error_src);
+ }
+ }
+}
+
+/**
+ * @brief Function for administrative actions to be taken when a security process has started.
+ *
+ * @param[in] conn_handle The connection the security process was attempted on.
+ * @param[in] success Whether the procedure was started successfully.
+ * @param[in] procedure The procedure that was started.
+ */
+static void sec_proc_start(uint16_t conn_handle, bool success, pm_conn_sec_procedure_t procedure)
+{
+ ble_conn_state_user_flag_set(conn_handle, m_flag_sec_proc, success);
+ if (success) {
+ ble_conn_state_user_flag_set(conn_handle, m_flag_sec_proc_pairing,
+ (procedure != PM_CONN_SEC_PROCEDURE_ENCRYPTION));
+ ble_conn_state_user_flag_set(conn_handle, m_flag_sec_proc_bonding,
+ (procedure == PM_CONN_SEC_PROCEDURE_BONDING));
+ sec_start_send(conn_handle, procedure);
+ }
+}
+
+#ifdef CONFIG_SOFTDEVICE_CENTRAL
+/**
+ * @brief Function for initiating encryption as a central. See @ref smd_link_secure for more
+ * info.
+ */
+static uint32_t link_secure_central_encryption(uint16_t conn_handle, pm_peer_id_t peer_id)
+{
+ pm_peer_data_flash_t peer_data;
+ uint32_t err_code;
+ ble_gap_enc_key_t const *p_existing_key = NULL;
+ bool lesc = false;
+
+ err_code = pdb_peer_data_ptr_get(peer_id, PM_PEER_DATA_ID_BONDING, &peer_data);
+
+ if (err_code == NRF_SUCCESS) {
+ /* Use peer's key since they are peripheral. */
+ p_existing_key = &(peer_data.p_bonding_data->peer_ltk);
+
+ lesc = peer_data.p_bonding_data->own_ltk.enc_info.lesc;
+ /* LESC was used during bonding. */
+ if (lesc) {
+ /* For LESC, always use own key. */
+ p_existing_key = &(peer_data.p_bonding_data->own_ltk);
+ }
+ }
+
+ if ((err_code != NRF_SUCCESS) && (err_code != NRF_ERROR_NOT_FOUND)) {
+ if (err_code != NRF_ERROR_BUSY) {
+ NRF_LOG_ERROR("Could not retrieve stored bond. pdb_peer_data_ptr_get() "
+ "returned %s. "
+ "peer_id: %d",
+ nrf_strerror_get(err_code), peer_id);
+ err_code = NRF_ERROR_INTERNAL;
+ }
+ /* There is no bonding data stored. This means that a
+ * bonding procedure is in ongoing, or that the records
+ * in flash are in a bad state.
+ */
+ } else if (p_existing_key == NULL) {
+ err_code = NRF_ERROR_BUSY;
+ } else if (!lesc && !im_master_id_is_valid(&(p_existing_key->master_id))) {
+ /* No LTK to encrypt with. */
+ err_code = NRF_ERROR_INVALID_DATA;
+ } else {
+ /* Encrypt with existing LTK. */
+ err_code = sd_ble_gap_encrypt(conn_handle, &(p_existing_key->master_id),
+ &(p_existing_key->enc_info));
+ }
+
+ sec_proc_start(conn_handle, err_code == NRF_SUCCESS, PM_CONN_SEC_PROCEDURE_ENCRYPTION);
+
+ return err_code;
+}
+
+/** @brief Function for intiating security as a central. See @ref smd_link_secure for more info. */
+static uint32_t link_secure_central(uint16_t conn_handle, ble_gap_sec_params_t *p_sec_params,
+ bool force_repairing)
+{
+ uint32_t err_code;
+ pm_peer_id_t peer_id;
+
+ if (p_sec_params == NULL) {
+ return link_secure_authenticate(conn_handle, NULL);
+ }
+
+ /* Set the default value for allowing repairing at the start of the sec proc.
+ * (for central)
+ */
+ ble_conn_state_user_flag_set(conn_handle, m_flag_allow_repairing, force_repairing);
+
+ peer_id = im_peer_id_get_by_conn_handle(conn_handle);
+
+ if ((peer_id != PM_PEER_ID_INVALID) && !force_repairing) {
+ /* There is already data in flash for this peer, and repairing has not been
+ * requested, so the link will be encrypted with the existing keys.
+ */
+ err_code = link_secure_central_encryption(conn_handle, peer_id);
+ } else {
+ /* There are no existing keys, or repairing has been explicitly requested, so
+ * pairing (possibly including bonding) will be performed to encrypt the link.
+ */
+ err_code = link_secure_authenticate(conn_handle, p_sec_params);
+ pm_conn_sec_procedure_t procedure = (p_sec_params && p_sec_params->bond)
+ ? PM_CONN_SEC_PROCEDURE_BONDING
+ : PM_CONN_SEC_PROCEDURE_PAIRING;
+ sec_proc_start(conn_handle, err_code == NRF_SUCCESS, procedure);
+ }
+
+ return err_code;
+}
+
+/**
+ * @brief Function for processing the @ref BLE_GAP_EVT_SEC_REQUEST event from the SoftDevice.
+ *
+ * @param[in] p_gap_evt The event from the SoftDevice.
+ */
+static void sec_request_process(ble_gap_evt_t const *p_gap_evt)
+{
+ if (sec_procedure(p_gap_evt->conn_handle)) {
+ /* Ignore request as per spec. */
+ return;
+ }
+
+ pm_evt_t evt = {.evt_id = PM_EVT_SLAVE_SECURITY_REQ, .conn_handle = p_gap_evt->conn_handle};
+
+ memcpy(&evt.params.slave_security_req, &p_gap_evt->params.sec_request,
+ sizeof(ble_gap_evt_sec_request_t));
+
+ evt_send(&evt);
+}
+#endif /* CONFIG_SOFTDEVICE_CENTRAL */
+
+#ifdef BLE_GAP_ROLE_PERIPH
+/** @brief Function for processing the @ref BLE_GAP_EVT_SEC_INFO_REQUEST event from the SoftDevice.
+ *
+ * @param[in] p_gap_evt The event from the SoftDevice.
+ */
+static void sec_info_request_process(ble_gap_evt_t const *p_gap_evt)
+{
+ uint32_t err_code;
+ ble_gap_enc_info_t const *p_enc_info = NULL;
+ pm_peer_data_flash_t peer_data;
+ pm_peer_id_t peer_id =
+ im_peer_id_get_by_master_id(&p_gap_evt->params.sec_info_request.master_id);
+
+ if (peer_id == PM_PEER_ID_INVALID) {
+ peer_id = im_peer_id_get_by_conn_handle(p_gap_evt->conn_handle);
+ } else {
+ /* The peer might have been unrecognized until now (since connecting). E.g. if using
+ * a random non-resolvable advertising address. Report the discovered peer ID just
+ * in case.
+ */
+ im_new_peer_id(p_gap_evt->conn_handle, peer_id);
+ }
+
+ sec_proc_start(p_gap_evt->conn_handle, true, PM_CONN_SEC_PROCEDURE_ENCRYPTION);
+
+ if (peer_id != PM_PEER_ID_INVALID) {
+ err_code = pdb_peer_data_ptr_get(peer_id, PM_PEER_DATA_ID_BONDING, &peer_data);
+
+ if (err_code == NRF_SUCCESS) {
+ /* There is stored bonding data for this peer. */
+ ble_gap_enc_key_t const *p_existing_key =
+ &peer_data.p_bonding_data->own_ltk;
+
+ if (p_gap_evt->params.sec_info_request.enc_info &&
+ (p_existing_key->enc_info.lesc ||
+ im_master_ids_compare(
+ &p_existing_key->master_id,
+ &p_gap_evt->params.sec_info_request.master_id))) {
+ p_enc_info = &p_existing_key->enc_info;
+ }
+ }
+ }
+
+ err_code = sd_ble_gap_sec_info_reply(p_gap_evt->conn_handle, p_enc_info, NULL, NULL);
+
+ if (err_code == NRF_ERROR_INVALID_STATE) {
+ /* Do nothing. If disconnecting, it will be caught later by the handling of the
+ * DISCONNECTED event. If there is no SEC_INFO_REQ pending, there is either a logic
+ * error, or the user is also calling sd_ble_gap_sec_info_reply(), but there is no
+ * way for the present code to detect which one is the case.
+ */
+ LOG_WRN("sd_ble_gap_sec_info_reply() returned NRF_ERROR_INVALID_STATE, which is an "
+ "error unless the link is disconnecting.");
+ } else if (err_code != NRF_SUCCESS) {
+ LOG_ERR("Could not complete encryption procedure. sd_ble_gap_sec_info_reply() "
+ "returned %s. conn_handle: %d, peer_id: %d.",
+ nrf_strerror_get(err_code), p_gap_evt->conn_handle, peer_id);
+ send_unexpected_error(p_gap_evt->conn_handle, err_code);
+ } else if (p_gap_evt->params.sec_info_request.enc_info && (p_enc_info == NULL)) {
+ encryption_failure(p_gap_evt->conn_handle, PM_CONN_SEC_ERROR_PIN_OR_KEY_MISSING,
+ BLE_GAP_SEC_STATUS_SOURCE_LOCAL);
+ }
+}
+#endif /* BLE_GAP_ROLE_PERIPH */
+
+/**
+ * @brief Function for sending a CONFIG_REQ event.
+ *
+ * @param[in] conn_handle The connection the sec parameters are needed for.
+ */
+static void send_config_req(uint16_t conn_handle)
+{
+ pm_evt_t evt;
+
+ memset(&evt, 0, sizeof(evt));
+
+ evt.evt_id = PM_EVT_CONN_SEC_CONFIG_REQ;
+ evt.conn_handle = conn_handle;
+
+ evt_send(&evt);
+}
+
+void smd_conn_sec_config_reply(uint16_t conn_handle, pm_conn_sec_config_t *p_conn_sec_config)
+{
+ NRF_PM_DEBUG_CHECK(m_module_initialized);
+ NRF_PM_DEBUG_CHECK(p_conn_sec_config != NULL);
+
+ ble_conn_state_user_flag_set(conn_handle, m_flag_allow_repairing,
+ p_conn_sec_config->allow_repairing);
+}
+
+/**
+ * @brief Function for processing the @ref BLE_GAP_EVT_DISCONNECT event from the SoftDevice.
+ *
+ * @param[in] p_gap_evt The event from the SoftDevice.
+ */
+static void disconnect_process(ble_gap_evt_t const *p_gap_evt)
+{
+ pm_sec_error_code_t error = (p_gap_evt->params.disconnected.reason ==
+ BLE_HCI_CONN_TERMINATED_DUE_TO_MIC_FAILURE)
+ ? PM_CONN_SEC_ERROR_MIC_FAILURE
+ : PM_CONN_SEC_ERROR_DISCONNECT;
+
+ link_secure_failure(p_gap_evt->conn_handle, error, BLE_GAP_SEC_STATUS_SOURCE_LOCAL);
+}
+
+/**
+ * @brief Function for sending a PARAMS_REQ event.
+ *
+ * @param[in] conn_handle The connection the security parameters are needed for.
+ * @param[in] p_peer_params The security parameters from the peer. Can be NULL if the peer's
+ * parameters are not yet available.
+ */
+static void send_params_req(uint16_t conn_handle, ble_gap_sec_params_t const *p_peer_params)
+{
+ pm_evt_t evt = {
+ .evt_id = PM_EVT_CONN_SEC_PARAMS_REQ,
+ .conn_handle = conn_handle,
+ .params = {
+ .conn_sec_params_req = {.p_peer_params = p_peer_params},
+ },
+ };
+
+ evt_send(&evt);
+}
+
+/**
+ * @brief Function for processing the @ref BLE_GAP_EVT_SEC_PARAMS_REQUEST event from the SoftDevice.
+ *
+ * @param[in] p_gap_evt The event from the SoftDevice.
+ */
+static void sec_params_request_process(ble_gap_evt_t const *p_gap_evt)
+{
+#ifdef BLE_GAP_ROLE_PERIPH
+ if (ble_conn_state_role(p_gap_evt->conn_handle) == BLE_GAP_ROLE_PERIPH) {
+ sec_proc_start(p_gap_evt->conn_handle, true,
+ p_gap_evt->params.sec_params_request.peer_params.bond
+ ? PM_CONN_SEC_PROCEDURE_BONDING
+ : PM_CONN_SEC_PROCEDURE_PAIRING);
+ }
+#endif /* BLE_GAP_ROLE_PERIPH */
+
+ send_params_req(p_gap_evt->conn_handle, &p_gap_evt->params.sec_params_request.peer_params);
+}
+
+/**
+ * @brief Function for sending a Peer Manager event indicating that pairing has succeeded.
+ *
+ * @param[in] p_gap_evt The AUTH_STATUS event from the SoftDevice that triggered this.
+ * @param[in] data_stored Whether bonding data was stored.
+ */
+static void pairing_success_evt_send(ble_gap_evt_t const *p_gap_evt, bool data_stored)
+{
+ pm_evt_t pairing_success_evt;
+
+ pairing_success_evt.evt_id = PM_EVT_CONN_SEC_SUCCEEDED;
+ pairing_success_evt.conn_handle = p_gap_evt->conn_handle;
+ pairing_success_evt.params.conn_sec_succeeded.procedure =
+ p_gap_evt->params.auth_status.bonded ? PM_CONN_SEC_PROCEDURE_BONDING
+ : PM_CONN_SEC_PROCEDURE_PAIRING;
+ pairing_success_evt.params.conn_sec_succeeded.data_stored = data_stored;
+
+ evt_send(&pairing_success_evt);
+}
+
+/**
+ * @brief Function for processing the @ref BLE_GAP_EVT_AUTH_STATUS event from the SoftDevice, when
+ * the auth_status is success.
+ *
+ * @param[in] p_gap_evt The event from the SoftDevice.
+ */
+static void auth_status_success_process(ble_gap_evt_t const *p_gap_evt)
+{
+ uint32_t err_code;
+ uint16_t conn_handle = p_gap_evt->conn_handle;
+ pm_peer_id_t peer_id;
+ pm_peer_data_t peer_data;
+ bool new_peer_id = false;
+
+ ble_conn_state_user_flag_set(conn_handle, m_flag_sec_proc, false);
+
+ if (!p_gap_evt->params.auth_status.bonded) {
+ pairing_success_evt_send(p_gap_evt, false);
+ return;
+ }
+
+ err_code = pdb_write_buf_get(PDB_TEMP_PEER_ID(conn_handle), PM_PEER_DATA_ID_BONDING, 1,
+ &peer_data);
+ if (err_code != NRF_SUCCESS) {
+ LOG_ERR("RAM buffer for new bond was unavailable. pdb_write_buf_get() "
+ "returned %s. conn_handle: %d.",
+ nrf_strerror_get(err_code), conn_handle);
+ send_unexpected_error(conn_handle, err_code);
+ pairing_success_evt_send(p_gap_evt, false);
+ return;
+ }
+
+ peer_id = im_peer_id_get_by_conn_handle(conn_handle);
+
+ if (peer_id == PM_PEER_ID_INVALID) {
+ peer_id = im_find_duplicate_bonding_data(peer_data.p_bonding_data,
+ PM_PEER_ID_INVALID);
+
+ if (peer_id != PM_PEER_ID_INVALID) {
+ /* The peer has been identified as someone we have already bonded with. */
+ im_new_peer_id(conn_handle, peer_id);
+
+ /* If the flag is true, the configuration has been requested before. */
+ if (!allow_repairing(conn_handle)) {
+ send_config_req(conn_handle);
+ if (!allow_repairing(conn_handle)) {
+ pairing_success_evt_send(p_gap_evt, false);
+ return;
+ }
+ }
+ }
+ }
+
+ if (peer_id == PM_PEER_ID_INVALID) {
+ peer_id = pds_peer_id_allocate();
+ if (peer_id == PM_PEER_ID_INVALID) {
+ LOG_ERR("Could not allocate new peer_id for incoming bond.");
+ send_unexpected_error(conn_handle, NRF_ERROR_NO_MEM);
+ pairing_success_evt_send(p_gap_evt, false);
+ return;
+ }
+ im_new_peer_id(conn_handle, peer_id);
+ new_peer_id = true;
+ }
+
+ err_code = pdb_write_buf_store(PDB_TEMP_PEER_ID(conn_handle), PM_PEER_DATA_ID_BONDING,
+ peer_id);
+
+ if (err_code == NRF_SUCCESS) {
+ pairing_success_evt_send(p_gap_evt, true);
+ } else if (err_code == NRF_ERROR_RESOURCES) {
+ send_storage_full_evt(conn_handle);
+ pairing_success_evt_send(p_gap_evt, true);
+ } else {
+ /* Unexpected error */
+ LOG_ERR("Could not store bond. pdb_write_buf_store() returned %s. "
+ "conn_handle: %d, peer_id: %d",
+ nrf_strerror_get(err_code), conn_handle, peer_id);
+ send_unexpected_error(conn_handle, err_code);
+ pairing_success_evt_send(p_gap_evt, false);
+ if (new_peer_id) {
+ UNUSED_RETURN_VALUE(
+ /* We are already in a bad state. */
+ im_peer_free(peer_id));
+ }
+ }
+}
+
+/**
+ * @brief Function for processing the @ref BLE_GAP_EVT_AUTH_STATUS event from the SoftDevice, when
+ * the auth_status is failure.
+ *
+ * @param[in] p_gap_evt The event from the SoftDevice.
+ */
+static void auth_status_failure_process(ble_gap_evt_t const *p_gap_evt)
+{
+ link_secure_failure(p_gap_evt->conn_handle, p_gap_evt->params.auth_status.auth_status,
+ p_gap_evt->params.auth_status.error_src);
+}
+
+/**
+ * @brief Function for processing the @ref BLE_GAP_EVT_AUTH_STATUS event from the SoftDevice.
+ *
+ * @param[in] p_gap_evt The event from the SoftDevice.
+ */
+static void auth_status_process(ble_gap_evt_t const *p_gap_evt)
+{
+ switch (p_gap_evt->params.auth_status.auth_status) {
+ case BLE_GAP_SEC_STATUS_SUCCESS:
+ auth_status_success_process(p_gap_evt);
+ break;
+
+ default:
+ auth_status_failure_process(p_gap_evt);
+#if CONFIG_PM_RA_PROTECTION_ENABLED
+ ast_auth_error_notify(p_gap_evt->conn_handle);
+#endif /* CONFIG_PM_RA_PROTECTION_ENABLED */
+ break;
+ }
+}
+
+/**
+ * @brief Function for processing the @ref BLE_GAP_EVT_CONN_SEC_UPDATE event from the SoftDevice.
+ *
+ * @param[in] p_gap_evt The event from the SoftDevice.
+ */
+static void conn_sec_update_process(ble_gap_evt_t const *p_gap_evt)
+{
+ if (!pairing(p_gap_evt->conn_handle)) {
+ /* This is an encryption procedure (not pairing), so this event marks the end of the
+ * procedure.
+ */
+ if (!ble_conn_state_encrypted(p_gap_evt->conn_handle)) {
+ encryption_failure(p_gap_evt->conn_handle,
+ PM_CONN_SEC_ERROR_PIN_OR_KEY_MISSING,
+ BLE_GAP_SEC_STATUS_SOURCE_REMOTE);
+ } else {
+ ble_conn_state_user_flag_set(p_gap_evt->conn_handle, m_flag_sec_proc,
+ false);
+
+ pm_evt_t evt;
+
+ evt.evt_id = PM_EVT_CONN_SEC_SUCCEEDED;
+ evt.conn_handle = p_gap_evt->conn_handle;
+ evt.params.conn_sec_succeeded.procedure = PM_CONN_SEC_PROCEDURE_ENCRYPTION;
+ evt.params.conn_sec_succeeded.data_stored = false;
+
+ evt_send(&evt);
+ }
+ }
+}
+
+/**
+ * @brief Function for initializing a BLE Connection State user flag.
+ *
+ * @param[out] flag_id The flag to initialize.
+ */
+static void flag_id_init(int *p_flag_id)
+{
+ if (*p_flag_id == CONFIG_BLE_CONN_STATE_USER_FLAG_COUNT) {
+ *p_flag_id = ble_conn_state_user_flag_acquire();
+ }
+}
+
+uint32_t smd_init(void)
+{
+ NRF_PM_DEBUG_CHECK(!m_module_initialized);
+
+ flag_id_init(&m_flag_sec_proc);
+ flag_id_init(&m_flag_sec_proc_pairing);
+ flag_id_init(&m_flag_sec_proc_bonding);
+ flag_id_init(&m_flag_allow_repairing);
+
+ if ((m_flag_sec_proc == CONFIG_BLE_CONN_STATE_USER_FLAG_COUNT) ||
+ (m_flag_sec_proc_pairing == CONFIG_BLE_CONN_STATE_USER_FLAG_COUNT) ||
+ (m_flag_sec_proc_bonding == CONFIG_BLE_CONN_STATE_USER_FLAG_COUNT) ||
+ (m_flag_allow_repairing == CONFIG_BLE_CONN_STATE_USER_FLAG_COUNT)) {
+ LOG_ERR("Could not acquire conn_state user flags. Increase "
+ "BLE_CONN_STATE_USER_FLAG_COUNT in the ble_conn_state module.");
+ return NRF_ERROR_INTERNAL;
+ }
+
+#if CONFIG_PM_RA_PROTECTION_ENABLED
+ uint32_t err_code = ast_init();
+
+ if (err_code != NRF_SUCCESS) {
+ return err_code;
+ }
+#endif /* CONFIG_PM_RA_PROTECTION_ENABLED */
+
+ m_module_initialized = true;
+
+ return NRF_SUCCESS;
+}
+
+/**
+ * @brief Function for putting retrieving a buffer and putting pointers into a @ref
+ * ble_gap_sec_keyset_t.
+ *
+ * @param[in] conn_handle The connection the security procedure is happening on.
+ * @param[in] role Our role in the connection.
+ * @param[in] p_public_key Pointer to a buffer holding the public key, or NULL.
+ * @param[out] p_sec_keyset Pointer to the keyset to be filled.
+ *
+ * @retval NRF_SUCCESS Success.
+ * @retval NRF_ERROR_BUSY Could not process request at this time. Reattempt later.
+ * @retval NRF_ERROR_INVALID_PARAM Data ID or Peer ID was invalid or unallocated.
+ * @retval NRF_ERROR_INVALID_STATE The link is disconnected.
+ * @retval NRF_ERROR_INTERNAL Fatal error.
+ */
+static uint32_t sec_keyset_fill(uint16_t conn_handle, uint8_t role,
+ ble_gap_lesc_p256_pk_t *p_public_key,
+ ble_gap_sec_keyset_t *p_sec_keyset)
+{
+ uint32_t err_code;
+ pm_peer_data_t peer_data;
+
+ if (p_sec_keyset == NULL) {
+ LOG_ERR("Internal error: %s received NULL for p_sec_keyset.", __func__);
+ return NRF_ERROR_INTERNAL;
+ }
+
+ /* Acquire a memory buffer to receive bonding data into. */
+ err_code = pdb_write_buf_get(PDB_TEMP_PEER_ID(conn_handle), PM_PEER_DATA_ID_BONDING, 1,
+ &peer_data);
+
+ if (err_code == NRF_ERROR_BUSY) {
+ /* No action. */
+ } else if (err_code != NRF_SUCCESS) {
+ LOG_ERR("Could not retrieve RAM buffer for incoming bond. pdb_write_buf_get() "
+ "returned %s. conn_handle: %d",
+ nrf_strerror_get(err_code), conn_handle);
+ err_code = NRF_ERROR_INTERNAL;
+ } else {
+ memset(peer_data.p_bonding_data, 0, sizeof(pm_peer_data_bonding_t));
+
+ peer_data.p_bonding_data->own_role = role;
+
+ p_sec_keyset->keys_own.p_enc_key = &peer_data.p_bonding_data->own_ltk;
+ p_sec_keyset->keys_own.p_pk = p_public_key;
+ p_sec_keyset->keys_peer.p_enc_key = &peer_data.p_bonding_data->peer_ltk;
+ p_sec_keyset->keys_peer.p_id_key = &peer_data.p_bonding_data->peer_ble_id;
+ p_sec_keyset->keys_peer.p_pk = &m_peer_pk;
+
+ /* Retrieve the address the peer used during connection establishment.
+ * This address will be overwritten if ID is shared. Should not fail.
+ */
+ err_code = im_ble_addr_get(conn_handle,
+ &peer_data.p_bonding_data->peer_ble_id.id_addr_info);
+ if (err_code != NRF_SUCCESS) {
+ LOG_WRN("im_ble_addr_get() returned %s. conn_handle: %d. Link was "
+ "likely disconnected.",
+ nrf_strerror_get(err_code), conn_handle);
+ return NRF_ERROR_INVALID_STATE;
+ }
+ }
+
+ return err_code;
+}
+
+uint32_t smd_params_reply(uint16_t conn_handle, ble_gap_sec_params_t *p_sec_params,
+ ble_gap_lesc_p256_pk_t *p_public_key)
+{
+ NRF_PM_DEBUG_CHECK(m_module_initialized);
+
+ uint8_t role = ble_conn_state_role(conn_handle);
+ uint32_t err_code = NRF_SUCCESS;
+ uint8_t sec_status = BLE_GAP_SEC_STATUS_SUCCESS;
+ ble_gap_sec_keyset_t sec_keyset;
+
+ memset(&sec_keyset, 0, sizeof(ble_gap_sec_keyset_t));
+#ifdef BLE_GAP_ROLE_PERIPH
+ if (role == BLE_GAP_ROLE_PERIPH) {
+ /* Set the default value for allowing repairing at the start of the sec proc. (for
+ * peripheral)
+ */
+ ble_conn_state_user_flag_set(conn_handle, m_flag_allow_repairing, false);
+ }
+#endif /* BLE_GAP_ROLE_PERIPH */
+
+ if (role == BLE_GAP_ROLE_INVALID) {
+ return BLE_ERROR_INVALID_CONN_HANDLE;
+ }
+
+#if CONFIG_PM_RA_PROTECTION_ENABLED
+ /* Check for repeated attempts here. */
+ if (ast_peer_blacklisted(conn_handle)) {
+ sec_status = BLE_GAP_SEC_STATUS_REPEATED_ATTEMPTS;
+ } else
+#endif /* CONFIG_PM_RA_PROTECTION_ENABLED */
+ if (p_sec_params == NULL) {
+ /* NULL params means reject pairing. */
+ sec_status = BLE_GAP_SEC_STATUS_PAIRING_NOT_SUPP;
+ } else {
+#ifdef BLE_GAP_ROLE_PERIPH
+ if ((im_peer_id_get_by_conn_handle(conn_handle) != PM_PEER_ID_INVALID) &&
+ (role == BLE_GAP_ROLE_PERIPH) && !allow_repairing(conn_handle)) {
+ /* Bond already exists. Reject the pairing request if the user
+ * doesn't intervene.
+ */
+ send_config_req(conn_handle);
+ if (!allow_repairing(conn_handle)) {
+ /* Reject pairing. */
+ sec_status = BLE_GAP_SEC_STATUS_PAIRING_NOT_SUPP;
+ }
+ }
+#endif /* BLE_GAP_ROLE_PERIPH */
+
+ if (!p_sec_params->bond) {
+ /* Pairing, no bonding. */
+ sec_keyset.keys_own.p_pk = p_public_key;
+ sec_keyset.keys_peer.p_pk = &m_peer_pk;
+ } else if (sec_status != BLE_GAP_SEC_STATUS_PAIRING_NOT_SUPP) {
+ /* Bonding is to be performed, prepare to receive bonding data. */
+ err_code = sec_keyset_fill(conn_handle, role, p_public_key,
+ &sec_keyset);
+ }
+ }
+
+ if (err_code == NRF_SUCCESS) {
+ /* Everything OK, reply to SoftDevice. If an error happened, the user is given an
+ * opportunity to change the parameters and retry the call.
+ */
+ ble_gap_sec_params_t *p_aux_sec_params = NULL;
+#ifdef BLE_GAP_ROLE_PERIPH
+ p_aux_sec_params = (role == BLE_GAP_ROLE_PERIPH) ? p_sec_params : NULL;
+#endif /* BLE_GAP_ROLE_PERIPH */
+
+ err_code = sd_ble_gap_sec_params_reply(conn_handle, sec_status, p_aux_sec_params,
+ &sec_keyset);
+ }
+
+ return err_code;
+}
+
+/**
+ * @brief Function for initiating pairing as a central, or all security as a periheral.
+ *
+ * See @ref smd_link_secure and @ref sd_ble_gap_authenticate for more information.
+ */
+static uint32_t link_secure_authenticate(uint16_t conn_handle, ble_gap_sec_params_t *p_sec_params)
+{
+ uint32_t err_code = sd_ble_gap_authenticate(conn_handle, p_sec_params);
+
+ if (err_code == NRF_ERROR_NO_MEM) {
+ /* sd_ble_gap_authenticate() returned NRF_ERROR_NO_MEM. Too many other sec
+ * procedures running.
+ */
+ err_code = NRF_ERROR_BUSY;
+ }
+
+ return err_code;
+}
+
+#ifdef BLE_GAP_ROLE_PERIPH
+/** @brief Function for asking the central to secure the link. See @ref smd_link_secure for more
+ * info.
+ */
+static uint32_t link_secure_peripheral(uint16_t conn_handle, ble_gap_sec_params_t *p_sec_params)
+{
+ uint32_t err_code = NRF_SUCCESS;
+
+ if (p_sec_params != NULL) {
+ err_code = link_secure_authenticate(conn_handle, p_sec_params);
+ }
+
+ return err_code;
+}
+#endif
+
+uint32_t smd_link_secure(uint16_t conn_handle, ble_gap_sec_params_t *p_sec_params,
+ bool force_repairing)
+{
+ NRF_PM_DEBUG_CHECK(m_module_initialized);
+
+ uint8_t role = ble_conn_state_role(conn_handle);
+
+ switch (role) {
+#ifdef BLE_GAP_ROLE_PERIPH
+ case BLE_GAP_ROLE_PERIPH:
+ return link_secure_peripheral(conn_handle, p_sec_params);
+#endif /* BLE_GAP_ROLE_PERIPH */
+
+ default:
+ return BLE_ERROR_INVALID_CONN_HANDLE;
+ }
+}
+
+void smd_ble_evt_handler(ble_evt_t const *p_ble_evt)
+{
+ switch (p_ble_evt->header.evt_id) {
+ case BLE_GAP_EVT_DISCONNECTED:
+ disconnect_process(&(p_ble_evt->evt.gap_evt));
+ break;
+
+ case BLE_GAP_EVT_SEC_PARAMS_REQUEST:
+ sec_params_request_process(&(p_ble_evt->evt.gap_evt));
+ break;
+
+#ifdef BLE_GAP_ROLE_PERIPH
+ case BLE_GAP_EVT_SEC_INFO_REQUEST:
+ sec_info_request_process(&(p_ble_evt->evt.gap_evt));
+ break;
+#endif /* BLE_GAP_ROLE_PERIPH */
+
+ case BLE_GAP_EVT_AUTH_STATUS:
+ auth_status_process(&(p_ble_evt->evt.gap_evt));
+ break;
+
+ case BLE_GAP_EVT_CONN_SEC_UPDATE:
+ conn_sec_update_process(&(p_ble_evt->evt.gap_evt));
+ break;
+ };
+}
diff --git a/lib/peer_manager/modules/security_manager.c b/lib/peer_manager/modules/security_manager.c
new file mode 100644
index 0000000000..1d723bda64
--- /dev/null
+++ b/lib/peer_manager/modules/security_manager.c
@@ -0,0 +1,679 @@
+/*
+ * Copyright (c) 2015-2025 Nordic Semiconductor ASA
+ *
+ * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
+ */
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#if CONFIG_PM_LESC_ENABLED
+#include
+#endif
+
+LOG_MODULE_DECLARE(peer_manager, CONFIG_PEER_MANAGER_LOG_LEVEL);
+
+/* The number of registered event handlers. */
+#define SM_EVENT_HANDLERS_CNT ARRAY_SIZE(m_evt_handlers)
+
+/* Security Manager event handler in Peer Manager. */
+extern void pm_sm_evt_handler(pm_evt_t *p_sm_evt);
+
+/* Security Manager events' handlers.
+ * The number of elements in this array is SM_EVENT_HANDLERS_CNT.
+ */
+static pm_evt_handler_internal_t const m_evt_handlers[] = {pm_sm_evt_handler};
+
+/* The context type that is used in PM_EVT_CONN_SEC_PARAMS_REQ events and in calls to
+ * sm_sec_params_reply().
+ */
+typedef struct {
+ /* The security parameters to use in the call to the security_dispatcher */
+ ble_gap_sec_params_t *p_sec_params;
+ /* The buffer for holding the security parameters. */
+ ble_gap_sec_params_t sec_params_mem;
+ /* Whether @ref sm_sec_params_reply has been called for this context instance. */
+ bool params_reply_called;
+} sec_params_reply_context_t;
+
+/* Whether the Security Manager module has been initialized. */
+static bool m_module_initialized;
+
+/** The buffer for the default security parameters set by @ref sm_sec_params_set. */
+static ble_gap_sec_params_t m_sec_params;
+/** The default security parameters set by @ref sm_sec_params_set. */
+static ble_gap_sec_params_t *mp_sec_params;
+/** Whether @ref sm_sec_params_set has been called. */
+static bool m_sec_params_set;
+
+#if CONFIG_PM_LESC_ENABLED == 0
+/* Pointer, provided by the user, to the public key to use for LESC procedures. */
+static ble_gap_lesc_p256_pk_t *m_p_public_key;
+#endif
+
+/* User flag indicating whether a connection has a pending call to @ref sm_link_secure because it
+ * returned @ref NRF_ERROR_BUSY.
+ */
+static int m_flag_link_secure_pending_busy = CONFIG_BLE_CONN_STATE_USER_FLAG_COUNT;
+/* User flag indicating whether a pending call to @ref sm_link_secure should be called with true for
+ * the force_repairing parameter.
+ */
+static int m_flag_link_secure_force_repairing = CONFIG_BLE_CONN_STATE_USER_FLAG_COUNT;
+/* User flag indicating whether a pending call to @ref sm_link_secure should be called with NULL
+ * security parameters.
+ */
+static int m_flag_link_secure_null_params = CONFIG_BLE_CONN_STATE_USER_FLAG_COUNT;
+/* User flag indicating whether a connection has a pending call to @ref sm_sec_params_reply because
+ * it returned @ref NRF_ERROR_BUSY.
+ */
+static int m_flag_params_reply_pending_busy = CONFIG_BLE_CONN_STATE_USER_FLAG_COUNT;
+
+/**
+ * @brief Function for sending an SM event to all registered event handlers.
+ *
+ * @param[in] p_event The event to send.
+ */
+static void evt_send(pm_evt_t *p_event)
+{
+ for (uint32_t i = 0; i < SM_EVENT_HANDLERS_CNT; i++) {
+ m_evt_handlers[i](p_event);
+ }
+}
+
+/**
+ * @brief Function for setting or clearing user flags based on error codes returned from @ref
+ * smd_link_secure or @ref smd_params_reply.
+ *
+ * @param[in] conn_handle The connection the call pertained to.
+ * @param[in] err_code The error code returned from @ref smd_link_secure or
+ * @ref smd_params_reply.
+ * @param[in] params_reply Whether the call was to @ref smd_params_reply.
+ */
+static void flags_set_from_err_code(uint16_t conn_handle, uint32_t err_code, bool params_reply)
+{
+ bool flag_value_busy = false;
+
+ if (err_code == NRF_ERROR_BUSY) {
+ flag_value_busy = true;
+ } else {
+ flag_value_busy = false;
+ }
+
+ if (params_reply) {
+ ble_conn_state_user_flag_set(conn_handle, m_flag_params_reply_pending_busy,
+ flag_value_busy);
+ ble_conn_state_user_flag_set(conn_handle, m_flag_link_secure_pending_busy, false);
+ } else {
+ ble_conn_state_user_flag_set(conn_handle, m_flag_link_secure_pending_busy,
+ flag_value_busy);
+ }
+}
+
+static inline pm_evt_t new_evt(pm_evt_id_t evt_id, uint16_t conn_handle)
+{
+ pm_evt_t evt = {.evt_id = evt_id,
+ .conn_handle = conn_handle,
+ .peer_id = im_peer_id_get_by_conn_handle(conn_handle)};
+ return evt;
+}
+
+/**
+ * @brief Function for sending a PM_EVT_ERROR_UNEXPECTED event.
+ *
+ * @param[in] conn_handle The connection handle the event pertains to.
+ * @param[in] err_code The unexpected error that occurred.
+ */
+static void send_unexpected_error(uint16_t conn_handle, uint32_t err_code)
+{
+ pm_evt_t error_evt = new_evt(PM_EVT_ERROR_UNEXPECTED, conn_handle);
+
+ error_evt.params.error_unexpected.error = err_code;
+ evt_send(&error_evt);
+}
+
+/**
+ * @brief Returns whether the LTK came from LESC bonding.
+ *
+ * @param[in] peer_id The peer to check.
+ *
+ * @return Whether the key is LESC or not.
+ */
+static bool key_is_lesc(pm_peer_id_t peer_id)
+{
+ pm_peer_data_flash_t peer_data;
+ uint32_t err_code;
+
+ err_code = pdb_peer_data_ptr_get(peer_id, PM_PEER_DATA_ID_BONDING, &peer_data);
+
+ return (err_code == NRF_SUCCESS) && (peer_data.p_bonding_data->own_ltk.enc_info.lesc);
+}
+
+/**
+ * @brief Function for sending an event based on error codes returned from @ref smd_link_secure or
+ * @ref smd_params_reply.
+ *
+ * @param[in] conn_handle The connection the event pertains to.
+ * @param[in] err_code The error code returned from @ref smd_link_secure or
+ * @ref smd_params_reply.
+ * @param[in] p_sec_params The security parameters attempted to pass in the call to
+ * @ref smd_link_secure or @ref smd_params_reply.
+ */
+static void events_send_from_err_code(uint16_t conn_handle, uint32_t err_code,
+ ble_gap_sec_params_t *p_sec_params)
+{
+ if ((err_code != NRF_SUCCESS) && (err_code != NRF_ERROR_BUSY) &&
+ (err_code != NRF_ERROR_INVALID_STATE)) {
+ if (err_code == NRF_ERROR_TIMEOUT) {
+ LOG_WRN("Cannot secure link because a previous security procedure "
+ "ended in timeout. "
+ "Disconnect and retry. smd_params_reply() or "
+ "smd_link_secure() returned "
+ "NRF_ERROR_TIMEOUT. conn_handle: %d",
+ conn_handle);
+
+ pm_evt_t evt = new_evt(PM_EVT_CONN_SEC_FAILED, conn_handle);
+
+ evt.params.conn_sec_failed.procedure =
+ ((p_sec_params != NULL) && p_sec_params->bond)
+ ? PM_CONN_SEC_PROCEDURE_BONDING
+ : PM_CONN_SEC_PROCEDURE_PAIRING;
+ evt.params.conn_sec_failed.error_src = BLE_GAP_SEC_STATUS_SOURCE_LOCAL;
+ evt.params.conn_sec_failed.error = PM_CONN_SEC_ERROR_SMP_TIMEOUT;
+ evt_send(&evt);
+ } else {
+ LOG_ERR("Could not perform security procedure. smd_params_reply() or "
+ "smd_link_secure() returned %s. conn_handle: %d",
+ nrf_strerror_get(err_code), conn_handle);
+ send_unexpected_error(conn_handle, err_code);
+ }
+ }
+}
+
+/**
+ * @brief Function for sending an PM_EVT_CONN_SEC_PARAMS_REQ event.
+ *
+ * @param[in] conn_handle The connection the event pertains to.
+ * @param[in] p_peer_params The peer's security parameters to include in the event. Can be NULL.
+ * @param[in] p_context Pointer to a context that the user must include in the call to @ref
+ * sm_sec_params_reply().
+ */
+static void params_req_send(uint16_t conn_handle, ble_gap_sec_params_t const *p_peer_params,
+ sec_params_reply_context_t *p_context)
+{
+ pm_evt_t evt = new_evt(PM_EVT_CONN_SEC_PARAMS_REQ, conn_handle);
+
+ evt.params.conn_sec_params_req.p_peer_params = p_peer_params;
+ evt.params.conn_sec_params_req.p_context = p_context;
+
+ evt_send(&evt);
+}
+
+/**
+ * @brief Function for creating a new @ref sec_params_reply_context_t with the correct initial
+ * values.
+ *
+ * @return The new context.
+ */
+static sec_params_reply_context_t new_context_get(void)
+{
+ sec_params_reply_context_t new_context = {.p_sec_params = mp_sec_params,
+ .params_reply_called = false};
+ return new_context;
+}
+
+/**
+ * @brief Internal function corresponding to @ref sm_link_secure.
+ *
+ * @param[in] conn_handle The connection to secure.
+ * @param[in] null_params Whether to pass NULL security parameters to the security_dispatcher.
+ * @param[in] force_repairing Whether to force rebonding if peer exists.
+ * @param[in] send_events Whether to send events based on the result of @ref smd_link_secure.
+ *
+ * @return Same return codes as @ref sm_link_secure.
+ */
+static uint32_t link_secure(uint16_t conn_handle, bool null_params, bool force_repairing,
+ bool send_events)
+{
+ uint32_t err_code;
+ uint32_t return_err_code;
+ ble_gap_sec_params_t *p_sec_params;
+
+ if (null_params) {
+ p_sec_params = NULL;
+ } else {
+ sec_params_reply_context_t context = new_context_get();
+
+ params_req_send(conn_handle, NULL, &context);
+ p_sec_params = context.p_sec_params;
+
+ if (!m_sec_params_set && !context.params_reply_called) {
+ /* Security parameters have not been set. */
+ return NRF_ERROR_NOT_FOUND;
+ }
+ }
+
+ err_code = smd_link_secure(conn_handle, p_sec_params, force_repairing);
+
+ flags_set_from_err_code(conn_handle, err_code, false);
+
+ switch (err_code) {
+ case NRF_ERROR_BUSY:
+ ble_conn_state_user_flag_set(conn_handle, m_flag_link_secure_null_params,
+ null_params);
+ ble_conn_state_user_flag_set(conn_handle, m_flag_link_secure_force_repairing,
+ force_repairing);
+ return_err_code = NRF_SUCCESS;
+ break;
+ case NRF_SUCCESS:
+ case NRF_ERROR_TIMEOUT:
+ case BLE_ERROR_INVALID_CONN_HANDLE:
+ case NRF_ERROR_INVALID_STATE:
+ case NRF_ERROR_INVALID_DATA:
+ return_err_code = err_code;
+ break;
+ default:
+ LOG_ERR("Could not perform security procedure. smd_link_secure() returned %s. "
+ "conn_handle: %d",
+ nrf_strerror_get(err_code), conn_handle);
+ return_err_code = NRF_ERROR_INTERNAL;
+ break;
+ }
+
+ if (send_events) {
+ events_send_from_err_code(conn_handle, err_code, p_sec_params);
+ }
+
+ return return_err_code;
+}
+
+/**
+ * @brief Function for requesting security parameters from the user and passing them to the
+ * security_dispatcher.
+ *
+ * @param[in] conn_handle The connection that needs security parameters.
+ * @param[in] p_peer_params The peer's security parameters if present. Otherwise NULL.
+ */
+static void smd_params_reply_perform(uint16_t conn_handle,
+ ble_gap_sec_params_t const *p_peer_params)
+{
+ uint32_t err_code;
+ ble_gap_lesc_p256_pk_t *p_public_key;
+ sec_params_reply_context_t context = new_context_get();
+
+ params_req_send(conn_handle, p_peer_params, &context);
+
+#if CONFIG_PM_LESC_ENABLED
+ p_public_key = nrf_ble_lesc_public_key_get();
+#else
+ p_public_key = m_p_public_key;
+#endif /* CONFIG_PM_LESC_ENABLED */
+ err_code = smd_params_reply(conn_handle, context.p_sec_params, p_public_key);
+
+ flags_set_from_err_code(conn_handle, err_code, true);
+ events_send_from_err_code(conn_handle, err_code, context.p_sec_params);
+}
+
+/**
+ * @brief Function for handling @ref PM_EVT_CONN_SEC_PARAMS_REQ events.
+ *
+ * @param[in] p_event The @ref PM_EVT_CONN_SEC_PARAMS_REQ event.
+ */
+static __INLINE void params_req_process(pm_evt_t const *p_event)
+{
+ smd_params_reply_perform(p_event->conn_handle,
+ p_event->params.conn_sec_params_req.p_peer_params);
+}
+
+uint32_t sm_conn_sec_status_get(uint16_t conn_handle, pm_conn_sec_status_t *p_conn_sec_status)
+{
+ VERIFY_PARAM_NOT_NULL(p_conn_sec_status);
+
+ int status = ble_conn_state_status(conn_handle);
+
+ if (status == BLE_CONN_STATUS_INVALID) {
+ return BLE_ERROR_INVALID_CONN_HANDLE;
+ }
+
+ pm_peer_id_t peer_id = im_peer_id_get_by_conn_handle(conn_handle);
+
+ p_conn_sec_status->connected = (status == BLE_CONN_STATUS_CONNECTED);
+ p_conn_sec_status->bonded = (peer_id != PM_PEER_ID_INVALID);
+ p_conn_sec_status->encrypted = ble_conn_state_encrypted(conn_handle);
+ p_conn_sec_status->mitm_protected = ble_conn_state_mitm_protected(conn_handle);
+ p_conn_sec_status->lesc = ble_conn_state_lesc(conn_handle) ||
+ (ble_conn_state_encrypted(conn_handle) && key_is_lesc(peer_id));
+ return NRF_SUCCESS;
+}
+
+bool sm_sec_is_sufficient(uint16_t conn_handle, pm_conn_sec_status_t *p_sec_status_req)
+{
+ /* Set all bits in reserved to 1 so they are ignored in subsequent logic. */
+ pm_conn_sec_status_t sec_status = {.reserved = ~0};
+ uint32_t err_code = sm_conn_sec_status_get(conn_handle, &sec_status);
+
+ __ASSERT(sizeof(pm_conn_sec_status_t) == sizeof(uint8_t), "");
+
+ uint8_t unmet_reqs = (~(*((uint8_t *)&sec_status)) & *((uint8_t *)p_sec_status_req));
+
+ return (err_code == NRF_SUCCESS) && !unmet_reqs;
+}
+
+#ifdef CONFIG_SOFTDEVICE_CENTRAL
+/**
+ * @brief Function for handling @ref PM_EVT_SLAVE_SECURITY_REQ events.
+ *
+ * @param[in] p_event The @ref PM_EVT_SLAVE_SECURITY_REQ event.
+ */
+static void sec_req_process(pm_evt_t const *p_event)
+{
+ uint32_t err_code;
+ bool null_params = false;
+ bool force_repairing = false;
+
+ if (mp_sec_params == NULL) {
+ null_params = true;
+ } else if (ble_conn_state_encrypted(p_event->conn_handle)) {
+ pm_conn_sec_status_t sec_status_req = {
+ .bonded = p_event->params.slave_security_req.bond,
+ .mitm_protected = p_event->params.slave_security_req.mitm,
+ .lesc = p_event->params.slave_security_req.lesc,
+ };
+
+ force_repairing = !sm_sec_is_sufficient(p_event->conn_handle, &sec_status_req);
+ }
+
+ err_code = link_secure(p_event->conn_handle, null_params, force_repairing, true);
+ /* The error code has been properly handled inside link_secure(). */
+ UNUSED_VARIABLE(err_code);
+}
+#endif
+
+/**
+ * @brief Function for translating an SMD event to an SM event and passing it on to SM event
+ * handlers.
+ *
+ * @param[in] p_event The event to forward.
+ */
+static void evt_forward(pm_evt_t *p_event)
+{
+ evt_send(p_event);
+}
+
+/**
+ * @brief Event handler for events from the Security Dispatcher module.
+ * This handler is extern in Security Dispatcher.
+ *
+ * @param[in] p_event The event that has happened.
+ */
+void sm_smd_evt_handler(pm_evt_t *p_event)
+{
+ switch (p_event->evt_id) {
+ case PM_EVT_CONN_SEC_PARAMS_REQ:
+ params_req_process(p_event);
+ break;
+ case PM_EVT_SLAVE_SECURITY_REQ:
+#ifdef CONFIG_SOFTDEVICE_CENTRAL
+ sec_req_process(p_event);
+#endif
+ /* fallthrough */
+ default:
+ /* Forward the event to all registered Security Manager event handlers. */
+ evt_forward(p_event);
+ break;
+ }
+}
+
+/** @brief Function handling a pending params_reply. See @ref ble_conn_state_user_function_t. */
+static void params_reply_pending_handle(uint16_t conn_handle, void *p_context)
+{
+ UNUSED_PARAMETER(p_context);
+ smd_params_reply_perform(conn_handle, NULL);
+}
+
+/** @brief Function handling a pending link_secure. See @ref ble_conn_state_user_function_t. */
+static void link_secure_pending_handle(uint16_t conn_handle, void *p_context)
+{
+ UNUSED_PARAMETER(p_context);
+
+ bool force_repairing =
+ ble_conn_state_user_flag_get(conn_handle, m_flag_link_secure_force_repairing);
+ bool null_params =
+ ble_conn_state_user_flag_get(conn_handle, m_flag_link_secure_null_params);
+
+ /* If this fails, it will be automatically retried. */
+ uint32_t err_code = link_secure(conn_handle, null_params, force_repairing, true);
+
+ UNUSED_VARIABLE(err_code);
+}
+
+/**
+ * @brief Event handler for events from the Peer Database module.
+ * This handler is extern in Peer Database.
+ *
+ * @param[in] p_event The event that has happened.
+ */
+void sm_pdb_evt_handler(pm_evt_t *p_event)
+{
+ switch (p_event->evt_id) {
+ case PM_EVT_FLASH_GARBAGE_COLLECTED:
+ case PM_EVT_PEER_DATA_UPDATE_SUCCEEDED:
+ case PM_EVT_PEER_DATA_UPDATE_FAILED:
+ case PM_EVT_PEER_DELETE_SUCCEEDED:
+ case PM_EVT_PEER_DELETE_FAILED:
+ (void)ble_conn_state_for_each_set_user_flag(m_flag_params_reply_pending_busy,
+ params_reply_pending_handle, NULL);
+ (void)ble_conn_state_for_each_set_user_flag(m_flag_link_secure_pending_busy,
+ link_secure_pending_handle, NULL);
+ break;
+ default:
+ /* Do nothing. */
+ break;
+ }
+}
+
+/**
+ * @brief Function for initializing a BLE Connection State user flag.
+ *
+ * @param[out] flag_id The flag to initialize.
+ */
+static void flag_id_init(int *p_flag_id)
+{
+ if (*p_flag_id == CONFIG_BLE_CONN_STATE_USER_FLAG_COUNT) {
+ *p_flag_id = ble_conn_state_user_flag_acquire();
+ }
+}
+
+uint32_t sm_init(void)
+{
+ NRF_PM_DEBUG_CHECK(!m_module_initialized);
+
+#if CONFIG_PM_LESC_ENABLED
+ uint32_t err_code = nrf_ble_lesc_init();
+
+ if (err_code != NRF_SUCCESS) {
+ return err_code;
+ }
+#endif
+
+ flag_id_init(&m_flag_link_secure_pending_busy);
+ flag_id_init(&m_flag_link_secure_force_repairing);
+ flag_id_init(&m_flag_link_secure_null_params);
+ flag_id_init(&m_flag_params_reply_pending_busy);
+
+ if (m_flag_params_reply_pending_busy == CONFIG_BLE_CONN_STATE_USER_FLAG_COUNT) {
+ LOG_ERR("Could not acquire conn_state user flags. Increase "
+ "BLE_CONN_STATE_USER_FLAG_COUNT in the ble_conn_state module.");
+ return NRF_ERROR_INTERNAL;
+ }
+
+ m_module_initialized = true;
+
+ return NRF_SUCCESS;
+}
+
+void sm_ble_evt_handler(ble_evt_t const *p_ble_evt)
+{
+ NRF_PM_DEBUG_CHECK(p_ble_evt != NULL);
+
+ smd_ble_evt_handler(p_ble_evt);
+#if CONFIG_PM_LESC_ENABLED
+ nrf_ble_lesc_on_ble_evt(p_ble_evt);
+#endif
+ (void)ble_conn_state_for_each_set_user_flag(m_flag_params_reply_pending_busy,
+ params_reply_pending_handle, NULL);
+ (void)ble_conn_state_for_each_set_user_flag(m_flag_link_secure_pending_busy,
+ link_secure_pending_handle, NULL);
+}
+
+/**
+ * @brief Function for checking whether security parameters are valid.
+ *
+ * @param[out] p_sec_params The security parameters to verify.
+ *
+ * @return Whether the security parameters are valid.
+ */
+static bool sec_params_verify(ble_gap_sec_params_t *p_sec_params)
+{
+ /* NULL check. */
+ if (p_sec_params == NULL) {
+ return false;
+ }
+
+ /* OOB not allowed unless MITM. */
+ if (!p_sec_params->mitm && p_sec_params->oob) {
+ return false;
+ }
+
+ /* IO Capabilities must be one of the valid values from @ref BLE_GAP_IO_CAPS. */
+ if (p_sec_params->io_caps > BLE_GAP_IO_CAPS_KEYBOARD_DISPLAY) {
+ return false;
+ }
+
+ /* Must have either IO capabilities or OOB if MITM. */
+ if (p_sec_params->mitm && (p_sec_params->io_caps == BLE_GAP_IO_CAPS_NONE) &&
+ !p_sec_params->oob) {
+ return false;
+ }
+
+ /* Minimum key size cannot be larger than maximum key size. */
+ if (p_sec_params->min_key_size > p_sec_params->max_key_size) {
+ return false;
+ }
+
+ /* Key size cannot be below 7 bytes. */
+ if (p_sec_params->min_key_size < 7) {
+ return false;
+ }
+
+ /* Key size cannot be above 16 bytes. */
+ if (p_sec_params->max_key_size > 16) {
+ return false;
+ }
+
+ /* Signing is not supported. */
+ if (p_sec_params->kdist_own.sign || p_sec_params->kdist_peer.sign) {
+ return false;
+ }
+
+ /* link bit must be 0. */
+ if (p_sec_params->kdist_own.link || p_sec_params->kdist_peer.link) {
+ return false;
+ }
+
+ /* If bonding is not enabled, no keys can be distributed. */
+ if (!p_sec_params->bond && (p_sec_params->kdist_own.enc || p_sec_params->kdist_own.id ||
+ p_sec_params->kdist_peer.enc || p_sec_params->kdist_peer.id)) {
+ return false;
+ }
+
+ /* If bonding is enabled, one or more keys must be distributed. */
+ if (p_sec_params->bond && !p_sec_params->kdist_own.enc && !p_sec_params->kdist_own.id &&
+ !p_sec_params->kdist_peer.enc && !p_sec_params->kdist_peer.id) {
+ return false;
+ }
+
+ return true;
+}
+
+uint32_t sm_sec_params_set(ble_gap_sec_params_t *p_sec_params)
+{
+ NRF_PM_DEBUG_CHECK(m_module_initialized);
+
+ if (p_sec_params == NULL) {
+ mp_sec_params = NULL;
+ m_sec_params_set = true;
+ return NRF_SUCCESS;
+ } else if (sec_params_verify(p_sec_params)) {
+ m_sec_params = *p_sec_params;
+ mp_sec_params = &m_sec_params;
+ m_sec_params_set = true;
+ return NRF_SUCCESS;
+ } else {
+ return NRF_ERROR_INVALID_PARAM;
+ }
+}
+
+void sm_conn_sec_config_reply(uint16_t conn_handle, pm_conn_sec_config_t *p_conn_sec_config)
+{
+ NRF_PM_DEBUG_CHECK(m_module_initialized);
+ NRF_PM_DEBUG_CHECK(p_conn_sec_config != NULL);
+
+ smd_conn_sec_config_reply(conn_handle, p_conn_sec_config);
+}
+
+uint32_t sm_sec_params_reply(uint16_t conn_handle, ble_gap_sec_params_t *p_sec_params,
+ void const *p_context)
+{
+ NRF_PM_DEBUG_CHECK(m_module_initialized);
+ VERIFY_PARAM_NOT_NULL(p_context);
+
+ sec_params_reply_context_t *p_sec_params_reply_context =
+ (sec_params_reply_context_t *)p_context;
+ if (p_sec_params == NULL) {
+ /* Set the store pointer to NULL, so that NULL is passed to the SoftDevice. */
+ p_sec_params_reply_context->p_sec_params = NULL;
+ } else if (sec_params_verify(p_sec_params)) {
+ /* Copy the provided sec_params into the store. */
+ p_sec_params_reply_context->sec_params_mem = *p_sec_params;
+ p_sec_params_reply_context->p_sec_params =
+ &p_sec_params_reply_context->sec_params_mem;
+ } else {
+ return NRF_ERROR_INVALID_PARAM;
+ }
+ p_sec_params_reply_context->params_reply_called = true;
+
+ return NRF_SUCCESS;
+}
+
+uint32_t sm_lesc_public_key_set(ble_gap_lesc_p256_pk_t *p_public_key)
+{
+ NRF_PM_DEBUG_CHECK(m_module_initialized);
+
+#if CONFIG_PM_LESC_ENABLED
+ return NRF_ERROR_FORBIDDEN;
+#else
+ m_p_public_key = p_public_key;
+
+ return NRF_SUCCESS;
+#endif /* CONFIG_PM_LESC_ENABLED */
+}
+
+uint32_t sm_link_secure(uint16_t conn_handle, bool force_repairing)
+{
+ uint32_t ret;
+
+ NRF_PM_DEBUG_CHECK(m_module_initialized);
+
+ ret = link_secure(conn_handle, false, force_repairing, false);
+ return ret;
+}
diff --git a/lib/peer_manager/nrf_strerror.c b/lib/peer_manager/nrf_strerror.c
new file mode 100644
index 0000000000..19cc67b131
--- /dev/null
+++ b/lib/peer_manager/nrf_strerror.c
@@ -0,0 +1,93 @@
+/*
+ * Copyright (c) 2011-2025 Nordic Semiconductor ASA
+ *
+ * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
+ */
+
+#include
+#include
+#include
+#include
+#include
+
+/**
+ * @brief Macro for adding an entity to the description array.
+ *
+ * Macro that helps to create a single entity in the description array.
+ */
+#define NRF_STRERROR_ENTITY(mnemonic) \
+ { \
+ .code = mnemonic, .name = #mnemonic \
+ }
+
+/**
+ * @brief Array entity element that describes an error.
+ */
+typedef struct {
+ uint32_t code; /**< Error code. */
+ char const *name; /**< Descriptive name (the same as the internal error mnemonic). */
+} nrf_strerror_desc_t;
+
+/**
+ * @brief Unknown error code.
+ *
+ * The constant string used by @ref nrf_strerror_get when the error description was not found.
+ */
+static char const m_unknown_str[] = "Unknown error code";
+
+/**
+ * @brief Array with error codes.
+ *
+ * Array that describes error codes.
+ *
+ * @note It is required for this array to have error codes placed in ascending order.
+ * This condition is checked in automatic unit test before the release.
+ */
+static nrf_strerror_desc_t const nrf_strerror_array[] = {
+ NRF_STRERROR_ENTITY(NRF_SUCCESS), NRF_STRERROR_ENTITY(NRF_ERROR_SVC_HANDLER_MISSING),
+ NRF_STRERROR_ENTITY(NRF_ERROR_SOFTDEVICE_NOT_ENABLED),
+ NRF_STRERROR_ENTITY(NRF_ERROR_INTERNAL), NRF_STRERROR_ENTITY(NRF_ERROR_NO_MEM),
+ NRF_STRERROR_ENTITY(NRF_ERROR_NOT_FOUND), NRF_STRERROR_ENTITY(NRF_ERROR_NOT_SUPPORTED),
+ NRF_STRERROR_ENTITY(NRF_ERROR_INVALID_PARAM), NRF_STRERROR_ENTITY(NRF_ERROR_INVALID_STATE),
+ NRF_STRERROR_ENTITY(NRF_ERROR_INVALID_LENGTH), NRF_STRERROR_ENTITY(NRF_ERROR_INVALID_FLAGS),
+ NRF_STRERROR_ENTITY(NRF_ERROR_INVALID_DATA), NRF_STRERROR_ENTITY(NRF_ERROR_DATA_SIZE),
+ NRF_STRERROR_ENTITY(NRF_ERROR_TIMEOUT), NRF_STRERROR_ENTITY(NRF_ERROR_NULL),
+ NRF_STRERROR_ENTITY(NRF_ERROR_FORBIDDEN), NRF_STRERROR_ENTITY(NRF_ERROR_INVALID_ADDR),
+ NRF_STRERROR_ENTITY(NRF_ERROR_BUSY),
+#ifdef NRF_ERROR_CONN_COUNT
+ NRF_STRERROR_ENTITY(NRF_ERROR_CONN_COUNT),
+#endif
+#ifdef NRF_ERROR_RESOURCES
+ NRF_STRERROR_ENTITY(NRF_ERROR_RESOURCES),
+#endif
+};
+
+char const *nrf_strerror_get(uint32_t code)
+{
+ char const *p_ret = nrf_strerror_find(code);
+
+ return (p_ret == NULL) ? m_unknown_str : p_ret;
+}
+
+char const *nrf_strerror_find(uint32_t code)
+{
+ nrf_strerror_desc_t const *p_start;
+ nrf_strerror_desc_t const *p_end;
+
+ p_start = nrf_strerror_array;
+ p_end = nrf_strerror_array + ARRAY_SIZE(nrf_strerror_array);
+
+ while (p_start < p_end) {
+ nrf_strerror_desc_t const *p_mid = p_start + ((p_end - p_start) / 2);
+ uint32_t mid_c = p_mid->code;
+
+ if (mid_c > code) {
+ p_end = p_mid;
+ } else if (mid_c < code) {
+ p_start = p_mid + 1;
+ } else {
+ return p_mid->name;
+ }
+ }
+ return NULL;
+}
diff --git a/lib/peer_manager/peer_manager.c b/lib/peer_manager/peer_manager.c
new file mode 100644
index 0000000000..4b29d1622c
--- /dev/null
+++ b/lib/peer_manager/peer_manager.c
@@ -0,0 +1,1052 @@
+/*
+ * Copyright (c) 2015-2025 Nordic Semiconductor ASA
+ *
+ * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
+ */
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+LOG_MODULE_REGISTER(peer_manager, CONFIG_PEER_MANAGER_LOG_LEVEL);
+
+/** Macro indicating whether the module has been initialized properly. */
+#define MODULE_INITIALIZED (m_module_initialized)
+#define VERIFY_MODULE_INITIALIZED() VERIFY_TRUE((MODULE_INITIALIZED), NRF_ERROR_INVALID_STATE)
+
+/** Whether or not @ref pm_init has been called successfully. */
+static bool m_module_initialized;
+/** Whether or not @ref rank_init has been called successfully. */
+static bool m_peer_rank_initialized;
+/** True from when @ref pm_peers_delete is called until all peers have been deleted. */
+static bool m_deleting_all;
+/** The store token of an ongoing peer rank update via a call to @ref pm_peer_rank_highest. If @ref
+ * PM_STORE_TOKEN_INVALID, there is no ongoing update.
+ */
+static pm_store_token_t m_peer_rank_token;
+/** The current highest peer rank. Used by @ref pm_peer_rank_highest. */
+static uint32_t m_current_highest_peer_rank;
+/** The peer with the highest peer rank. Used by @ref pm_peer_rank_highest. */
+static pm_peer_id_t m_highest_ranked_peer;
+/** The subscribers to Peer Manager events, as registered through @ref pm_register. */
+static pm_evt_handler_t m_evt_handlers[CONFIG_PM_MAX_REGISTRANTS];
+/** The number of event handlers registered through @ref pm_register. */
+static uint8_t m_n_registrants;
+
+/** User flag indicating whether a connection is excluded from being handled by the Peer Manager. */
+static int m_flag_conn_excluded = CONFIG_BLE_CONN_STATE_USER_FLAG_COUNT;
+
+/**
+ * @brief Function for sending a Peer Manager event to all subscribers.
+ *
+ * @param[in] p_pm_evt The event to send.
+ */
+static void evt_send(pm_evt_t const *p_pm_evt)
+{
+ for (int i = 0; i < m_n_registrants; i++) {
+ m_evt_handlers[i](p_pm_evt);
+ }
+}
+
+#if CONFIG_PM_PEER_RANKS_ENABLED == 1
+/** @brief Function for initializing peer rank static variables. */
+static void rank_vars_update(void)
+{
+ uint32_t err_code =
+ pm_peer_ranks_get(&m_highest_ranked_peer, &m_current_highest_peer_rank, NULL, NULL);
+
+ if (err_code == NRF_ERROR_NOT_FOUND) {
+ m_highest_ranked_peer = PM_PEER_ID_INVALID;
+ m_current_highest_peer_rank = 0;
+ }
+
+ m_peer_rank_initialized = ((err_code == NRF_SUCCESS) || (err_code == NRF_ERROR_NOT_FOUND));
+}
+#endif
+
+/**
+ * @brief Event handler for events from the Peer Database module.
+ * This handler is extern in the Peer Database module.
+ *
+ * @param[in] p_pdb_evt The incoming Peer Database event.
+ */
+void pm_pdb_evt_handler(pm_evt_t *p_pdb_evt)
+{
+ bool send_evt = true;
+
+ p_pdb_evt->conn_handle = im_conn_handle_get(p_pdb_evt->peer_id);
+
+ switch (p_pdb_evt->evt_id) {
+#if CONFIG_PM_PEER_RANKS_ENABLED == 1
+ case PM_EVT_PEER_DATA_UPDATE_SUCCEEDED:
+ if (p_pdb_evt->params.peer_data_update_succeeded.action == PM_PEER_DATA_OP_UPDATE) {
+ if ((m_peer_rank_token != PM_STORE_TOKEN_INVALID) &&
+ (m_peer_rank_token ==
+ p_pdb_evt->params.peer_data_update_succeeded.token)) {
+ m_peer_rank_token = PM_STORE_TOKEN_INVALID;
+ m_highest_ranked_peer = p_pdb_evt->peer_id;
+
+ p_pdb_evt->params.peer_data_update_succeeded.token =
+ PM_STORE_TOKEN_INVALID;
+ } else if (m_peer_rank_initialized &&
+ (p_pdb_evt->peer_id == m_highest_ranked_peer) &&
+ (p_pdb_evt->params.peer_data_update_succeeded.data_id ==
+ PM_PEER_DATA_ID_PEER_RANK)) {
+ /* Update peer rank variable if highest ranked peer has changed its
+ * rank.
+ */
+ rank_vars_update();
+ }
+ } else if (p_pdb_evt->params.peer_data_update_succeeded.action ==
+ PM_PEER_DATA_OP_DELETE) {
+ if (m_peer_rank_initialized &&
+ (p_pdb_evt->peer_id == m_highest_ranked_peer) &&
+ (p_pdb_evt->params.peer_data_update_succeeded.data_id ==
+ PM_PEER_DATA_ID_PEER_RANK)) {
+ /* Update peer rank variable if highest ranked peer has deleted its
+ * rank.
+ */
+ rank_vars_update();
+ }
+ }
+ break;
+
+ case PM_EVT_PEER_DATA_UPDATE_FAILED:
+ if (p_pdb_evt->params.peer_data_update_succeeded.action == PM_PEER_DATA_OP_UPDATE) {
+ if ((m_peer_rank_token != PM_STORE_TOKEN_INVALID) &&
+ (m_peer_rank_token ==
+ p_pdb_evt->params.peer_data_update_failed.token)) {
+ m_peer_rank_token = PM_STORE_TOKEN_INVALID;
+ m_current_highest_peer_rank -= 1;
+
+ p_pdb_evt->params.peer_data_update_succeeded.token =
+ PM_STORE_TOKEN_INVALID;
+ }
+ }
+ break;
+#endif
+
+ case PM_EVT_PEER_DELETE_SUCCEEDED:
+ /* Check that no peers marked for deletion are left. */
+ if (m_deleting_all &&
+ (pds_next_peer_id_get(PM_PEER_ID_INVALID) == PM_PEER_ID_INVALID) &&
+ (pds_next_deleted_peer_id_get(PM_PEER_ID_INVALID) == PM_PEER_ID_INVALID)) {
+ /* pm_peers_delete() has been called and this is the last peer to be
+ * deleted.
+ */
+ m_deleting_all = false;
+
+ pm_evt_t pm_delete_all_evt;
+
+ memset(&pm_delete_all_evt, 0, sizeof(pm_evt_t));
+ pm_delete_all_evt.evt_id = PM_EVT_PEERS_DELETE_SUCCEEDED;
+ pm_delete_all_evt.peer_id = PM_PEER_ID_INVALID;
+ pm_delete_all_evt.conn_handle = BLE_CONN_HANDLE_INVALID;
+
+ send_evt = false;
+
+ /* Forward the event to all registered Peer Manager event handlers.
+ * Ensure that PEER_DELETE_SUCCEEDED arrives before PEERS_DELETE_SUCCEEDED.
+ */
+ evt_send(p_pdb_evt);
+ evt_send(&pm_delete_all_evt);
+ }
+
+#if CONFIG_PM_PEER_RANKS_ENABLED == 1
+ if (m_peer_rank_initialized && (p_pdb_evt->peer_id == m_highest_ranked_peer)) {
+ /* Update peer rank variable if highest ranked peer has been deleted. */
+ rank_vars_update();
+ }
+#endif
+ break;
+
+ case PM_EVT_PEER_DELETE_FAILED:
+ if (m_deleting_all) {
+ /* pm_peers_delete() was called and has thus failed. */
+
+ m_deleting_all = false;
+
+ pm_evt_t pm_delete_all_evt;
+
+ memset(&pm_delete_all_evt, 0, sizeof(pm_evt_t));
+ pm_delete_all_evt.evt_id = PM_EVT_PEERS_DELETE_FAILED;
+ pm_delete_all_evt.peer_id = PM_PEER_ID_INVALID;
+ pm_delete_all_evt.conn_handle = BLE_CONN_HANDLE_INVALID;
+ pm_delete_all_evt.params.peers_delete_failed_evt.error =
+ p_pdb_evt->params.peer_delete_failed.error;
+
+ send_evt = false;
+
+ /* Forward the event to all registered Peer Manager event handlers.
+ * Ensure that PEER_DELETE_FAILED arrives before PEERS_DELETE_FAILED.
+ */
+ evt_send(p_pdb_evt);
+ evt_send(&pm_delete_all_evt);
+ }
+ break;
+
+ default:
+ /* Do nothing. */
+ break;
+ }
+
+ if (send_evt) {
+ /* Forward the event to all registered Peer Manager event handlers. */
+ evt_send(p_pdb_evt);
+ }
+}
+
+/**
+ * @brief Event handler for events from the Security Manager module.
+ * This handler is extern in the Security Manager module.
+ *
+ * @param[in] p_sm_evt The incoming Security Manager event.
+ */
+void pm_sm_evt_handler(pm_evt_t *p_sm_evt)
+{
+ VERIFY_PARAM_NOT_NULL_VOID(p_sm_evt);
+
+ /* Forward the event to all registered Peer Manager event handlers. */
+ evt_send(p_sm_evt);
+}
+
+/**
+ * @brief Event handler for events from the GATT Cache Manager module.
+ * This handler is extern in GATT Cache Manager.
+ *
+ * @param[in] p_gcm_evt The incoming GATT Cache Manager event.
+ */
+void pm_gcm_evt_handler(pm_evt_t *p_gcm_evt)
+{
+ /* Forward the event to all registered Peer Manager event handlers. */
+ evt_send(p_gcm_evt);
+}
+
+/**
+ * @brief Event handler for events from the GATTS Cache Manager module.
+ * This handler is extern in GATTS Cache Manager.
+ *
+ * @param[in] p_gscm_evt The incoming GATTS Cache Manager event.
+ */
+void pm_gscm_evt_handler(pm_evt_t *p_gscm_evt)
+{
+ /* Forward the event to all registered Peer Manager event handlers. */
+ evt_send(p_gscm_evt);
+}
+
+/**
+ * @brief Event handler for events from the ID Manager module.
+ * This function is registered in the ID Manager.
+ *
+ * @param[in] p_im_evt The incoming ID Manager event.
+ */
+void pm_im_evt_handler(pm_evt_t *p_im_evt)
+{
+ /* Forward the event to all registered Peer Manager event handlers. */
+ evt_send(p_im_evt);
+}
+
+static bool is_conn_handle_excluded(ble_evt_t const *p_ble_evt)
+{
+ uint16_t conn_handle = p_ble_evt->evt.gap_evt.conn_handle;
+
+ switch (p_ble_evt->header.evt_id) {
+ case BLE_GAP_EVT_CONNECTED: {
+ pm_evt_t pm_conn_config_req_evt;
+ bool is_excluded = false;
+
+ memset(&pm_conn_config_req_evt, 0, sizeof(pm_evt_t));
+ pm_conn_config_req_evt.evt_id = PM_EVT_CONN_CONFIG_REQ;
+ pm_conn_config_req_evt.peer_id = PM_PEER_ID_INVALID;
+ pm_conn_config_req_evt.conn_handle = conn_handle;
+
+ pm_conn_config_req_evt.params.conn_config_req.p_peer_params =
+ &p_ble_evt->evt.gap_evt.params.connected;
+ pm_conn_config_req_evt.params.conn_config_req.p_context = &is_excluded;
+
+ evt_send(&pm_conn_config_req_evt);
+ ble_conn_state_user_flag_set(conn_handle, m_flag_conn_excluded, is_excluded);
+
+ return is_excluded;
+ }
+
+ default:
+ return ble_conn_state_user_flag_get(conn_handle, m_flag_conn_excluded);
+ }
+}
+
+/**
+ * @brief Function for handling BLE events.
+ *
+ * @param[in] p_ble_evt Event received from the BLE stack.
+ * @param[in] p_context Context.
+ */
+static void ble_evt_handler(ble_evt_t const *p_ble_evt, void *p_context)
+{
+ VERIFY_MODULE_INITIALIZED_VOID();
+
+ if (is_conn_handle_excluded(p_ble_evt)) {
+ LOG_DBG("Filtering BLE event with ID: 0x%04X targeting 0x%04X connection handle",
+ p_ble_evt->header.evt_id, p_ble_evt->evt.gap_evt.conn_handle);
+ return;
+ }
+
+ im_ble_evt_handler(p_ble_evt);
+ sm_ble_evt_handler(p_ble_evt);
+ gcm_ble_evt_handler(p_ble_evt);
+}
+
+NRF_SDH_BLE_OBSERVER(m_ble_evt_observer, ble_evt_handler, NULL, CONFIG_PM_BLE_OBSERVER_PRIO);
+
+/** @brief Function for resetting the internal state of this module. */
+static void internal_state_reset(void)
+{
+ m_highest_ranked_peer = PM_PEER_ID_INVALID;
+ m_peer_rank_token = PM_STORE_TOKEN_INVALID;
+}
+
+uint32_t pm_init(void)
+{
+ uint32_t err_code;
+
+ err_code = pds_init();
+ if (err_code != NRF_SUCCESS) {
+ LOG_ERR("%s failed because pds_init() returned %s.", __func__,
+ nrf_strerror_get(err_code));
+ return NRF_ERROR_INTERNAL;
+ }
+
+ err_code = pdb_init();
+ if (err_code != NRF_SUCCESS) {
+ LOG_ERR("%s failed because pdb_init() returned %s.", __func__,
+ nrf_strerror_get(err_code));
+ return NRF_ERROR_INTERNAL;
+ }
+
+ err_code = sm_init();
+ if (err_code != NRF_SUCCESS) {
+ LOG_ERR("%s failed because sm_init() returned %s.", __func__,
+ nrf_strerror_get(err_code));
+ return NRF_ERROR_INTERNAL;
+ }
+
+ err_code = smd_init();
+ if (err_code != NRF_SUCCESS) {
+ LOG_ERR("%s failed because smd_init() returned %s.", __func__,
+ nrf_strerror_get(err_code));
+ return NRF_ERROR_INTERNAL;
+ }
+
+ err_code = gcm_init();
+ if (err_code != NRF_SUCCESS) {
+ LOG_ERR("%s failed because gcm_init() returned %s.", __func__,
+ nrf_strerror_get(err_code));
+ return NRF_ERROR_INTERNAL;
+ }
+
+ err_code = gscm_init();
+ if (err_code != NRF_SUCCESS) {
+ LOG_ERR("%s failed because gscm_init() returned %s.", __func__,
+ nrf_strerror_get(err_code));
+ return NRF_ERROR_INTERNAL;
+ }
+
+ internal_state_reset();
+
+ m_peer_rank_initialized = false;
+ m_module_initialized = true;
+
+ m_flag_conn_excluded = ble_conn_state_user_flag_acquire();
+
+ /* If CONFIG_PM_PEER_RANKS_ENABLED is 0, these variables are unused. */
+ UNUSED_VARIABLE(m_peer_rank_initialized);
+ UNUSED_VARIABLE(m_peer_rank_token);
+ UNUSED_VARIABLE(m_current_highest_peer_rank);
+ UNUSED_VARIABLE(m_highest_ranked_peer);
+
+ return NRF_SUCCESS;
+}
+
+uint32_t pm_register(pm_evt_handler_t event_handler)
+{
+ VERIFY_MODULE_INITIALIZED();
+
+ if (m_n_registrants >= CONFIG_PM_MAX_REGISTRANTS) {
+ return NRF_ERROR_NO_MEM;
+ }
+
+ m_evt_handlers[m_n_registrants] = event_handler;
+ m_n_registrants += 1;
+
+ return NRF_SUCCESS;
+}
+
+uint32_t pm_sec_params_set(ble_gap_sec_params_t *p_sec_params)
+{
+ VERIFY_MODULE_INITIALIZED();
+
+ uint32_t err_code;
+
+ err_code = sm_sec_params_set(p_sec_params);
+
+ /* NRF_ERROR_INVALID_PARAM if parameters are invalid,
+ * NRF_SUCCESS otherwise.
+ */
+ return err_code;
+}
+
+uint32_t pm_conn_secure(uint16_t conn_handle, bool force_repairing)
+{
+ VERIFY_MODULE_INITIALIZED();
+
+ uint32_t err_code;
+
+ err_code = sm_link_secure(conn_handle, force_repairing);
+
+ if (err_code == NRF_ERROR_INVALID_STATE) {
+ err_code = NRF_ERROR_BUSY;
+ }
+
+ return err_code;
+}
+
+uint32_t pm_conn_exclude(uint16_t conn_handle, void const *p_context)
+{
+ VERIFY_PARAM_NOT_NULL(p_context);
+
+ bool *p_is_conn_excluded = (bool *)p_context;
+
+ *p_is_conn_excluded = true;
+
+ return NRF_SUCCESS;
+}
+
+void pm_conn_sec_config_reply(uint16_t conn_handle, pm_conn_sec_config_t *p_conn_sec_config)
+{
+ if (p_conn_sec_config != NULL) {
+ sm_conn_sec_config_reply(conn_handle, p_conn_sec_config);
+ }
+}
+
+uint32_t pm_conn_sec_params_reply(uint16_t conn_handle, ble_gap_sec_params_t *p_sec_params,
+ void const *p_context)
+{
+ VERIFY_MODULE_INITIALIZED();
+
+ return sm_sec_params_reply(conn_handle, p_sec_params, p_context);
+}
+
+void pm_local_database_has_changed(void)
+{
+#if !defined(CONFIG_PM_SERVICE_CHANGED_ENABLED) || (CONFIG_PM_SERVICE_CHANGED_ENABLED == 1)
+ VERIFY_MODULE_INITIALIZED_VOID();
+
+ gcm_local_database_has_changed();
+#endif
+}
+
+uint32_t pm_id_addr_set(ble_gap_addr_t const *p_addr)
+{
+ VERIFY_MODULE_INITIALIZED();
+ return im_id_addr_set(p_addr);
+}
+
+uint32_t pm_id_addr_get(ble_gap_addr_t *p_addr)
+{
+ VERIFY_MODULE_INITIALIZED();
+ VERIFY_PARAM_NOT_NULL(p_addr);
+ return im_id_addr_get(p_addr);
+}
+
+uint32_t pm_privacy_set(pm_privacy_params_t const *p_privacy_params)
+{
+ VERIFY_MODULE_INITIALIZED();
+ VERIFY_PARAM_NOT_NULL(p_privacy_params);
+ return im_privacy_set(p_privacy_params);
+}
+
+uint32_t pm_privacy_get(pm_privacy_params_t *p_privacy_params)
+{
+ VERIFY_MODULE_INITIALIZED();
+ VERIFY_PARAM_NOT_NULL(p_privacy_params);
+ VERIFY_PARAM_NOT_NULL(p_privacy_params->p_device_irk);
+ return im_privacy_get(p_privacy_params);
+}
+
+bool pm_address_resolve(ble_gap_addr_t const *p_addr, ble_gap_irk_t const *p_irk)
+{
+ VERIFY_MODULE_INITIALIZED();
+
+ if ((p_addr == NULL) || (p_irk == NULL)) {
+ return false;
+ } else {
+ return im_address_resolve(p_addr, p_irk);
+ }
+}
+
+uint32_t pm_whitelist_set(pm_peer_id_t const *p_peers, uint32_t peer_cnt)
+{
+ VERIFY_MODULE_INITIALIZED();
+ return im_whitelist_set(p_peers, peer_cnt);
+}
+
+uint32_t pm_whitelist_get(ble_gap_addr_t *p_addrs, uint32_t *p_addr_cnt, ble_gap_irk_t *p_irks,
+ uint32_t *p_irk_cnt)
+{
+ VERIFY_MODULE_INITIALIZED();
+
+ if (((p_addrs == NULL) && (p_irks == NULL)) ||
+ ((p_addrs != NULL) && (p_addr_cnt == NULL)) ||
+ ((p_irks != NULL) && (p_irk_cnt == NULL))) {
+ /* The buffers can't be both NULL, and if a buffer is provided its size must be
+ * specified.
+ */
+ return NRF_ERROR_NULL;
+ }
+
+ return im_whitelist_get(p_addrs, p_addr_cnt, p_irks, p_irk_cnt);
+}
+
+uint32_t pm_device_identities_list_set(pm_peer_id_t const *p_peers, uint32_t peer_cnt)
+{
+ VERIFY_MODULE_INITIALIZED();
+ return im_device_identities_list_set(p_peers, peer_cnt);
+}
+
+uint32_t pm_conn_sec_status_get(uint16_t conn_handle, pm_conn_sec_status_t *p_conn_sec_status)
+{
+ VERIFY_MODULE_INITIALIZED();
+ return sm_conn_sec_status_get(conn_handle, p_conn_sec_status);
+}
+
+bool pm_sec_is_sufficient(uint16_t conn_handle, pm_conn_sec_status_t *p_sec_status_req)
+{
+ VERIFY_MODULE_INITIALIZED_BOOL();
+ return sm_sec_is_sufficient(conn_handle, p_sec_status_req);
+}
+
+uint32_t pm_lesc_public_key_set(ble_gap_lesc_p256_pk_t *p_public_key)
+{
+ VERIFY_MODULE_INITIALIZED();
+ return sm_lesc_public_key_set(p_public_key);
+}
+
+uint32_t pm_conn_handle_get(pm_peer_id_t peer_id, uint16_t *p_conn_handle)
+{
+ VERIFY_MODULE_INITIALIZED();
+ VERIFY_PARAM_NOT_NULL(p_conn_handle);
+ *p_conn_handle = im_conn_handle_get(peer_id);
+ return NRF_SUCCESS;
+}
+
+uint32_t pm_peer_id_get(uint16_t conn_handle, pm_peer_id_t *p_peer_id)
+{
+ VERIFY_MODULE_INITIALIZED();
+ VERIFY_PARAM_NOT_NULL(p_peer_id);
+ *p_peer_id = im_peer_id_get_by_conn_handle(conn_handle);
+ return NRF_SUCCESS;
+}
+
+uint32_t pm_peer_count(void)
+{
+ if (!MODULE_INITIALIZED) {
+ return 0;
+ }
+ return pds_peer_count_get();
+}
+
+pm_peer_id_t pm_next_peer_id_get(pm_peer_id_t prev_peer_id)
+{
+ pm_peer_id_t next_peer_id = prev_peer_id;
+
+ if (!MODULE_INITIALIZED) {
+ return PM_PEER_ID_INVALID;
+ }
+
+ do {
+ next_peer_id = pds_next_peer_id_get(next_peer_id);
+ } while (pds_peer_id_is_deleted(next_peer_id));
+
+ return next_peer_id;
+}
+
+/**
+ * @brief Function for checking if the peer has a valid Identity Resolving Key.
+ *
+ * @param[in] p_irk Pointer to the Identity Resolving Key.
+ */
+static bool peer_is_irk(ble_gap_irk_t const *const p_irk)
+{
+ for (uint32_t i = 0; i < ARRAY_SIZE(p_irk->irk); i++) {
+ if (p_irk->irk[i] != 0) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+uint32_t pm_peer_id_list(pm_peer_id_t *p_peer_list, uint32_t *const p_list_size,
+ pm_peer_id_t first_peer_id, pm_peer_id_list_skip_t skip_id)
+{
+ VERIFY_MODULE_INITIALIZED();
+ VERIFY_PARAM_NOT_NULL(p_list_size);
+ VERIFY_PARAM_NOT_NULL(p_peer_list);
+
+ uint32_t err_code;
+ uint32_t size = *p_list_size;
+ uint32_t current_size = 0;
+ pm_peer_data_t pm_car_data;
+ pm_peer_data_t pm_bond_data;
+ pm_peer_id_t current_peer_id = first_peer_id;
+ ble_gap_addr_t const *p_gap_addr;
+ bool skip_no_addr = skip_id & PM_PEER_ID_LIST_SKIP_NO_ID_ADDR;
+ bool skip_no_irk = skip_id & PM_PEER_ID_LIST_SKIP_NO_IRK;
+ bool skip_no_car = skip_id & PM_PEER_ID_LIST_SKIP_NO_CAR;
+
+ if ((*p_list_size < 1) ||
+ (skip_id > (PM_PEER_ID_LIST_SKIP_NO_ID_ADDR | PM_PEER_ID_LIST_SKIP_ALL))) {
+ return NRF_ERROR_INVALID_PARAM;
+ }
+
+ *p_list_size = 0;
+
+ if (current_peer_id == PM_PEER_ID_INVALID) {
+ current_peer_id = pm_next_peer_id_get(current_peer_id);
+
+ if (current_peer_id == PM_PEER_ID_INVALID) {
+ return NRF_SUCCESS;
+ }
+ }
+
+ memset(&pm_car_data, 0, sizeof(pm_peer_data_t));
+ memset(&pm_bond_data, 0, sizeof(pm_peer_data_t));
+
+ while (current_peer_id != PM_PEER_ID_INVALID) {
+ bool skip = false;
+
+ if (skip_no_addr || skip_no_irk) {
+ /* Get data */
+ pm_bond_data.p_bonding_data = NULL;
+
+ err_code = pds_peer_data_read(current_peer_id, PM_PEER_DATA_ID_BONDING,
+ &pm_bond_data, NULL);
+
+ if (err_code == NRF_ERROR_NOT_FOUND) {
+ skip = true;
+ } else {
+ VERIFY_SUCCESS(err_code);
+ }
+
+ /* Check data */
+ if (skip_no_addr) {
+ p_gap_addr = &pm_bond_data.p_bonding_data->peer_ble_id.id_addr_info;
+
+ if ((p_gap_addr->addr_type != BLE_GAP_ADDR_TYPE_PUBLIC) &&
+ (p_gap_addr->addr_type != BLE_GAP_ADDR_TYPE_RANDOM_STATIC)) {
+ skip = true;
+ }
+ }
+ if (skip_no_irk) {
+ if (!peer_is_irk(
+ &pm_bond_data.p_bonding_data->peer_ble_id.id_info)) {
+ skip = true;
+ }
+ }
+ }
+
+ if (skip_no_car) {
+ /* Get data */
+ pm_car_data.p_central_addr_res = NULL;
+
+ err_code = pds_peer_data_read(current_peer_id,
+ PM_PEER_DATA_ID_CENTRAL_ADDR_RES,
+ &pm_car_data, NULL);
+
+ if (err_code == NRF_ERROR_NOT_FOUND) {
+ skip = true;
+ } else {
+ VERIFY_SUCCESS(err_code);
+ }
+
+ /* Check data */
+ if (*pm_car_data.p_central_addr_res == 0) {
+ skip = true;
+ }
+ }
+
+ if (!skip) {
+ p_peer_list[current_size++] = current_peer_id;
+
+ if (current_size >= size) {
+ break;
+ }
+ }
+
+ current_peer_id = pm_next_peer_id_get(current_peer_id);
+ }
+
+ *p_list_size = current_size;
+
+ return NRF_SUCCESS;
+}
+
+uint32_t pm_peer_data_load(pm_peer_id_t peer_id, pm_peer_data_id_t data_id, void *p_data,
+ uint32_t *p_length)
+{
+ VERIFY_MODULE_INITIALIZED();
+ VERIFY_PARAM_NOT_NULL(p_data);
+ VERIFY_PARAM_NOT_NULL(p_length);
+
+ pm_peer_data_t peer_data;
+
+ memset(&peer_data, 0, sizeof(peer_data));
+ peer_data.p_all_data = p_data;
+
+ return pds_peer_data_read(peer_id, data_id, &peer_data, p_length);
+}
+
+uint32_t pm_peer_data_bonding_load(pm_peer_id_t peer_id, pm_peer_data_bonding_t *p_data)
+{
+ uint32_t length = sizeof(pm_peer_data_bonding_t);
+
+ return pm_peer_data_load(peer_id, PM_PEER_DATA_ID_BONDING, p_data, &length);
+}
+
+uint32_t pm_peer_data_remote_db_load(pm_peer_id_t peer_id, ble_gatt_db_srv_t *p_data,
+ uint32_t *p_length)
+{
+ return pm_peer_data_load(peer_id, PM_PEER_DATA_ID_GATT_REMOTE, p_data, p_length);
+}
+
+uint32_t pm_peer_data_app_data_load(pm_peer_id_t peer_id, void *p_data, uint32_t *p_length)
+{
+ return pm_peer_data_load(peer_id, PM_PEER_DATA_ID_APPLICATION, p_data, p_length);
+}
+
+uint32_t pm_peer_data_store(pm_peer_id_t peer_id, pm_peer_data_id_t data_id, void const *p_data,
+ uint32_t length, pm_store_token_t *p_token)
+{
+ VERIFY_MODULE_INITIALIZED();
+ VERIFY_PARAM_NOT_NULL(p_data);
+ if (ALIGN_NUM(4, length) != length) {
+ return NRF_ERROR_INVALID_PARAM;
+ }
+
+ if (data_id == PM_PEER_DATA_ID_BONDING) {
+ pm_peer_id_t dupl_peer_id;
+
+ dupl_peer_id =
+ im_find_duplicate_bonding_data((pm_peer_data_bonding_t *)p_data, peer_id);
+
+ if (dupl_peer_id != PM_PEER_ID_INVALID) {
+ return NRF_ERROR_FORBIDDEN;
+ }
+ }
+
+ pm_peer_data_flash_t peer_data;
+
+ memset(&peer_data, 0, sizeof(peer_data));
+ peer_data.length_words = BYTES_TO_WORDS(length);
+ peer_data.data_id = data_id;
+ peer_data.p_all_data = p_data;
+
+ return pds_peer_data_store(peer_id, &peer_data, p_token);
+}
+
+uint32_t pm_peer_data_bonding_store(pm_peer_id_t peer_id, pm_peer_data_bonding_t const *p_data,
+ pm_store_token_t *p_token)
+{
+ return pm_peer_data_store(peer_id, PM_PEER_DATA_ID_BONDING, p_data,
+ ALIGN_NUM(4, sizeof(pm_peer_data_bonding_t)), p_token);
+}
+
+uint32_t pm_peer_data_remote_db_store(pm_peer_id_t peer_id, ble_gatt_db_srv_t const *p_data,
+ uint32_t length, pm_store_token_t *p_token)
+{
+ return pm_peer_data_store(peer_id, PM_PEER_DATA_ID_GATT_REMOTE, p_data, length, p_token);
+}
+
+uint32_t pm_peer_data_app_data_store(pm_peer_id_t peer_id, void const *p_data, uint32_t length,
+ pm_store_token_t *p_token)
+{
+ return pm_peer_data_store(peer_id, PM_PEER_DATA_ID_APPLICATION, p_data, length, p_token);
+}
+
+uint32_t pm_peer_data_delete(pm_peer_id_t peer_id, pm_peer_data_id_t data_id)
+{
+ VERIFY_MODULE_INITIALIZED();
+
+ if (data_id == PM_PEER_DATA_ID_BONDING) {
+ return NRF_ERROR_INVALID_PARAM;
+ }
+
+ return pds_peer_data_delete(peer_id, data_id);
+}
+
+uint32_t pm_peer_new(pm_peer_id_t *p_new_peer_id, pm_peer_data_bonding_t *p_bonding_data,
+ pm_store_token_t *p_token)
+{
+ uint32_t err_code;
+ pm_peer_id_t peer_id;
+ pm_peer_data_flash_t peer_data;
+
+ VERIFY_MODULE_INITIALIZED();
+ VERIFY_PARAM_NOT_NULL(p_bonding_data);
+ VERIFY_PARAM_NOT_NULL(p_new_peer_id);
+
+ memset(&peer_data, 0, sizeof(pm_peer_data_flash_t));
+
+ /* Search through existing bonds to look for a duplicate. */
+ pds_peer_data_iterate_prepare();
+
+ /* @note This check is not thread safe since data is not copied while iterating. */
+ while (pds_peer_data_iterate(PM_PEER_DATA_ID_BONDING, &peer_id, &peer_data)) {
+ if (im_is_duplicate_bonding_data(p_bonding_data, peer_data.p_bonding_data)) {
+ *p_new_peer_id = peer_id;
+ return NRF_SUCCESS;
+ }
+ }
+
+ /* If no duplicate data is found, prepare to write a new bond to flash. */
+
+ *p_new_peer_id = pds_peer_id_allocate();
+
+ if (*p_new_peer_id == PM_PEER_ID_INVALID) {
+ return NRF_ERROR_NO_MEM;
+ }
+
+ memset(&peer_data, 0, sizeof(pm_peer_data_flash_t));
+
+ peer_data.data_id = PM_PEER_DATA_ID_BONDING;
+ peer_data.p_bonding_data = p_bonding_data;
+ peer_data.length_words = BYTES_TO_WORDS(sizeof(pm_peer_data_bonding_t));
+
+ err_code = pds_peer_data_store(*p_new_peer_id, &peer_data, p_token);
+
+ if (err_code != NRF_SUCCESS) {
+ uint32_t err_code_free = im_peer_free(*p_new_peer_id);
+
+ if (err_code_free != NRF_SUCCESS) {
+ LOG_ERR("Fatal error during cleanup of a failed call to %s. im_peer_free() "
+ "returned %s. peer_id: %d",
+ __func__, nrf_strerror_get(err_code_free), *p_new_peer_id);
+ return NRF_ERROR_INTERNAL;
+ }
+
+ /* NRF_ERROR_RESOURCES, if no space in flash.
+ * NRF_ERROR_BUSY, if flash filesystem was busy.
+ * NRF_ERROR_INVALID_ADDR, if bonding data is unaligned.
+ * NRF_ERROR_INTENRAL, on internal error.
+ */
+ return err_code;
+ }
+
+ return NRF_SUCCESS;
+}
+
+uint32_t pm_peer_delete(pm_peer_id_t peer_id)
+{
+ VERIFY_MODULE_INITIALIZED();
+
+ return im_peer_free(peer_id);
+}
+
+uint32_t pm_peers_delete(void)
+{
+ VERIFY_MODULE_INITIALIZED();
+
+ m_deleting_all = true;
+
+ pm_peer_id_t current_peer_id = pds_next_peer_id_get(PM_PEER_ID_INVALID);
+
+ if (current_peer_id == PM_PEER_ID_INVALID) {
+ /* No peers bonded. */
+ m_deleting_all = false;
+
+ pm_evt_t pm_delete_all_evt;
+
+ memset(&pm_delete_all_evt, 0, sizeof(pm_evt_t));
+ pm_delete_all_evt.evt_id = PM_EVT_PEERS_DELETE_SUCCEEDED;
+ pm_delete_all_evt.peer_id = PM_PEER_ID_INVALID;
+ pm_delete_all_evt.conn_handle = BLE_CONN_HANDLE_INVALID;
+
+ evt_send(&pm_delete_all_evt);
+ }
+
+ while (current_peer_id != PM_PEER_ID_INVALID) {
+ uint32_t err_code = pm_peer_delete(current_peer_id);
+
+ if (err_code != NRF_SUCCESS) {
+ LOG_ERR("%s() failed because a call to pm_peer_delete() returned %s. "
+ "peer_id: %d",
+ __func__, nrf_strerror_get(err_code), current_peer_id);
+ return NRF_ERROR_INTERNAL;
+ }
+
+ current_peer_id = pds_next_peer_id_get(current_peer_id);
+ }
+
+ return NRF_SUCCESS;
+}
+
+uint32_t pm_peer_ranks_get(pm_peer_id_t *p_highest_ranked_peer, uint32_t *p_highest_rank,
+ pm_peer_id_t *p_lowest_ranked_peer, uint32_t *p_lowest_rank)
+{
+#if CONFIG_PM_PEER_RANKS_ENABLED == 0
+ return NRF_ERROR_NOT_SUPPORTED;
+#else
+ VERIFY_MODULE_INITIALIZED();
+
+ pm_peer_id_t peer_id = pds_next_peer_id_get(PM_PEER_ID_INVALID);
+ uint32_t peer_rank = 0;
+ uint32_t length = sizeof(peer_rank);
+ pm_peer_data_t peer_data = {.p_peer_rank = &peer_rank};
+ uint32_t err_code =
+ pds_peer_data_read(peer_id, PM_PEER_DATA_ID_PEER_RANK, &peer_data, &length);
+ uint32_t highest_rank = 0;
+ uint32_t lowest_rank = 0xFFFFFFFF;
+ pm_peer_id_t highest_ranked_peer = PM_PEER_ID_INVALID;
+ pm_peer_id_t lowest_ranked_peer = PM_PEER_ID_INVALID;
+
+ if (err_code == NRF_ERROR_INVALID_PARAM) {
+ /* No peer IDs exist. */
+ return NRF_ERROR_NOT_FOUND;
+ }
+
+ while ((err_code == NRF_SUCCESS) || (err_code == NRF_ERROR_NOT_FOUND)) {
+ if (err_code == NRF_SUCCESS) {
+ if (peer_rank >= highest_rank) {
+ highest_rank = peer_rank;
+ highest_ranked_peer = peer_id;
+ }
+ if (peer_rank < lowest_rank) {
+ lowest_rank = peer_rank;
+ lowest_ranked_peer = peer_id;
+ }
+ }
+ peer_id = pds_next_peer_id_get(peer_id);
+ err_code =
+ pds_peer_data_read(peer_id, PM_PEER_DATA_ID_PEER_RANK, &peer_data, &length);
+ }
+ if (peer_id == PM_PEER_ID_INVALID) {
+ if ((highest_ranked_peer == PM_PEER_ID_INVALID) ||
+ (lowest_ranked_peer == PM_PEER_ID_INVALID)) {
+ err_code = NRF_ERROR_NOT_FOUND;
+ } else {
+ err_code = NRF_SUCCESS;
+ }
+
+ if (p_highest_ranked_peer != NULL) {
+ *p_highest_ranked_peer = highest_ranked_peer;
+ }
+ if (p_highest_rank != NULL) {
+ *p_highest_rank = highest_rank;
+ }
+ if (p_lowest_ranked_peer != NULL) {
+ *p_lowest_ranked_peer = lowest_ranked_peer;
+ }
+ if (p_lowest_rank != NULL) {
+ *p_lowest_rank = lowest_rank;
+ }
+ } else {
+ LOG_ERR("Could not retrieve ranks. pdb_peer_data_load() returned %s. peer_id: %d",
+ nrf_strerror_get(err_code), peer_id);
+ err_code = NRF_ERROR_INTERNAL;
+ }
+ return err_code;
+#endif
+}
+
+#if CONFIG_PM_PEER_RANKS_ENABLED == 1
+/** @brief Function for initializing peer rank functionality. */
+static void rank_init(void)
+{
+ rank_vars_update();
+}
+#endif
+
+uint32_t pm_peer_rank_highest(pm_peer_id_t peer_id)
+{
+#if CONFIG_PM_PEER_RANKS_ENABLED == 0
+ return NRF_ERROR_NOT_SUPPORTED;
+#else
+ VERIFY_MODULE_INITIALIZED();
+
+ uint32_t err_code;
+ pm_peer_data_flash_t peer_data = {
+ .length_words = BYTES_TO_WORDS(sizeof(m_current_highest_peer_rank)),
+ .data_id = PM_PEER_DATA_ID_PEER_RANK,
+ .p_peer_rank = &m_current_highest_peer_rank};
+
+ if (!m_peer_rank_initialized) {
+ rank_init();
+ }
+
+ if (!m_peer_rank_initialized || (m_peer_rank_token != PM_STORE_TOKEN_INVALID)) {
+ err_code = NRF_ERROR_BUSY;
+ } else {
+ if ((peer_id == m_highest_ranked_peer) && (m_current_highest_peer_rank > 0)) {
+ pm_evt_t pm_evt;
+
+ /* The reported peer is already regarded as highest (provided it has an
+ * index at all)
+ */
+ err_code = NRF_SUCCESS;
+
+ memset(&pm_evt, 0, sizeof(pm_evt));
+ pm_evt.evt_id = PM_EVT_PEER_DATA_UPDATE_SUCCEEDED;
+ pm_evt.conn_handle = im_conn_handle_get(peer_id);
+ pm_evt.peer_id = peer_id;
+ pm_evt.params.peer_data_update_succeeded.data_id =
+ PM_PEER_DATA_ID_PEER_RANK;
+ pm_evt.params.peer_data_update_succeeded.action = PM_PEER_DATA_OP_UPDATE;
+ pm_evt.params.peer_data_update_succeeded.token = PM_STORE_TOKEN_INVALID;
+ pm_evt.params.peer_data_update_succeeded.flash_changed = false;
+
+ evt_send(&pm_evt);
+ } else {
+ if (m_current_highest_peer_rank == UINT32_MAX) {
+ err_code = NRF_ERROR_DATA_SIZE;
+ } else {
+ m_current_highest_peer_rank += 1;
+ err_code = pds_peer_data_store(peer_id, &peer_data,
+ &m_peer_rank_token);
+ if (err_code != NRF_SUCCESS) {
+ m_peer_rank_token = PM_STORE_TOKEN_INVALID;
+ m_current_highest_peer_rank -= 1;
+ /* Assume INVALID_PARAM
+ * refers to peer_id, not
+ * data_id.
+ */
+ if ((err_code != NRF_ERROR_BUSY) &&
+ (err_code != NRF_ERROR_RESOURCES) &&
+ (err_code != NRF_ERROR_INVALID_PARAM)) {
+ LOG_ERR("Could not update rank. "
+ "pdb_raw_store() returned %s. "
+ "peer_id: %d",
+ nrf_strerror_get(err_code), peer_id);
+ err_code = NRF_ERROR_INTERNAL;
+ }
+ }
+ }
+ }
+ }
+ return err_code;
+#endif
+}
diff --git a/samples/bluetooth/ble_hrs/prj.conf b/samples/bluetooth/ble_hrs/prj.conf
index ef28cda77c..e663003b9a 100644
--- a/samples/bluetooth/ble_hrs/prj.conf
+++ b/samples/bluetooth/ble_hrs/prj.conf
@@ -9,6 +9,16 @@ CONFIG_NRF_SECURITY=y
CONFIG_MBEDTLS_PSA_CRYPTO_C=y
CONFIG_PSA_WANT_GENERATE_RANDOM=y
+# Enable Crypto functionality required by LE Secure Connection
+CONFIG_PSA_WANT_ALG_ECDH=y
+CONFIG_PSA_WANT_KEY_TYPE_ECC_KEY_PAIR_GENERATE=y
+CONFIG_PSA_WANT_KEY_TYPE_ECC_KEY_PAIR_IMPORT=y
+CONFIG_PSA_WANT_KEY_TYPE_ECC_KEY_PAIR_EXPORT=y
+CONFIG_PSA_WANT_ECC_SECP_R1_256=y
+CONFIG_MBEDTLS_PSA_STATIC_KEY_SLOTS=y
+CONFIG_MBEDTLS_PSA_KEY_SLOT_COUNT=1
+CONFIG_MBEDTLS_PSA_STATIC_KEY_SLOT_BUFFER_SIZE=65
+
CONFIG_BLE_ADV=y
CONFIG_BLE_ADV_NAME="nRF_BM_HRS"
CONFIG_BLE_ADV_EXTENDED_ADVERTISING=n
@@ -33,3 +43,9 @@ CONFIG_BLE_HRS=y
# To simulate measurements
CONFIG_SENSORSIM=y
CONFIG_BM_TIMER=y
+
+# Peer Manager
+CONFIG_PEER_MANAGER=y
+CONFIG_PM_LESC_ENABLED=y
+CONFIG_BLE_CONN_STATE=y
+CONFIG_BM_ZMS=y
diff --git a/samples/bluetooth/ble_hrs/src/main.c b/samples/bluetooth/ble_hrs/src/main.c
index 76341fa858..ecfc6a5b2e 100644
--- a/samples/bluetooth/ble_hrs/src/main.c
+++ b/samples/bluetooth/ble_hrs/src/main.c
@@ -19,10 +19,29 @@
#include
#include
+#include
+
LOG_MODULE_REGISTER(app, CONFIG_BLE_HRS_SAMPLE_LOG_LEVEL);
#define CONN_TAG 1
+/* Perform bonding. */
+#define SEC_PARAM_BOND 1
+/* Man In The Middle protection not required. */
+#define SEC_PARAM_MITM 0
+/* LE Secure Connections enabled. */
+#define SEC_PARAM_LESC 1
+/* Keypress notifications not enabled. */
+#define SEC_PARAM_KEYPRESS 0
+/* No I/O capabilities. */
+#define SEC_PARAM_IO_CAPABILITIES BLE_GAP_IO_CAPS_NONE
+/* Out Of Band data not available. */
+#define SEC_PARAM_OOB 0
+/* Minimum encryption key size. */
+#define SEC_PARAM_MIN_KEY_SIZE 7
+/* Maximum encryption key size. */
+#define SEC_PARAM_MAX_KEY_SIZE 16
+
BLE_ADV_DEF(ble_adv); /* BLE advertising instance */
BLE_BAS_DEF(ble_bas); /* BLE battery service instance */
BLE_HRS_DEF(ble_hrs); /* BLE heart rate service instance */
@@ -313,6 +332,88 @@ static void ble_hrs_evt_handler(struct ble_hrs *hrs, const struct ble_hrs_evt *e
}
}
+static void delete_bonds(void)
+{
+ uint32_t err;
+
+ printk("Erase bonds!\n");
+
+ err = pm_peers_delete();
+ if (err) {
+ printk("Failed to delete peers, err %d\n", err);
+ }
+}
+
+static void advertising_start(bool erase_bonds)
+{
+ int err;
+
+ if (erase_bonds) {
+ delete_bonds();
+ } else {
+ err = ble_adv_start(&ble_adv, BLE_ADV_MODE_FAST);
+ if (err) {
+ printk("Failed to start advertising, err %d\n", err);
+ }
+ }
+}
+
+static void pm_evt_handler(pm_evt_t const *p_evt)
+{
+ pm_handler_on_pm_evt(p_evt);
+ pm_handler_disconnect_on_sec_failure(p_evt);
+ pm_handler_flash_clean(p_evt);
+
+ switch (p_evt->evt_id) {
+ case PM_EVT_PEERS_DELETE_SUCCEEDED:
+ advertising_start(false);
+ break;
+ default:
+ break;
+ }
+}
+
+static void peer_manager_init(void)
+{
+ ble_gap_sec_params_t sec_param;
+ int err;
+
+ err = pm_init();
+ if (err) {
+ return;
+ }
+
+ memset(&sec_param, 0, sizeof(ble_gap_sec_params_t));
+
+ /* Security parameters to be used for all security procedures. */
+ sec_param = (ble_gap_sec_params_t) {
+ .bond = SEC_PARAM_BOND,
+ .mitm = SEC_PARAM_MITM,
+ .lesc = SEC_PARAM_LESC,
+ .keypress = SEC_PARAM_KEYPRESS,
+ .io_caps = SEC_PARAM_IO_CAPABILITIES,
+ .oob = SEC_PARAM_OOB,
+ .min_key_size = SEC_PARAM_MIN_KEY_SIZE,
+ .max_key_size = SEC_PARAM_MAX_KEY_SIZE,
+ .kdist_own.enc = 1,
+ .kdist_own.id = 1,
+ .kdist_peer.enc = 1,
+ .kdist_peer.id = 1,
+ };
+
+ err = pm_sec_params_set(&sec_param);
+ if (err) {
+ printk("pm_sec_params_set() failed, err: %d\n", err);
+ return;
+ }
+
+ err = pm_register(pm_evt_handler);
+ if (err) {
+ printk("pm_register() failed, err: %d\n", err);
+ return;
+ }
+}
+
int main(void)
{
int err;
@@ -369,6 +470,8 @@ int main(void)
LOG_INF("Bluetooth enabled");
+ peer_manager_init();
+
err = ble_hrs_init(&ble_hrs, &hrs_cfg);
if (err) {
LOG_ERR("Failed to initialize heart rate service, err %d", err);