hal: route BLE FW flashing through Memory HAL
What changed, and why it matters
This commit is a software architecture refactor: it moves the Bluetooth firmware flashing logic behind a higher-level 'Memory HAL' interface. It does not change the actual security behavior of the firmware upgrade process; it only reorganizes the code so that the same operations go through a trait method instead of being called directly. There is no indication this fixes a vulnerability or introduces a new one.
No security action required. Treat as normal code-quality refactor. If auditing Bluetooth firmware upgrade security, focus on the unchanged authorization and verification logic in bluetooth.rs rather than this HAL abstraction.
Security signals we found
Refactor only: same underlying SPI write call with same addresses and chunk size
Added chunk length and address-overflow validation in trait implementation
No change to upgrade authorization (hardcoded ALLOWED_HASH remains in bluetooth.rs)
No vendor security disclosure or advisory referenced
Evidence from the diff
The change introduces BleFirmwareSlot, BLE_FW_FLASH_CHUNK_SIZE, and ble_firmware_flash_chunk() to the Memory trait and updates the Bluetooth upgrade code to use this abstraction. The production implementation (BitBox02Memory) still delegates to crate::spi_mem::write_protected() with the same base addresses (BLE_FIRMWARE_1_ADDR, BLE_FIRMWARE_2_ADDR) and the same 4096-byte chunk size. Bounds checks on chunk length and address arithmetic overflow are added. The test implementation validates chunk size. No security bug is fixed or created by this refactor.
Changed components
src/rust/bitbox-hal/src/memory.rssrc/rust/bitbox02-rust/src/hal/testing/memory.rssrc/rust/bitbox02-rust/src/hww/api/bluetooth.rssrc/rust/bitbox02/src/hal/memory.rsInspect captured patch +71 / −16
diff --git a/src/rust/bitbox-hal/src/memory.rs b/src/rust/bitbox-hal/src/memory.rs
index 00fbbdc..9953ef3 100644
--- a/src/rust/bitbox-hal/src/memory.rs
+++ b/src/rust/bitbox-hal/src/memory.rs
@@ -43,10 +43,26 @@ pub struct BleMetadata {
pub firmware_checksums: [u8; 2],
}
+#[derive(Copy, Clone, Debug, Eq, PartialEq)]
+pub enum BleFirmwareSlot {
+ First,
+ Second,
+}
+
pub trait Memory {
+ /// We want to write FW to the memory chip in erase-size chunks, so that we don't repeatedly
+ /// need to read-erase-write the same sector.
+ const BLE_FW_FLASH_CHUNK_SIZE: u32;
+
fn ble_enabled(&mut self) -> bool;
fn ble_enable(&mut self, enable: bool) -> Result<(), ()>;
fn get_active_ble_firmware_version(&mut self) -> Result<String, Error>;
+ fn ble_firmware_flash_chunk(
+ &mut self,
+ slot: BleFirmwareSlot,
+ chunk_index: u32,
+ chunk: &[u8],
+ ) -> Result<(), Error>;
fn ble_get_metadata(&mut self) -> BleMetadata;
fn set_ble_metadata(&mut self, metadata: &BleMetadata) -> Result<(), Error>;
fn get_securechip_type(&mut self) -> Result<SecurechipType, ()>;
diff --git a/src/rust/bitbox02-rust/src/hal/testing/memory.rs b/src/rust/bitbox02-rust/src/hal/testing/memory.rs
index be2f36e..6e7a624 100644
--- a/src/rust/bitbox02-rust/src/hal/testing/memory.rs
+++ b/src/rust/bitbox02-rust/src/hal/testing/memory.rs
@@ -3,7 +3,9 @@
use alloc::string::String;
use alloc::vec::Vec;
-use crate::hal::memory::{BleMetadata, Error, PasswordStretchAlgo, Platform, SecurechipType};
+use crate::hal::memory::{
+ BleFirmwareSlot, BleMetadata, Error, PasswordStretchAlgo, Platform, SecurechipType,
+};
pub struct TestingMemory {
ble_enabled: bool,
@@ -91,6 +93,8 @@ impl TestingMemory {
}
impl crate::hal::Memory for TestingMemory {
+ const BLE_FW_FLASH_CHUNK_SIZE: u32 = 4096;
+
fn ble_enabled(&mut self) -> bool {
self.ble_enabled
}
@@ -104,6 +108,18 @@ impl crate::hal::Memory for TestingMemory {
Ok(self.active_ble_firmware_version.clone())
}
+ fn ble_firmware_flash_chunk(
+ &mut self,
+ _slot: BleFirmwareSlot,
+ _chunk_index: u32,
+ chunk: &[u8],
+ ) -> Result<(), Error> {
+ if chunk.len() > Self::BLE_FW_FLASH_CHUNK_SIZE as usize {
+ return Err(Error::InvalidInput);
+ }
+ Ok(())
+ }
+
fn ble_get_metadata(&mut self) -> BleMetadata {
self.ble_metadata
}
diff --git a/src/rust/bitbox02-rust/src/hww/api/bluetooth.rs b/src/rust/bitbox02-rust/src/hww/api/bluetooth.rs
index 83287c4..0479a74 100644
--- a/src/rust/bitbox02-rust/src/hww/api/bluetooth.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/bluetooth.rs
@@ -21,10 +21,6 @@ use bitbox02::spi_mem;
const ALLOWED_HASH: [u8; 32] =
hex!("1e4aa8364e935c0785e4f891208307d832f788172e4bf61621de6df9ec3c215f");
-// We want to write FW to the memory chip in erase-size chunks, so that we don't repeatedly need to
-// read-erase-write the same sector.
-const SPI_ERASE_SIZE: u32 = 4096;
-
/// Like `hww::next_request`, but for Bluetooth requests/responses.
async fn next_request(response: Response) -> Result<Request, Error> {
let request =
@@ -56,8 +52,8 @@ trait Funcs {
async fn get_fw_chunk(&mut self, offset: u32, length: u32) -> Result<Vec<u8>, Error>;
}
-async fn _process_upgrade(
- memory: &mut impl Memory,
+async fn _process_upgrade<M: Memory>(
+ memory: &mut M,
funcs: &mut impl Funcs,
progress: &mut impl Progress,
request: &pb::BluetoothUpgradeInitRequest,
@@ -71,22 +67,24 @@ async fn _process_upgrade(
// We work on the inactive firmware memory area.
let inactive_index: u8 = if ble_metadata.active_index == 0 { 1 } else { 0 };
- let inactive_ble_fw_address = if inactive_index == 0 {
- spi_mem::BLE_FIRMWARE_1_ADDR
+ let inactive_slot = if inactive_index == 0 {
+ hal_memory::BleFirmwareSlot::First
} else {
- spi_mem::BLE_FIRMWARE_2_ADDR
+ hal_memory::BleFirmwareSlot::Second
};
let mut firmware_hasher = Sha256::new();
let mut firmware_checksum = 0u8;
+ let flash_chunk_size = M::BLE_FW_FLASH_CHUNK_SIZE;
+
// The host needs to send this many chunks.
- let num_chunks = request.firmware_length.div_ceil(SPI_ERASE_SIZE);
+ let num_chunks = request.firmware_length.div_ceil(flash_chunk_size);
// Stream chunks from host.
for chunk_index in 0..num_chunks {
- let chunk_offset = chunk_index * SPI_ERASE_SIZE;
- let chunk_length = core::cmp::min(SPI_ERASE_SIZE, request.firmware_length - chunk_offset);
+ let chunk_offset = chunk_index * flash_chunk_size;
+ let chunk_length = core::cmp::min(flash_chunk_size, request.firmware_length - chunk_offset);
let chunk: Vec<u8> = funcs.get_fw_chunk(chunk_offset, chunk_length).await?;
if chunk.len() != chunk_length as usize {
return Err(Error::InvalidInput);
@@ -96,8 +94,7 @@ async fn _process_upgrade(
firmware_checksum ^= byte;
}
- spi_mem::write_protected(inactive_ble_fw_address + chunk_offset, &chunk)
- .map_err(|_| Error::Memory)?;
+ memory.ble_firmware_flash_chunk(inactive_slot, chunk_index, &chunk)?;
// Update progress.
progress.set((chunk_index + 1) as f32 / (num_chunks as f32));
diff --git a/src/rust/bitbox02/src/hal/memory.rs b/src/rust/bitbox02/src/hal/memory.rs
index e46e146..444df07 100644
--- a/src/rust/bitbox02/src/hal/memory.rs
+++ b/src/rust/bitbox02/src/hal/memory.rs
@@ -4,7 +4,9 @@ use alloc::string::String;
use alloc::vec::Vec;
use bitbox_hal::Memory;
-use bitbox_hal::memory::{BleMetadata, Error, PasswordStretchAlgo, Platform, SecurechipType};
+use bitbox_hal::memory::{
+ BleFirmwareSlot, BleMetadata, Error, PasswordStretchAlgo, Platform, SecurechipType,
+};
pub struct BitBox02Memory;
@@ -77,6 +79,10 @@ fn to_bitbox02_ble_metadata(metadata: &BleMetadata) -> crate::memory::BleMetadat
}
impl Memory for BitBox02Memory {
+ /// We want to write FW to the memory chip in erase-size chunks, so that we don't repeatedly
+ /// need to read-erase-write the same sector.
+ const BLE_FW_FLASH_CHUNK_SIZE: u32 = 4096;
+
fn ble_enabled(&mut self) -> bool {
crate::memory::ble_enabled()
}
@@ -89,6 +95,26 @@ impl Memory for BitBox02Memory {
crate::spi_mem::get_active_ble_firmware_version().map_err(|_| Error::Unknown)
}
+ fn ble_firmware_flash_chunk(
+ &mut self,
+ slot: BleFirmwareSlot,
+ chunk_index: u32,
+ chunk: &[u8],
+ ) -> Result<(), Error> {
+ if chunk.len() > Self::BLE_FW_FLASH_CHUNK_SIZE as usize {
+ return Err(Error::InvalidInput);
+ }
+ let base = match slot {
+ BleFirmwareSlot::First => crate::spi_mem::BLE_FIRMWARE_1_ADDR,
+ BleFirmwareSlot::Second => crate::spi_mem::BLE_FIRMWARE_2_ADDR,
+ };
+ let chunk_offset: u32 = chunk_index
+ .checked_mul(Self::BLE_FW_FLASH_CHUNK_SIZE)
+ .ok_or(Error::InvalidInput)?;
+ let address = base.checked_add(chunk_offset).ok_or(Error::InvalidInput)?;
+ crate::spi_mem::write_protected(address, chunk).map_err(|_| Error::Unknown)
+ }
+
fn ble_get_metadata(&mut self) -> BleMetadata {
to_hal_ble_metadata(crate::memory::get_ble_metadata())
}
Why this scored 12/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.