From d237e155d6b2cc63b7bfe1c0c8bfb15597c91f49 Mon Sep 17 00:00:00 2001 From: tychovrahe Date: Wed, 24 Jan 2024 13:59:09 +0100 Subject: [PATCH] feat(core): rudimentary BLE support [no changelog] Co-authored-by: Martin Milata --- .../extmod/modtrezorio/modtrezorio-ble.h | 201 ++++++ .../extmod/modtrezorio/modtrezorio-poll.h | 89 ++- core/embed/extmod/modtrezorio/modtrezorio.c | 16 +- .../extmod/modtrezorutils/modtrezorutils.c | 6 + core/embed/firmware/main.c | 13 + core/embed/lib/ble/ble.h | 9 + core/embed/lib/ble/dfu.c | 137 ++++ core/embed/lib/ble/dfu.h | 16 + core/embed/lib/ble/fwu.c | 664 ++++++++++++++++++ core/embed/lib/ble/fwu.h | 128 ++++ core/embed/lib/ble/int_comm_defs.h | 48 ++ core/embed/lib/ble/messages.c | 127 ++++ core/embed/lib/ble/messages.h | 22 + core/embed/lib/ble/state.c | 70 ++ core/embed/lib/ble/state.h | 29 + core/embed/rust/Cargo.toml | 1 + core/embed/trezorhal/ble_hal.h | 32 + core/embed/trezorhal/unix/ble.c | 32 + core/mocks/generated/trezorio/__init__.pyi | 5 +- core/mocks/generated/trezorio/ble.pyi | 69 ++ core/mocks/generated/trezorutils.pyi | 1 + .../site_tools/micropython/__init__.py | 2 + core/src/all_modules.py | 14 + core/src/apps/management/ble/__init__.py | 35 + .../apps/management/ble/comparison_request.py | 19 + core/src/apps/management/ble/disconnect.py | 16 + core/src/apps/management/ble/erase_bonds.py | 16 + .../apps/management/ble/pairing_request.py | 19 + .../src/apps/management/ble/repair_request.py | 14 + .../management/ble/upload_firmware_init.py | 64 ++ core/src/apps/workflow_handlers.py | 22 +- core/src/main.py | 4 + core/src/session.py | 6 + core/src/trezor/utils.py | 1 + core/src/trezor/wire/__init__.py | 27 +- core/src/trezor/wire/context.py | 44 +- 36 files changed, 1977 insertions(+), 41 deletions(-) create mode 100644 core/embed/extmod/modtrezorio/modtrezorio-ble.h create mode 100644 core/embed/lib/ble/ble.h create mode 100644 core/embed/lib/ble/dfu.c create mode 100644 core/embed/lib/ble/dfu.h create mode 100644 core/embed/lib/ble/fwu.c create mode 100644 core/embed/lib/ble/fwu.h create mode 100644 core/embed/lib/ble/int_comm_defs.h create mode 100644 core/embed/lib/ble/messages.c create mode 100644 core/embed/lib/ble/messages.h create mode 100644 core/embed/lib/ble/state.c create mode 100644 core/embed/lib/ble/state.h create mode 100644 core/embed/trezorhal/ble_hal.h create mode 100644 core/embed/trezorhal/unix/ble.c create mode 100644 core/mocks/generated/trezorio/ble.pyi create mode 100644 core/src/apps/management/ble/__init__.py create mode 100644 core/src/apps/management/ble/comparison_request.py create mode 100644 core/src/apps/management/ble/disconnect.py create mode 100644 core/src/apps/management/ble/erase_bonds.py create mode 100644 core/src/apps/management/ble/pairing_request.py create mode 100644 core/src/apps/management/ble/repair_request.py create mode 100644 core/src/apps/management/ble/upload_firmware_init.py diff --git a/core/embed/extmod/modtrezorio/modtrezorio-ble.h b/core/embed/extmod/modtrezorio/modtrezorio-ble.h new file mode 100644 index 000000000..9d8f134ad --- /dev/null +++ b/core/embed/extmod/modtrezorio/modtrezorio-ble.h @@ -0,0 +1,201 @@ +/* + * This file is part of the Trezor project, https://trezor.io/ + * + * Copyright (c) SatoshiLabs + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "ble/dfu.h" +#include "ble/messages.h" + +/// package: trezorio.ble + +/// INTERNAL: int # interface id for internal (stm<->nrf) connection +/// EXTERNAL: int # interface id for ble client connection + +/// def update_init(data: bytes, binsize: int) -> bool: +/// """ +/// Initializes the BLE firmware update. Returns true if the update finished +/// with only the initial chunk. False means calling `update_chunk` is +/// expected. +/// """ +STATIC mp_obj_t mod_trezorio_BLE_update_init(mp_obj_t data, mp_obj_t binsize) { + mp_buffer_info_t buffer = {0}; + mp_int_t binsize_int = mp_obj_get_int(binsize); + + mp_get_buffer_raise(data, &buffer, MP_BUFFER_READ); + + ble_set_dfu_mode(true); + + dfu_result_t result = dfu_update_init(buffer.buf, buffer.len, binsize_int); + if (result == DFU_NEXT_CHUNK) { + return mp_const_false; + } else if (result == DFU_SUCCESS) { + ble_set_dfu_mode(false); + return mp_const_true; + } else { + ble_set_dfu_mode(false); + mp_raise_msg(&mp_type_RuntimeError, "Upload failed."); + } +} +STATIC MP_DEFINE_CONST_FUN_OBJ_2(mod_trezorio_BLE_update_init_obj, + mod_trezorio_BLE_update_init); + +/// def update_chunk(chunk: bytes) -> bool: +/// """ +/// Writes next chunk of BLE firmware update. Returns true if the update is +/// finished, or false if more chunks are expected. +/// """ +STATIC mp_obj_t mod_trezorio_BLE_update_chunk(mp_obj_t data) { + mp_buffer_info_t buffer = {0}; + + mp_get_buffer_raise(data, &buffer, MP_BUFFER_READ); + + dfu_result_t result = dfu_update_chunk(buffer.buf, buffer.len); + + if (result == DFU_NEXT_CHUNK) { + return mp_const_false; + } else if (result == DFU_SUCCESS) { + ble_set_dfu_mode(false); + return mp_const_true; + } else { + ble_set_dfu_mode(false); + mp_raise_msg(&mp_type_RuntimeError, "Upload failed."); + } +} +STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorio_BLE_update_chunk_obj, + mod_trezorio_BLE_update_chunk); + +/// def write_int(self, msg: bytes) -> int: +/// """ +/// Sends internal message to NRF. +/// """ +STATIC mp_obj_t mod_trezorio_BLE_write_int(mp_obj_t self, mp_obj_t msg) { + mp_buffer_info_t buf = {0}; + mp_get_buffer_raise(msg, &buf, MP_BUFFER_READ); + ble_int_comm_send(buf.buf, buf.len, INTERNAL_MESSAGE); + return MP_OBJ_NEW_SMALL_INT(buf.len); +} +STATIC MP_DEFINE_CONST_FUN_OBJ_2(mod_trezorio_BLE_write_int_obj, + mod_trezorio_BLE_write_int); + +/// def write_ext(self, msg: bytes) -> int: +/// """ +/// Sends message over BLE +/// """ +STATIC mp_obj_t mod_trezorio_BLE_write_ext(mp_obj_t self, mp_obj_t msg) { + mp_buffer_info_t buf = {0}; + mp_get_buffer_raise(msg, &buf, MP_BUFFER_READ); + ble_int_comm_send(buf.buf, buf.len, EXTERNAL_MESSAGE); + return MP_OBJ_NEW_SMALL_INT(buf.len); +} +STATIC MP_DEFINE_CONST_FUN_OBJ_2(mod_trezorio_BLE_write_ext_obj, + mod_trezorio_BLE_write_ext); + +/// def erase_bonds() -> None: +/// """ +/// Erases all BLE bonds +/// """ +STATIC mp_obj_t mod_trezorio_BLE_erase_bonds(void) { + bool result = send_erase_bonds(); + if (result) { + return mp_const_none; + } else { + mp_raise_msg(&mp_type_RuntimeError, "Erase bonds failed."); + } +} +STATIC MP_DEFINE_CONST_FUN_OBJ_0(mod_trezorio_BLE_erase_bonds_obj, + mod_trezorio_BLE_erase_bonds); + +/// def start_comm() -> None: +/// """ +/// Start communication with BLE chip +/// """ +STATIC mp_obj_t mod_trezorio_BLE_start_comm(void) { + ble_comm_start(); + auto_start_advertising(); + return mp_const_none; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_0(mod_trezorio_BLE_start_comm_obj, + mod_trezorio_BLE_start_comm); + +/// def start_advertising(whitelist: bool) -> None: +/// """ +/// Start advertising +/// """ +STATIC mp_obj_t mod_trezorio_BLE_start_advertising(mp_obj_t whitelist) { + bool whitelist_bool = mp_obj_is_true(whitelist); + + start_advertising(whitelist_bool); + return mp_const_none; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorio_BLE_start_advertising_obj, + mod_trezorio_BLE_start_advertising); + +/// def stop_advertising(whitelist: bool) -> None: +/// """ +/// Stop advertising +/// """ +STATIC mp_obj_t mod_trezorio_BLE_stop_advertising(void) { + ble_comm_start(); + return mp_const_none; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_0(mod_trezorio_BLE_stop_advertising_obj, + mod_trezorio_BLE_stop_advertising); + +/// def disconnect() -> None: +/// """ +/// Disconnect BLE +/// """ +STATIC mp_obj_t mod_trezorio_BLE_disconnect(void) { + bool result = send_disconnect(); + if (result) { + return mp_const_none; + } else { + mp_raise_msg(&mp_type_RuntimeError, "Disconnect failed."); + } +} +STATIC MP_DEFINE_CONST_FUN_OBJ_0(mod_trezorio_BLE_disconnect_obj, + mod_trezorio_BLE_disconnect); + +STATIC const mp_rom_map_elem_t mod_trezorio_BLE_globals_table[] = { + {MP_ROM_QSTR(MP_QSTR_INTERNAL), MP_ROM_INT(BLE_IFACE_INT)}, + {MP_ROM_QSTR(MP_QSTR_EXTERNAL), MP_ROM_INT(BLE_IFACE_EXT)}, + {MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_ble)}, + {MP_ROM_QSTR(MP_QSTR_update_init), + MP_ROM_PTR(&mod_trezorio_BLE_update_init_obj)}, + {MP_ROM_QSTR(MP_QSTR_update_chunk), + MP_ROM_PTR(&mod_trezorio_BLE_update_chunk_obj)}, + {MP_ROM_QSTR(MP_QSTR_write_int), + MP_ROM_PTR(&mod_trezorio_BLE_write_int_obj)}, + {MP_ROM_QSTR(MP_QSTR_write_ext), + MP_ROM_PTR(&mod_trezorio_BLE_write_ext_obj)}, + {MP_ROM_QSTR(MP_QSTR_erase_bonds), + MP_ROM_PTR(&mod_trezorio_BLE_erase_bonds_obj)}, + {MP_ROM_QSTR(MP_QSTR_start_comm), + MP_ROM_PTR(&mod_trezorio_BLE_start_comm_obj)}, + {MP_ROM_QSTR(MP_QSTR_start_advertising), + MP_ROM_PTR(&mod_trezorio_BLE_start_advertising_obj)}, + {MP_ROM_QSTR(MP_QSTR_stop_advertising), + MP_ROM_PTR(&mod_trezorio_BLE_stop_advertising_obj)}, + {MP_ROM_QSTR(MP_QSTR_disconnect), + MP_ROM_PTR(&mod_trezorio_BLE_disconnect_obj)}, +}; +STATIC MP_DEFINE_CONST_DICT(mod_trezorio_BLE_globals, + mod_trezorio_BLE_globals_table); + +STATIC const mp_obj_module_t mod_trezorio_BLE_module = { + .base = {&mp_type_module}, + .globals = (mp_obj_dict_t *)&mod_trezorio_BLE_globals}; diff --git a/core/embed/extmod/modtrezorio/modtrezorio-poll.h b/core/embed/extmod/modtrezorio/modtrezorio-poll.h index da5b12247..5b703970f 100644 --- a/core/embed/extmod/modtrezorio/modtrezorio-poll.h +++ b/core/embed/extmod/modtrezorio/modtrezorio-poll.h @@ -24,14 +24,22 @@ #include "display.h" #include "embed/extmod/trezorobj.h" +#ifdef USE_BLE +#include "ble/int_comm_defs.h" +#include "ble/state.h" +#include "ble_hal.h" +#endif + +#define BLE_EVENTS_IFACE (252) #define USB_DATA_IFACE (253) #define BUTTON_IFACE (254) #define TOUCH_IFACE (255) +#define USB_RW_IFACE_MAX (15) // 0-15 reserved for USB +#define BLE_IFACE_INT (16) +#define BLE_IFACE_EXT (17) #define POLL_READ (0x0000) #define POLL_WRITE (0x0100) -extern bool usb_connected_previously; - /// package: trezorio.__init__ /// def poll(ifaces: Iterable[int], list_ref: list, timeout_ms: int) -> bool: @@ -145,35 +153,80 @@ STATIC mp_obj_t mod_trezorio_poll(mp_obj_t ifaces, mp_obj_t list_ref, } } #endif - else if (mode == POLL_READ) { - if (sectrue == usb_hid_can_read(iface)) { - uint8_t buf[64] = {0}; - int len = usb_hid_read(iface, buf, sizeof(buf)); - if (len > 0) { - ret->items[0] = MP_OBJ_NEW_SMALL_INT(i); - ret->items[1] = mp_obj_new_bytes(buf, len); - return mp_const_true; +#ifdef USE_BLE + else if (iface == BLE_EVENTS_IFACE) { + ble_event_poll(); + uint8_t connected = ble_connected(); + if (connected != ble_connected_previously) { + ble_connected_previously = connected; + ret->items[0] = MP_OBJ_NEW_SMALL_INT(i); + ret->items[1] = connected ? mp_const_true : mp_const_false; + return mp_const_true; + } + } +#endif + else if (iface <= USB_RW_IFACE_MAX) { + if (mode == POLL_READ) { + if (sectrue == usb_hid_can_read(iface)) { + uint8_t buf[64] = {0}; + int len = usb_hid_read(iface, buf, sizeof(buf)); + if (len > 0) { + ret->items[0] = MP_OBJ_NEW_SMALL_INT(i); + ret->items[1] = mp_obj_new_bytes(buf, len); + return mp_const_true; + } + } else if (sectrue == usb_webusb_can_read(iface)) { + uint8_t buf[64] = {0}; + int len = usb_webusb_read(iface, buf, sizeof(buf)); + if (len > 0) { + ret->items[0] = MP_OBJ_NEW_SMALL_INT(i); + ret->items[1] = mp_obj_new_bytes(buf, len); + return mp_const_true; + } } - } else if (sectrue == usb_webusb_can_read(iface)) { - uint8_t buf[64] = {0}; - int len = usb_webusb_read(iface, buf, sizeof(buf)); - if (len > 0) { + } else if (mode == POLL_WRITE) { + if (sectrue == usb_hid_can_write(iface)) { ret->items[0] = MP_OBJ_NEW_SMALL_INT(i); - ret->items[1] = mp_obj_new_bytes(buf, len); + ret->items[1] = mp_const_none; + return mp_const_true; + } else if (sectrue == usb_webusb_can_write(iface)) { + ret->items[0] = MP_OBJ_NEW_SMALL_INT(i); + ret->items[1] = mp_const_none; return mp_const_true; } } - } else if (mode == POLL_WRITE) { - if (sectrue == usb_hid_can_write(iface)) { + } +#ifdef USE_BLE + else if (iface == BLE_IFACE_INT) { + if (mode == POLL_READ) { + uint8_t buf[64] = {0}; + int len = ble_int_comm_receive(buf, sizeof(buf)); + if (len > 0) { + ret->items[0] = MP_OBJ_NEW_SMALL_INT(i); + ret->items[1] = mp_obj_new_bytes(buf, len); + return mp_const_true; + } + } else if (mode == POLL_WRITE) { ret->items[0] = MP_OBJ_NEW_SMALL_INT(i); ret->items[1] = mp_const_none; return mp_const_true; - } else if (sectrue == usb_webusb_can_write(iface)) { + } + } else if (iface == BLE_IFACE_EXT) { + if (mode == POLL_READ) { + uint8_t buf[BLE_PACKET_SIZE] = {0}; + int len = ble_ext_comm_receive(buf, sizeof(buf)); + if (len > 0) { + ret->items[0] = MP_OBJ_NEW_SMALL_INT(i); + ret->items[1] = mp_obj_new_bytes(buf, len); + return mp_const_true; + } + } else if (mode == POLL_WRITE) { ret->items[0] = MP_OBJ_NEW_SMALL_INT(i); ret->items[1] = mp_const_none; return mp_const_true; } } +#endif } if (mp_hal_ticks_ms() >= deadline) { diff --git a/core/embed/extmod/modtrezorio/modtrezorio.c b/core/embed/extmod/modtrezorio/modtrezorio.c index 72fa781f4..b75ef562c 100644 --- a/core/embed/extmod/modtrezorio/modtrezorio.c +++ b/core/embed/extmod/modtrezorio/modtrezorio.c @@ -34,6 +34,9 @@ // Whether USB data pins were connected on last check (USB configured) bool usb_connected_previously = true; +#ifdef USE_BLE +bool ble_connected_previously = false; +#endif #define CHECK_PARAM_RANGE(value, minimum, maximum) \ if (value < minimum || value > maximum) { \ @@ -48,6 +51,9 @@ bool usb_connected_previously = true; #include "modtrezorio-webusb.h" #include "modtrezorio-usb.h" // clang-format on +#ifdef USE_BLE +#include "modtrezorio-ble.h" +#endif #ifdef USE_SBU #include "modtrezorio-sbu.h" #endif @@ -57,7 +63,7 @@ bool usb_connected_previously = true; #endif /// package: trezorio.__init__ -/// from . import fatfs, sdcard +/// from . import fatfs, sdcard, ble /// POLL_READ: int # wait until interface is readable and return read data /// POLL_WRITE: int # wait until interface is writable @@ -74,8 +80,9 @@ bool usb_connected_previously = true; /// BUTTON_RIGHT: int # button number of right button /// USB_CHECK: int # interface id for check of USB data connection +/// BLE_CHECK: int # interface id for check of BLE data connection -/// WireInterface = Union[HID, WebUSB] +/// WireInterface = Union[HID, WebUSB, BleInterface] STATIC const mp_rom_map_elem_t mp_module_trezorio_globals_table[] = { {MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_trezorio)}, @@ -89,6 +96,10 @@ STATIC const mp_rom_map_elem_t mp_module_trezorio_globals_table[] = { {MP_ROM_QSTR(MP_QSTR_sdcard), MP_ROM_PTR(&mod_trezorio_sdcard_module)}, #endif +#ifdef USE_BLE + {MP_ROM_QSTR(MP_QSTR_ble), MP_ROM_PTR(&mod_trezorio_BLE_module)}, +#endif + #ifdef USE_TOUCH {MP_ROM_QSTR(MP_QSTR_TOUCH), MP_ROM_INT(TOUCH_IFACE)}, {MP_ROM_QSTR(MP_QSTR_TOUCH_START), MP_ROM_INT((TOUCH_START >> 24) & 0xFFU)}, @@ -117,6 +128,7 @@ STATIC const mp_rom_map_elem_t mp_module_trezorio_globals_table[] = { {MP_ROM_QSTR(MP_QSTR_POLL_WRITE), MP_ROM_INT(POLL_WRITE)}, {MP_ROM_QSTR(MP_QSTR_USB_CHECK), MP_ROM_INT(USB_DATA_IFACE)}, + {MP_ROM_QSTR(MP_QSTR_BLE_CHECK), MP_ROM_INT(BLE_EVENTS_IFACE)}, }; STATIC MP_DEFINE_CONST_DICT(mp_module_trezorio_globals, diff --git a/core/embed/extmod/modtrezorutils/modtrezorutils.c b/core/embed/extmod/modtrezorutils/modtrezorutils.c index 33f181381..f0898648a 100644 --- a/core/embed/extmod/modtrezorutils/modtrezorutils.c +++ b/core/embed/extmod/modtrezorutils/modtrezorutils.c @@ -378,6 +378,7 @@ STATIC mp_obj_str_t mod_trezorutils_full_name_obj = { /// """Minor version.""" /// VERSION_PATCH: int /// """Patch version.""" +/// USE_BLE: bool /// USE_SD_CARD: bool /// """Whether the hardware supports SD card.""" /// USE_BACKLIGHT: bool @@ -427,6 +428,11 @@ STATIC const mp_rom_map_elem_t mp_module_trezorutils_globals_table[] = { #else {MP_ROM_QSTR(MP_QSTR_USE_SD_CARD), mp_const_false}, #endif +#ifdef USE_BLE + {MP_ROM_QSTR(MP_QSTR_USE_BLE), mp_const_true}, +#else + {MP_ROM_QSTR(MP_QSTR_USE_BLE), mp_const_false}, +#endif #ifdef USE_BACKLIGHT {MP_ROM_QSTR(MP_QSTR_USE_BACKLIGHT), mp_const_true}, #else diff --git a/core/embed/firmware/main.c b/core/embed/firmware/main.c index 1a0d2d546..c8e770488 100644 --- a/core/embed/firmware/main.c +++ b/core/embed/firmware/main.c @@ -77,6 +77,12 @@ #include "optiga_transport.h" #include "secret.h" #endif +#ifdef USE_BLE +#include "ble/dfu.h" +#include "ble/messages.h" +#include "ble/state.h" +#include "ble_hal.h" +#endif #include "unit_variant.h" #ifdef SYSTEM_VIEW @@ -181,6 +187,13 @@ int main(void) { memzero(secret, sizeof(secret)); #endif +#ifdef USE_BLE + dfu_init(); + ble_comm_init(); + send_state_request(); + wait_for_answer(); +#endif + #if !defined TREZOR_MODEL_1 drop_privileges(); #endif diff --git a/core/embed/lib/ble/ble.h b/core/embed/lib/ble/ble.h new file mode 100644 index 000000000..78f8d69c2 --- /dev/null +++ b/core/embed/lib/ble/ble.h @@ -0,0 +1,9 @@ +#ifndef BLE_BLE_H +#define BLE_BLE_H + +#include "state.h" +#include "messages.h" +#include "fwu.h" +#include "dfu.h" + +#endif diff --git a/core/embed/lib/ble/dfu.c b/core/embed/lib/ble/dfu.c new file mode 100644 index 000000000..61e38d33d --- /dev/null +++ b/core/embed/lib/ble/dfu.c @@ -0,0 +1,137 @@ +/* + * This file is part of the Trezor project, https://trezor.io/ + * + * Copyright (c) SatoshiLabs + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "common.h" +#include TREZOR_BOARD +#include "ble_hal.h" +#include "common.h" +#include "dfu.h" +#include "fwu.h" + +const uint32_t TIMEOUT_MS = 2000; + +static TFwu sFwu; + +static uint32_t tick_start = 0; + +void txFunction(struct SFwu *fwu, uint8_t *buf, uint8_t len); +static uint8_t readData(uint8_t *data, int maxLen); + +void dfu_init(void) {} + +dfu_result_t dfu_update_process(void) { + while (1) { + // Can send 4 chars... + // (On a microcontroller, you'd use the TX Empty interrupt or test a + // register.) + + fwuCanSendData(&sFwu, 4); + + // Data available? Get up to 4 bytes... + // (On a microcontroller, you'd use the RX Available interrupt or test a + // register.) + uint8_t rxBuf[4]; + uint8_t rxLen = readData(rxBuf, 4); + if (rxLen > 0) { + fwuDidReceiveData(&sFwu, rxBuf, rxLen); + } + + // Give the firmware update module a timeslot to continue the process. + EFwuProcessStatus status = fwuYield(&sFwu, 0); + + if (status == FWU_STATUS_COMPLETION) { + ble_reset(); + return DFU_SUCCESS; + } + + if (status == FWU_STATUS_FAILURE) { + return DFU_FAIL; + } + + if (hal_ticks_ms() - tick_start > TIMEOUT_MS) { + return DFU_FAIL; + } + + if (fwuIsReadyForChunk(&sFwu)) { + return DFU_NEXT_CHUNK; + } + } +} + +dfu_result_t dfu_update_init(uint8_t *data, uint32_t len, uint32_t binary_len) { + sFwu.commandObject = data; + sFwu.commandObjectLen = len; + sFwu.dataObject = NULL; + sFwu.dataObjectLen = binary_len; + sFwu.txFunction = txFunction; + sFwu.responseTimeoutMillisec = TIMEOUT_MS; + + if (!ble_reset_to_bootloader()) { + return DFU_FAIL; + } + + tick_start = hal_ticks_ms(); + + // Prepare the firmware update process. + fwuInit(&sFwu); + + // Start the firmware update process. + fwuExec(&sFwu); + + return dfu_update_process(); +} + +dfu_result_t dfu_update_chunk(uint8_t *data, uint32_t len) { + tick_start = hal_ticks_ms(); + + fwuSendChunk(&sFwu, data, len); + + return dfu_update_process(); +} + +dfu_result_t dfu_update_do(uint8_t *datfile, uint32_t datfile_len, + uint8_t *binfile, uint32_t binfile_len) { + uint32_t chunk_offset = 0; + uint32_t rem_data = binfile_len; + + dfu_result_t res = dfu_update_init(datfile, datfile_len, binfile_len); + + while (res == DFU_NEXT_CHUNK) { + // Send the next chunk of the data object. + uint32_t chunk_size = 4096; + if (rem_data < 4096) { + chunk_size = rem_data; + rem_data = 0; + } else { + rem_data -= 4096; + } + res = dfu_update_chunk(&binfile[chunk_offset], chunk_size); + chunk_offset += chunk_size; + } + + return res; +} + +void txFunction(struct SFwu *fwu, uint8_t *buf, uint8_t len) { + ble_comm_send(buf, len); +} + +static uint8_t readData(uint8_t *data, int maxLen) { + return ble_comm_receive(data, maxLen); +} diff --git a/core/embed/lib/ble/dfu.h b/core/embed/lib/ble/dfu.h new file mode 100644 index 000000000..cd064b035 --- /dev/null +++ b/core/embed/lib/ble/dfu.h @@ -0,0 +1,16 @@ +#ifndef __DFU_H__ +#define __DFU_H__ + +typedef enum { + DFU_NEXT_CHUNK, + DFU_SUCCESS, + DFU_FAIL, +} dfu_result_t; + +void dfu_init(void); +dfu_result_t dfu_update_init(uint8_t *data, uint32_t len, uint32_t binary_len); +dfu_result_t dfu_update_chunk(uint8_t *data, uint32_t len); +dfu_result_t dfu_update_do(uint8_t *datfile, uint32_t datfile_len, + uint8_t *binfile, uint32_t binfile_len); + +#endif diff --git a/core/embed/lib/ble/fwu.c b/core/embed/lib/ble/fwu.c new file mode 100644 index 000000000..4937aed4e --- /dev/null +++ b/core/embed/lib/ble/fwu.c @@ -0,0 +1,664 @@ +// +// fwu.c +// nrf52-dfu +// +// C library for the Nordic firmware update protocol. +// +// Created by Andreas Schweizer on 30.11.2018. +// Copyright © 2018-2019 Classy Code GmbH +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +#include "fwu.h" +#include + +// TODO too big, split in separate files! + +typedef enum { + FWU_PS_IDLE = 0, + FWU_PS_PING = 10, + FWU_PS_RCPT_NOTIF = 20, + FWU_PS_MTU = 30, + FWU_PS_OBJ1_SELECT = 40, + FWU_PS_OBJ1_CREATE = 50, + FWU_PS_OBJ1_WRITE = 60, + FWU_PS_OBJ1_CRC_GET = 70, + FWU_PS_OBJ1_EXECUTE = 80, + FWU_PS_OBJ2_SELECT = 90, + FWU_PS_OBJ2_WAIT_FOR_CHUNK = 91, + FWU_PS_OBJ2_CREATE = 100, + FWU_PS_OBJ2_WRITE = 110, + FWU_PS_OBJ2_CRC_GET = 120, + FWU_PS_OBJ2_EXECUTE = 130, + FWU_PS_FAIL = 254, + FWU_PS_DONE = 255, +} EFwuProcessState; + +// Process requests, triggering process state transitions. +typedef enum { + FWU_PR_NONE = 0, + FWU_PR_START = 1, + FWU_PR_RECEIVED_RESPONSE, + FWU_PR_REQUEST_FAILED, + FWU_PR_REQUEST_SENT, +} EFwuProcessRequest; + +typedef enum { + FWU_CS_IDLE = 0, + FWU_CS_SEND = 1, // sending data from the private request buffer + FWU_CS_RECEIVE = 2, // receiving data into the private response buffer + FWU_CS_FAIL = 3, + FWU_CS_DONE = 4, +} EFwuCommandState; + +// Command requests, triggering command state transitions. +typedef enum { + FWU_CR_NONE = 0, + FWU_CR_SEND = 1, + FWU_CR_SENDONLY = 2, + FWU_CR_EOM_RECEIVED = 3, + FWU_CR_RX_OVERFLOW = 4, + FWU_CR_INVALID_ESCAPE_SEQ, +} EFwuCommandRequest; + +#define FWU_EOM 0xC0 +#define FWU_RESPONSE_START 0x60 +#define FWU_RESPONSE_SUCCESS 0x01 + +// PING 09 01 C0 -> 60 09 01 01 C0 +static uint8_t sPingRequest[] = {0x09, 0x01}; +static uint8_t sPingRequestLen = 2; + +// SET RECEIPT 02 00 00 C0 -> 60 02 01 C0 +static uint8_t sSetReceiptRequest[] = {0x02, 0x00, 0x00}; +static uint8_t sSetReceiptRequestLen = 3; + +// Get the preferred MTU size on the request. +// GET MTU 07 -> 60 07 01 83 00 C0 +static uint8_t sGetMtuRequest[] = {0x07}; +static uint8_t sGetMtuRequestLen = 1; + +// Triggers the last transferred object of the specified type to be selected +// and queries information (max size, cur offset, cur CRC) about the object. +// If there's no object of the specified type, the object type is still +// selected, CRC and offset are 0 in this case. +// SELECT OBJECT 06 01 C0 -> 60 06 01 00 01 00 00 00 00 00 00 00 00 00 00 C0 +static uint8_t sSelectObjectRequest[] = {0x06, 0x01}; +static uint8_t sSelectObjectRequestLen = 2; + +// Creating a command or data object; the target reserves the space, resets the +// progress since the last Execute command and selects the new object.) +// CREATE OBJECT 01 01 87 00 00 00 C0 -> 60 01 01 C0 +static uint8_t sCreateObjectRequest[] = {0x01, 0x01, 0x87, 0x00, 0x00, 0x00}; +static uint8_t sCreateObjectRequestLen = 6; + +// CRC GET 03 C0 -> 60 03 01 87 00 00 00 38 f4 97 72 C0 +static uint8_t sGetCrcRequest[] = {0x03}; +static uint8_t sGetCrcRequestLen = 1; + +// Execute an object after it has been fully transmitted. +// EXECUTE OBJECT 04 C0 -> 60 04 01 C0 +static uint8_t sExecuteObjectRequest[] = {0x04}; +static uint8_t sExecuteObjectRequestLen = 1; + +static void fwuYieldProcessFsm(TFwu *fwu, uint32_t elapsedMillisec); +static void fwuYieldCommandFsm(TFwu *fwu, uint32_t elapsedMillisec); + +static EFwuResponseStatus fwuTestReceivedPacketValid(TFwu *fwu); + +// Don't send more than FWU_REQUEST_BUF_SIZE bytes. +// Don't include the EOM. +static void fwuPrepareSendBuffer(TFwu *fwu, uint8_t *data, uint8_t len); + +static void fwuPrepareLargeObjectSendBuffer(TFwu *fwu, uint8_t requestCode); + +// static void fwuDebugPrintStatus(TFwu *fwu, char *msg); + +static void updateCrc(TFwu *fwu, uint8_t b); +static void fwuSignalFailure(TFwu *fwu, EFwuResponseStatus reason); +static inline uint16_t fwuLittleEndianToHost16(uint8_t *bytes); +static inline uint32_t fwuLittleEndianToHost32(uint8_t *bytes); +static inline void fwuHostToLittleEndian32(uint32_t v, uint8_t *bytes); + +// First function to call to set up the internal state in the FWU structure. +void fwuInit(TFwu *fwu) { + fwu->privateProcessState = FWU_PS_IDLE; + fwu->privateProcessRequest = FWU_PR_NONE; + fwu->privateCommandState = FWU_CS_IDLE; + + fwu->processStatus = FWU_STATUS_UNDEFINED; + fwu->responseStatus = FWU_RSP_OK; +} + +// Execute the firmware update. +void fwuExec(TFwu *fwu) { + // Start with sending a PING command to the target to see if it's there... + fwu->privateProcessRequest = FWU_PR_START; +} + +// Call regularly to allow asynchronous processing to continue. +EFwuProcessStatus fwuYield(TFwu *fwu, uint32_t elapsedMillisec) { + // Nothing to do if processing has failed or successfully completed... + if (fwu->processStatus == FWU_STATUS_FAILURE || + fwu->privateProcessState == FWU_PS_FAIL) { + return FWU_STATUS_FAILURE; + } else if (fwu->processStatus == FWU_STATUS_COMPLETION || + fwu->privateProcessState == FWU_PS_DONE) { + return FWU_STATUS_COMPLETION; + } + + // Processing is ongoing, yield to FSMs. + fwuYieldCommandFsm(fwu, elapsedMillisec); + fwuYieldProcessFsm(fwu, elapsedMillisec); + + return fwu->processStatus; +} + +// Call after data from the target has been received. +void fwuDidReceiveData(TFwu *fwu, uint8_t *bytes, uint8_t len) { + while (len > 0) { + if (fwu->privateResponseLen == FWU_RESPONSE_BUF_SIZE) { + fwu->privateCommandRequest = FWU_CR_RX_OVERFLOW; + return; + } + uint8_t c = *bytes++; + if (c == FWU_EOM) { + fwu->privateCommandRequest = FWU_CR_EOM_RECEIVED; + } + + if (c == 0xDB) { + fwu->privateResponseEscapeCharacter = 1; + } else { + if (fwu->privateResponseEscapeCharacter) { + fwu->privateResponseEscapeCharacter = 0; + if (c == 0xDC) { + c = 0xC0; + } else if (c == 0xDD) { + c = 0xDB; + } else { + fwu->privateCommandRequest = FWU_CR_INVALID_ESCAPE_SEQ; + return; + } + } + fwu->privateResponseBuf[fwu->privateResponseLen++] = c; + } + len--; + } +} + +// Inform the FWU module that it may send maxLen bytes of data to the target. +void fwuCanSendData(TFwu *fwu, uint8_t maxLen) { + fwu->privateSendBufSpace = maxLen; +} + +void fwuSendChunk(TFwu *fwu, uint8_t *buf, uint32_t len) { + if (fwu->privateProcessState == FWU_PS_OBJ2_WAIT_FOR_CHUNK && + fwu->privateDataObjectSize == len) { + fwu->dataObject = buf; + fwu->privateProcessState = FWU_PS_OBJ2_CREATE; + } +} + +bool fwuIsReadyForChunk(TFwu *fwu) { + return fwu->privateProcessState == FWU_PS_OBJ2_WAIT_FOR_CHUNK; +} + +static void fwuYieldProcessFsm(TFwu *fwu, uint32_t elapsedMillisec) { + uint8_t tmpPrivateProcessRequest = fwu->privateProcessRequest; + fwu->privateProcessRequest = FWU_PR_NONE; + + // No processing in final states + if (fwu->privateProcessState == FWU_PS_DONE || + fwu->privateProcessState == FWU_PS_FAIL) { + return; + } + + // Failure handling + if (tmpPrivateProcessRequest == FWU_PR_REQUEST_FAILED) { + fwu->privateProcessState = FWU_PS_FAIL; + fwu->processStatus = FWU_STATUS_FAILURE; + return; + } + + // Executing the firmware update process. + switch (fwu->privateProcessState) { + case FWU_PS_IDLE: + if (tmpPrivateProcessRequest == FWU_PR_START) { + // Send a PING and switch to the PING state to wait for the response. + fwuPrepareSendBuffer(fwu, sPingRequest, sPingRequestLen); + fwu->privateProcessState = FWU_PS_PING; + } + break; + + // PING: Check if the nRF52 DFU code is listening + case FWU_PS_PING: + // Wait for the PING response, then verify it. + if (tmpPrivateProcessRequest == FWU_PR_RECEIVED_RESPONSE) { + // ID match? + if (fwu->privateRequestBuf[1] == fwu->privateResponseBuf[3]) { + // Send a SET_RECEIPT and switch to the corresponding state to wait + // for the response. + fwuPrepareSendBuffer(fwu, sSetReceiptRequest, sSetReceiptRequestLen); + fwu->privateProcessState = FWU_PS_RCPT_NOTIF; + } else { + fwuSignalFailure(fwu, FWU_RSP_PING_ID_MISMATCH); + } + } + break; + + // RCPT_NOTIF: Define Receipt settings + case FWU_PS_RCPT_NOTIF: + // Wait for the SET_RECEIPT response. + if (tmpPrivateProcessRequest == FWU_PR_RECEIVED_RESPONSE) { + // Send a SET_RECEIPT and switch to the corresponding state to wait for + // the response. + fwuPrepareSendBuffer(fwu, sGetMtuRequest, sGetMtuRequestLen); + fwu->privateProcessState = FWU_PS_MTU; + } + break; + + // FWU_PS_MTU: Get maximum transmission unit size + case FWU_PS_MTU: + if (tmpPrivateProcessRequest == FWU_PR_RECEIVED_RESPONSE) { + fwu->privateMtuSize = + fwuLittleEndianToHost16(&fwu->privateResponseBuf[3]); + // Send a SET_RECEIPT and switch to the corresponding state to wait for + // the response. + sSelectObjectRequest[1] = 0x01; // select object 1 (command object) + fwuPrepareSendBuffer(fwu, sSelectObjectRequest, + sSelectObjectRequestLen); + fwu->privateProcessState = FWU_PS_OBJ1_SELECT; + } + break; + + // FWU_PS_OBJ1_SELECT: Select the INIT command object + case FWU_PS_OBJ1_SELECT: + if (tmpPrivateProcessRequest == FWU_PR_RECEIVED_RESPONSE) { + uint32_t maxSize = fwuLittleEndianToHost32(&fwu->privateResponseBuf[3]); + if (maxSize < fwu->commandObjectLen) { + fwuSignalFailure(fwu, FWU_RSP_INIT_COMMAND_TOO_LARGE); + } else { + sCreateObjectRequest[1] = 0x01; // create type 1 object (COMMAND) + fwuHostToLittleEndian32(fwu->commandObjectLen, + &sCreateObjectRequest[2]); + fwuPrepareSendBuffer(fwu, sCreateObjectRequest, + sCreateObjectRequestLen); + fwu->privateProcessState = FWU_PS_OBJ1_CREATE; + } + } + break; + + // FWU_PS_OBJ1_CREATE: Create the INIT command object + case FWU_PS_OBJ1_CREATE: + if (tmpPrivateProcessRequest == FWU_PR_RECEIVED_RESPONSE) { + fwu->privateProcessState = FWU_PS_OBJ1_WRITE; + fwu->privateObjectBuf = fwu->commandObject; + fwu->privateObjectLen = fwu->commandObjectLen; + fwu->privateObjectIx = 0; + fwu->privateObjectCrc = 0xffffffff; + fwuPrepareLargeObjectSendBuffer(fwu, 0x08); + } + break; + + // FWU_PS_OBJ1_WRITE: Write the INIT command object + case FWU_PS_OBJ1_WRITE: + if (tmpPrivateProcessRequest == FWU_PR_REQUEST_SENT) { + // more to send? + if (fwu->privateObjectIx == fwu->privateObjectLen) { + // no - request the CRC of the written data... + fwuPrepareSendBuffer(fwu, sGetCrcRequest, sGetCrcRequestLen); + fwu->privateProcessState = FWU_PS_OBJ1_CRC_GET; + } else { + fwuPrepareLargeObjectSendBuffer(fwu, 0x08); + } + } + break; + + // FWU_PS_OBJ1_CRC_GET: Checksum verification + case FWU_PS_OBJ1_CRC_GET: + if (tmpPrivateProcessRequest == FWU_PR_RECEIVED_RESPONSE) { + // uint32_t actualLen = + // fwuLittleEndianToHost32(&fwu->privateResponseBuf[3]); + uint32_t actualCks = + fwuLittleEndianToHost32(&fwu->privateResponseBuf[7]); + if (actualCks == ~fwu->privateObjectCrc) { + // Checksum is OK; execute the command! + fwuPrepareSendBuffer(fwu, sExecuteObjectRequest, + sExecuteObjectRequestLen); + fwu->privateProcessState = FWU_PS_OBJ1_EXECUTE; + } else { + fwuSignalFailure(fwu, FWU_RSP_CHECKSUM_ERROR); + } + } + break; + + case FWU_PS_OBJ1_EXECUTE: + if (tmpPrivateProcessRequest == FWU_PR_RECEIVED_RESPONSE) { + sSelectObjectRequest[1] = 0x02; // select object 2 (DATA object) + fwu->privateDataObjectOffset = 0; // from the beginning + fwuPrepareSendBuffer(fwu, sSelectObjectRequest, + sSelectObjectRequestLen); + fwu->privateProcessState = FWU_PS_OBJ2_SELECT; + } + break; + + // FWU_PS_OBJ2_SELECT: Select the DATA object + case FWU_PS_OBJ2_SELECT: + if (tmpPrivateProcessRequest == FWU_PR_RECEIVED_RESPONSE) { + fwu->privateDataObjectMaxSize = + fwuLittleEndianToHost32(&fwu->privateResponseBuf[3]); + fwu->privateObjectCrc = + 0xffffffff; // do it here because it's global for the entire blob + // We'll create and execute multiple data objects, so it's ok if the + // actual size is greater than max size. + fwu->privateDataObjectSize = + (fwu->dataObjectLen - + fwu->privateDataObjectOffset); // nof bytes remaining + if (fwu->privateDataObjectSize > fwu->privateDataObjectMaxSize) { + fwu->privateDataObjectSize = fwu->privateDataObjectMaxSize; + } + sCreateObjectRequest[1] = 0x02; // create type 2 object (COMMAND) + fwuHostToLittleEndian32(fwu->privateDataObjectSize, + &sCreateObjectRequest[2]); + fwuPrepareSendBuffer(fwu, sCreateObjectRequest, + sCreateObjectRequestLen); + fwu->privateProcessState = FWU_PS_OBJ2_WAIT_FOR_CHUNK; + } + break; + case FWU_PS_OBJ2_WAIT_FOR_CHUNK: + break; + // FWU_PS_OBJ2_CREATE: Create the DATA object + case FWU_PS_OBJ2_CREATE: + if (tmpPrivateProcessRequest == FWU_PR_RECEIVED_RESPONSE) { + fwu->privateProcessState = FWU_PS_OBJ2_WRITE; + fwu->privateObjectBuf = fwu->dataObject; + fwu->privateObjectLen = fwu->privateDataObjectSize; + fwu->privateObjectIx = 0; + fwuPrepareLargeObjectSendBuffer(fwu, 0x08); + } + break; + + // FWU_PS_OBJ2_WRITE: Write the DATA object + case FWU_PS_OBJ2_WRITE: + if (tmpPrivateProcessRequest == FWU_PR_REQUEST_SENT) { + // more to send? + if (fwu->privateObjectIx == fwu->privateObjectLen) { + // no - request the CRC of the written data... + fwuPrepareSendBuffer(fwu, sGetCrcRequest, sGetCrcRequestLen); + fwu->privateProcessState = FWU_PS_OBJ2_CRC_GET; + } else { + fwuPrepareLargeObjectSendBuffer(fwu, 0x08); + } + } + break; + + // FWU_PS_OBJ2_CRC_GET: Checksum verification + case FWU_PS_OBJ2_CRC_GET: + if (tmpPrivateProcessRequest == FWU_PR_RECEIVED_RESPONSE) { + // uint32_t actualLen = + // fwuLittleEndianToHost32(&fwu->privateResponseBuf[3]); + uint32_t actualCks = + fwuLittleEndianToHost32(&fwu->privateResponseBuf[7]); + if (actualCks == ~fwu->privateObjectCrc) { + // Checksum is OK; execute the command! + fwuPrepareSendBuffer(fwu, sExecuteObjectRequest, + sExecuteObjectRequestLen); + fwu->privateProcessState = FWU_PS_OBJ2_EXECUTE; + + } else { + fwuSignalFailure(fwu, FWU_RSP_CHECKSUM_ERROR); + } + } + break; + + case FWU_PS_OBJ2_EXECUTE: + if (tmpPrivateProcessRequest == FWU_PR_RECEIVED_RESPONSE) { + fwu->privateDataObjectOffset += fwu->privateDataObjectSize; + if (fwu->privateDataObjectOffset == fwu->dataObjectLen) { + fwu->privateProcessState = FWU_PS_DONE; + fwu->processStatus = FWU_STATUS_COMPLETION; + + } else { + // We'll create and execute multiple data objects, so it's ok if the + // actual size is greater than max size. + fwu->privateDataObjectSize = + (fwu->dataObjectLen - + fwu->privateDataObjectOffset); // nof bytes remaining + if (fwu->privateDataObjectSize > fwu->privateDataObjectMaxSize) { + fwu->privateDataObjectSize = fwu->privateDataObjectMaxSize; + } + sCreateObjectRequest[1] = 0x02; // create type 2 object (COMMAND) + fwuHostToLittleEndian32(fwu->privateDataObjectSize, + &sCreateObjectRequest[2]); + fwuPrepareSendBuffer(fwu, sCreateObjectRequest, + sCreateObjectRequestLen); + fwu->privateProcessState = FWU_PS_OBJ2_WAIT_FOR_CHUNK; + } + } + break; + + default: + fwu->privateProcessState = FWU_PS_FAIL; + break; + } +} + +static void fwuYieldCommandFsm(TFwu *fwu, uint32_t elapsedMillisec) { + uint8_t toSend; + + // Automatically return from final states to IDLE. + if (fwu->privateCommandState == FWU_CS_DONE || + fwu->privateCommandState == FWU_CS_FAIL) { + fwu->privateCommandState = FWU_CS_IDLE; + } + + // Timeout? + if (fwu->privateCommandState != FWU_CS_IDLE) { + if (fwu->privateCommandTimeoutRemainingMillisec < elapsedMillisec) { + fwu->privateCommandTimeoutRemainingMillisec = 0; + } else { + fwu->privateCommandTimeoutRemainingMillisec -= elapsedMillisec; + } + if (fwu->privateCommandTimeoutRemainingMillisec == 0) { + fwuSignalFailure(fwu, FWU_RSP_TIMEOUT); + return; + } + } + + // Catch errors + if (fwu->privateCommandRequest == FWU_CR_RX_OVERFLOW) { + fwuSignalFailure(fwu, FWU_RSP_RX_OVERFLOW); + return; + } + if (fwu->privateCommandRequest == FWU_CR_INVALID_ESCAPE_SEQ) { + fwuSignalFailure(fwu, FWU_RSP_RX_INVALID_ESCAPE_SEQ); + return; + } + + switch (fwu->privateCommandState) { + case FWU_CS_IDLE: + // Ready and waiting for a transmission request. + if (fwu->privateCommandRequest == FWU_CR_SEND || + fwu->privateCommandRequest == FWU_CR_SENDONLY) { + fwu->privateCommandSendOnly = + fwu->privateCommandRequest == FWU_CR_SENDONLY ? 1 : 0; + fwu->privateCommandRequest = FWU_CR_NONE; + fwu->privateCommandState = FWU_CS_SEND; + fwu->privateCommandTimeoutRemainingMillisec = + fwu->responseTimeoutMillisec; + } + break; + case FWU_CS_SEND: + // Continue sending data until the entire request has been sent. + toSend = fwu->privateRequestLen - fwu->privateRequestIx; + if (toSend == 0) { + if (fwu->privateCommandSendOnly) { + // This was a fire-and-forget request; we don't expect a response. + fwu->privateProcessRequest = FWU_PR_REQUEST_SENT; + fwu->privateCommandState = FWU_CS_DONE; + } else { + // The request has been sent; wait for response. + fwu->privateCommandState = FWU_CS_RECEIVE; + } + } else if (fwu->privateSendBufSpace > 0) { + uint8_t n = fwu->privateSendBufSpace; + if (n > toSend) { + n = toSend; + } + fwu->txFunction(fwu, &fwu->privateRequestBuf[fwu->privateRequestIx], n); + fwu->privateRequestIx += n; + } + break; + case FWU_CS_RECEIVE: + // Continue receiving data until the end-of-message marker has been + // received. + if (fwu->privateCommandRequest == FWU_CR_EOM_RECEIVED) { + fwu->privateCommandRequest = FWU_CR_NONE; + EFwuResponseStatus responseStatus = fwuTestReceivedPacketValid(fwu); + if (responseStatus == FWU_RSP_OK) { + // Inform the process state machine that command reception has + // completed. + fwu->privateProcessRequest = FWU_PR_RECEIVED_RESPONSE; + fwu->privateCommandState = FWU_CS_DONE; + } else { + fwu->responseStatus = responseStatus; + fwu->privateCommandState = FWU_CS_FAIL; + } + } + break; + default: + fwu->privateCommandState = FWU_CS_FAIL; + break; + } +} + +static EFwuResponseStatus fwuTestReceivedPacketValid(TFwu *fwu) { + // 60 C0 + if (fwu->privateResponseLen < 4) { + return FWU_RSP_TOO_SHORT; + } + if (fwu->privateResponseBuf[0] != FWU_RESPONSE_START) { + return FWU_RSP_START_MARKER_MISSING; + } + if (fwu->privateResponseBuf[1] != fwu->privateRequestBuf[0]) { + return FWU_RSP_REQUEST_REFERENCE_INVALID; + } + if (fwu->privateResponseBuf[2] != FWU_RESPONSE_SUCCESS) { + return FWU_RSP_ERROR_RESPONSE; + } + if (fwu->privateResponseBuf[fwu->privateResponseLen - 1] != FWU_EOM) { + return FWU_RSP_END_MARKER_MISSING; + } + return FWU_RSP_OK; +} + +static void fwuPrepareLargeObjectSendBuffer(TFwu *fwu, uint8_t requestCode) { + uint16_t bytesTodo = fwu->privateObjectLen - fwu->privateObjectIx; + uint16_t bufSpace = FWU_REQUEST_BUF_SIZE - 2; + + uint16_t i; + uint8_t *p = &fwu->privateRequestBuf[0]; + + *p++ = requestCode; + fwu->privateRequestLen = 2; // including requestCode and FWU_EOM + fwu->privateRequestIx = 0; + + if (bytesTodo > 32) { + bytesTodo = 32; + } + + for (i = 0; i < bytesTodo && bufSpace >= 2; i++) { + uint8_t b = fwu->privateObjectBuf[fwu->privateObjectIx]; + // SLIP escape characters: C0->DBDC, DB->DBDD + if (b == 0xC0 || b == 0xDB) { + *p++ = 0xDB; + *p++ = (b == 0xC0) ? 0xDC : 0xDD; + fwu->privateRequestLen += 2; + bufSpace -= 2; + } else { + *p++ = b; + fwu->privateRequestLen++; + bufSpace--; + } + updateCrc(fwu, b); + fwu->privateObjectIx++; + } + + *p = FWU_EOM; + fwu->privateCommandRequest = FWU_CR_SENDONLY; +} + +static void fwuPrepareSendBuffer(TFwu *fwu, uint8_t *data, uint8_t len) { + // TODO assert privateCommandState == FWU_CS_IDLE | _DONE | _FAIL + // TODO assert len <= FWU_REQUEST_BUF_SIZE + + uint8_t i; + uint8_t *p = &fwu->privateRequestBuf[0]; + + fwu->privateRequestIx = 0; + fwu->privateRequestLen = len + 1; + fwu->privateResponseLen = 0; + + // Copy the data into our internal buffer. + for (i = 0; i < len; i++) { + *p++ = *data++; + } + + // Add the end-of-message marker. + *p = FWU_EOM; + + // Ready to send! + fwu->privateCommandRequest = FWU_CR_SEND; +} + +static void updateCrc(TFwu *fwu, uint8_t b) { + uint8_t i; + uint32_t crc = fwu->privateObjectCrc; + crc ^= b; + for (i = 0; i < 8; i++) { + uint32_t m = (crc & 1) ? 0xffffffff : 0; + crc = (crc >> 1) ^ (0xedb88320u & m); + } + fwu->privateObjectCrc = crc; +} + +static void fwuSignalFailure(TFwu *fwu, EFwuResponseStatus reason) { + fwu->responseStatus = reason; + fwu->privateCommandState = FWU_CS_FAIL; + // Signal failure to process state machine + fwu->privateProcessRequest = FWU_PR_REQUEST_FAILED; +} + +static inline uint16_t fwuLittleEndianToHost16(uint8_t *bytes) { + return bytes[0] | ((uint16_t)bytes[1] << 8); +} + +static inline uint32_t fwuLittleEndianToHost32(uint8_t *bytes) { + return bytes[0] | ((uint16_t)bytes[1] << 8) | ((uint32_t)bytes[2] << 16) | + ((uint32_t)bytes[3] << 24); +} + +static inline void fwuHostToLittleEndian32(uint32_t v, uint8_t *bytes) { + uint8_t i; + for (i = 0; i < 4; i++) { + *bytes++ = v & 0xff; + v = v >> 8; + } +} diff --git a/core/embed/lib/ble/fwu.h b/core/embed/lib/ble/fwu.h new file mode 100644 index 000000000..a380b40c1 --- /dev/null +++ b/core/embed/lib/ble/fwu.h @@ -0,0 +1,128 @@ +// +// fwu.h +// nrf52-dfu +// +// C library for the Nordic firmware update protocol. +// +// Created by Andreas Schweizer on 30.11.2018. +// Copyright © 2018-2019 Classy Code GmbH +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +#ifndef __FWU_H__ +#define __FWU_H__ 1 + +#include +#include + +struct SFwu; + +#define FWU_REQUEST_BUF_SIZE 67 +#define FWU_RESPONSE_BUF_SIZE 16 + +typedef enum { + FWU_STATUS_UNDEFINED = 0, + FWU_STATUS_FAILURE = 1, + FWU_STATUS_COMPLETION = 2, +} EFwuProcessStatus; + +typedef enum { + FWU_RSP_OK = 0, + FWU_RSP_TOO_SHORT = 1, + FWU_RSP_START_MARKER_MISSING = 2, + FWU_RSP_END_MARKER_MISSING = 3, + FWU_RSP_REQUEST_REFERENCE_INVALID = 4, + FWU_RSP_ERROR_RESPONSE = 5, + FWU_RSP_TIMEOUT = 6, + FWU_RSP_PING_ID_MISMATCH = 7, + FWU_RSP_RX_OVERFLOW = 8, + FWU_RSP_INIT_COMMAND_TOO_LARGE = 9, + FWU_RSP_CHECKSUM_ERROR = 10, + FWU_RSP_DATA_OBJECT_TOO_LARGE = 11, + FWU_RSP_RX_INVALID_ESCAPE_SEQ = 12, +} EFwuResponseStatus; + +typedef void (*FTxFunction)(struct SFwu *fwu, uint8_t *buf, uint8_t len); + +typedef struct SFwu { + // --- public - define these before calling fwuInit --- + // .dat + uint8_t *commandObject; + uint32_t commandObjectLen; + // .bin + uint8_t *dataObject; + uint32_t dataObjectLen; + // Sending bytes to the target + FTxFunction txFunction; + // Timeout when waiting for a response from the target + uint32_t responseTimeoutMillisec; + // --- public - result codes + // Overall process status code + EFwuProcessStatus processStatus; + // Response status code + EFwuResponseStatus responseStatus; + // --- private, don't modify --- + uint32_t privateDataObjectOffset; + uint32_t privateDataObjectSize; + uint32_t privateDataObjectMaxSize; + uint8_t privateProcessState; + uint8_t privateCommandState; + uint8_t privateCommandSendOnly; + uint32_t privateCommandTimeoutRemainingMillisec; + uint8_t privateRequestBuf[FWU_REQUEST_BUF_SIZE + 1]; + uint8_t privateRequestLen; + uint8_t privateRequestIx; + uint8_t privateResponseBuf[FWU_RESPONSE_BUF_SIZE]; + uint8_t privateResponseEscapeCharacter; + uint8_t privateResponseLen; + uint32_t privateResponseTimeElapsedMillisec; + uint8_t privateSendBufSpace; + uint8_t privateProcessRequest; + uint8_t privateCommandRequest; + uint16_t privateMtuSize; + // sending a large object buffer + uint8_t *privateObjectBuf; + uint32_t privateObjectLen; + uint32_t privateObjectIx; + uint32_t privateObjectCrc; +} TFwu; + +// First function to call to set up the internal state in the FWU structure. +void fwuInit(TFwu *fwu); + +// Execute the firmware update. +void fwuExec(TFwu *fwu); + +// Call regularly to allow asynchronous processing to continue. +EFwuProcessStatus fwuYield(TFwu *fwu, uint32_t elapsedMillisec); + +// Call after data from the target has been received. +void fwuDidReceiveData(TFwu *fwu, uint8_t *bytes, uint8_t len); + +// Inform the FWU module that it may send maxLen bytes of data to the target. +void fwuCanSendData(TFwu *fwu, uint8_t maxLen); + +// Call to send a chunk of the data object to the target. +void fwuSendChunk(TFwu *fwu, uint8_t *buf, uint32_t len); + +// Call to check if a chunk of the data object can be sent to the target. +bool fwuIsReadyForChunk(TFwu *fwu); + +#endif // __FWU_H__ diff --git a/core/embed/lib/ble/int_comm_defs.h b/core/embed/lib/ble/int_comm_defs.h new file mode 100644 index 000000000..ea5d90fc2 --- /dev/null +++ b/core/embed/lib/ble/int_comm_defs.h @@ -0,0 +1,48 @@ +#ifndef __INT_COMM_DEFS__ +#define __INT_COMM_DEFS__ + +#define BLE_PACKET_SIZE (244) +#define USB_DATA_SIZE (64) + +#define COMM_HEADER_SIZE (3) +#define COMM_FOOTER_SIZE (1) +#define OVERHEAD_SIZE (COMM_HEADER_SIZE + COMM_FOOTER_SIZE) +#define UART_PACKET_SIZE (USB_DATA_SIZE + OVERHEAD_SIZE) + +#define EOM (0x55) +#define INTERNAL_EVENT (0xA2) +#define EXTERNAL_MESSAGE (0xA1) +#define INTERNAL_MESSAGE (0xA0) + +typedef struct { + uint8_t msg_id; + uint8_t connected; + uint8_t advertising; + uint8_t advertising_whitelist; + + uint8_t peer_count; + uint8_t reserved[2]; + uint8_t sd_version_number; + + uint16_t sd_company_id; + uint16_t sd_subversion_number; + + uint32_t app_version; + uint32_t bld_version; +} event_status_msg_t; + +typedef enum { + INTERNAL_EVENT_STATUS = 0x01, + INTERNAL_EVENT_SUCCESS = 0x02, + INTERNAL_EVENT_FAILURE = 0x03, +} InternalEvent_t; + +typedef enum { + INTERNAL_CMD_SEND_STATE = 0x00, + INTERNAL_CMD_ADVERTISING_ON = 0x01, + INTERNAL_CMD_ADVERTISING_OFF = 0x02, + INTERNAL_CMD_ERASE_BONDS = 0x03, + INTERNAL_CMD_DISCONNECT = 0x04, + INTERNAL_CMD_ACK = 0x05, +} InternalCmd_t; +#endif diff --git a/core/embed/lib/ble/messages.c b/core/embed/lib/ble/messages.c new file mode 100644 index 000000000..a91bdc449 --- /dev/null +++ b/core/embed/lib/ble/messages.c @@ -0,0 +1,127 @@ + +#include +#include + +#include "ble_hal.h" +#include "common.h" +#include "int_comm_defs.h" +#include "messages.h" +#include "state.h" + +void process_poll(uint8_t *data, uint32_t len) { + uint8_t cmd = data[0]; + + switch (cmd) { + case INTERNAL_EVENT_STATUS: { + event_status_msg_t *msg = (event_status_msg_t *)data; + set_status(msg); + break; + } + default: + break; + } +} + +bool wait_for_answer(void) { + uint8_t buf[64] = {0}; + + uint32_t ticks_start = hal_ticks_ms(); + int len = 0; + + while (len == 0) { + if (hal_ticks_ms() - ticks_start > 1000) { + // timeout + return false; + } + + len = ble_int_event_receive(buf, sizeof(buf)); + + if (len > 0) { + process_poll(buf, len); + } + } + + return true; +} + +bool ble_initialize(void) { + if (!ble_firmware_running()) { + return false; + } + send_state_request(); + + return wait_for_answer(); +} + +void send_state_request(void) { + uint8_t cmd = INTERNAL_CMD_SEND_STATE; + ble_int_comm_send(&cmd, sizeof(cmd), INTERNAL_EVENT); +} + +void send_advertising_on(bool whitelist) { + uint8_t data[2]; + data[0] = INTERNAL_CMD_ADVERTISING_ON; + data[1] = whitelist ? 1 : 0; + ble_int_comm_send(data, sizeof(data), INTERNAL_EVENT); +} + +void send_advertising_off(void) { + uint8_t cmd = INTERNAL_CMD_ADVERTISING_OFF; + ble_int_comm_send(&cmd, sizeof(cmd), INTERNAL_EVENT); +} + +bool send_erase_bonds(void) { + if (!ble_firmware_running()) { + return false; + } + uint8_t cmd = INTERNAL_CMD_ERASE_BONDS; + ble_int_comm_send(&cmd, sizeof(cmd), INTERNAL_EVENT); + + uint8_t buf[64] = {0}; + + uint32_t ticks_start = hal_ticks_ms(); + int len = 0; + + while (len == 0) { + len = ble_int_event_receive(buf, sizeof(buf)); + + if (hal_ticks_ms() - ticks_start > 1000) { + // timeout + return false; + } + } + + if (buf[0] == INTERNAL_EVENT_SUCCESS) { + return true; + } + + return false; +} + +bool send_disconnect(void) { + if (!ble_firmware_running()) { + return false; + } + uint8_t cmd = INTERNAL_CMD_DISCONNECT; + ble_int_comm_send(&cmd, sizeof(cmd), INTERNAL_EVENT); + + uint8_t buf[64] = {0}; + + uint32_t ticks_start = hal_ticks_ms(); + int len = 0; + + while (len == 0) { + len = ble_int_event_receive(buf, sizeof(buf)); + + if (hal_ticks_ms() - ticks_start > 1000) { + // timeout + return false; + } + } + + if (buf[0] == INTERNAL_EVENT_SUCCESS) { + return true; + } + + return false; +} diff --git a/core/embed/lib/ble/messages.h b/core/embed/lib/ble/messages.h new file mode 100644 index 000000000..a7a8a167c --- /dev/null +++ b/core/embed/lib/ble/messages.h @@ -0,0 +1,22 @@ +#ifndef __BLE_MESSAGES__ +#define __BLE_MESSAGES__ + +#include + +bool ble_initialize(void); + +bool wait_for_answer(void); + +void process_poll(uint8_t *data, uint32_t len); + +void send_state_request(void); + +void send_advertising_on(bool whitelist); + +void send_advertising_off(void); + +bool send_erase_bonds(void); + +bool send_disconnect(void); + +#endif diff --git a/core/embed/lib/ble/state.c b/core/embed/lib/ble/state.c new file mode 100644 index 000000000..a2d1f916a --- /dev/null +++ b/core/embed/lib/ble/state.c @@ -0,0 +1,70 @@ +#include "ble/state.h" +#include "ble_hal.h" +#include "messages.h" + +static bool ble_state_connected = false; +static bool ble_state_initialized = false; +static bool ble_advertising_wanted = false; +static bool ble_advertising_wl_wanted = false; +static bool ble_advertising = false; +static bool ble_advertising_wl = false; +static bool ble_dfu_mode = false; +static uint8_t ble_peer_count = 0; + +bool ble_connected(void) { + return ble_state_connected && ble_firmware_running(); +} + +void set_connected(bool connected) {} + +static void configure_ble(bool advertising, bool whitelist) { + if (ble_advertising != advertising || (ble_advertising_wl != whitelist)) { + if (advertising) { + send_advertising_on(whitelist); + } + if (!advertising && ble_advertising) { + send_advertising_off(); + } + } + + ble_advertising_wanted = advertising; + ble_advertising_wl_wanted = whitelist; +} + +void set_status(event_status_msg_t *msg) { + ble_state_connected = msg->connected; + ble_peer_count = msg->peer_count; + ble_advertising = msg->advertising; + ble_advertising_wl = msg->advertising_whitelist; + + set_initialized(true); + + configure_ble(ble_advertising_wanted, ble_advertising_wl_wanted); +} + +void set_initialized(bool initialized) { ble_state_initialized = initialized; } + +bool ble_initialized(void) { + return ble_state_initialized && ble_firmware_running(); +} + +void start_advertising(bool whitelist) { configure_ble(true, whitelist); } + +void auto_start_advertising(void) { + if (ble_peer_count > 0) { + configure_ble(true, true); + } else { + configure_ble(false, false); + } +} + +void stop_advertising(void) { configure_ble(false, false); } + +void ble_set_dfu_mode(bool dfu) { ble_dfu_mode = dfu; } + +bool is_ble_dfu_mode(void) { return ble_dfu_mode; } + +void ble_stop_all_comm(void) { + stop_advertising(); + ble_comm_stop(); +} diff --git a/core/embed/lib/ble/state.h b/core/embed/lib/ble/state.h new file mode 100644 index 000000000..ae21b383a --- /dev/null +++ b/core/embed/lib/ble/state.h @@ -0,0 +1,29 @@ +#ifndef __BLE_STATE__ +#define __BLE_STATE__ + +#include +#include + +#include "int_comm_defs.h" + +bool ble_initialized(void); + +void set_initialized(bool initialized); + +bool ble_connected(void); + +void set_status(event_status_msg_t *msg); + +void start_advertising(bool whitelist); + +void auto_start_advertising(void); + +void stop_advertising(void); + +void ble_set_dfu_mode(bool dfu); + +bool is_ble_dfu_mode(void); + +void ble_stop_all_comm(void); + +#endif diff --git a/core/embed/rust/Cargo.toml b/core/embed/rust/Cargo.toml index 4e7ca7bdc..92e022b76 100644 --- a/core/embed/rust/Cargo.toml +++ b/core/embed/rust/Cargo.toml @@ -25,6 +25,7 @@ clippy = [] jpeg = [] disp_i8080_8bit_dw = [] # write pixels directly to peripheral disp_i8080_16bit_dw = [] # write pixels directly to peripheral +ble = [] debug = ["ui_debug"] sbu = [] sd_card = [] diff --git a/core/embed/trezorhal/ble_hal.h b/core/embed/trezorhal/ble_hal.h new file mode 100644 index 000000000..b3c22e752 --- /dev/null +++ b/core/embed/trezorhal/ble_hal.h @@ -0,0 +1,32 @@ +#ifndef __BLE_COMM_H__ +#define __BLE_COMM_H__ + +#include +#include +#include "ble/int_comm_defs.h" + +void ble_comm_init(void); + +void ble_comm_start(void); +void ble_comm_stop(void); +bool ble_comm_running(void); + +void ble_comm_send(uint8_t *data, uint32_t len); +uint32_t ble_comm_receive(uint8_t *data, uint32_t len); + +void ble_int_comm_send(uint8_t *data, uint32_t len, uint8_t message_type); +uint32_t ble_int_event_receive(uint8_t *data, uint32_t len); +uint32_t ble_int_comm_receive(uint8_t *data, uint32_t len); +uint32_t ble_ext_comm_receive(uint8_t *data, uint32_t len); + +void ble_event_poll(void); + +bool ble_firmware_running(void); + +bool ble_reset_to_bootloader(void); + +bool ble_reset(void); +void ble_signal_running(void); +void ble_signal_off(void); + +#endif diff --git a/core/embed/trezorhal/unix/ble.c b/core/embed/trezorhal/unix/ble.c new file mode 100644 index 000000000..5c1237e0b --- /dev/null +++ b/core/embed/trezorhal/unix/ble.c @@ -0,0 +1,32 @@ +#include "ble_hal.h" + +// TODO: send data over UDP + +static bool firmware_running = true; + +void ble_comm_init(void) {} +void ble_comm_start(void) {} +void ble_comm_stop(void) {} + +void ble_comm_send(uint8_t *data, uint32_t len) {} + +uint32_t ble_comm_receive(uint8_t *data, uint32_t len) { return 0; } + +void ble_int_comm_send(uint8_t *data, uint32_t len, uint8_t message_type) {} +uint32_t ble_int_event_receive(uint8_t *data, uint32_t len) { return 0; } +uint32_t ble_int_comm_receive(uint8_t *data, uint32_t len) { return 0; } +uint32_t ble_ext_comm_receive(uint8_t *data, uint32_t len) { return 0; } + +void ble_event_poll(void) {} + +bool ble_firmware_running(void) { return firmware_running; } + +bool ble_reset_to_bootloader(void) { + firmware_running = false; + return true; +} + +bool ble_reset(void) { + firmware_running = true; + return true; +} diff --git a/core/mocks/generated/trezorio/__init__.pyi b/core/mocks/generated/trezorio/__init__.pyi index e84de507c..c0b42a5d7 100644 --- a/core/mocks/generated/trezorio/__init__.pyi +++ b/core/mocks/generated/trezorio/__init__.pyi @@ -190,7 +190,7 @@ class WebUSB: """ Sends message using USB WebUSB (device) or UDP (emulator). """ -from . import fatfs, sdcard +from . import fatfs, sdcard, ble POLL_READ: int # wait until interface is readable and return read data POLL_WRITE: int # wait until interface is writable TOUCH: int # interface id of the touch events @@ -203,4 +203,5 @@ BUTTON_RELEASED: int # button up event BUTTON_LEFT: int # button number of left button BUTTON_RIGHT: int # button number of right button USB_CHECK: int # interface id for check of USB data connection -WireInterface = Union[HID, WebUSB] +BLE_CHECK: int # interface id for check of BLE data connection +WireInterface = Union[HID, WebUSB, BleInterface] diff --git a/core/mocks/generated/trezorio/ble.pyi b/core/mocks/generated/trezorio/ble.pyi new file mode 100644 index 000000000..f810c6270 --- /dev/null +++ b/core/mocks/generated/trezorio/ble.pyi @@ -0,0 +1,69 @@ +from typing import * +INTERNAL: int # interface id for internal (stm<->nrf) connection +EXTERNAL: int # interface id for ble client connection + + +# extmod/modtrezorio/modtrezorio-ble.h +def update_init(data: bytes, binsize: int) -> bool: + """ + Initializes the BLE firmware update. Returns true if the update finished + with only the initial chunk. False means calling `update_chunk` is + expected. + """ + + +# extmod/modtrezorio/modtrezorio-ble.h +def update_chunk(chunk: bytes) -> bool: + """ + Writes next chunk of BLE firmware update. Returns true if the update is + finished, or false if more chunks are expected. + """ + + +# extmod/modtrezorio/modtrezorio-ble.h +def write_int(self, msg: bytes) -> int: + """ + Sends internal message to NRF. + """ + + +# extmod/modtrezorio/modtrezorio-ble.h +def write_ext(self, msg: bytes) -> int: + """ + Sends message over BLE + """ + + +# extmod/modtrezorio/modtrezorio-ble.h +def erase_bonds() -> None: + """ + Erases all BLE bonds + """ + + +# extmod/modtrezorio/modtrezorio-ble.h +def start_comm() -> None: + """ + Start communication with BLE chip + """ + + +# extmod/modtrezorio/modtrezorio-ble.h +def start_advertising(whitelist: bool) -> None: + """ + Start advertising + """ + + +# extmod/modtrezorio/modtrezorio-ble.h +def stop_advertising(whitelist: bool) -> None: + """ + Stop advertising + """ + + +# extmod/modtrezorio/modtrezorio-ble.h +def disconnect() -> None: + """ + Disconnect BLE + """ diff --git a/core/mocks/generated/trezorutils.pyi b/core/mocks/generated/trezorutils.pyi index 73d9bda80..9308aab98 100644 --- a/core/mocks/generated/trezorutils.pyi +++ b/core/mocks/generated/trezorutils.pyi @@ -112,6 +112,7 @@ VERSION_MINOR: int """Minor version.""" VERSION_PATCH: int """Patch version.""" +USE_BLE: bool USE_SD_CARD: bool """Whether the hardware supports SD card.""" USE_BACKLIGHT: bool diff --git a/core/site_scons/site_tools/micropython/__init__.py b/core/site_scons/site_tools/micropython/__init__.py index 80a03ca6b..2e5510fc0 100644 --- a/core/site_scons/site_tools/micropython/__init__.py +++ b/core/site_scons/site_tools/micropython/__init__.py @@ -39,6 +39,7 @@ def generate(env): is_t2b1 = env["TREZOR_MODEL"] == "R" backlight = env["backlight"] optiga = env["optiga"] + ble = env["ble"] layout_tt = env["ui_layout"] == "UI_LAYOUT_TT" layout_tr = env["ui_layout"] == "UI_LAYOUT_TR" interim = f"{target[:-4]}.i" # replace .mpy with .i @@ -48,6 +49,7 @@ def generate(env): rf"-e 's/utils\.BITCOIN_ONLY/{btc_only}/g'", rf"-e 's/utils\.USE_BACKLIGHT/{backlight}/g'", rf"-e 's/utils\.USE_OPTIGA/{optiga}/g'", + rf"-e 's/utils\.USE_BLE/{ble}/g'", rf"-e 's/utils\.UI_LAYOUT == \"TT\"/{layout_tt}/g'", rf"-e 's/utils\.UI_LAYOUT == \"TR\"/{layout_tr}/g'", r"-e 's/if TYPE_CHECKING/if False/'", diff --git a/core/src/all_modules.py b/core/src/all_modules.py index aba9d6a11..241c56450 100644 --- a/core/src/all_modules.py +++ b/core/src/all_modules.py @@ -321,6 +321,20 @@ apps.management.backup_device import apps.management.backup_device apps.management.backup_types import apps.management.backup_types +apps.management.ble +import apps.management.ble +apps.management.ble.comparison_request +import apps.management.ble.comparison_request +apps.management.ble.disconnect +import apps.management.ble.disconnect +apps.management.ble.erase_bonds +import apps.management.ble.erase_bonds +apps.management.ble.pairing_request +import apps.management.ble.pairing_request +apps.management.ble.repair_request +import apps.management.ble.repair_request +apps.management.ble.upload_firmware_init +import apps.management.ble.upload_firmware_init apps.management.change_pin import apps.management.change_pin apps.management.change_wipe_code diff --git a/core/src/apps/management/ble/__init__.py b/core/src/apps/management/ble/__init__.py new file mode 100644 index 000000000..f7af657f0 --- /dev/null +++ b/core/src/apps/management/ble/__init__.py @@ -0,0 +1,35 @@ +from micropython import const +from trezorio import ble + +from trezor import wire + +_PROTOBUF_BUFFER_SIZE_INTERNAL = const(256) +_WIRE_BUFFER_INTERNAL = bytearray(_PROTOBUF_BUFFER_SIZE_INTERNAL) + + +class BleInterfaceInternal: + IS_BLE_INTERNAL = True + + def iface_num(self) -> int: + return ble.INTERNAL + + def write(self, msg: bytes) -> int: + return ble.write_int(self, msg) + + +class BleInterfaceExternal: + def iface_num(self) -> int: + return ble.EXTERNAL + + def write(self, msg: bytes) -> int: + return ble.write_ext(self, msg) + + +# interface used for trezor wire protocol +iface_ble_int = BleInterfaceInternal() +iface_ble_ext = BleInterfaceExternal() + + +def boot() -> None: + wire.setup(iface_ble_int, buffer=_WIRE_BUFFER_INTERNAL) + wire.setup(iface_ble_ext) diff --git a/core/src/apps/management/ble/comparison_request.py b/core/src/apps/management/ble/comparison_request.py new file mode 100644 index 000000000..16872ea94 --- /dev/null +++ b/core/src/apps/management/ble/comparison_request.py @@ -0,0 +1,19 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from trezor.messages import BLEComparisonRequest, Success + + +async def comparison_request(msg: BLEComparisonRequest) -> Success: + from trezor.messages import Success + from trezor.ui.layouts import confirm_action + from trezor.wire import context + + await context.with_context( + None, + confirm_action( + "", "DO THE NUMBERS MATCH?", description=msg.key.decode("utf-8") + ), + ) + + return Success() diff --git a/core/src/apps/management/ble/disconnect.py b/core/src/apps/management/ble/disconnect.py new file mode 100644 index 000000000..b24a88052 --- /dev/null +++ b/core/src/apps/management/ble/disconnect.py @@ -0,0 +1,16 @@ +from trezorio import ble +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from trezor.messages import BLEDisconnect, Success + + +async def disconnect(_msg: BLEDisconnect) -> Success: + from trezor.messages import Success + from trezor.ui.layouts import confirm_action + + await confirm_action("ble_disconnect", "DISCONNECT") + + ble.disconnect() + + return Success() diff --git a/core/src/apps/management/ble/erase_bonds.py b/core/src/apps/management/ble/erase_bonds.py new file mode 100644 index 000000000..3161d9228 --- /dev/null +++ b/core/src/apps/management/ble/erase_bonds.py @@ -0,0 +1,16 @@ +from trezorio import ble +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from trezor.messages import BLEEraseBonds, Success + + +async def erase_bonds(_msg: BLEEraseBonds) -> Success: + from trezor.messages import Success + from trezor.ui.layouts import confirm_action + + await confirm_action("ble_erase_bonds", "ERASE BONDS") + + ble.erase_bonds() + + return Success() diff --git a/core/src/apps/management/ble/pairing_request.py b/core/src/apps/management/ble/pairing_request.py new file mode 100644 index 000000000..c0d1f0852 --- /dev/null +++ b/core/src/apps/management/ble/pairing_request.py @@ -0,0 +1,19 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from trezor.messages import BLEAuthKey, BLEPairingRequest + + +async def pairing_request(_msg: BLEPairingRequest) -> BLEAuthKey: + from trezor.messages import BLEAuthKey + from trezor.ui.layouts import request_pin_on_device + from trezor.wire import context + + pin = await context.with_context( + None, request_pin_on_device("PAIRING", None, True, False) + ) + + if len(pin) != 6: + pin = "000000" + + return BLEAuthKey(key=pin.encode()) diff --git a/core/src/apps/management/ble/repair_request.py b/core/src/apps/management/ble/repair_request.py new file mode 100644 index 000000000..b6c4b40ae --- /dev/null +++ b/core/src/apps/management/ble/repair_request.py @@ -0,0 +1,14 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from trezor.messages import BLERepairRequest, Success + + +async def repair_request(_msg: BLERepairRequest) -> Success: + from trezor.messages import Success + from trezor.ui.layouts import confirm_action + from trezor.wire import context + + await context.with_context(None, confirm_action("", "RE-PAIR DEVICE")) + + return Success() diff --git a/core/src/apps/management/ble/upload_firmware_init.py b/core/src/apps/management/ble/upload_firmware_init.py new file mode 100644 index 000000000..7ab1234da --- /dev/null +++ b/core/src/apps/management/ble/upload_firmware_init.py @@ -0,0 +1,64 @@ +from trezorio import ble +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from trezor.messages import BLEUploadFirmwareChunk, BLEUploadFirmwareInit, Success + + +async def upload_firmware_chunk(msg: BLEUploadFirmwareChunk) -> int: + result = ble.update_chunk(msg.data) + + return result + + +async def upload_firmware_init(msg: BLEUploadFirmwareInit) -> Success: + from trezor.enums import ButtonRequestType + from trezor.messages import ( + BLEUploadFirmwareChunk, + BLEUploadFirmwareNextChunk, + Success, + ) + from trezor.ui.layouts import confirm_action + + await confirm_action( + "confirm_upload_ble_firmware", + "Upload BLE firmware", + "", + "Update BLE FW?\n", + reverse=True, + verb="Confirm", + br_code=ButtonRequestType.Other, + ) + + from trezor.enums import MessageType + from trezor.ui.layouts import progress + from trezor.wire.context import get_context + + ctx = get_context() + + progress_layout = progress.progress("Uploading...") + + upload_progress = 0 + + p = int(1000 * upload_progress / msg.binsize) + progress_layout.report(p) + + finished = ble.update_init(msg.init_data, msg.binsize) + while not finished: + await ctx.write(BLEUploadFirmwareNextChunk()) + + received_msg = await ctx.read( + (MessageType.BLEUploadFirmwareChunk,), + BLEUploadFirmwareChunk, + ) + + finished = await upload_firmware_chunk(received_msg) + + upload_progress += len(received_msg.data) + p = int(1000 * upload_progress / msg.binsize) + progress_layout.report(p) + del received_msg + + progress_layout.report(1000) + + return Success(message="BLE firmware update successful") diff --git a/core/src/apps/workflow_handlers.py b/core/src/apps/workflow_handlers.py index 8b373c4c4..9df081cb5 100644 --- a/core/src/apps/workflow_handlers.py +++ b/core/src/apps/workflow_handlers.py @@ -14,7 +14,7 @@ def register(wire_type: int, handler: Handler[Msg]) -> None: workflow_handlers[wire_type] = handler -def _find_message_handler_module(msg_type: int) -> str: +def _find_message_handler_module(msg_type: int, iface: WireInterface) -> str: """Statically find the appropriate workflow handler. For now, new messages must be registered by hand in the if-elif manner below. @@ -27,6 +27,24 @@ def _find_message_handler_module(msg_type: int) -> str: from trezor import utils from trezor.enums import MessageType + if utils.USE_BLE: + if getattr(iface, "IS_BLE_INTERNAL", False): + if msg_type == MessageType.BLEPairingRequest: + return "apps.management.ble.pairing_request" + if msg_type == MessageType.BLERepairRequest: + return "apps.management.ble.repair_request" + if msg_type == MessageType.BLEComparisonRequest: + return "apps.management.ble.comparison_request" + # only the messages above are accepted internally + raise ValueError + + if msg_type == MessageType.BLEUploadFirmwareInit: + return "apps.management.ble.upload_firmware_init" + if msg_type == MessageType.BLEEraseBonds: + return "apps.management.ble.erase_bonds" + if msg_type == MessageType.BLEDisconnect: + return "apps.management.ble.disconnect" + # debug if __debug__ and msg_type == MessageType.LoadDevice: return "apps.debug.load_device" @@ -207,7 +225,7 @@ def find_registered_handler(iface: WireInterface, msg_type: int) -> Handler | No return workflow_handlers[msg_type] try: - modname = _find_message_handler_module(msg_type) + modname = _find_message_handler_module(msg_type, iface) handler_name = modname[modname.rfind(".") + 1 :] module = __import__(modname, None, None, (handler_name,), 0) return getattr(module, handler_name) diff --git a/core/src/main.py b/core/src/main.py index f0c9a54b0..96992560e 100644 --- a/core/src/main.py +++ b/core/src/main.py @@ -49,6 +49,10 @@ import storage.device usb.bus.open(storage.device.get_device_id()) +if utils.USE_BLE: + from trezorio import ble + ble.start_comm() + # run the endless loop while True: with unimport_manager: diff --git a/core/src/session.py b/core/src/session.py index 4f161c36c..4826269c2 100644 --- a/core/src/session.py +++ b/core/src/session.py @@ -25,6 +25,12 @@ wire.setup(usb.iface_wire) if __debug__: wire.setup(usb.iface_debug, is_debug_session=True) +if utils.USE_BLE: + from apps.management import ble + + ble.boot() + + loop.run() if __debug__: diff --git a/core/src/trezor/utils.py b/core/src/trezor/utils.py index eb90879f1..528a1f9ef 100644 --- a/core/src/trezor/utils.py +++ b/core/src/trezor/utils.py @@ -9,6 +9,7 @@ from trezorutils import ( # noqa: F401 SCM_REVISION, UI_LAYOUT, USE_BACKLIGHT, + USE_BLE, USE_OPTIGA, USE_SD_CARD, VERSION_MAJOR, diff --git a/core/src/trezor/wire/__init__.py b/core/src/trezor/wire/__init__.py index e18755fe5..8c8352f35 100644 --- a/core/src/trezor/wire/__init__.py +++ b/core/src/trezor/wire/__init__.py @@ -65,9 +65,13 @@ if TYPE_CHECKING: EXPERIMENTAL_ENABLED = False -def setup(iface: WireInterface, is_debug_session: bool = False) -> None: +def setup( + iface: WireInterface, + is_debug_session: bool = False, + buffer: bytearray | None = None, +) -> None: """Initialize the wire stack on passed USB interface.""" - loop.schedule(handle_session(iface, codec_v1.SESSION_ID, is_debug_session)) + loop.schedule(handle_session(iface, codec_v1.SESSION_ID, is_debug_session, buffer)) def wrap_protobuf_load( @@ -93,6 +97,7 @@ def wrap_protobuf_load( _PROTOBUF_BUFFER_SIZE = const(8192) WIRE_BUFFER = bytearray(_PROTOBUF_BUFFER_SIZE) +WIRE_BUFFER_LOCK = context.BufferLock() if __debug__: PROTOBUF_BUFFER_SIZE_DEBUG = 1024 @@ -205,14 +210,20 @@ async def _handle_single_message( async def handle_session( - iface: WireInterface, session_id: int, is_debug_session: bool = False + iface: WireInterface, + session_id: int, + is_debug_session: bool = False, + buffer: bytearray | None = None, ) -> None: - if __debug__ and is_debug_session: - ctx_buffer = WIRE_BUFFER_DEBUG - else: - ctx_buffer = WIRE_BUFFER + buffer_lock = None + if buffer is None: + if __debug__ and is_debug_session: + buffer = WIRE_BUFFER_DEBUG + else: + buffer = WIRE_BUFFER + buffer_lock = WIRE_BUFFER_LOCK - ctx = context.Context(iface, session_id, ctx_buffer) + ctx = context.Context(iface, session_id, buffer, buffer_lock) next_msg: codec_v1.Message | None = None if __debug__ and is_debug_session: diff --git a/core/src/trezor/wire/context.py b/core/src/trezor/wire/context.py index 9df347354..29f46fdb9 100644 --- a/core/src/trezor/wire/context.py +++ b/core/src/trezor/wire/context.py @@ -39,6 +39,27 @@ if TYPE_CHECKING: LoadedMessageType = TypeVar("LoadedMessageType", bound=protobuf.MessageType) +class BufferLock: + def __init__(self) -> None: + self.in_use = False + + def __enter__(self) -> None: + if self.in_use: + raise RuntimeError("wire buffer already used by another context") + self.in_use = True + + def __exit__(self, exc_type: Any, value: Any, traceback: Any) -> None: + self.in_use = False + + +class DummyLock: + def __enter__(self) -> None: + pass + + def __exit__(self, exc_type: Any, value: Any, traceback: Any) -> None: + pass + + class UnexpectedMessage(Exception): """A message was received that is not part of the current workflow. @@ -58,14 +79,18 @@ class Context: (i.e., wire, debug, single BT connection, etc.) """ - def __init__(self, iface: WireInterface, sid: int, buffer: bytearray) -> None: + def __init__( + self, iface: WireInterface, sid: int, buffer: bytearray, buffer_lock: Any = None + ) -> None: self.iface = iface self.sid = sid self.buffer = buffer + self.buffer_lock = buffer_lock or DummyLock() def read_from_wire(self) -> Awaitable[codec_v1.Message]: """Read a whole message from the wire without parsing it.""" - return codec_v1.read_message(self.iface, self.buffer) + with self.buffer_lock: + return codec_v1.read_message(self.iface, self.buffer) if TYPE_CHECKING: @@ -147,13 +172,14 @@ class Context: # message is too big, we need to allocate a new buffer buffer = bytearray(msg_size) - msg_size = protobuf.encode(buffer, msg) + with self.buffer_lock: + msg_size = protobuf.encode(buffer, msg) - await codec_v1.write_message( - self.iface, - msg.MESSAGE_WIRE_TYPE, - memoryview(buffer)[:msg_size], - ) + await codec_v1.write_message( + self.iface, + msg.MESSAGE_WIRE_TYPE, + memoryview(buffer)[:msg_size], + ) CURRENT_CONTEXT: Context | None = None @@ -235,7 +261,7 @@ def get_context() -> Context: return CURRENT_CONTEXT -def with_context(ctx: Context, workflow: loop.Task) -> Generator: +def with_context(ctx: Context | None, workflow: loop.Task) -> Generator: """Run a workflow in a particular context. Stores the context in a closure and installs it into the global variable every time