Skip to content

Commit 0d5beed

Browse files
authored
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
1 parent 30077dd commit 0d5beed

File tree

11 files changed

+390
-85
lines changed

11 files changed

+390
-85
lines changed

applications/debug/unit_tests/tests/bt/bt_test.c

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,23 @@
44

55
#include <bt/bt_service/bt_keys_storage.h>
66
#include <storage/storage.h>
7+
#include <toolbox/saved_struct.h>
78

89
#define BT_TEST_KEY_STORAGE_FILE_PATH EXT_PATH("unit_tests/bt_test.keys")
10+
#define BT_TEST_MIGRATION_FILE_PATH EXT_PATH("unit_tests/bt_migration_test.keys")
911
#define BT_TEST_NVM_RAM_BUFF_SIZE (507 * 4) // The same as in ble NVM storage
1012

13+
// Identity root key
14+
static const uint8_t gap_legacy_irk[16] =
15+
{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0};
16+
// Encryption root key
17+
static const uint8_t gap_legacy_erk[16] =
18+
{0xfe, 0xdc, 0xba, 0x09, 0x87, 0x65, 0x43, 0x21, 0xfe, 0xdc, 0xba, 0x09, 0x87, 0x65, 0x43, 0x21};
19+
20+
// Test constants for migration (matching bt_keys_storage.c)
21+
#define BT_KEYS_STORAGE_MAGIC_TEST (0x18)
22+
#define BT_KEYS_STORAGE_VERSION_1_TEST (1)
23+
1124
typedef struct {
1225
Storage* storage;
1326
BtKeysStorage* bt_keys_storage;
@@ -88,6 +101,134 @@ static void bt_test_keys_remove_test_file(void) {
88101
"Can't remove test file");
89102
}
90103

