mirror of
https://github.com/flipperdevices/flipperzero-firmware.git
synced 2025-12-12 04:41:26 +04:00
BLE: improved pairing security (#4240)
* ble: use unique root security keys for new pairings after pairing reset; added migrations for existing pairing data; unit_tests: added migration tests * bt: lower logging level * hal: bt: updated doxygen strings * hal: ble: Added checks for root_keys ptr * service: ble: bt_keys_storage minor cleanup
This commit is contained in:
@@ -4,10 +4,23 @@
|
||||
|
||||
#include <bt/bt_service/bt_keys_storage.h>
|
||||
#include <storage/storage.h>
|
||||
#include <toolbox/saved_struct.h>
|
||||
|
||||
#define BT_TEST_KEY_STORAGE_FILE_PATH EXT_PATH("unit_tests/bt_test.keys")
|
||||
#define BT_TEST_MIGRATION_FILE_PATH EXT_PATH("unit_tests/bt_migration_test.keys")
|
||||
#define BT_TEST_NVM_RAM_BUFF_SIZE (507 * 4) // The same as in ble NVM storage
|
||||
|
||||
// Identity root key
|
||||
static const uint8_t gap_legacy_irk[16] =
|
||||
{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0};
|
||||
// Encryption root key
|
||||
static const uint8_t gap_legacy_erk[16] =
|
||||
{0xfe, 0xdc, 0xba, 0x09, 0x87, 0x65, 0x43, 0x21, 0xfe, 0xdc, 0xba, 0x09, 0x87, 0x65, 0x43, 0x21};
|
||||
|
||||
// Test constants for migration (matching bt_keys_storage.c)
|
||||
#define BT_KEYS_STORAGE_MAGIC_TEST (0x18)
|
||||
#define BT_KEYS_STORAGE_VERSION_1_TEST (1)
|
||||
|
||||
typedef struct {
|
||||
Storage* storage;
|
||||
BtKeysStorage* bt_keys_storage;
|
||||
@@ -88,6 +101,134 @@ static void bt_test_keys_remove_test_file(void) {
|
||||
"Can't remove test file");
|
||||
}
|
||||
|
||||
// Helper function to create a version 0 file manually
|
||||
static bool
|
||||
bt_test_create_v0_file(const char* file_path, const uint8_t* nvm_data, size_t nvm_size) {
|
||||
// Version 0 files use saved_struct format with magic 0x18, version 0, containing only BLE pairing data
|
||||
return saved_struct_save(
|
||||
file_path,
|
||||
nvm_data,
|
||||
nvm_size,
|
||||
BT_KEYS_STORAGE_MAGIC_TEST,
|
||||
0); // Version 0
|
||||
}
|
||||
|
||||
// Helper function to verify file format version
|
||||
static bool bt_test_verify_file_version(const char* file_path, uint32_t expected_version) {
|
||||
uint8_t magic, version;
|
||||
size_t size;
|
||||
|
||||
if(!saved_struct_get_metadata(file_path, &magic, &version, &size)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (magic == BT_KEYS_STORAGE_MAGIC_TEST && version == expected_version);
|
||||
}
|
||||
|
||||
// Test migration from version 0 to version 1, including root key preservation
|
||||
static void bt_test_migration_v0_to_v1(void) {
|
||||
// Create test NVM data
|
||||
const size_t test_nvm_size = 100;
|
||||
uint8_t test_nvm_data[test_nvm_size];
|
||||
for(size_t i = 0; i < test_nvm_size; i++) {
|
||||
test_nvm_data[i] = (uint8_t)(i & 0xFF);
|
||||
}
|
||||
|
||||
// Create a version 0 file
|
||||
mu_assert(
|
||||
bt_test_create_v0_file(BT_TEST_MIGRATION_FILE_PATH, test_nvm_data, test_nvm_size),
|
||||
"Failed to create version 0 test file");
|
||||
|
||||
// Create BT keys storage and load the v0 file (should trigger migration)
|
||||
BtKeysStorage* migration_storage = bt_keys_storage_alloc(BT_TEST_MIGRATION_FILE_PATH);
|
||||
uint8_t loaded_buffer[BT_TEST_NVM_RAM_BUFF_SIZE];
|
||||
memset(loaded_buffer, 0, sizeof(loaded_buffer));
|
||||
bt_keys_storage_set_ram_params(migration_storage, loaded_buffer, sizeof(loaded_buffer));
|
||||
|
||||
// Load should succeed and migrate v0 to v1
|
||||
mu_assert(bt_keys_storage_load(migration_storage), "Failed to load and migrate v0 file");
|
||||
|
||||
// Verify the file is now version 1
|
||||
mu_assert(
|
||||
bt_test_verify_file_version(BT_TEST_MIGRATION_FILE_PATH, BT_KEYS_STORAGE_VERSION_1_TEST),
|
||||
"File was not migrated to version 1");
|
||||
|
||||
// Verify the NVM data was preserved during migration
|
||||
mu_assert(
|
||||
memcmp(test_nvm_data, loaded_buffer, test_nvm_size) == 0,
|
||||
"NVM data was corrupted during migration");
|
||||
|
||||
// Verify that legacy root keys are used after migration
|
||||
const GapRootSecurityKeys* migrated_keys = bt_keys_storage_get_root_keys(migration_storage);
|
||||
mu_assert(
|
||||
memcmp(migrated_keys->irk, gap_legacy_irk, sizeof(gap_legacy_irk)) == 0,
|
||||
"IRK not set to legacy after migration");
|
||||
mu_assert(
|
||||
memcmp(migrated_keys->erk, gap_legacy_erk, sizeof(gap_legacy_erk)) == 0,
|
||||
"ERK not set to legacy after migration");
|
||||
|
||||
bt_keys_storage_free(migration_storage);
|
||||
storage_simply_remove(bt_test->storage, BT_TEST_MIGRATION_FILE_PATH);
|
||||
}
|
||||
|
||||
// Test that migration preserves existing pairing data and root keys are not changed on reload
|
||||
static void bt_test_migration_preserves_pairings_and_keys(void) {
|
||||
const size_t pairing_data_size = 200;
|
||||
uint8_t pairing_data[pairing_data_size];
|
||||
for(size_t i = 0; i < pairing_data_size; i++) {
|
||||
pairing_data[i] = (uint8_t)((i * 7 + 42) & 0xFF);
|
||||
}
|
||||
mu_assert(
|
||||
bt_test_create_v0_file(BT_TEST_MIGRATION_FILE_PATH, pairing_data, pairing_data_size),
|
||||
"Failed to create v0 file with pairing data");
|
||||
|
||||
GapRootSecurityKeys keys_after_first_load;
|
||||
for(int iteration = 0; iteration < 2; iteration++) {
|
||||
BtKeysStorage* storage = bt_keys_storage_alloc(BT_TEST_MIGRATION_FILE_PATH);
|
||||
uint8_t buffer[BT_TEST_NVM_RAM_BUFF_SIZE];
|
||||
memset(buffer, 0, sizeof(buffer));
|
||||
bt_keys_storage_set_ram_params(storage, buffer, sizeof(buffer));
|
||||
mu_assert(bt_keys_storage_load(storage), "Failed to load on iteration");
|
||||
mu_assert(
|
||||
memcmp(pairing_data, buffer, pairing_data_size) == 0,
|
||||
"Pairing data corrupted on iteration");
|
||||
const GapRootSecurityKeys* keys = bt_keys_storage_get_root_keys(storage);
|
||||
if(iteration == 0)
|
||||
memcpy(&keys_after_first_load, keys, sizeof(GapRootSecurityKeys));
|
||||
else
|
||||
mu_assert(
|
||||
memcmp(&keys_after_first_load, keys, sizeof(GapRootSecurityKeys)) == 0,
|
||||
"Root keys changed after reload");
|
||||
bt_keys_storage_free(storage);
|
||||
}
|
||||
storage_simply_remove(bt_test->storage, BT_TEST_MIGRATION_FILE_PATH);
|
||||
}
|
||||
|
||||
// Test that delete operation generates new secure keys in v1 and does not match legacy
|
||||
static void bt_test_delete_generates_new_keys_and_not_legacy(void) {
|
||||
BtKeysStorage* storage = bt_keys_storage_alloc(BT_TEST_MIGRATION_FILE_PATH);
|
||||
uint8_t buffer[BT_TEST_NVM_RAM_BUFF_SIZE];
|
||||
memset(buffer, 0x55, sizeof(buffer));
|
||||
bt_keys_storage_set_ram_params(storage, buffer, sizeof(buffer));
|
||||
mu_assert(bt_keys_storage_update(storage, buffer, 100), "Failed to create initial v1 file");
|
||||
const GapRootSecurityKeys* original_keys = bt_keys_storage_get_root_keys(storage);
|
||||
uint8_t original_keys_copy[sizeof(GapRootSecurityKeys)];
|
||||
memcpy(original_keys_copy, original_keys, sizeof(original_keys_copy));
|
||||
bt_keys_storage_delete(storage);
|
||||
const GapRootSecurityKeys* new_keys = bt_keys_storage_get_root_keys(storage);
|
||||
mu_assert(
|
||||
memcmp(original_keys_copy, new_keys, sizeof(original_keys_copy)) != 0,
|
||||
"Root keys were not regenerated after delete");
|
||||
mu_assert(
|
||||
memcmp(new_keys->irk, gap_legacy_irk, sizeof(gap_legacy_irk)) != 0,
|
||||
"IRK after delete should not match legacy");
|
||||
mu_assert(
|
||||
memcmp(new_keys->erk, gap_legacy_erk, sizeof(gap_legacy_erk)) != 0,
|
||||
"ERK after delete should not match legacy");
|
||||
bt_keys_storage_free(storage);
|
||||
storage_simply_remove(bt_test->storage, BT_TEST_MIGRATION_FILE_PATH);
|
||||
}
|
||||
|
||||
MU_TEST(bt_test_keys_storage_serial_profile) {
|
||||
furi_check(bt_test);
|
||||
|
||||
@@ -96,10 +237,28 @@ MU_TEST(bt_test_keys_storage_serial_profile) {
|
||||
bt_test_keys_remove_test_file();
|
||||
}
|
||||
|
||||
MU_TEST(bt_test_migration_v0_to_v1_test) {
|
||||
furi_check(bt_test);
|
||||
bt_test_migration_v0_to_v1();
|
||||
}
|
||||
|
||||
MU_TEST(bt_test_migration_preserves_pairings_and_keys_test) {
|
||||
furi_check(bt_test);
|
||||
bt_test_migration_preserves_pairings_and_keys();
|
||||
}
|
||||
|
||||
MU_TEST(bt_test_delete_generates_new_keys_and_not_legacy_test) {
|
||||
furi_check(bt_test);
|
||||
bt_test_delete_generates_new_keys_and_not_legacy();
|
||||
}
|
||||
|
||||
MU_TEST_SUITE(test_bt) {
|
||||
bt_test_alloc();
|
||||
|
||||
MU_RUN_TEST(bt_test_keys_storage_serial_profile);
|
||||
MU_RUN_TEST(bt_test_migration_v0_to_v1_test);
|
||||
MU_RUN_TEST(bt_test_migration_preserves_pairings_and_keys_test);
|
||||
MU_RUN_TEST(bt_test_delete_generates_new_keys_and_not_legacy_test);
|
||||
|
||||
bt_test_free();
|
||||
}
|
||||
|
||||
@@ -403,6 +403,7 @@ static void bt_change_profile(Bt* bt, BtMessage* message) {
|
||||
bt->current_profile = furi_hal_bt_change_app(
|
||||
message->data.profile.template,
|
||||
message->data.profile.params,
|
||||
bt_keys_storage_get_root_keys(bt->keys_storage),
|
||||
bt_on_gap_event_callback,
|
||||
bt);
|
||||
if(bt->current_profile) {
|
||||
@@ -458,7 +459,6 @@ static void bt_load_keys(Bt* bt) {
|
||||
bt_keys_storage_load(bt->keys_storage);
|
||||
|
||||
bt->current_profile = NULL;
|
||||
|
||||
} else {
|
||||
FURI_LOG_I(TAG, "Keys unchanged");
|
||||
}
|
||||
@@ -466,8 +466,12 @@ static void bt_load_keys(Bt* bt) {
|
||||
|
||||
static void bt_start_application(Bt* bt) {
|
||||
if(!bt->current_profile) {
|
||||
bt->current_profile =
|
||||
furi_hal_bt_change_app(ble_profile_serial, NULL, bt_on_gap_event_callback, bt);
|
||||
bt->current_profile = furi_hal_bt_change_app(
|
||||
ble_profile_serial,
|
||||
NULL,
|
||||
bt_keys_storage_get_root_keys(bt->keys_storage),
|
||||
bt_on_gap_event_callback,
|
||||
bt);
|
||||
|
||||
if(!bt->current_profile) {
|
||||
FURI_LOG_E(TAG, "BLE App start failed");
|
||||
|
||||
@@ -2,21 +2,92 @@
|
||||
|
||||
#include <furi.h>
|
||||
#include <furi_hal_bt.h>
|
||||
#include <lib/toolbox/saved_struct.h>
|
||||
#include <storage/storage.h>
|
||||
#include <furi_hal_random.h>
|
||||
|
||||
#define BT_KEYS_STORAGE_VERSION (0)
|
||||
#define BT_KEYS_STORAGE_MAGIC (0x18)
|
||||
#include <gap.h>
|
||||
|
||||
#include <storage/storage.h>
|
||||
#include <toolbox/saved_struct.h>
|
||||
|
||||
#define BT_KEYS_STORAGE_MAGIC (0x18)
|
||||
#define BT_KEYS_STORAGE_VERSION (1)
|
||||
#define BT_KEYS_STORAGE_LEGACY_VERSION (0) // Legacy version with no root keys
|
||||
|
||||
#define TAG "BtKeyStorage"
|
||||
|
||||
// Identity root key
|
||||
static const uint8_t gap_legacy_irk[16] =
|
||||
{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0};
|
||||
// Encryption root key
|
||||
static const uint8_t gap_legacy_erk[16] =
|
||||
{0xfe, 0xdc, 0xba, 0x09, 0x87, 0x65, 0x43, 0x21, 0xfe, 0xdc, 0xba, 0x09, 0x87, 0x65, 0x43, 0x21};
|
||||
|
||||
struct BtKeysStorage {
|
||||
uint8_t* nvm_sram_buff;
|
||||
uint16_t nvm_sram_buff_size;
|
||||
uint16_t current_size;
|
||||
FuriString* file_path;
|
||||
GapRootSecurityKeys root_keys;
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
GapRootSecurityKeys root_keys;
|
||||
uint8_t pairing_data[];
|
||||
} BtKeysStorageFile;
|
||||
|
||||
static bool bt_keys_storage_save(BtKeysStorage* instance) {
|
||||
furi_assert(instance);
|
||||
|
||||
size_t total_size = sizeof(BtKeysStorageFile) + instance->current_size;
|
||||
BtKeysStorageFile* save_data = malloc(total_size);
|
||||
|
||||
memcpy(&save_data->root_keys, &instance->root_keys, sizeof(GapRootSecurityKeys));
|
||||
|
||||
furi_hal_bt_nvm_sram_sem_acquire();
|
||||
memcpy(save_data->pairing_data, instance->nvm_sram_buff, instance->current_size);
|
||||
furi_hal_bt_nvm_sram_sem_release();
|
||||
|
||||
bool saved = saved_struct_save(
|
||||
furi_string_get_cstr(instance->file_path),
|
||||
save_data,
|
||||
sizeof(GapRootSecurityKeys) + instance->current_size,
|
||||
BT_KEYS_STORAGE_MAGIC,
|
||||
BT_KEYS_STORAGE_VERSION);
|
||||
|
||||
free(save_data);
|
||||
return saved;
|
||||
}
|
||||
|
||||
static bool bt_keys_storage_load_keys_and_pairings(
|
||||
BtKeysStorage* instance,
|
||||
const uint8_t* file_data,
|
||||
size_t data_size) {
|
||||
if(data_size < sizeof(GapRootSecurityKeys)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const BtKeysStorageFile* loaded = (const BtKeysStorageFile*)file_data;
|
||||
memcpy(&instance->root_keys, &loaded->root_keys, sizeof(GapRootSecurityKeys));
|
||||
|
||||
size_t ble_data_size = data_size - sizeof(GapRootSecurityKeys);
|
||||
if(ble_data_size > instance->nvm_sram_buff_size) {
|
||||
FURI_LOG_E(TAG, "BLE data too large for SRAM buffer");
|
||||
return false;
|
||||
}
|
||||
|
||||
furi_hal_bt_nvm_sram_sem_acquire();
|
||||
memcpy(instance->nvm_sram_buff, loaded->pairing_data, ble_data_size);
|
||||
instance->current_size = ble_data_size;
|
||||
furi_hal_bt_nvm_sram_sem_release();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void bt_keys_storage_regenerate_root_keys(BtKeysStorage* instance) {
|
||||
furi_hal_random_fill_buf(instance->root_keys.erk, sizeof(instance->root_keys.erk));
|
||||
furi_hal_random_fill_buf(instance->root_keys.irk, sizeof(instance->root_keys.irk));
|
||||
}
|
||||
|
||||
bool bt_keys_storage_delete(BtKeysStorage* instance) {
|
||||
furi_assert(instance);
|
||||
|
||||
@@ -25,6 +96,15 @@ bool bt_keys_storage_delete(BtKeysStorage* instance) {
|
||||
|
||||
furi_hal_bt_stop_advertising();
|
||||
delete_succeed = furi_hal_bt_clear_white_list();
|
||||
|
||||
FURI_LOG_I(TAG, "Root keys regen");
|
||||
bt_keys_storage_regenerate_root_keys(instance);
|
||||
|
||||
instance->current_size = 0;
|
||||
if(!bt_keys_storage_save(instance)) {
|
||||
FURI_LOG_E(TAG, "Save after delete failed");
|
||||
}
|
||||
|
||||
if(bt_is_active) {
|
||||
furi_hal_bt_start_advertising();
|
||||
}
|
||||
@@ -42,6 +122,7 @@ BtKeysStorage* bt_keys_storage_alloc(const char* keys_storage_path) {
|
||||
instance->file_path = furi_string_alloc();
|
||||
furi_string_set_str(instance->file_path, keys_storage_path);
|
||||
|
||||
bt_keys_storage_regenerate_root_keys(instance);
|
||||
return instance;
|
||||
}
|
||||
|
||||
@@ -76,20 +157,31 @@ static bool bt_keys_storage_file_exists(const char* file_path) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
static bool bt_keys_storage_validate_file(const char* file_path, size_t* payload_size) {
|
||||
static bool bt_keys_storage_validate_file(
|
||||
const char* file_path,
|
||||
size_t* payload_size,
|
||||
uint8_t* file_version) {
|
||||
uint8_t magic, version;
|
||||
size_t size;
|
||||
|
||||
if(!saved_struct_get_metadata(file_path, &magic, &version, &size)) {
|
||||
FURI_LOG_E(TAG, "Failed to get metadata");
|
||||
FURI_LOG_W(TAG, "Failed to get metadata");
|
||||
return false;
|
||||
|
||||
} else if(magic != BT_KEYS_STORAGE_MAGIC || version != BT_KEYS_STORAGE_VERSION) {
|
||||
FURI_LOG_E(TAG, "File version mismatch");
|
||||
} else if(magic != BT_KEYS_STORAGE_MAGIC) {
|
||||
FURI_LOG_W(TAG, "File magic mismatch");
|
||||
return false;
|
||||
} else if(version > BT_KEYS_STORAGE_VERSION) {
|
||||
FURI_LOG_E(
|
||||
TAG,
|
||||
"File version %d is newer than supported version %d",
|
||||
version,
|
||||
BT_KEYS_STORAGE_VERSION);
|
||||
return false;
|
||||
}
|
||||
|
||||
*payload_size = size;
|
||||
*file_version = version;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -102,32 +194,45 @@ bool bt_keys_storage_is_changed(BtKeysStorage* instance) {
|
||||
do {
|
||||
const char* file_path = furi_string_get_cstr(instance->file_path);
|
||||
size_t payload_size;
|
||||
uint8_t file_version;
|
||||
|
||||
if(!bt_keys_storage_file_exists(file_path)) {
|
||||
FURI_LOG_W(TAG, "Missing or empty file");
|
||||
is_changed = true;
|
||||
break;
|
||||
}
|
||||
|
||||
} else if(!bt_keys_storage_validate_file(file_path, &payload_size)) {
|
||||
FURI_LOG_E(TAG, "Invalid or corrupted file");
|
||||
if(!bt_keys_storage_validate_file(file_path, &payload_size, &file_version)) {
|
||||
FURI_LOG_W(TAG, "Invalid or corrupted file");
|
||||
is_changed = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Early check for legacy version: always considered changed, no need to load
|
||||
if(file_version == BT_KEYS_STORAGE_LEGACY_VERSION) {
|
||||
is_changed = true;
|
||||
break;
|
||||
}
|
||||
|
||||
data_buffer = malloc(payload_size);
|
||||
|
||||
const bool data_loaded = saved_struct_load(
|
||||
file_path, data_buffer, payload_size, BT_KEYS_STORAGE_MAGIC, BT_KEYS_STORAGE_VERSION);
|
||||
file_path, data_buffer, payload_size, BT_KEYS_STORAGE_MAGIC, file_version);
|
||||
|
||||
if(!data_loaded) {
|
||||
FURI_LOG_E(TAG, "Failed to load file");
|
||||
break;
|
||||
}
|
||||
|
||||
} else if(payload_size == instance->current_size) {
|
||||
// At this point, it's version 1 file we have
|
||||
const BtKeysStorageFile* loaded = (const BtKeysStorageFile*)data_buffer;
|
||||
size_t expected_file_size = sizeof(GapRootSecurityKeys) + instance->current_size;
|
||||
if(payload_size == expected_file_size) {
|
||||
furi_hal_bt_nvm_sram_sem_acquire();
|
||||
is_changed = memcmp(data_buffer, instance->nvm_sram_buff, payload_size);
|
||||
is_changed =
|
||||
memcmp(loaded->pairing_data, instance->nvm_sram_buff, instance->current_size);
|
||||
furi_hal_bt_nvm_sram_sem_release();
|
||||
|
||||
} else {
|
||||
FURI_LOG_D(TAG, "Size mismatch");
|
||||
FURI_LOG_D(TAG, "NVRAM sz mismatch (v1)");
|
||||
is_changed = true;
|
||||
}
|
||||
} while(false);
|
||||
@@ -139,45 +244,59 @@ bool bt_keys_storage_is_changed(BtKeysStorage* instance) {
|
||||
return is_changed;
|
||||
}
|
||||
|
||||
static bool bt_keys_storage_load_legacy_pairings(
|
||||
BtKeysStorage* instance,
|
||||
const uint8_t* file_data,
|
||||
size_t payload_size) {
|
||||
FURI_LOG_I(TAG, "Loaded v0, upgrading to v1");
|
||||
memcpy(instance->root_keys.irk, gap_legacy_irk, sizeof(instance->root_keys.irk));
|
||||
memcpy(instance->root_keys.erk, gap_legacy_erk, sizeof(instance->root_keys.erk));
|
||||
if(payload_size > instance->nvm_sram_buff_size) {
|
||||
FURI_LOG_E(TAG, "Pairing too large");
|
||||
return false;
|
||||
}
|
||||
furi_hal_bt_nvm_sram_sem_acquire();
|
||||
memcpy(instance->nvm_sram_buff, file_data, payload_size);
|
||||
instance->current_size = payload_size;
|
||||
furi_hal_bt_nvm_sram_sem_release();
|
||||
if(!bt_keys_storage_save(instance)) {
|
||||
FURI_LOG_W(TAG, "Upgrade to v1 failed");
|
||||
} else {
|
||||
FURI_LOG_I(TAG, "Upgraded to v1");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool bt_keys_storage_load(BtKeysStorage* instance) {
|
||||
furi_assert(instance);
|
||||
|
||||
const char* file_path = furi_string_get_cstr(instance->file_path);
|
||||
size_t payload_size;
|
||||
uint8_t file_version;
|
||||
if(!bt_keys_storage_validate_file(file_path, &payload_size, &file_version)) {
|
||||
FURI_LOG_E(TAG, "Invalid or corrupted file");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool loaded = false;
|
||||
uint8_t* file_data = malloc(payload_size);
|
||||
|
||||
do {
|
||||
const char* file_path = furi_string_get_cstr(instance->file_path);
|
||||
|
||||
// Get payload size
|
||||
size_t payload_size;
|
||||
if(!bt_keys_storage_validate_file(file_path, &payload_size)) {
|
||||
FURI_LOG_E(TAG, "Invalid or corrupted file");
|
||||
break;
|
||||
|
||||
} else if(payload_size > instance->nvm_sram_buff_size) {
|
||||
FURI_LOG_E(TAG, "NVM RAM buffer overflow");
|
||||
if(!saved_struct_load(
|
||||
file_path, file_data, payload_size, BT_KEYS_STORAGE_MAGIC, file_version)) {
|
||||
FURI_LOG_E(TAG, "Failed to load");
|
||||
break;
|
||||
}
|
||||
|
||||
// Load saved data to ram
|
||||
furi_hal_bt_nvm_sram_sem_acquire();
|
||||
const bool data_loaded = saved_struct_load(
|
||||
file_path,
|
||||
instance->nvm_sram_buff,
|
||||
payload_size,
|
||||
BT_KEYS_STORAGE_MAGIC,
|
||||
BT_KEYS_STORAGE_VERSION);
|
||||
furi_hal_bt_nvm_sram_sem_release();
|
||||
|
||||
if(!data_loaded) {
|
||||
FURI_LOG_E(TAG, "Failed to load file");
|
||||
if(file_version == BT_KEYS_STORAGE_LEGACY_VERSION) {
|
||||
loaded = bt_keys_storage_load_legacy_pairings(instance, file_data, payload_size);
|
||||
break;
|
||||
}
|
||||
|
||||
instance->current_size = payload_size;
|
||||
|
||||
loaded = true;
|
||||
// Only v1 left
|
||||
loaded = bt_keys_storage_load_keys_and_pairings(instance, file_data, payload_size);
|
||||
} while(false);
|
||||
|
||||
free(file_data);
|
||||
return loaded;
|
||||
}
|
||||
|
||||
@@ -203,14 +322,8 @@ bool bt_keys_storage_update(BtKeysStorage* instance, uint8_t* start_addr, uint32
|
||||
|
||||
instance->current_size = new_size;
|
||||
|
||||
furi_hal_bt_nvm_sram_sem_acquire();
|
||||
bool data_updated = saved_struct_save(
|
||||
furi_string_get_cstr(instance->file_path),
|
||||
instance->nvm_sram_buff,
|
||||
new_size,
|
||||
BT_KEYS_STORAGE_MAGIC,
|
||||
BT_KEYS_STORAGE_VERSION);
|
||||
furi_hal_bt_nvm_sram_sem_release();
|
||||
// Save using version 1 format with embedded root keys
|
||||
bool data_updated = bt_keys_storage_save(instance);
|
||||
|
||||
if(!data_updated) {
|
||||
FURI_LOG_E(TAG, "Failed to update key storage");
|
||||
@@ -222,3 +335,9 @@ bool bt_keys_storage_update(BtKeysStorage* instance, uint8_t* start_addr, uint32
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
const GapRootSecurityKeys* bt_keys_storage_get_root_keys(BtKeysStorage* instance) {
|
||||
furi_assert(instance);
|
||||
|
||||
return &instance->root_keys;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#include <gap.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
@@ -19,6 +21,8 @@ void bt_keys_storage_set_ram_params(BtKeysStorage* instance, uint8_t* buff, uint
|
||||
|
||||
bool bt_keys_storage_is_changed(BtKeysStorage* instance);
|
||||
|
||||
const GapRootSecurityKeys* bt_keys_storage_get_root_keys(BtKeysStorage* instance);
|
||||
|
||||
bool bt_keys_storage_load(BtKeysStorage* instance);
|
||||
|
||||
bool bt_keys_storage_update(BtKeysStorage* instance, uint8_t* start_addr, uint32_t size);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "bt_settings_filename.h"
|
||||
|
||||
#include <furi.h>
|
||||
|
||||
#include <storage/storage.h>
|
||||
#include <toolbox/saved_struct.h>
|
||||
|
||||
@@ -14,12 +15,13 @@
|
||||
void bt_settings_load(BtSettings* bt_settings) {
|
||||
furi_assert(bt_settings);
|
||||
|
||||
const bool success = saved_struct_load(
|
||||
const bool load_success = saved_struct_load(
|
||||
BT_SETTINGS_PATH, bt_settings, sizeof(BtSettings), BT_SETTINGS_MAGIC, BT_SETTINGS_VERSION);
|
||||
|
||||
if(!success) {
|
||||
if(!load_success) {
|
||||
FURI_LOG_W(TAG, "Failed to load settings, using defaults");
|
||||
memset(bt_settings, 0, sizeof(BtSettings));
|
||||
|
||||
bt_settings->enabled = false;
|
||||
bt_settings_save(bt_settings);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
entry,status,name,type,params
|
||||
Version,+,86.1,,
|
||||
Version,+,87.0,,
|
||||
Header,+,applications/services/bt/bt_service/bt.h,,
|
||||
Header,+,applications/services/bt/bt_service/bt_keys_storage.h,,
|
||||
Header,+,applications/services/cli/cli.h,,
|
||||
@@ -703,6 +703,7 @@ Function,+,bt_forget_bonded_devices,void,Bt*
|
||||
Function,+,bt_keys_storage_alloc,BtKeysStorage*,const char*
|
||||
Function,+,bt_keys_storage_delete,_Bool,BtKeysStorage*
|
||||
Function,+,bt_keys_storage_free,void,BtKeysStorage*
|
||||
Function,+,bt_keys_storage_get_root_keys,const GapRootSecurityKeys*,BtKeysStorage*
|
||||
Function,+,bt_keys_storage_is_changed,_Bool,BtKeysStorage*
|
||||
Function,+,bt_keys_storage_load,_Bool,BtKeysStorage*
|
||||
Function,+,bt_keys_storage_set_default_path,void,Bt*
|
||||
@@ -1173,7 +1174,7 @@ Function,+,furi_hal_adc_convert_vref,float,"FuriHalAdcHandle*, uint16_t"
|
||||
Function,+,furi_hal_adc_init,void,
|
||||
Function,+,furi_hal_adc_read,uint16_t,"FuriHalAdcHandle*, FuriHalAdcChannel"
|
||||
Function,+,furi_hal_adc_release,void,FuriHalAdcHandle*
|
||||
Function,+,furi_hal_bt_change_app,FuriHalBleProfileBase*,"const FuriHalBleProfileTemplate*, FuriHalBleProfileParams, GapEventCallback, void*"
|
||||
Function,+,furi_hal_bt_change_app,FuriHalBleProfileBase*,"const FuriHalBleProfileTemplate*, FuriHalBleProfileParams, const GapRootSecurityKeys*, GapEventCallback, void*"
|
||||
Function,+,furi_hal_bt_check_profile_type,_Bool,"FuriHalBleProfileBase*, const FuriHalBleProfileTemplate*"
|
||||
Function,+,furi_hal_bt_clear_white_list,_Bool,
|
||||
Function,+,furi_hal_bt_dump_state,void,FuriString*
|
||||
@@ -1200,7 +1201,7 @@ Function,+,furi_hal_bt_nvm_sram_sem_release,void,
|
||||
Function,+,furi_hal_bt_reinit,void,
|
||||
Function,+,furi_hal_bt_set_key_storage_change_callback,void,"BleGlueKeyStorageChangedCallback, void*"
|
||||
Function,+,furi_hal_bt_start_advertising,void,
|
||||
Function,+,furi_hal_bt_start_app,FuriHalBleProfileBase*,"const FuriHalBleProfileTemplate*, FuriHalBleProfileParams, GapEventCallback, void*"
|
||||
Function,+,furi_hal_bt_start_app,FuriHalBleProfileBase*,"const FuriHalBleProfileTemplate*, FuriHalBleProfileParams, const GapRootSecurityKeys*, GapEventCallback, void*"
|
||||
Function,+,furi_hal_bt_start_packet_rx,void,"uint8_t, uint8_t"
|
||||
Function,+,furi_hal_bt_start_packet_tx,void,"uint8_t, uint8_t, uint8_t"
|
||||
Function,+,furi_hal_bt_start_radio_stack,_Bool,
|
||||
@@ -1740,7 +1741,7 @@ Function,-,gap_extra_beacon_set_data,_Bool,"const uint8_t*, uint8_t"
|
||||
Function,-,gap_extra_beacon_start,_Bool,
|
||||
Function,-,gap_extra_beacon_stop,_Bool,
|
||||
Function,-,gap_get_state,GapState,
|
||||
Function,-,gap_init,_Bool,"GapConfig*, GapEventCallback, void*"
|
||||
Function,-,gap_init,_Bool,"GapConfig*, const GapRootSecurityKeys*, GapEventCallback, void*"
|
||||
Function,-,gap_start_advertising,void,
|
||||
Function,-,gap_stop_advertising,void,
|
||||
Function,-,gap_thread_stop,void,
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
entry,status,name,type,params
|
||||
Version,+,86.1,,
|
||||
Version,+,87.0,,
|
||||
Header,+,applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h,,
|
||||
Header,+,applications/services/bt/bt_service/bt.h,,
|
||||
Header,+,applications/services/bt/bt_service/bt_keys_storage.h,,
|
||||
@@ -780,6 +780,7 @@ Function,+,bt_forget_bonded_devices,void,Bt*
|
||||
Function,+,bt_keys_storage_alloc,BtKeysStorage*,const char*
|
||||
Function,+,bt_keys_storage_delete,_Bool,BtKeysStorage*
|
||||
Function,+,bt_keys_storage_free,void,BtKeysStorage*
|
||||
Function,+,bt_keys_storage_get_root_keys,const GapRootSecurityKeys*,BtKeysStorage*
|
||||
Function,+,bt_keys_storage_is_changed,_Bool,BtKeysStorage*
|
||||
Function,+,bt_keys_storage_load,_Bool,BtKeysStorage*
|
||||
Function,+,bt_keys_storage_set_default_path,void,Bt*
|
||||
@@ -1285,7 +1286,7 @@ Function,+,furi_hal_adc_convert_vref,float,"FuriHalAdcHandle*, uint16_t"
|
||||
Function,+,furi_hal_adc_init,void,
|
||||
Function,+,furi_hal_adc_read,uint16_t,"FuriHalAdcHandle*, FuriHalAdcChannel"
|
||||
Function,+,furi_hal_adc_release,void,FuriHalAdcHandle*
|
||||
Function,+,furi_hal_bt_change_app,FuriHalBleProfileBase*,"const FuriHalBleProfileTemplate*, FuriHalBleProfileParams, GapEventCallback, void*"
|
||||
Function,+,furi_hal_bt_change_app,FuriHalBleProfileBase*,"const FuriHalBleProfileTemplate*, FuriHalBleProfileParams, const GapRootSecurityKeys*, GapEventCallback, void*"
|
||||
Function,+,furi_hal_bt_check_profile_type,_Bool,"FuriHalBleProfileBase*, const FuriHalBleProfileTemplate*"
|
||||
Function,+,furi_hal_bt_clear_white_list,_Bool,
|
||||
Function,+,furi_hal_bt_dump_state,void,FuriString*
|
||||
@@ -1312,7 +1313,7 @@ Function,+,furi_hal_bt_nvm_sram_sem_release,void,
|
||||
Function,+,furi_hal_bt_reinit,void,
|
||||
Function,+,furi_hal_bt_set_key_storage_change_callback,void,"BleGlueKeyStorageChangedCallback, void*"
|
||||
Function,+,furi_hal_bt_start_advertising,void,
|
||||
Function,+,furi_hal_bt_start_app,FuriHalBleProfileBase*,"const FuriHalBleProfileTemplate*, FuriHalBleProfileParams, GapEventCallback, void*"
|
||||
Function,+,furi_hal_bt_start_app,FuriHalBleProfileBase*,"const FuriHalBleProfileTemplate*, FuriHalBleProfileParams, const GapRootSecurityKeys*, GapEventCallback, void*"
|
||||
Function,+,furi_hal_bt_start_packet_rx,void,"uint8_t, uint8_t"
|
||||
Function,+,furi_hal_bt_start_packet_tx,void,"uint8_t, uint8_t, uint8_t"
|
||||
Function,+,furi_hal_bt_start_radio_stack,_Bool,
|
||||
@@ -1961,7 +1962,7 @@ Function,-,gap_extra_beacon_set_data,_Bool,"const uint8_t*, uint8_t"
|
||||
Function,-,gap_extra_beacon_start,_Bool,
|
||||
Function,-,gap_extra_beacon_stop,_Bool,
|
||||
Function,-,gap_get_state,GapState,
|
||||
Function,-,gap_init,_Bool,"GapConfig*, GapEventCallback, void*"
|
||||
Function,-,gap_init,_Bool,"GapConfig*, const GapRootSecurityKeys*, GapEventCallback, void*"
|
||||
Function,-,gap_start_advertising,void,
|
||||
Function,-,gap_stop_advertising,void,
|
||||
Function,-,gap_thread_stop,void,
|
||||
|
||||
|
@@ -51,13 +51,6 @@ typedef enum {
|
||||
GapCommandKillThread,
|
||||
} GapCommand;
|
||||
|
||||
// Identity root key
|
||||
static const uint8_t gap_irk[16] =
|
||||
{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0};
|
||||
// Encryption root key
|
||||
static const uint8_t gap_erk[16] =
|
||||
{0xfe, 0xdc, 0xba, 0x09, 0x87, 0x65, 0x43, 0x21, 0xfe, 0xdc, 0xba, 0x09, 0x87, 0x65, 0x43, 0x21};
|
||||
|
||||
static Gap* gap = NULL;
|
||||
|
||||
static void gap_advertise_start(GapState new_state);
|
||||
@@ -333,7 +326,9 @@ static void set_manufacturer_data(uint8_t* mfg_data, uint8_t mfg_data_len) {
|
||||
gap->service.mfg_data_len += mfg_data_len;
|
||||
}
|
||||
|
||||
static void gap_init_svc(Gap* gap) {
|
||||
static void gap_init_svc(Gap* gap, const GapRootSecurityKeys* root_keys) {
|
||||
furi_check(root_keys);
|
||||
|
||||
tBleStatus status;
|
||||
uint32_t srd_bd_addr[2];
|
||||
|
||||
@@ -351,9 +346,9 @@ static void gap_init_svc(Gap* gap) {
|
||||
aci_hal_write_config_data(
|
||||
CONFIG_DATA_RANDOM_ADDRESS_OFFSET, CONFIG_DATA_RANDOM_ADDRESS_LEN, (uint8_t*)srd_bd_addr);
|
||||
// Set Identity root key used to derive LTK and CSRK
|
||||
aci_hal_write_config_data(CONFIG_DATA_IR_OFFSET, CONFIG_DATA_IR_LEN, (uint8_t*)gap_irk);
|
||||
aci_hal_write_config_data(CONFIG_DATA_IR_OFFSET, CONFIG_DATA_IR_LEN, root_keys->irk);
|
||||
// Set Encryption root key used to derive LTK and CSRK
|
||||
aci_hal_write_config_data(CONFIG_DATA_ER_OFFSET, CONFIG_DATA_ER_LEN, (uint8_t*)gap_erk);
|
||||
aci_hal_write_config_data(CONFIG_DATA_ER_OFFSET, CONFIG_DATA_ER_LEN, root_keys->erk);
|
||||
// Set TX Power to 0 dBm
|
||||
aci_hal_set_tx_power_level(1, 0x19);
|
||||
// Initialize GATT interface
|
||||
@@ -535,7 +530,11 @@ static void gap_advetise_timer_callback(void* context) {
|
||||
furi_check(furi_message_queue_put(gap->command_queue, &command, 0) == FuriStatusOk);
|
||||
}
|
||||
|
||||
bool gap_init(GapConfig* config, GapEventCallback on_event_cb, void* context) {
|
||||
bool gap_init(
|
||||
GapConfig* config,
|
||||
const GapRootSecurityKeys* root_keys,
|
||||
GapEventCallback on_event_cb,
|
||||
void* context) {
|
||||
if(!ble_glue_is_radio_stack_ready()) {
|
||||
return false;
|
||||
}
|
||||
@@ -548,7 +547,7 @@ bool gap_init(GapConfig* config, GapEventCallback on_event_cb, void* context) {
|
||||
gap->advertise_timer = furi_timer_alloc(gap_advetise_timer_callback, FuriTimerTypeOnce, NULL);
|
||||
// Initialization of GATT & GAP layer
|
||||
gap->service.adv_name = config->adv_name;
|
||||
gap_init_svc(gap);
|
||||
gap_init_svc(gap, root_keys);
|
||||
ble_event_dispatcher_init();
|
||||
// Initialization of the GAP state
|
||||
gap->state_mutex = furi_mutex_alloc(FuriMutexTypeNormal);
|
||||
@@ -573,14 +572,13 @@ bool gap_init(GapConfig* config, GapEventCallback on_event_cb, void* context) {
|
||||
set_manufacturer_data(gap->config->mfg_data, gap->config->mfg_data_len);
|
||||
}
|
||||
|
||||
gap->service.adv_svc_uuid_len = 1;
|
||||
if(gap->config->adv_service.UUID_Type == UUID_TYPE_16) {
|
||||
uint8_t adv_service_uid[2];
|
||||
gap->service.adv_svc_uuid_len = 1;
|
||||
adv_service_uid[0] = gap->config->adv_service.Service_UUID_16 & 0xff;
|
||||
adv_service_uid[1] = gap->config->adv_service.Service_UUID_16 >> 8;
|
||||
set_advertisment_service_uid(adv_service_uid, sizeof(adv_service_uid));
|
||||
} else if(gap->config->adv_service.UUID_Type == UUID_TYPE_128) {
|
||||
gap->service.adv_svc_uuid_len = 1;
|
||||
set_advertisment_service_uid(
|
||||
gap->config->adv_service.Service_UUID_128,
|
||||
sizeof(gap->config->adv_service.Service_UUID_128));
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <furi_hal_version.h>
|
||||
|
||||
#define GAP_MAC_ADDR_SIZE (6)
|
||||
#define GAP_KEY_SIZE (0x10)
|
||||
|
||||
/*
|
||||
* GAP helpers - background thread that handles BLE GAP events and advertising.
|
||||
@@ -83,7 +84,18 @@ typedef struct {
|
||||
GapConnectionParamsRequest conn_param;
|
||||
} GapConfig;
|
||||
|
||||
bool gap_init(GapConfig* config, GapEventCallback on_event_cb, void* context);
|
||||
typedef struct {
|
||||
// Encryption Root key. Must be unique per-device (or app)
|
||||
uint8_t erk[GAP_KEY_SIZE];
|
||||
// Identity Root key. Used for resolving RPAs, if configured
|
||||
uint8_t irk[GAP_KEY_SIZE];
|
||||
} GapRootSecurityKeys;
|
||||
|
||||
bool gap_init(
|
||||
GapConfig* config,
|
||||
const GapRootSecurityKeys* root_keys,
|
||||
GapEventCallback on_event_cb,
|
||||
void* context);
|
||||
|
||||
void gap_start_advertising(void);
|
||||
|
||||
|
||||
@@ -36,6 +36,9 @@ static FuriHalBt furi_hal_bt = {
|
||||
.stack = FuriHalBtStackUnknown,
|
||||
};
|
||||
|
||||
static FuriHalBleProfileBase* current_profile = NULL;
|
||||
static GapConfig current_config = {0};
|
||||
|
||||
void furi_hal_bt_init(void) {
|
||||
FURI_LOG_I(TAG, "Start BT initialization");
|
||||
furi_hal_bus_enable(FuriHalBusHSEM);
|
||||
@@ -149,9 +152,6 @@ bool furi_hal_bt_is_testing_supported(void) {
|
||||
}
|
||||
}
|
||||
|
||||
static FuriHalBleProfileBase* current_profile = NULL;
|
||||
static GapConfig current_config = {0};
|
||||
|
||||
bool furi_hal_bt_check_profile_type(
|
||||
FuriHalBleProfileBase* profile,
|
||||
const FuriHalBleProfileTemplate* profile_template) {
|
||||
@@ -165,10 +165,12 @@ bool furi_hal_bt_check_profile_type(
|
||||
FuriHalBleProfileBase* furi_hal_bt_start_app(
|
||||
const FuriHalBleProfileTemplate* profile_template,
|
||||
FuriHalBleProfileParams params,
|
||||
const GapRootSecurityKeys* root_keys,
|
||||
GapEventCallback event_cb,
|
||||
void* context) {
|
||||
furi_check(event_cb);
|
||||
furi_check(profile_template);
|
||||
furi_check(root_keys);
|
||||
furi_check(current_profile == NULL);
|
||||
|
||||
do {
|
||||
@@ -183,7 +185,7 @@ FuriHalBleProfileBase* furi_hal_bt_start_app(
|
||||
|
||||
profile_template->get_gap_config(¤t_config, params);
|
||||
|
||||
if(!gap_init(¤t_config, event_cb, context)) {
|
||||
if(!gap_init(¤t_config, root_keys, event_cb, context)) {
|
||||
gap_thread_stop();
|
||||
FURI_LOG_E(TAG, "Failed to init GAP");
|
||||
break;
|
||||
@@ -239,12 +241,11 @@ void furi_hal_bt_reinit(void) {
|
||||
FuriHalBleProfileBase* furi_hal_bt_change_app(
|
||||
const FuriHalBleProfileTemplate* profile_template,
|
||||
FuriHalBleProfileParams profile_params,
|
||||
const GapRootSecurityKeys* root_keys,
|
||||
GapEventCallback event_cb,
|
||||
void* context) {
|
||||
furi_check(event_cb);
|
||||
|
||||
furi_hal_bt_reinit();
|
||||
return furi_hal_bt_start_app(profile_template, profile_params, event_cb, context);
|
||||
return furi_hal_bt_start_app(profile_template, profile_params, root_keys, event_cb, context);
|
||||
}
|
||||
|
||||
bool furi_hal_bt_is_active(void) {
|
||||
|
||||
@@ -77,6 +77,7 @@ bool furi_hal_bt_check_profile_type(
|
||||
*
|
||||
* @param profile_template FuriHalBleProfileTemplate instance
|
||||
* @param params Parameters to pass to the profile. Can be NULL
|
||||
* @param root_keys pointer to root keys
|
||||
* @param event_cb GapEventCallback instance
|
||||
* @param context pointer to context
|
||||
*
|
||||
@@ -85,6 +86,7 @@ bool furi_hal_bt_check_profile_type(
|
||||
FURI_WARN_UNUSED FuriHalBleProfileBase* furi_hal_bt_start_app(
|
||||
const FuriHalBleProfileTemplate* profile_template,
|
||||
FuriHalBleProfileParams params,
|
||||
const GapRootSecurityKeys* root_keys,
|
||||
GapEventCallback event_cb,
|
||||
void* context);
|
||||
|
||||
@@ -100,6 +102,7 @@ void furi_hal_bt_reinit(void);
|
||||
* @param profile_template FuriHalBleProfileTemplate instance
|
||||
* @param profile_params Parameters to pass to the profile. Can be NULL
|
||||
* @param event_cb GapEventCallback instance
|
||||
* @param root_keys pointer to root keys
|
||||
* @param context pointer to context
|
||||
*
|
||||
* @return instance of profile, NULL on failure
|
||||
@@ -107,6 +110,7 @@ void furi_hal_bt_reinit(void);
|
||||
FURI_WARN_UNUSED FuriHalBleProfileBase* furi_hal_bt_change_app(
|
||||
const FuriHalBleProfileTemplate* profile_template,
|
||||
FuriHalBleProfileParams profile_params,
|
||||
const GapRootSecurityKeys* root_keys,
|
||||
GapEventCallback event_cb,
|
||||
void* context);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user