What changed, and why it matters
This commit fixes a bug in the BitBox02 hardware wallet firmware where two Bluetooth privacy values were accidentally created from overlapping random bytes. The device intended to use the first 16 random bytes for a secret key (IRK) and a separate later 6 bytes for the public Bluetooth address. Due to an indexing mistake, the address was taken from bytes 6-11 instead of bytes 16-21, meaning 6 bytes of the secret key were also broadcast as part of the public address. This leaks part of the secret key to anyone scanning Bluetooth signals nearby, slightly weakening Bluetooth privacy. A unit test was added to prevent the bug from returning.
Users with BitBox02 Plus devices should update to firmware containing this fix to ensure their BLE IRK is not partially leaked via the identity address. Reviewers should confirm the regression test passes and consider whether any previously generated BLE addresses should be rotated.
Security signals we found
Information disclosure: 6 bytes of the 16-byte BLE Identity Resolving Key leaked into the public BLE identity address.
Privacy weakening: partial IRK exposure reduces the effectiveness of BLE privacy/resolving mechanisms.
Bug class: off-by/incorrect buffer offset in memcpy.
Regression test added to enforce correct random byte partitioning.
Evidence from the diff
In memory_reset_hww(), the firmware generates 32 random bytes and splits them into ble_identity_resolving_key (16 bytes) and ble_identity_address (6 bytes). The buggy code copied the address from random_bytes[sizeof(ble_identity_address)] (i.e., offset 6), causing it to overlap with the IRK at offsets 6-11. The fix changes the offset to sizeof(ble_identity_resolving_key) (16), so the two values are disjoint as designed. The identity address is then OR’ed with 0xc to mark it as a public static address and is transmitted over BLE, so 6 bytes of the 16-byte IRK were effectively exposed in every advertisement. The patch is a one-line offset correction plus a regression test that verifies the IRK uses bytes 0-15 and the address uses bytes 16-21.
Changed components
src/memory/memory.c - memory_reset_hww() BLE initialization pathBitBox02 Plus BLE identity address and IRK generationtest/unit-test/test_memory.c - new _test_memory_reset_hww_ble regression testInspect captured patch +55 / −1
diff --git a/src/memory/memory.c b/src/memory/memory.c
index 3873d8b..cef2812 100644
--- a/src/memory/memory.c
+++ b/src/memory/memory.c
@@ -386,7 +386,7 @@ bool memory_reset_hww(void)
sizeof(chunk_shared.fields.ble_identity_resolving_key));
memcpy(
&chunk_shared.fields.ble_identity_address[0],
- &random_bytes[sizeof(chunk_shared.fields.ble_identity_address)],
+ &random_bytes[sizeof(chunk_shared.fields.ble_identity_resolving_key)],
sizeof(chunk_shared.fields.ble_identity_address));
// Two most significant bits must be set to indicate "public static address". See ch. 1.3.2
diff --git a/test/unit-test/test_memory.c b/test/unit-test/test_memory.c
index e29b7ec..b71287c 100644
--- a/test/unit-test/test_memory.c
+++ b/test/unit-test/test_memory.c
@@ -375,6 +375,59 @@ static void _test_memory_reset_hww(void** state)
assert_false(memory_reset_hww());
}
+static void _test_memory_reset_hww_ble(void** state)
+{
+ // Make the platform BitBox02 Plus so that the BLE branch in
+ // memory_reset_hww() is executed.
+ EMPTYCHUNK(empty_shared_chunk);
+ chunk_shared_t shared_chunk = {0};
+ memcpy(shared_chunk.bytes, empty_shared_chunk, CHUNK_SIZE);
+ shared_chunk.fields.platform = MEMORY_PLATFORM_BITBOX02_PLUS;
+
+ // First shared bootdata read is used by memory_get_platform()
+ will_return(__wrap_memory_read_shared_bootdata_fake, shared_chunk.bytes);
+
+ EXPECT_RESET;
+
+ // Second random_32_bytes() call in memory_reset_hww()
+ uint8_t ble_random[32] = {
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a,
+ 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15,
+ 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
+ };
+ will_return(_mock_random_32_bytes, ble_random);
+
+ // Second shared bootdata read is used inside the BLE branch of memory_reset_hww()
+ chunk_shared_t shared_chunk2 = {0};
+ memcpy(shared_chunk2.bytes, empty_shared_chunk, CHUNK_SIZE);
+ shared_chunk2.fields.platform = MEMORY_PLATFORM_BITBOX02_PLUS;
+ will_return(__wrap_memory_read_shared_bootdata_fake, shared_chunk2.bytes);
+
+ // Build expected shared chunk after BLE re-initialization
+ chunk_shared_t expected_shared = shared_chunk2;
+
+ // IRK must be the first 16 bytes of ble_random.
+ memcpy(expected_shared.fields.ble_identity_resolving_key, &ble_random[0], MEMORY_BLE_IRK_LEN);
+
+ // Identity address must be the next 6 bytes of ble_random (bytes 16..21).
+ // Top bits of the first byte are OR'ed with 0xc.
+ memcpy(
+ expected_shared.fields.ble_identity_address,
+ &ble_random[MEMORY_BLE_IRK_LEN],
+ MEMORY_BLE_ADDR_LEN);
+ expected_shared.fields.ble_identity_address[0] |= 0xc;
+
+ // Bond DB must be reset to 0xff
+ memset(expected_shared.fields.ble_bond_db, 0xff, sizeof(expected_shared.fields.ble_bond_db));
+
+ // Expect write to FLASH_SHARED_DATA_START with the updated shared chunk
+ expect_value(__wrap_memory_write_to_address_fake, base, FLASH_SHARED_DATA_START);
+ expect_value(__wrap_memory_write_to_address_fake, addr, 0);
+ expect_memory(__wrap_memory_write_to_address_fake, chunk, expected_shared.bytes, CHUNK_SIZE);
+
+ assert_true(memory_reset_hww());
+}
+
static void _test_memory_get_device_name_default(void** state)
{
char name_out[MEMORY_DEVICE_NAME_MAX_LEN] = {0};
@@ -573,6 +626,7 @@ int main(void)
cmocka_unit_test(_test_memory_is_mnemonic_passphrase_enabled),
cmocka_unit_test(_test_memory_set_mnemonic_passphrase_enabled),
cmocka_unit_test(_test_memory_reset_hww),
+ cmocka_unit_test(_test_memory_reset_hww_ble),
cmocka_unit_test(_test_memory_get_device_name_default),
cmocka_unit_test(_test_memory_get_device_name_default_bluetooth),
cmocka_unit_test(_test_memory_get_device_name_invalid),
Why this scored 51/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.