104+
// Helper function to create a version 0 file manually
105+
static bool
106+
bt_test_create_v0_file(const char* file_path, const uint8_t* nvm_data, size_t nvm_size) {
107+
// Version 0 files use saved_struct format with magic 0x18, version 0, containing only BLE pairing data
108+
return saved_struct_save(
109+
file_path,
110+
nvm_data,
111+
nvm_size,
112+
BT_KEYS_STORAGE_MAGIC_TEST,
113+
0); // Version 0
114+
}
115+
116+
// Helper function to verify file format version
117+
static bool bt_test_verify_file_version(const char* file_path, uint32_t expected_version) {
118+
uint8_t magic, version;
119+
size_t size;
120+
121+
if(!saved_struct_get_metadata(file_path, &magic, &version, &size)) {
122+
return false;
123+
}
124+
125+
return (magic == BT_KEYS_STORAGE_MAGIC_TEST && version == expected_version);
126+
}
127+
128+
// Test migration from version 0 to version 1, including root key preservation
129+
static void bt_test_migration_v0_to_v1(void) {
130+
// Create test NVM data
131+
const size_t test_nvm_size = 100;
132+
uint8_t test_nvm_data[test_nvm_size];
133+
for(size_t i = 0; i < test_nvm_size; i++) {
134+
test_nvm_data[i] = (uint8_t)(i & 0xFF);
135+
}
136+
137+
// Create a version 0 file
138+
mu_assert(
139+
bt_test_create_v0_file(BT_TEST_MIGRATION_FILE_PATH, test_nvm_data, test_nvm_size),
140+
"Failed to create version 0 test file");
141+
142+
// Create BT keys storage and load the v0 file (should trigger migration)
143+
BtKeysStorage* migration_storage = bt_keys_storage_alloc(BT_TEST_MIGRATION_FILE_PATH);
144+
uint8_t loaded_buffer[BT_TEST_NVM_RAM_BUFF_SIZE];
145+
memset(loaded_buffer, 0, sizeof(loaded_buffer));
146+
bt_keys_storage_set_ram_params(migration_storage, loaded_buffer, sizeof(loaded_buffer));
147+
148+
// Load should succeed and migrate v0 to v1
149+
mu_assert(bt_keys_storage_load(migration_storage), "Failed to load and migrate v0 file");
150+
151+
// Verify the file is now version 1
152+
mu_assert(
153+
bt_test_verify_file_version(BT_TEST_MIGRATION_FILE_PATH, BT_KEYS_STORAGE_VERSION_1_TEST),
154+
"File was not migrated to version 1");
155+
156+
// Verify the NVM data was preserved during migration
157+
mu_assert(
158+
memcmp(test_nvm_data, loaded_buffer, test_nvm_size) == 0,
159+
"NVM data was corrupted during migration");
160+
161+
// Verify that legacy root keys are used after migration
162+
const GapRootSecurityKeys* migrated_keys = bt_keys_storage_get_root_keys(migration_storage);
163+
mu_assert(
164+
memcmp(migrated_keys->irk, gap_legacy_irk, sizeof(gap_legacy_irk)) == 0,
165+
"IRK not set to legacy after migration");
166+
mu_assert(
167+
memcmp(migrated_keys->erk, gap_legacy_erk, sizeof(gap_legacy_erk)) == 0,
168+
"ERK not set to legacy after migration");
169+
170+
bt_keys_storage_free(migration_storage);
171+
storage_simply_remove(bt_test->storage, BT_TEST_MIGRATION_FILE_PATH);
172+
}
173+
174+
// Test that migration preserves existing pairing data and root keys are not changed on reload
175+
static void bt_test_migration_preserves_pairings_and_keys(void) {
176+
const size_t pairing_data_size = 200;
177+
uint8_t pairing_data[pairing_data_size];
178+
for(size_t i = 0; i < pairing_data_size; i++) {
179+
pairing_data[i] = (uint8_t)((i * 7 + 42) & 0xFF);
180+
}
181+
mu_assert(
182+
bt_test_create_v0_file(BT_TEST_MIGRATION_FILE_PATH, pairing_data, pairing_data_size),
183+
"Failed to create v0 file with pairing data");
184+
185+
GapRootSecurityKeys keys_after_first_load;
186+
for(int iteration = 0; iteration < 2; iteration++) {
187+
BtKeysStorage* storage = bt_keys_storage_alloc(BT_TEST_MIGRATION_FILE_PATH);
188+
uint8_t buffer[BT_TEST_NVM_RAM_BUFF_SIZE];
189+
memset(buffer, 0, sizeof(buffer));
190+
bt_keys_storage_set_ram_params(storage, buffer, sizeof(buffer));
191+
mu_assert(bt_keys_storage_load(storage), "Failed to load on iteration");
192+
mu_assert(
193+
memcmp(pairing_data, buffer, pairing_data_size) == 0,
194+
"Pairing data corrupted on iteration");
195+
const GapRootSecurityKeys* keys = bt_keys_storage_get_root_keys(storage);
196+
if(iteration == 0)
197+
memcpy(&keys_after_first_load, keys, sizeof(GapRootSecurityKeys));
198+
else
199+
mu_assert(
200+
memcmp(&keys_after_first_load, keys, sizeof(GapRootSecurityKeys)) == 0,
201+
"Root keys changed after reload");
202+
bt_keys_storage_free(storage);
203+
}
204+
storage_simply_remove(bt_test->storage, BT_TEST_MIGRATION_FILE_PATH);
205+
}
206+
207+
// Test that delete operation generates new secure keys in v1 and does not match legacy
208+
static void bt_test_delete_generates_new_keys_and_not_legacy(void) {
209+
BtKeysStorage* storage = bt_keys_storage_alloc(BT_TEST_MIGRATION_FILE_PATH);
210+
uint8_t buffer[BT_TEST_NVM_RAM_BUFF_SIZE];
211+
memset(buffer, 0x55, sizeof(buffer));
212+
bt_keys_storage_set_ram_params(storage, buffer, sizeof(buffer));
213+
mu_assert(bt_keys_storage_update(storage, buffer, 100), "Failed to create initial v1 file");
214+
const GapRootSecurityKeys* original_keys = bt_keys_storage_get_root_keys(storage);
215+
uint8_t original_keys_copy[sizeof(GapRootSecurityKeys)];
216+
memcpy(original_keys_copy, original_keys, sizeof(original_keys_copy));
217+
bt_keys_storage_delete(storage);
218+
const GapRootSecurityKeys* new_keys = bt_keys_storage_get_root_keys(storage);
219+
mu_assert(
220+
memcmp(original_keys_copy, new_keys, sizeof(original_keys_copy)) != 0,
221+
"Root keys were not regenerated after delete");
222+
mu_assert(
223+
memcmp(new_keys->irk, gap_legacy_irk, sizeof(gap_legacy_irk)) != 0,
224+
"IRK after delete should not match legacy");
225+
mu_assert(
226+
memcmp(new_keys->erk, gap_legacy_erk, sizeof(gap_legacy_erk)) != 0,
227+
"ERK after delete should not match legacy");
228+
bt_keys_storage_free(storage);
229+
storage_simply_remove(bt_test->storage, BT_TEST_MIGRATION_FILE_PATH);
230+
}
231+
91232
MU_TEST(bt_test_keys_storage_serial_profile) {
92233
furi_check(bt_test);
93234

@@ -96,10 +237,28 @@ MU_TEST(bt_test_keys_storage_serial_profile) {
96237
bt_test_keys_remove_test_file();
97238
}
98239

240+
MU_TEST(bt_test_migration_v0_to_v1_test) {
241+
furi_check(bt_test);
242+
bt_test_migration_v0_to_v1();
243+
}
244+
245+
MU_TEST(bt_test_migration_preserves_pairings_and_keys_test) {
246+
furi_check(bt_test);
247+
bt_test_migration_preserves_pairings_and_keys();
248+
}
249+
250+
MU_TEST(bt_test_delete_generates_new_keys_and_not_legacy_test) {
251+
furi_check(bt_test);
252+
bt_test_delete_generates_new_keys_and_not_legacy();
253+
}
254+
99255
MU_TEST_SUITE(test_bt) {
100256
bt_test_alloc();
101257

102258
MU_RUN_TEST(bt_test_keys_storage_serial_profile);
259+
MU_RUN_TEST(bt_test_migration_v0_to_v1_test);
260+
MU_RUN_TEST(bt_test_migration_preserves_pairings_and_keys_test);
261+
MU_RUN_TEST(bt_test_delete_generates_new_keys_and_not_legacy_test);
103262

104263
bt_test_free();
105264
}

applications/services/bt/bt_service/bt.c

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,7 @@ static void bt_change_profile(Bt* bt, BtMessage* message) {
403403
bt->current_profile = furi_hal_bt_change_app(
404404
message->data.profile.template,
405405
message->data.profile.params,
406+
bt_keys_storage_get_root_keys(bt->keys_storage),
406407
bt_on_gap_event_callback,
407408
bt);
408409
if(bt->current_profile) {
@@ -458,16 +459,19 @@ static void bt_load_keys(Bt* bt) {
458459
bt_keys_storage_load(bt->keys_storage);
459460

460461
bt->current_profile = NULL;
461-
462462
} else {
463463
FURI_LOG_I(TAG, "Keys unchanged");
464464
}
465465
}
466466

467467
static void bt_start_application(Bt* bt) {
468468
if(!bt->current_profile) {
469-
bt->current_profile =
470-
furi_hal_bt_change_app(ble_profile_serial, NULL, bt_on_gap_event_callback, bt);
469+
bt->current_profile = furi_hal_bt_change_app(
470+
ble_profile_serial,
471+
NULL,
472+
bt_keys_storage_get_root_keys(bt->keys_storage),
473+
bt_on_gap_event_callback,
474+
bt);
471475

472476
if(!bt->current_profile) {
473477
FURI_LOG_E(TAG, "BLE App start failed");

0 commit comments

Comments
 (0)