Merge branch 'master' into release/23.09

release/23.09 core/bl2.1.3
matejcik 8 months ago
commit bb5b91b920

@ -349,10 +349,10 @@ combine: ## combine boardloader + bootloader + prodtest into one combined image
$(PRODTEST_BUILD_DIR)/combined.bin
upload: ## upload firmware using trezorctl
trezorctl firmware_update -f $(FIRMWARE_BUILD_DIR)/firmware.bin
trezorctl firmware_update -s -f $(FIRMWARE_BUILD_DIR)/firmware.bin
upload_prodtest: ## upload prodtest using trezorctl
trezorctl firmware_update -f $(PRODTEST_BUILD_DIR)/prodtest.bin
trezorctl firmware_update -s -f $(PRODTEST_BUILD_DIR)/prodtest.bin
coverage: ## generate coverage report
./tools/coverage-report

@ -136,6 +136,7 @@ if TREZOR_MODEL in ('R', ):
]
SOURCE_TREZORHAL += [
'embed/trezorhal/unix/secret.c',
'embed/trezorhal/unix/optiga_hal.c',
]
SOURCE_UNIX = [
@ -206,7 +207,7 @@ env.Replace(
'TREZOR_EMULATOR',
CPU_MODEL,
'HW_MODEL=' + MODEL_AS_NUMBER,
'HW_REVISION=' + ('6' if TREZOR_MODEL in ('R',) else '0'),
'HW_REVISION=' + ('10' if TREZOR_MODEL in ('R',) else '0'),
'TREZOR_MODEL_'+TREZOR_MODEL,
'TREZOR_BOARD=\\"boards/board-unix.h\\"',
'MCU_TYPE='+CPU_MODEL,

@ -380,6 +380,21 @@ env = Environment(ENV=os.environ, CFLAGS=f"{ARGUMENTS.get('CFLAGS', '')} -DPRODU
FEATURES_AVAILABLE = tools.configure_board(TREZOR_MODEL, FEATURES_WANTED, env, CPPDEFINES_HAL, SOURCE_HAL, PATH_HAL)
if TREZOR_MODEL in ('T', 'DISC1'):
UI_LAYOUT = 'UI_LAYOUT_TT'
ui_layout_feature = 'model_tt'
elif TREZOR_MODEL in ('1', 'R'):
UI_LAYOUT = 'UI_LAYOUT_TR'
ui_layout_feature = 'model_tr'
else:
raise ValueError('Unknown Trezor model')
if 'sd_card' in FEATURES_AVAILABLE:
SDCARD = True
else:
SDCARD = False
env.Tool('micropython')
env.Replace(
@ -430,6 +445,7 @@ env.Replace(
'FIRMWARE',
'TREZOR_MODEL_'+TREZOR_MODEL,
'USE_HAL_DRIVER',
UI_LAYOUT,
] + CPPDEFINES_MOD + CPPDEFINES_HAL,
ASFLAGS=env.get('ENV')['CPU_ASFLAGS'],
ASPPFLAGS='$CFLAGS $CCFLAGS',
@ -518,7 +534,7 @@ if FROZEN:
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/*.py',
exclude=[
SOURCE_PY_DIR + 'trezor/sdcard.py',
] if TREZOR_MODEL not in ('T',) else []
] if not SDCARD else []
))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/crypto/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/*.py'))
@ -529,27 +545,27 @@ if FROZEN:
SOURCE_PY_DIR + 'trezor/ui/layouts/fido.py',
] if not EVERYTHING else []
))
if TREZOR_MODEL in ('T', 'DISC1'):
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/*.py',
if UI_LAYOUT == 'UI_LAYOUT_TT':
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt/*.py',
exclude=[
SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/fido.py',
SOURCE_PY_DIR + 'trezor/ui/layouts/tt/fido.py',
] if not EVERYTHING else []
))
elif TREZOR_MODEL in ('1', 'R'):
elif UI_LAYOUT == 'UI_LAYOUT_TR':
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tr/*.py',
exclude=[
SOURCE_PY_DIR + 'trezor/ui/layouts/tr/fido.py',
] if not EVERYTHING else []
))
else:
raise ValueError('Unknown Trezor model')
raise ValueError('Unknown layout')
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/wire/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'storage/*.py',
exclude=[
SOURCE_PY_DIR + 'storage/sd_salt.py',
] if "sd_card" not in FEATURES_AVAILABLE else []
] if not SDCARD else []
))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/messages/__init__.py'))
@ -573,14 +589,14 @@ if FROZEN:
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/common/*.py',
exclude=[
SOURCE_PY_DIR + 'apps/common/sdcard.py',
] if "sd_card" not in FEATURES_AVAILABLE else []
] if not SDCARD else []
))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/debug/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/homescreen/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/management/*.py',
exclude=[
SOURCE_PY_DIR + 'apps/management/sd_protect.py',
] if 'sd_card' not in FEATURES_AVAILABLE else [])
] if not SDCARD else [])
)
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/management/*/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/misc/*.py'))
@ -645,7 +661,8 @@ if FROZEN:
source_dir=SOURCE_PY_DIR,
bitcoin_only=BITCOIN_ONLY,
backlight='backlight' in FEATURES_AVAILABLE,
optiga='optiga' in FEATURES_AVAILABLE
optiga='optiga' in FEATURES_AVAILABLE,
ui_layout=UI_LAYOUT,
)
source_mpyc = env.FrozenCFile(
@ -681,13 +698,7 @@ def cargo_build():
else:
profile = ''
# T1 does not have its own Rust feature, it shares it with TR
if TREZOR_MODEL in ('1', 'R'):
model_feature = 'model_tr'
else:
model_feature = 'model_tt'
features = ['micropython', 'protobuf', model_feature]
features = ['micropython', 'protobuf', ui_layout_feature]
if BITCOIN_ONLY == '1':
features.append('bitcoin_only')
features.append('ui')

@ -387,7 +387,6 @@ SOURCE_UNIX = [
if TREZOR_MODEL in ('T', 'R'):
SOURCE_UNIX += [
'embed/trezorhal/unix/sbu.c',
'embed/trezorhal/unix/sdcard.c',
]
if TREZOR_MODEL == 'R':
@ -424,6 +423,24 @@ else:
env = Environment(ENV=os.environ, CFLAGS='%s -DPYOPT=%s -DBITCOIN_ONLY=%s %s' % (ARGUMENTS.get('CFLAGS', ''), PYOPT, BITCOIN_ONLY, STATIC))
if TREZOR_MODEL in ('T',):
UI_LAYOUT = 'UI_LAYOUT_TT'
ui_layout_feature = 'model_tt'
elif TREZOR_MODEL in ('1', 'R'):
UI_LAYOUT = 'UI_LAYOUT_TR'
ui_layout_feature = 'model_tr'
else:
raise ValueError('Unknown Trezor model')
if TREZOR_MODEL in ('T',):
SDCARD = True
SOURCE_UNIX += [
'embed/trezorhal/unix/sdcard.c',
]
else:
SDCARD = False
env.Tool('micropython')
env.Replace(
@ -508,6 +525,7 @@ env.Replace(
'TREZOR_BOARD=\\"boards/board-unix.h\\"',
'MCU_TYPE='+CPU_MODEL,
('MP_CONFIGFILE', '\\"embed/unix/mpconfigport.h\\"'),
UI_LAYOUT,
] + CPPDEFINES_MOD,
ASPPFLAGS='$CFLAGS $CCFLAGS', )
@ -599,7 +617,7 @@ if FROZEN:
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/*.py',
exclude=[
SOURCE_PY_DIR + 'trezor/sdcard.py',
] if TREZOR_MODEL not in ('T',) else []
] if not SDCARD else []
))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/crypto/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/*.py'))
@ -610,27 +628,27 @@ if FROZEN:
SOURCE_PY_DIR + 'trezor/ui/layouts/fido.py',
] if not EVERYTHING else []
))
if TREZOR_MODEL in ('T',):
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/*.py',
if UI_LAYOUT == 'UI_LAYOUT_TT':
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt/*.py',
exclude=[
SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/fido.py',
SOURCE_PY_DIR + 'trezor/ui/layouts/tt/fido.py',
] if not EVERYTHING else []
))
elif TREZOR_MODEL in ('R'):
elif UI_LAYOUT == 'UI_LAYOUT_TR':
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tr/*.py',
exclude=[
SOURCE_PY_DIR + 'trezor/ui/layouts/tr/fido.py',
] if not EVERYTHING else []
))
else:
raise ValueError('Unknown Trezor model')
raise ValueError('Unknown layout')
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/wire/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'storage/*.py',
exclude=[
SOURCE_PY_DIR + 'storage/sd_salt.py',
] if TREZOR_MODEL not in ('T',) else []
] if not SDCARD else []
))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/messages/__init__.py'))
@ -654,14 +672,14 @@ if FROZEN:
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/common/*.py',
exclude=[
SOURCE_PY_DIR + 'apps/common/sdcard.py',
] if TREZOR_MODEL not in ('T',) else []
] if not SDCARD else []
))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/debug/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/homescreen/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/management/*.py',
exclude=[
SOURCE_PY_DIR + 'apps/management/sd_protect.py',
] if TREZOR_MODEL not in ('T',) else [] + [
] if not SDCARD else [] + [
SOURCE_PY_DIR + 'apps/management/authenticate_device.py',
] if TREZOR_MODEL not in ('R',) else [])
)
@ -728,7 +746,8 @@ if FROZEN:
source_dir=SOURCE_PY_DIR,
bitcoin_only=BITCOIN_ONLY,
backlight=TREZOR_MODEL in ('T',),
optiga=TREZOR_MODEL in ('R',)
optiga=TREZOR_MODEL in ('R',),
ui_layout=UI_LAYOUT,
)
source_mpyc = env.FrozenCFile(
@ -764,9 +783,7 @@ RUST_LIB = 'trezor_lib'
RUST_LIBPATH = f'{RUST_LIBDIR}/lib{RUST_LIB}.a'
def cargo_build():
# T1 does not have its own Rust feature, it shares it with TR
model_feature = 'model_tr' if TREZOR_MODEL == '1' else f'model_t{TREZOR_MODEL.lower()}'
features = ['micropython', 'protobuf', model_feature]
features = ['micropython', 'protobuf', ui_layout_feature]
if BITCOIN_ONLY == '1':
features.append('bitcoin_only')
features.append('ui')

@ -0,0 +1 @@
No longer erases seed when firmware is corrupted but firmware header is correct and signed. Added firmware corrupted info to bootloader screen.

@ -192,16 +192,18 @@ void ui_screen_boot_click(void) {
void ui_screen_welcome(void) { screen_welcome(); }
uint32_t ui_screen_intro(const vendor_header *const vhdr,
const image_header *const hdr) {
const image_header *const hdr, bool fw_ok) {
char bld_ver[32];
char ver_str[64];
format_ver("%d.%d.%d", VERSION_UINT32, bld_ver, sizeof(bld_ver));
format_ver("%d.%d.%d", hdr->version, ver_str, sizeof(ver_str));
return screen_intro(bld_ver, vhdr->vstr, vhdr->vstr_len, ver_str);
return screen_intro(bld_ver, vhdr->vstr, vhdr->vstr_len, ver_str, fw_ok);
}
uint32_t ui_screen_menu(void) { return screen_menu(); }
uint32_t ui_screen_menu(secbool firmware_present) {
return screen_menu(firmware_present);
}
// install UI
@ -242,17 +244,7 @@ void ui_screen_wipe_progress(int pos, int len) {
// done UI
void ui_screen_done(uint8_t restart_seconds, secbool full_redraw) {
const char *str;
char count_str[24];
if (restart_seconds >= 1) {
mini_snprintf(count_str, sizeof(count_str), "RESTARTING IN %d",
restart_seconds);
str = count_str;
} else {
str = "RECONNECT THE DEVICE";
}
screen_install_success(str, initial_setup, full_redraw);
screen_install_success(restart_seconds, initial_setup, full_redraw);
}
void ui_screen_boot_empty(bool fading) { screen_boot_empty(fading); }

@ -31,6 +31,7 @@ typedef enum {
SCREEN_WIPE_CONFIRM = 2,
SCREEN_FINGER_PRINT = 3,
SCREEN_WAIT_FOR_HOST = 4,
SCREEN_WELCOME = 5,
} screen_t;
void ui_screen_boot(const vendor_header* const vhdr,
@ -42,9 +43,9 @@ void ui_click(void);
void ui_screen_welcome(void);
uint32_t ui_screen_intro(const vendor_header* const vhdr,
const image_header* const hdr);
const image_header* const hdr, bool fw_ok);
uint32_t ui_screen_menu(void);
uint32_t ui_screen_menu(secbool firmware_present);
uint32_t ui_screen_install_confirm(const vendor_header* const vhdr,
const image_header* const hdr,

@ -87,7 +87,8 @@ __attribute__((noreturn)) void jump_to(void *addr) {
"STORAGE WAS ERASED");
} else {
printf("storage was retained\n");
screen_install_success("STORAGE WAS RETAINED", true, true);
screen_fatal_error_rust("BOOTLOADER EXIT", "Jumped to firmware",
"STORAGE WAS RETAINED");
}
display_backlight(180);
display_refresh();

@ -35,6 +35,9 @@
#ifdef USE_I2C
#include "i2c.h"
#endif
#ifdef USE_OPTIGA
#include "optiga_hal.h"
#endif
#ifdef USE_TOUCH
#include "touch.h"
#endif
@ -81,11 +84,15 @@ static const uint8_t * const BOOTLOADER_KEYS[] = {
#define USB_IFACE_NUM 0
typedef enum {
CONTINUE = 0,
RETURN = 1,
SHUTDOWN = 2,
SHUTDOWN = 0,
CONTINUE_TO_FIRMWARE = 0xAABBCCDD,
RETURN_TO_MENU = 0x55667788,
} usb_result_t;
volatile secbool dont_optimize_out_true = sectrue;
void failed_jump_to_firmware(void);
volatile void (*firmware_jump_fn)(void) = failed_jump_to_firmware;
static void usb_init_all(secbool usb21_landing) {
usb_dev_info_t dev_info = {
.device_class = 0x00,
@ -164,7 +171,7 @@ static usb_result_t bootloader_usb_loop(const vendor_header *const vhdr,
hal_delay(100);
usb_stop();
usb_deinit();
return RETURN;
return RETURN_TO_MENU;
}
ui_screen_wipe();
r = process_msg_WipeDevice(USB_IFACE_NUM, msg_size, buf);
@ -200,7 +207,7 @@ static usb_result_t bootloader_usb_loop(const vendor_header *const vhdr,
hal_delay(100);
usb_stop();
usb_deinit();
return RETURN;
return RETURN_TO_MENU;
} else if (r == 0) { // last chunk received
ui_screen_install_progress_upload(1000);
ui_screen_done(4, sectrue);
@ -213,7 +220,7 @@ static usb_result_t bootloader_usb_loop(const vendor_header *const vhdr,
usb_stop();
usb_deinit();
ui_screen_boot_empty(true);
return CONTINUE;
return CONTINUE_TO_FIRMWARE;
}
break;
case MessageType_MessageType_GetFeatures:
@ -227,7 +234,7 @@ static usb_result_t bootloader_usb_loop(const vendor_header *const vhdr,
hal_delay(100);
usb_stop();
usb_deinit();
return RETURN;
return RETURN_TO_MENU;
}
process_msg_UnlockBootloader(USB_IFACE_NUM, msg_size, buf);
screen_unlock_bootloader_success();
@ -294,6 +301,79 @@ static void check_bootloader_version(void) {
#endif
void failed_jump_to_firmware(void) {
error_shutdown("INTERNAL ERROR", "(glitch)");
}
void real_jump_to_firmware(void) {
const image_header *hdr = NULL;
vendor_header vhdr = {0};
ensure(read_vendor_header((const uint8_t *)FIRMWARE_START, &vhdr),
"Firmware is corrupted");
ensure(check_vendor_header_keys(&vhdr), "Firmware is corrupted");
ensure(check_vendor_header_lock(&vhdr), "Unauthorized vendor keys");
hdr =
read_image_header((const uint8_t *)(size_t)(FIRMWARE_START + vhdr.hdrlen),
FIRMWARE_IMAGE_MAGIC, FIRMWARE_IMAGE_MAXSIZE);
ensure(hdr == (const image_header *)(size_t)(FIRMWARE_START + vhdr.hdrlen)
? sectrue
: secfalse,
"Firmware is corrupted");
ensure(check_image_model(hdr), "Wrong firmware model");
ensure(check_image_header_sig(hdr, vhdr.vsig_m, vhdr.vsig_n, vhdr.vpub),
"Firmware is corrupted");
ensure(check_image_contents(hdr, IMAGE_HEADER_SIZE + vhdr.hdrlen,
&FIRMWARE_AREA),
"Firmware is corrupted");
#ifdef USE_OPTIGA
if (((vhdr.vtrust & VTRUST_SECRET) != 0) && (sectrue != secret_wiped())) {
ui_screen_install_restricted();
trezor_shutdown();
}
#endif
// if all VTRUST flags are unset = ultimate trust => skip the procedure
if ((vhdr.vtrust & VTRUST_ALL) != VTRUST_ALL) {
ui_fadeout();
ui_screen_boot(&vhdr, hdr);
ui_fadein();
int delay = (vhdr.vtrust & VTRUST_WAIT) ^ VTRUST_WAIT;
if (delay > 1) {
while (delay > 0) {
ui_screen_boot_wait(delay);
hal_delay(1000);
delay--;
}
} else if (delay == 1) {
hal_delay(1000);
}
if ((vhdr.vtrust & VTRUST_CLICK) == 0) {
ui_screen_boot_click();
}
ui_screen_boot_empty(false);
}
ensure_compatible_settings();
// mpu_config_firmware();
// jump_to_unprivileged(FIRMWARE_START + vhdr.hdrlen + IMAGE_HEADER_SIZE);
mpu_config_off();
jump_to(FIRMWARE_START + vhdr.hdrlen + IMAGE_HEADER_SIZE);
}
#ifndef TREZOR_EMULATOR
int main(void) {
// grab "stay in bootloader" flag as soon as possible
@ -315,41 +395,55 @@ int bootloader_main(void) {
mpu_config_bootloader();
#ifdef TREZOR_EMULATOR
// wait a bit so that the empty lock icon is visible
// (on a real device, we are waiting for touch init which takes longer)
hal_delay(400);
#endif
const image_header *hdr = NULL;
vendor_header vhdr;
// detect whether the device contains a valid firmware
secbool firmware_present = sectrue;
if (sectrue != read_vendor_header((const uint8_t *)FIRMWARE_START, &vhdr)) {
firmware_present = secfalse;
}
if (sectrue == firmware_present) {
firmware_present = check_vendor_header_keys(&vhdr);
// detect whether the device contains a valid firmware
volatile secbool vhdr_present = secfalse;
volatile secbool vhdr_keys_ok = secfalse;
volatile secbool vhdr_lock_ok = secfalse;
volatile secbool img_hdr_ok = secfalse;
volatile secbool model_ok = secfalse;
volatile secbool header_present = secfalse;
volatile secbool firmware_present = secfalse;
volatile secbool firmware_present_backup = secfalse;
vhdr_present = read_vendor_header((const uint8_t *)FIRMWARE_START, &vhdr);
if (sectrue == vhdr_present) {
vhdr_keys_ok = check_vendor_header_keys(&vhdr);
}
if (sectrue == firmware_present) {
firmware_present = check_vendor_header_lock(&vhdr);
if (sectrue == vhdr_keys_ok) {
vhdr_lock_ok = check_vendor_header_lock(&vhdr);
}
if (sectrue == firmware_present) {
if (sectrue == vhdr_lock_ok) {
hdr = read_image_header(
(const uint8_t *)(size_t)(FIRMWARE_START + vhdr.hdrlen),
FIRMWARE_IMAGE_MAGIC, FIRMWARE_IMAGE_MAXSIZE);
if (hdr != (const image_header *)(size_t)(FIRMWARE_START + vhdr.hdrlen)) {
firmware_present = secfalse;
if (hdr == (const image_header *)(size_t)(FIRMWARE_START + vhdr.hdrlen)) {
img_hdr_ok = sectrue;
}
}
if (sectrue == firmware_present) {
firmware_present = check_image_model(hdr);
if (sectrue == img_hdr_ok) {
model_ok = check_image_model(hdr);
}
if (sectrue == firmware_present) {
firmware_present =
if (sectrue == model_ok) {
header_present =
check_image_header_sig(hdr, vhdr.vsig_m, vhdr.vsig_n, vhdr.vpub);
}
if (sectrue == firmware_present) {
if (sectrue == header_present) {
firmware_present = check_image_contents(
hdr, IMAGE_HEADER_SIZE + vhdr.hdrlen, &FIRMWARE_AREA);
firmware_present_backup = firmware_present;
}
#if defined TREZOR_MODEL_T
@ -361,6 +455,10 @@ int bootloader_main(void) {
i2c_init();
#endif
#ifdef USE_OPTIGA
optiga_hal_init();
#endif
#ifdef USE_TOUCH
touch_power_on();
touch_init();
@ -390,6 +488,9 @@ int bootloader_main(void) {
stay_in_bootloader = sectrue;
}
ensure(dont_optimize_out_true * (firmware_present == firmware_present_backup),
NULL);
// delay to detect touch or skip if we know we are staying in bootloader
// anyway
uint32_t touched = 0;
@ -414,54 +515,64 @@ int bootloader_main(void) {
}
#endif
// start the bootloader if no or broken firmware found ...
if (firmware_present != sectrue) {
#ifdef TREZOR_EMULATOR
// wait a bit so that the empty lock icon is visible
// (on a real device, we are waiting for touch init which takes longer)
hal_delay(400);
#endif
// ignore stay in bootloader
stay_in_bootloader = secfalse;
touched = false;
ensure(dont_optimize_out_true * (firmware_present == firmware_present_backup),
NULL);
// start the bootloader ...
// ... if user touched the screen on start
// ... or we have stay_in_bootloader flag to force it
// ... or there is no valid firmware
if (touched || stay_in_bootloader == sectrue || firmware_present != sectrue) {
screen_t screen;
if (header_present == sectrue) {
ui_set_initial_setup(false);
screen = SCREEN_INTRO;
} else {
screen = SCREEN_WELCOME;
ui_set_initial_setup(true);
// erase storage
ensure(flash_area_erase_bulk(STORAGE_AREAS, STORAGE_AREAS_COUNT, NULL),
NULL);
// keep the model screen up for a while
ui_set_initial_setup(true);
// keep the model screen up for a while
#ifndef USE_BACKLIGHT
hal_delay(1500);
hal_delay(1500);
#else
// backlight fading takes some time so the explicit delay here is shorter
hal_delay(1000);
// backlight fading takes some time so the explicit delay here is
// shorter
hal_delay(1000);
#endif
// show welcome screen
ui_screen_welcome();
// erase storage
ensure(flash_area_erase_bulk(STORAGE_AREAS, STORAGE_AREAS_COUNT, NULL),
NULL);
// and start the usb loop
if (bootloader_usb_loop(NULL, NULL) != CONTINUE) {
return 1;
}
}
// ... or if user touched the screen on start
// ... or we have stay_in_bootloader flag to force it
if (touched || stay_in_bootloader == sectrue) {
ui_set_initial_setup(false);
screen_t screen = SCREEN_INTRO;
while (true) {
bool continue_to_firmware = false;
volatile secbool continue_to_firmware = secfalse;
volatile secbool continue_to_firmware_backup = secfalse;
uint32_t ui_result = 0;
switch (screen) {
case SCREEN_WELCOME:
ui_screen_welcome();
// and start the usb loop
switch (bootloader_usb_loop(NULL, NULL)) {
case CONTINUE_TO_FIRMWARE:
continue_to_firmware = sectrue;
continue_to_firmware_backup = sectrue;
break;
case RETURN_TO_MENU:
break;
default:
case SHUTDOWN:
return 1;
break;
}
break;
case SCREEN_INTRO:
ui_result = ui_screen_intro(&vhdr, hdr);
ui_result = ui_screen_intro(&vhdr, hdr, firmware_present);
if (ui_result == 1) {
screen = SCREEN_MENU;
}
@ -470,15 +581,16 @@ int bootloader_main(void) {
}
break;
case SCREEN_MENU:
ui_result = ui_screen_menu();
if (ui_result == 1) { // exit menu
ui_result = ui_screen_menu(firmware_present);
if (ui_result == 0xAABBCCDD) { // exit menu
screen = SCREEN_INTRO;
}
if (ui_result == 2) { // reboot
if (ui_result == 0x11223344) { // reboot
ui_screen_boot_empty(true);
continue_to_firmware = true;
continue_to_firmware = firmware_present;
continue_to_firmware_backup = firmware_present_backup;
}
if (ui_result == 3) { // wipe
if (ui_result == 0x55667788) { // wipe
screen = SCREEN_WIPE_CONFIRM;
}
break;
@ -503,10 +615,11 @@ int bootloader_main(void) {
case SCREEN_WAIT_FOR_HOST:
screen_connect();
switch (bootloader_usb_loop(&vhdr, hdr)) {
case CONTINUE:
continue_to_firmware = true;
case CONTINUE_TO_FIRMWARE:
continue_to_firmware = sectrue;
continue_to_firmware_backup = sectrue;
break;
case RETURN:
case RETURN_TO_MENU:
screen = SCREEN_INTRO;
break;
case SHUTDOWN:
@ -520,75 +633,29 @@ int bootloader_main(void) {
break;
}
if (continue_to_firmware) {
if (continue_to_firmware != continue_to_firmware_backup) {
// erase storage if we saw flips randomly flip, most likely due to
// glitch
ensure(flash_area_erase_bulk(STORAGE_AREAS, STORAGE_AREAS_COUNT, NULL),
NULL);
}
ensure(dont_optimize_out_true *
(continue_to_firmware == continue_to_firmware_backup),
NULL);
if (sectrue == continue_to_firmware) {
firmware_jump_fn = real_jump_to_firmware;
break;
}
}
}
ensure(read_vendor_header((const uint8_t *)FIRMWARE_START, &vhdr),
"Firmware is corrupted");
ensure(check_vendor_header_keys(&vhdr), "Firmware is corrupted");
ensure(check_vendor_header_lock(&vhdr), "Unauthorized vendor keys");
hdr =
read_image_header((const uint8_t *)(size_t)(FIRMWARE_START + vhdr.hdrlen),
FIRMWARE_IMAGE_MAGIC, FIRMWARE_IMAGE_MAXSIZE);
ensure(hdr == (const image_header *)(size_t)(FIRMWARE_START + vhdr.hdrlen)
? sectrue
: secfalse,
"Firmware is corrupted");
ensure(check_image_model(hdr), "Wrong firmware model");
ensure(check_image_header_sig(hdr, vhdr.vsig_m, vhdr.vsig_n, vhdr.vpub),
"Firmware is corrupted");
ensure(check_image_contents(hdr, IMAGE_HEADER_SIZE + vhdr.hdrlen,
&FIRMWARE_AREA),
"Firmware is corrupted");
#ifdef USE_OPTIGA
if (((vhdr.vtrust & VTRUST_SECRET) != 0) && (sectrue != secret_wiped())) {
ui_screen_install_restricted();
return 1;
}
#endif
// if all VTRUST flags are unset = ultimate trust => skip the procedure
if ((vhdr.vtrust & VTRUST_ALL) != VTRUST_ALL) {
ui_fadeout();
ui_screen_boot(&vhdr, hdr);
ui_fadein();
int delay = (vhdr.vtrust & VTRUST_WAIT) ^ VTRUST_WAIT;
if (delay > 1) {
while (delay > 0) {
ui_screen_boot_wait(delay);
hal_delay(1000);
delay--;
}
} else if (delay == 1) {
hal_delay(1000);
}
if ((vhdr.vtrust & VTRUST_CLICK) == 0) {
ui_screen_boot_click();
}
ui_screen_boot_empty(false);
ensure(dont_optimize_out_true * (firmware_present == firmware_present_backup),
NULL);
if (sectrue == firmware_present) {
firmware_jump_fn = real_jump_to_firmware;
}
ensure_compatible_settings();
// mpu_config_firmware();
// jump_to_unprivileged(FIRMWARE_START + vhdr.hdrlen + IMAGE_HEADER_SIZE);
mpu_config_off();
jump_to(FIRMWARE_START + vhdr.hdrlen + IMAGE_HEADER_SIZE);
firmware_jump_fn();
return 0;
}

@ -295,6 +295,7 @@ STATIC mp_obj_str_t mod_trezorutils_model_name_obj = {
/// INTERNAL_MODEL: str
/// EMULATOR: bool
/// BITCOIN_ONLY: bool
/// UI_LAYOUT: str
STATIC const mp_rom_map_elem_t mp_module_trezorutils_globals_table[] = {
{MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_trezorutils)},
@ -348,6 +349,13 @@ STATIC const mp_rom_map_elem_t mp_module_trezorutils_globals_table[] = {
#else
{MP_ROM_QSTR(MP_QSTR_BITCOIN_ONLY), mp_const_false},
#endif
#ifdef UI_LAYOUT_TT
{MP_ROM_QSTR(MP_QSTR_UI_LAYOUT), MP_ROM_QSTR(MP_QSTR_TT)},
#elif UI_LAYOUT_TR
{MP_ROM_QSTR(MP_QSTR_UI_LAYOUT), MP_ROM_QSTR(MP_QSTR_TR)},
#else
#error Unknown layout
#endif
};
STATIC MP_DEFINE_CONST_DICT(mp_module_trezorutils_globals,

@ -300,6 +300,9 @@ void SVC_C_Handler(uint32_t *stack) {
// raising privileges.
stack[6] = (uintptr_t)reboot_to_bootloader;
return;
case SVC_GET_SYSTICK_VAL: {
systick_val_copy = SysTick->VAL;
} break;
default:
stack[0] = 0xffffffff;
break;

@ -1,6 +1,6 @@
#define VERSION_MAJOR 2
#define VERSION_MINOR 6
#define VERSION_PATCH 1
#define VERSION_PATCH 2
#define VERSION_BUILD 0
#define FIX_VERSION_MAJOR 2

@ -6,12 +6,15 @@
// - the third, fourth and fifth bytes are advance, bearingX and bearingY of the horizontal metrics of the glyph
// - the rest is packed 1-bit glyph data
// NOTE: hand-changed all the advances with 8 to 7
// NOTE: hand-changed the visuals of "m", "w", "M" and "W" to make it narrower
/* */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_32[] = { 0, 0, 7, 0, 0 }; // width hand-changed from 8 to 7 to have 9px space between words
/* ! */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_33[] = { 1, 7, 7, 2, 7, 250 };
/* " */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_34[] = { 3, 3, 7, 1, 7, 182, 128 };
/* # */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_35[] = { 6, 6, 7, 0, 6, 75, 244, 146, 253, 32 };
/* $ */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_36[] = { 5, 7, 7, 0, 7, 35, 168, 226, 248, 128 };
/* % */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_37[] = { 7, 7, 8, 0, 7, 65, 74, 162, 162, 169, 65, 0 };
/* % */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_37[] = { 7, 7, 7, 0, 7, 65, 74, 162, 162, 169, 65, 0 };
/* & */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_38[] = { 5, 7, 7, 0, 7, 116, 96, 232, 197, 224 };
/* ' */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_39[] = { 1, 3, 7, 2, 7, 224 };
/* ( */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_40[] = { 3, 7, 7, 2, 7, 42, 72, 136 };
@ -38,7 +41,7 @@
/* = */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_61[] = { 4, 3, 7, 1, 5, 240, 240 };
/* > */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_62[] = { 3, 5, 7, 1, 6, 136, 168 };
/* ? */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_63[] = { 5, 7, 7, 0, 7, 116, 66, 34, 0, 128 };
/* @ */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_64[] = { 7, 8, 8, 0, 7, 125, 6, 109, 90, 179, 160, 62, 0 };
/* @ */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_64[] = { 7, 8, 7, 0, 7, 125, 6, 109, 90, 179, 160, 62, 0 };
/* A */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_65[] = { 5, 7, 7, 0, 7, 116, 99, 31, 198, 32 };
/* B */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_66[] = { 5, 7, 7, 0, 7, 244, 99, 232, 199, 192 };
/* C */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_67[] = { 5, 7, 7, 0, 7, 116, 97, 8, 69, 192 };
@ -51,7 +54,7 @@
/* J */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_74[] = { 6, 7, 7, 0, 7, 60, 32, 130, 10, 39, 0 };
/* K */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_75[] = { 5, 7, 7, 0, 7, 140, 169, 138, 74, 32 };
/* L */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_76[] = { 5, 7, 7, 0, 7, 132, 33, 8, 67, 224 };
/* M */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_77[] = { 7, 7, 8, 0, 7, 131, 7, 29, 89, 48, 96, 128 };
/* M */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_77[] = { 5, 7, 7, 0, 7, 142, 235, 24, 198, 32 };
/* N */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_78[] = { 5, 7, 7, 0, 7, 140, 115, 89, 198, 32 };
/* O */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_79[] = { 5, 7, 7, 0, 7, 116, 99, 24, 197, 192 };
/* P */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_80[] = { 5, 7, 7, 0, 7, 244, 99, 31, 66, 0 };
@ -61,7 +64,7 @@
/* T */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_84[] = { 5, 7, 7, 0, 7, 249, 8, 66, 16, 128 };
/* U */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_85[] = { 5, 7, 7, 0, 7, 140, 99, 24, 197, 192 };
/* V */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_86[] = { 5, 7, 7, 0, 7, 140, 99, 24, 168, 128 };
/* W */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_87[] = { 7, 7, 8, 0, 7, 131, 6, 76, 153, 50, 91, 0 };
/* W */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_87[] = { 5, 7, 7, 0, 7, 140, 107, 90, 213, 64 };
/* X */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_88[] = { 5, 7, 7, 0, 7, 140, 84, 69, 70, 32 };
/* Y */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_89[] = { 5, 7, 7, 0, 7, 140, 84, 66, 16, 128 };
/* Z */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_90[] = { 5, 7, 7, 0, 7, 248, 68, 68, 67, 224 };
@ -69,7 +72,7 @@
/* \ */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_92[] = { 3, 7, 7, 1, 7, 145, 36, 72 };
/* ] */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_93[] = { 3, 7, 7, 0, 7, 228, 146, 120 };
/* ^ */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_94[] = { 5, 3, 7, 0, 7, 34, 162 };
/* _ */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_95[] = { 8, 1, 8, 0, 0, 255, 0 };
/* _ */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_95[] = { 8, 1, 7, 0, 0, 255, 0 };
/* ` */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_96[] = { 2, 2, 7, 1, 7, 144 };
/* a */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_97[] = { 5, 5, 7, 0, 5, 112, 95, 23, 128 };
/* b */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_98[] = { 5, 7, 7, 0, 7, 132, 61, 24, 199, 192 };
@ -83,7 +86,7 @@
/* j */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_106[] = { 5, 8, 7, 0, 7, 8, 14, 16, 134, 46, 0 };
/* k */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_107[] = { 5, 7, 7, 0, 7, 132, 37, 78, 74, 32 };
/* l */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_108[] = { 5, 7, 7, 0, 7, 225, 8, 66, 19, 224 };
/* m */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_109[] = { 7, 5, 8, 0, 5, 237, 38, 76, 24, 32 };
/* m */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_109[] = { 5, 5, 7, 0, 5, 213, 107, 24, 128 };
/* n */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_110[] = { 5, 5, 7, 0, 5, 244, 99, 24, 128 };
/* o */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_111[] = { 5, 5, 7, 0, 5, 116, 99, 23, 0 };
/* p */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_112[] = { 5, 6, 7, 0, 5, 244, 99, 232, 64 };
@ -93,7 +96,7 @@
/* t */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_116[] = { 4, 6, 7, 1, 6, 79, 68, 67, 0 };
/* u */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_117[] = { 5, 5, 7, 0, 5, 140, 99, 23, 0 };
/* v */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_118[] = { 5, 5, 7, 0, 5, 140, 98, 162, 0 };
/* w */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_119[] = { 7, 5, 8, 0, 5, 131, 6, 76, 150, 192 };
/* w */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_119[] = { 5, 5, 7, 0, 5, 140, 107, 85, 0 };
/* x */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_120[] = { 5, 5, 7, 0, 5, 138, 136, 168, 128 };
/* y */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_121[] = { 5, 6, 7, 0, 5, 140, 98, 240, 184 };
/* z */ static const uint8_t Font_PixelOperatorMono_Regular_8_glyph_122[] = { 5, 5, 7, 0, 5, 248, 136, 143, 128 };

@ -12,14 +12,15 @@ void screen_install_progress(int16_t progress, bool initialize,
bool initial_setup);
void screen_wipe_progress(int16_t progress, bool initialize);
uint32_t screen_intro(const char* bld_version_str, const char* vendor_str,
uint8_t vendor_str_len, const char* version_str);
uint32_t screen_menu(void);
uint8_t vendor_str_len, const char* version_str,
bool fw_ok);
uint32_t screen_menu(secbool firmware_present);
void screen_connect(void);
void screen_fatal_error_rust(const char* title, const char* msg,
const char* footer);
void screen_wipe_success(void);
void screen_wipe_fail(void);
uint32_t screen_install_success(const char* reboot_msg, bool initial_setup,
uint32_t screen_install_success(uint8_t restart_seconds, bool initial_setup,
bool complete_draw);
uint32_t screen_install_fail(void);
void screen_welcome_model(void);

@ -9,7 +9,7 @@ pub mod dma2d;
mod ffi;
pub mod io;
pub mod random;
#[cfg(feature = "model_tr")]
#[cfg(feature = "rgb_led")]
pub mod rgb_led;
pub mod slip39;
pub mod storage;
@ -18,6 +18,8 @@ pub mod uzlib;
pub mod wordlist;
pub mod buffers;
pub mod secbool;
#[cfg(not(feature = "micropython"))]
pub mod time;

@ -0,0 +1,3 @@
use super::ffi;
pub use ffi::{secbool, secfalse, sectrue};

@ -59,8 +59,6 @@ pub struct Chunks {
pub chunk_size: usize,
/// How big will be the space between chunks (in pixels).
pub x_offset: i16,
/// Optional characters that are wider and should be accounted for
pub wider_chars: Option<&'static str>,
}
impl Chunks {
@ -68,20 +66,6 @@ impl Chunks {
Chunks {
chunk_size,
x_offset,
wider_chars: None,
}
}
pub const fn with_wider_chars(mut self, wider_chars: &'static str) -> Self {
self.wider_chars = Some(wider_chars);
self
}
pub fn is_char_wider(self, ch: char) -> bool {
if let Some(wider_chars) = self.wider_chars {
wider_chars.contains(ch)
} else {
false
}
}
}
@ -192,6 +176,14 @@ impl TextStyle {
self.text_font.text_width(ELLIPSIS)
}
}
fn prev_page_ellipsis_icon_width(&self) -> i16 {
if let Some((icon, _)) = self.prev_page_ellipsis_icon {
icon.toif.width()
} else {
0
}
}
}
impl TextLayout {
@ -262,9 +254,21 @@ impl TextLayout {
PageBreaking::CutAndInsertEllipsisBoth
) && self.continues_from_prev_page
{
sink.prev_page_ellipsis(*cursor, self);
// Move the cursor to the right, always the same distance
cursor.x += self.style.prev_page_ellipsis_width();
// Special case in chunkifying text - move the cursor so that we
// start with the second chunk.
if let Some(chunk_config) = self.style.chunks {
// Showing the arrow at the last chunk position
// Assuming mono-font, so all the letters have the same width
let letter_size = self.style.text_font.text_width("a");
let icon_offset = self.style.prev_page_ellipsis_icon_width() + 2;
cursor.x += chunk_config.chunk_size as i16 * letter_size - icon_offset;
sink.prev_page_ellipsis(*cursor, self);
cursor.x += icon_offset + chunk_config.x_offset;
} else {
sink.prev_page_ellipsis(*cursor, self);
cursor.x += self.style.prev_page_ellipsis_width();
}
}
while !remaining_text.is_empty() {
@ -285,6 +289,20 @@ impl TextLayout {
self.style.chunks,
);
if let Some(chunk_config) = self.style.chunks {
// Last chunk on the page should not be rendered, put just ellipsis there
// Chunks is last when the next chunk would not fit on the page horizontally
let is_last_chunk = (2 * span.advance.x - chunk_config.x_offset) > remaining_width;
if is_last_line && is_last_chunk && remaining_text.len() > chunk_config.chunk_size {
// Making sure no text is rendered here, and that we force a line break
span.length = 0;
span.advance.x = 2; // To start at the same horizontal line as the chunk itself
span.advance.y = self.bounds.y1;
span.insert_hyphen_before_line_break = false;
span.skip_next_chars = 0;
}
}
cursor.x += match self.align {
Alignment::Start => 0,
Alignment::Center => (remaining_width - span.advance.x) / 2,
@ -598,7 +616,6 @@ impl Span {
let mut span_width = 0;
let mut found_any_whitespace = false;
let mut chunks_wider_chars = 0;
let mut char_indices_iter = text.char_indices().peekable();
// Iterating manually because we need a reference to the iterator inside the
@ -611,14 +628,8 @@ impl Span {
if let Some(chunkify_config) = chunks {
if i == chunkify_config.chunk_size {
line.advance.y = 0;
// Decreasing the offset for each wider character in the chunk
line.advance.x += chunkify_config.x_offset - chunks_wider_chars;
line.advance.x += chunkify_config.x_offset;
return line;
} else {
// Counting all the wider characters in the chunk
if chunkify_config.is_char_wider(ch) {
chunks_wider_chars += 1;
}
}
}

@ -155,7 +155,7 @@ impl<'a> Component for Confirm<'a> {
let msg = self.buttons.event(ctx, event);
if self.showing_info_screen {
// Showing the info screen currently - going back with the left button
if let Some(ButtonControllerMsg::Triggered(ButtonPos::Left)) = msg {
if let Some(ButtonControllerMsg::Triggered(ButtonPos::Left, _)) = msg {
self.showing_info_screen = false;
self.update_everything(ctx);
};
@ -163,11 +163,13 @@ impl<'a> Component for Confirm<'a> {
} else if self.has_info_screen() {
// Being on the "main" screen but with an info screen available on the right
match msg {
Some(ButtonControllerMsg::Triggered(ButtonPos::Left)) => Some(ConfirmMsg::Cancel),
Some(ButtonControllerMsg::Triggered(ButtonPos::Middle)) => {
Some(ButtonControllerMsg::Triggered(ButtonPos::Left, _)) => {
Some(ConfirmMsg::Cancel)
}
Some(ButtonControllerMsg::Triggered(ButtonPos::Middle, _)) => {
Some(ConfirmMsg::Confirm)
}
Some(ButtonControllerMsg::Triggered(ButtonPos::Right)) => {
Some(ButtonControllerMsg::Triggered(ButtonPos::Right, _)) => {
self.showing_info_screen = true;
self.update_everything(ctx);
None
@ -176,8 +178,10 @@ impl<'a> Component for Confirm<'a> {
}
} else if self.two_btn_confirm {
match msg {
Some(ButtonControllerMsg::Triggered(ButtonPos::Left)) => Some(ConfirmMsg::Cancel),
Some(ButtonControllerMsg::Triggered(ButtonPos::Middle)) => {
Some(ButtonControllerMsg::Triggered(ButtonPos::Left, _)) => {
Some(ConfirmMsg::Cancel)
}
Some(ButtonControllerMsg::Triggered(ButtonPos::Middle, _)) => {
Some(ConfirmMsg::Confirm)
}
_ => None,
@ -185,8 +189,12 @@ impl<'a> Component for Confirm<'a> {
} else {
// There is just one main screen without info screen
match msg {
Some(ButtonControllerMsg::Triggered(ButtonPos::Left)) => Some(ConfirmMsg::Cancel),
Some(ButtonControllerMsg::Triggered(ButtonPos::Right)) => Some(ConfirmMsg::Confirm),
Some(ButtonControllerMsg::Triggered(ButtonPos::Left, _)) => {
Some(ConfirmMsg::Cancel)
}
Some(ButtonControllerMsg::Triggered(ButtonPos::Right, _)) => {
Some(ConfirmMsg::Confirm)
}
_ => None,
}
}

@ -1,6 +1,6 @@
use crate::ui::{
component::{Child, Component, Event, EventCtx, Label, Pad},
geometry::{Alignment2D, Rect},
geometry::{Alignment, Alignment2D, Rect},
};
use super::{
@ -32,10 +32,11 @@ pub struct Intro<'a> {
title: Child<Label<&'a str>>,
buttons: Child<ButtonController<&'static str>>,
text: Child<Label<&'a str>>,
warn: Option<Child<Label<&'a str>>>,
}
impl<'a> Intro<'a> {
pub fn new(title: &'a str, content: &'a str) -> Self {
pub fn new(title: &'a str, content: &'a str, fw_ok: bool) -> Self {
Self {
bg: Pad::with_background(BLD_BG).with_clear(),
title: Child::new(Label::centered(title, TEXT_NORMAL).vertically_centered()),
@ -44,6 +45,10 @@ impl<'a> Intro<'a> {
RIGHT_BUTTON_TEXT,
))),
text: Child::new(Label::left_aligned(content, TEXT_NORMAL).vertically_centered()),
warn: (!fw_ok).then_some(Child::new(
Label::new("FIRMWARE CORRUPTED", Alignment::Start, TEXT_NORMAL)
.vertically_centered(),
)),
}
}
}
@ -61,16 +66,25 @@ impl<'a> Component for Intro<'a> {
self.title.place(title_area);
self.buttons.place(buttons_area);
self.text.place(text_area);
if self.warn.is_some() {
let (warn_area, text_area) = text_area.split_top(10);
self.warn.place(warn_area);
self.text.place(text_area);
} else {
self.text.place(text_area);
}
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
let msg = self.buttons.event(ctx, event);
if let Some(Triggered(ButtonPos::Left)) = msg {
if let Some(Triggered(ButtonPos::Left, _)) = msg {
return Some(Self::Msg::InstallFirmware);
};
if let Some(Triggered(ButtonPos::Right)) = msg {
if let Some(Triggered(ButtonPos::Right, _)) = msg {
return Some(Self::Msg::GoToMenu);
};
None
@ -82,6 +96,7 @@ impl<'a> Component for Intro<'a> {
let area = self.bg.area;
ICON_WARN_TITLE.draw(area.top_left(), Alignment2D::TOP_LEFT, BLD_FG, BLD_BG);
ICON_WARN_TITLE.draw(area.top_right(), Alignment2D::TOP_RIGHT, BLD_FG, BLD_BG);
self.warn.paint();
self.text.paint();
self.buttons.paint();
}
@ -89,6 +104,7 @@ impl<'a> Component for Intro<'a> {
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
self.title.bounds(sink);
self.warn.bounds(sink);
self.text.bounds(sink);
self.buttons.bounds(sink);
}

@ -1,11 +1,14 @@
#[cfg(feature = "ui_debug")]
use crate::trace::{Trace, Tracer};
use crate::ui::{
component::{Child, Component, Event, EventCtx, Pad},
constant::screen,
display,
display::{Font, Icon},
geometry::{Alignment2D, Offset, Point, Rect},
use crate::{
trezorhal::secbool::{secbool, sectrue},
ui::{
component::{Child, Component, Event, EventCtx, Pad},
constant::screen,
display,
display::{Font, Icon},
geometry::{Alignment2D, Offset, Point, Rect},
},
};
use super::{
@ -17,9 +20,9 @@ use super::{
#[repr(u32)]
#[derive(Copy, Clone)]
pub enum MenuMsg {
Close = 1,
Reboot = 2,
FactoryReset = 3,
Close = 0xAABBCCDD,
Reboot = 0x11223344,
FactoryReset = 0x55667788,
}
impl ReturnToC for MenuMsg {
fn return_to_c(self) -> u32 {
@ -74,17 +77,19 @@ impl Trace for MenuChoice {
}
}
pub struct MenuChoiceFactory;
pub struct MenuChoiceFactory {
firmware_present: secbool,
}
impl MenuChoiceFactory {
const CHOICES: [(&'static str, &'static str, Icon); CHOICE_LENGTH] = [
("Factory", "reset", ICON_TRASH),
("Reboot", "Trezor", ICON_REDO),
("Exit", "menu", ICON_EXIT),
("Reboot", "Trezor", ICON_REDO),
];
pub fn new() -> Self {
Self {}
pub fn new(firmware_present: secbool) -> Self {
Self { firmware_present }
}
}
@ -93,7 +98,11 @@ impl ChoiceFactory<&'static str> for MenuChoiceFactory {
type Item = MenuChoice;
fn count(&self) -> usize {
CHOICE_LENGTH
if self.firmware_present == sectrue {
CHOICE_LENGTH
} else {
CHOICE_LENGTH - 1
}
}
fn get(&self, choice_index: usize) -> (Self::Item, Self::Action) {
@ -104,8 +113,8 @@ impl ChoiceFactory<&'static str> for MenuChoiceFactory {
);
let action = match choice_index {
0 => MenuMsg::FactoryReset,
1 => MenuMsg::Reboot,
2 => MenuMsg::Close,
1 => MenuMsg::Close,
2 if self.firmware_present == sectrue => MenuMsg::Reboot,
_ => unreachable!(),
};
(choice_item, action)
@ -118,8 +127,8 @@ pub struct Menu {
}
impl Menu {
pub fn new() -> Self {
let choices = MenuChoiceFactory::new();
pub fn new(firmware_present: secbool) -> Self {
let choices = MenuChoiceFactory::new(firmware_present);
Self {
pad: Pad::with_background(BLD_BG).with_clear(),
choice_page: Child::new(
@ -141,7 +150,7 @@ impl Component for Menu {
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
self.choice_page.event(ctx, event)
self.choice_page.event(ctx, event).map(|evt| evt.0)
}
fn paint(&mut self) {

@ -21,11 +21,14 @@ mod menu;
mod theme;
mod welcome;
use crate::ui::{
constant,
constant::HEIGHT,
geometry::Point,
model_tr::theme::{ICON_ARM_LEFT, ICON_ARM_RIGHT, WHITE},
use crate::{
trezorhal::secbool::secbool,
ui::{
constant,
constant::HEIGHT,
geometry::Point,
model_tr::theme::{ICON_ARM_LEFT, ICON_ARM_RIGHT, WHITE},
},
};
use confirm::Confirm;
use connect::Connect;
@ -192,8 +195,8 @@ extern "C" fn screen_unlock_bootloader_success() {
}
#[no_mangle]
extern "C" fn screen_menu(_bld_version: *const cty::c_char) -> u32 {
run(&mut Menu::new())
extern "C" fn screen_menu(firmware_present: secbool) -> u32 {
run(&mut Menu::new(firmware_present))
}
#[no_mangle]
@ -202,6 +205,7 @@ extern "C" fn screen_intro(
vendor_str: *const cty::c_char,
vendor_str_len: u8,
version: *const cty::c_char,
fw_ok: bool,
) -> u32 {
let vendor = unwrap!(unsafe { from_c_array(vendor_str, vendor_str_len as usize) });
let version = unwrap!(unsafe { from_c_str(version) });
@ -217,7 +221,7 @@ extern "C" fn screen_intro(
unwrap!(version_str.push_str("\nby "));
unwrap!(version_str.push_str(vendor));
let mut frame = Intro::new(title_str.as_str(), version_str.as_str());
let mut frame = Intro::new(title_str.as_str(), version_str.as_str(), fw_ok);
run(&mut frame)
}
@ -301,7 +305,7 @@ extern "C" fn screen_wipe_success() {
let title = Label::centered("Trezor Reset", theme::TEXT_BOLD).vertically_centered();
let content =
Label::centered("Reconnect\nthe device", theme::TEXT_NORMAL).vertically_centered();
Label::centered("Please reconnect\nthe device", theme::TEXT_NORMAL).vertically_centered();
let mut frame = ResultScreen::new(BLD_FG, BLD_BG, ICON_SPINNER, title, content, true);
show(&mut frame);
@ -339,15 +343,24 @@ extern "C" fn screen_install_fail() {
#[no_mangle]
extern "C" fn screen_install_success(
reboot_msg: *const cty::c_char,
restart_seconds: u8,
_initial_setup: bool,
complete_draw: bool,
) {
let msg = unwrap!(unsafe { from_c_str(reboot_msg) });
let mut reboot_msg = BootloaderString::new();
if restart_seconds >= 1 {
unwrap!(reboot_msg.push_str("Restarting in "));
// in practice, restart_seconds is 5 or less so this is fine
let seconds_char = b'0' + restart_seconds % 10;
unwrap!(reboot_msg.push(seconds_char as char));
} else {
unwrap!(reboot_msg.push_str("Reconnect the device"));
}
let title = Label::centered("Firmware installed", theme::TEXT_BOLD).vertically_centered();
let content = Label::centered(msg, theme::TEXT_NORMAL).vertically_centered();
let content = Label::centered(reboot_msg.as_str(), theme::TEXT_NORMAL).vertically_centered();
let mut frame = ResultScreen::new(BLD_FG, BLD_BG, ICON_SPINNER, title, content, complete_draw);
show(&mut frame);

@ -204,7 +204,7 @@ where
};
let button_event = self.buttons.event(ctx, event);
if let Some(ButtonControllerMsg::Triggered(button)) = button_event {
if let Some(ButtonControllerMsg::Triggered(button, _)) = button_event {
if self.is_in_subpage() {
match button {
ButtonPos::Left => {

@ -349,6 +349,7 @@ pub struct ButtonDetails<T> {
with_arms: bool,
fixed_width: Option<i16>,
offset: Offset,
pub send_long_press: bool,
}
impl<T> ButtonDetails<T>
@ -364,6 +365,7 @@ where
with_arms: false,
fixed_width: None,
offset: Offset::zero(),
send_long_press: false,
}
}
@ -376,6 +378,7 @@ where
with_arms: false,
fixed_width: None,
offset: Offset::zero(),
send_long_press: false,
}
}

@ -3,7 +3,7 @@ use super::{
};
use crate::{
strutil::StringType,
time::Duration,
time::{Duration, Instant},
ui::{
component::{base::Event, Component, EventCtx, Pad, TimerToken},
event::{ButtonEvent, PhysicalButton},
@ -39,8 +39,13 @@ enum ButtonState {
}
pub enum ButtonControllerMsg {
/// Button was pressed down.
Pressed(ButtonPos),
Triggered(ButtonPos),
/// Which button was triggered, and whether it was pressed for a longer
/// time before releasing.
Triggered(ButtonPos, bool),
/// Button was pressed and held for longer time (not released yet).
LongPressed(ButtonPos),
}
/// Defines what kind of button should be currently used.
@ -105,6 +110,16 @@ where
{
pos: ButtonPos,
button_type: ButtonType<T>,
/// Holds the timestamp of when the button was pressed.
pressed_since: Option<Instant>,
/// How long the button should be pressed to send `long_press=true` in
/// `ButtonControllerMsg::Triggered`
long_press_ms: u32,
/// Timer for sending `ButtonControllerMsg::LongPressed`
long_pressed_timer: Option<TimerToken>,
/// Whether it should even send `ButtonControllerMsg::LongPressed` events
/// (optional)
send_long_press: bool,
}
impl<T> ButtonContainer<T>
@ -114,9 +129,17 @@ where
/// Supplying `None` as `btn_details` marks the button inactive
/// (it can be later activated in `set()`).
pub fn new(pos: ButtonPos, btn_details: Option<ButtonDetails<T>>) -> Self {
const DEFAULT_LONG_PRESS_MS: u32 = 1000;
let send_long_press = btn_details
.as_ref()
.map_or(false, |btn| btn.send_long_press);
Self {
pos,
button_type: ButtonType::from_button_details(pos, btn_details),
pressed_since: None,
long_press_ms: DEFAULT_LONG_PRESS_MS,
long_pressed_timer: None,
send_long_press,
}
}
@ -124,6 +147,9 @@ where
///
/// Passing `None` as `btn_details` will mark the button as inactive.
pub fn set(&mut self, btn_details: Option<ButtonDetails<T>>, button_area: Rect) {
self.send_long_press = btn_details
.as_ref()
.map_or(false, |btn| btn.send_long_press);
self.button_type = ButtonType::from_button_details(self.pos, btn_details);
self.button_type.place(button_area);
}
@ -151,7 +177,15 @@ where
/// hold.
pub fn maybe_trigger(&mut self, ctx: &mut EventCtx) -> Option<ButtonControllerMsg> {
match self.button_type {
ButtonType::Button(_) => Some(ButtonControllerMsg::Triggered(self.pos)),
ButtonType::Button(_) => {
// Finding out whether the button was long-pressed
let long_press = self.pressed_since.map_or(false, |since| {
Instant::now().saturating_duration_since(since).to_millis() > self.long_press_ms
});
self.pressed_since = None;
self.long_pressed_timer = None;
Some(ButtonControllerMsg::Triggered(self.pos, long_press))
}
_ => {
self.hold_ended(ctx);
None
@ -169,6 +203,27 @@ where
false
}
/// Saving the timestamp of when the button was pressed.
/// Also requesting a timer for long-press if wanted.
pub fn got_pressed(&mut self, ctx: &mut EventCtx) {
self.pressed_since = Some(Instant::now());
if self.send_long_press {
self.long_pressed_timer =
Some(ctx.request_timer(Duration::from_millis(self.long_press_ms)));
}
}
/// Reset the pressed information.
pub fn reset(&mut self) {
self.pressed_since = None;
self.long_pressed_timer = None;
}
/// Whether token matches what we have
pub fn is_timer_token(&self, token: TimerToken) -> bool {
self.long_pressed_timer == Some(token)
}
/// Registering hold event.
pub fn hold_started(&mut self, ctx: &mut EventCtx) {
if let ButtonType::HoldToConfirm(htc) = &mut self.button_type {
@ -283,18 +338,63 @@ where
if self.left_btn.htc_got_triggered(ctx, event) {
self.state = ButtonState::HTCNeedsRelease(PhysicalButton::Left);
self.set_pressed(ctx, false, false, false);
return Some(ButtonControllerMsg::Triggered(ButtonPos::Left));
return Some(ButtonControllerMsg::Triggered(ButtonPos::Left, true));
} else if self.middle_btn.htc_got_triggered(ctx, event) {
self.state = ButtonState::Nothing;
self.set_pressed(ctx, false, false, false);
return Some(ButtonControllerMsg::Triggered(ButtonPos::Middle));
return Some(ButtonControllerMsg::Triggered(ButtonPos::Middle, true));
} else if self.right_btn.htc_got_triggered(ctx, event) {
self.state = ButtonState::HTCNeedsRelease(PhysicalButton::Right);
self.set_pressed(ctx, false, false, false);
return Some(ButtonControllerMsg::Triggered(ButtonPos::Right));
return Some(ButtonControllerMsg::Triggered(ButtonPos::Right, true));
}
None
}
fn reset_button_presses(&mut self) {
self.left_btn.reset();
self.middle_btn.reset();
self.right_btn.reset();
}
fn got_pressed(&mut self, ctx: &mut EventCtx, pos: ButtonPos) {
// Only one (virtual) button can be pressed at the same time
self.reset_button_presses();
match pos {
ButtonPos::Left => {
self.left_btn.got_pressed(ctx);
}
ButtonPos::Middle => {
self.middle_btn.got_pressed(ctx);
}
ButtonPos::Right => {
self.right_btn.got_pressed(ctx);
}
}
}
fn handle_long_press_timer_token(&mut self, token: TimerToken) -> Option<ButtonPos> {
if self.left_btn.is_timer_token(token) {
return Some(ButtonPos::Left);
}
if self.middle_btn.is_timer_token(token) {
return Some(ButtonPos::Middle);
}
if self.right_btn.is_timer_token(token) {
return Some(ButtonPos::Right);
}
None
}
/// Resetting the state of the controller.
pub fn reset_state(&mut self, ctx: &mut EventCtx) {
self.state = ButtonState::Nothing;
self.reset_button_presses();
self.set_pressed(ctx, false, false, false);
if let Some(ignore_btn_delay) = &mut self.ignore_btn_delay {
ignore_btn_delay.reset();
}
}
}
impl<T> Component for ButtonController<T>
@ -325,11 +425,13 @@ where
match which {
// ▼ *
PhysicalButton::Left => {
self.got_pressed(ctx, ButtonPos::Left);
self.left_btn.hold_started(ctx);
Some(ButtonControllerMsg::Pressed(ButtonPos::Left))
}
// * ▼
PhysicalButton::Right => {
self.got_pressed(ctx, ButtonPos::Right);
self.right_btn.hold_started(ctx);
Some(ButtonControllerMsg::Pressed(ButtonPos::Right))
}
@ -369,6 +471,7 @@ where
return None;
}
}
self.got_pressed(ctx, ButtonPos::Middle);
self.middle_hold_started(ctx);
(
// ↓ ↓
@ -399,7 +502,7 @@ where
// ▲ * | * ▲
ButtonEvent::ButtonReleased(b) if b != which_up => {
// _ _
// Both button needs to be clickable now
// Both buttons need to be clickable now
if let Some(ignore_btn_delay) = &mut self.ignore_btn_delay {
ignore_btn_delay.make_button_clickable(ButtonPos::Left);
ignore_btn_delay.make_button_clickable(ButtonPos::Right);
@ -447,6 +550,9 @@ where
if let Some(ignore_btn_delay) = &mut self.ignore_btn_delay {
ignore_btn_delay.handle_timer_token(token);
}
if let Some(pos) = self.handle_long_press_timer_token(token) {
return Some(ButtonControllerMsg::LongPressed(pos));
}
self.handle_htc_expiration(ctx, event)
}
_ => None,
@ -544,6 +650,13 @@ impl IgnoreButtonDelay {
self.right_clickable_timer = None;
}
}
pub fn reset(&mut self) {
self.left_clickable = true;
self.right_clickable = true;
self.left_clickable_timer = None;
self.right_clickable_timer = None;
}
}
/// Component allowing for automatically moving through items (e.g. Choice

@ -256,7 +256,7 @@ where
// Do something when a button was triggered
// and we have some action connected with it
if let Some(ButtonControllerMsg::Triggered(pos)) = button_event {
if let Some(ButtonControllerMsg::Triggered(pos, _)) = button_event {
// When there is a previous or next screen in the current flow,
// handle that first and in case it triggers, then do not continue
if self.event_consumed_by_current_choice(ctx, pos) {

@ -12,7 +12,7 @@ use crate::{
use super::{
super::constant, common::display_center, theme, ButtonController, ButtonControllerMsg,
ButtonLayout,
ButtonLayout, ButtonPos, CancelConfirmMsg,
};
const AREA: Rect = constant::screen();
@ -144,7 +144,7 @@ where
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
Self::event_usb(self, ctx, event);
// HTC press of any button will lock the device
if let Some(ButtonControllerMsg::Triggered(_)) = self.invisible_buttons.event(ctx, event) {
if let Some(ButtonControllerMsg::Triggered(..)) = self.invisible_buttons.event(ctx, event) {
return Some(());
}
None
@ -202,7 +202,7 @@ where
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
// Press of any button will unlock the device
if let Some(ButtonControllerMsg::Triggered(_)) = self.invisible_buttons.event(ctx, event) {
if let Some(ButtonControllerMsg::Triggered(..)) = self.invisible_buttons.event(ctx, event) {
return Some(());
}
None
@ -220,6 +220,70 @@ where
}
}
pub struct ConfirmHomescreen<T, F>
where
T: StringType,
{
title: Child<Label<T>>,
buffer_func: F,
buttons: Child<ButtonController<T>>,
}
impl<T, F> ConfirmHomescreen<T, F>
where
T: StringType + Clone,
{
pub fn new(title: T, buffer_func: F) -> Self {
let btn_layout = ButtonLayout::cancel_none_text("CHANGE".into());
ConfirmHomescreen {
title: Child::new(Label::centered(title, theme::TEXT_BOLD)),
buffer_func,
buttons: Child::new(ButtonController::new(btn_layout)),
}
}
}
impl<'a, T, F> Component for ConfirmHomescreen<T, F>
where
T: StringType + Clone,
F: Fn() -> &'a [u8],
{
type Msg = CancelConfirmMsg;
fn place(&mut self, bounds: Rect) -> Rect {
let (title_content_area, button_area) = bounds.split_bottom(theme::BUTTON_HEIGHT);
let title_height = theme::TEXT_BOLD.text_font.line_height();
let (title_area, _) = title_content_area.split_top(title_height);
self.title.place(title_area);
self.buttons.place(button_area);
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
// Left button cancels, right confirms
if let Some(ButtonControllerMsg::Triggered(pos, _)) = self.buttons.event(ctx, event) {
match pos {
ButtonPos::Left => return Some(CancelConfirmMsg::Cancelled),
ButtonPos::Right => return Some(CancelConfirmMsg::Confirmed),
_ => {}
}
}
None
}
fn paint(&mut self) {
// Drawing the image full-screen first and then other things on top
let toif_data = unwrap!(Toif::new((self.buffer_func)()));
toif_data.draw(TOP_CENTER, Alignment2D::TOP_CENTER, theme::FG, theme::BG);
// Need to make all the title background black, so the title text is well
// visible
let title_area = self.title.inner().area();
rect_fill(title_area, theme::BG);
self.title.paint();
self.buttons.paint();
}
}
// DEBUG-ONLY SECTION BELOW
#[cfg(feature = "ui_debug")]
@ -243,3 +307,14 @@ where
t.child("label", &self.label);
}
}
#[cfg(feature = "ui_debug")]
impl<T, F> crate::trace::Trace for ConfirmHomescreen<T, F>
where
T: StringType,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("ConfirmHomescreen");
t.child("title", &self.title);
}
}

@ -29,6 +29,12 @@ pub trait Choice<T: StringType> {
fn btn_layout(&self) -> ButtonLayout<T> {
ButtonLayout::default_three_icons()
}
/// Whether it is possible to do the middle action event without
/// releasing the button - after long-press duration is reached.
fn trigger_middle_without_release(&self) -> bool {
false
}
}
/// Interface for a specific component efficiently giving
@ -130,7 +136,7 @@ where
/// Need to update the initial button layout.
pub fn with_initial_page_counter(mut self, page_counter: usize) -> Self {
self.page_counter = page_counter;
let initial_btn_layout = self.get_current_choice().0.btn_layout();
let initial_btn_layout = self.get_current_item().btn_layout();
self.buttons = Child::new(
ButtonController::new(initial_btn_layout)
.with_ignore_btn_delay(constant::IGNORE_OTHER_BTN_MS),
@ -235,7 +241,7 @@ where
}
// Getting the remaining left and right areas.
let center_width = self.get_current_choice().0.width_center();
let center_width = self.get_current_item().width_center();
let (left_area, _center_area, right_area) = center_row_area.split_center(center_width);
// Possibly drawing on the left side.
@ -277,14 +283,23 @@ where
}
/// Getting the choice on the current index
pub fn get_current_choice(&self) -> (<F as ChoiceFactory<T>>::Item, A) {
fn get_current_choice(&self) -> (<F as ChoiceFactory<T>>::Item, A) {
self.choices.get(self.page_counter)
}
/// Getting the current item
pub fn get_current_item(&self) -> <F as ChoiceFactory<T>>::Item {
self.get_current_choice().0
}
/// Getting the current action
pub fn get_current_action(&self) -> A {
self.get_current_choice().1
}
/// Display the current choice in the middle.
fn show_current_choice(&mut self, area: Rect) {
self.get_current_choice()
.0
self.get_current_item()
.paint_center(area, self.inverse_selected_item);
// Color inversion is just one-time thing.
@ -406,7 +421,7 @@ where
/// If defined in the current choice, setting their text,
/// whether they are long-pressed, and painting them.
fn set_buttons(&mut self, ctx: &mut EventCtx) {
let btn_layout = self.get_current_choice().0.btn_layout();
let btn_layout = self.get_current_item().btn_layout();
self.buttons.mutate(ctx, |ctx, buttons| {
buttons.set(btn_layout);
// When user holds one of the buttons, highlighting it.
@ -474,7 +489,7 @@ where
F: ChoiceFactory<T, Action = A>,
T: StringType + Clone,
{
type Msg = A;
type Msg = (A, bool);
fn place(&mut self, bounds: Rect) -> Rect {
let (content_area, button_area) = bounds.split_bottom(theme::BUTTON_HEIGHT);
@ -516,7 +531,7 @@ where
// Stopping the automatic movement when the released button is the same as the
// direction we were moving, or when the pressed button is the
// opposite one (user does middle-click).
if matches!(button_event, Some(ButtonControllerMsg::Triggered(pos)) if pos == moving_direction)
if matches!(button_event, Some(ButtonControllerMsg::Triggered(pos, _)) if pos == moving_direction)
|| matches!(button_event, Some(ButtonControllerMsg::Pressed(pos)) if pos != moving_direction)
{
self.holding_mover.stop_moving();
@ -534,7 +549,7 @@ where
}
// There was a legitimate button event - doing some action
if let Some(ButtonControllerMsg::Triggered(pos)) = button_event {
if let Some(ButtonControllerMsg::Triggered(pos, long_press)) = button_event {
match pos {
ButtonPos::Left => {
// Clicked BACK. Decrease the page counter.
@ -547,12 +562,24 @@ where
self.move_right(ctx);
}
ButtonPos::Middle => {
// Clicked SELECT. Send current choice index
// Clicked SELECT. Send current choice index with information about long-press
self.clear_and_repaint(ctx);
return Some(self.get_current_choice().1);
return Some((self.get_current_action(), long_press));
}
}
};
// The middle button was pressed for longer time - sending the Event with long
// press. Also resetting the functional and visual state of the buttons.
// Only doing this when the item is configured to do so
if let Some(ButtonControllerMsg::LongPressed(ButtonPos::Middle)) = button_event {
if self.get_current_item().trigger_middle_without_release() {
self.buttons.mutate(ctx, |ctx, buttons| {
buttons.reset_state(ctx);
});
self.clear_and_repaint(ctx);
return Some((self.get_current_action(), true));
}
};
// The middle button was pressed, highlighting the current choice by color
// inversion.
if let Some(ButtonControllerMsg::Pressed(ButtonPos::Middle)) = button_event {

@ -19,6 +19,7 @@ pub struct ChoiceItem<T: StringType> {
icon: Option<Icon>,
btn_layout: ButtonLayout<T>,
font: Font,
middle_action_without_release: bool,
}
impl<T: StringType> ChoiceItem<T> {
@ -28,6 +29,7 @@ impl<T: StringType> ChoiceItem<T> {
icon: None,
btn_layout,
font: theme::FONT_CHOICE_ITEMS,
middle_action_without_release: false,
}
}
@ -43,6 +45,15 @@ impl<T: StringType> ChoiceItem<T> {
self
}
/// Allows for middle action without release.
pub fn with_middle_action_without_release(mut self) -> Self {
self.middle_action_without_release = true;
if let Some(middle) = self.btn_layout.btn_middle.as_mut() {
middle.send_long_press = true;
}
self
}
/// Setting left button.
pub fn set_left_btn(&mut self, btn_left: Option<ButtonDetails<T>>) {
self.btn_layout.btn_left = btn_left;
@ -117,6 +128,11 @@ where
fn btn_layout(&self) -> ButtonLayout<T> {
self.btn_layout.clone()
}
/// Whether to do middle action without release
fn trigger_middle_without_release(&self) -> bool {
self.middle_action_without_release
}
}
fn paint_rounded_highlight(area: Rect, size: Offset, inverse: bool) {

@ -78,7 +78,7 @@ where
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
self.choice_page.event(ctx, event)
self.choice_page.event(ctx, event).map(|evt| evt.0)
}
fn paint(&mut self) {

@ -43,50 +43,63 @@ const DIGITS_INDEX: usize = 5;
const SPECIAL_INDEX: usize = 6;
const SPACE_INDEX: usize = 7;
// Menu text, action, icon data, middle button with CONFIRM
const MENU: [(&str, PassphraseAction, Option<Icon>, bool); MENU_LENGTH] = [
("SHOW", PassphraseAction::Show, Some(theme::ICON_EYE), true),
/// Menu text, action, icon data, middle button with CONFIRM, without_release
const MENU: [(&str, PassphraseAction, Option<Icon>, bool, bool); MENU_LENGTH] = [
(
"SHOW",
PassphraseAction::Show,
Some(theme::ICON_EYE),
true,
false,
),
(
"CANCEL_OR_DELETE", // will be chosen dynamically
PassphraseAction::CancelOrDelete,
None,
true,
true, // without_release
),
(
"ENTER",
PassphraseAction::Enter,
Some(theme::ICON_TICK),
true,
false,
),
(
"abc",
PassphraseAction::Category(ChoiceCategory::LowercaseLetter),
None,
false,
false,
),
(
"ABC",
PassphraseAction::Category(ChoiceCategory::UppercaseLetter),
None,
false,
false,
),
(
"123",
PassphraseAction::Category(ChoiceCategory::Digit),
None,
false,
false,
),
(
"#$!",
PassphraseAction::Category(ChoiceCategory::SpecialSymbol),
None,
false,
false,
),
(
"SPACE",
PassphraseAction::Character(' '),
Some(theme::ICON_SPACE),
false,
false,
),
];
@ -164,7 +177,7 @@ impl ChoiceFactoryPassphrase {
choice_index: usize,
) -> (ChoiceItem<T>, PassphraseAction) {
// More options for CANCEL/DELETE button
let (mut text, action, mut icon, show_confirm) = MENU[choice_index];
let (mut text, action, mut icon, show_confirm, without_release) = MENU[choice_index];
if matches!(action, PassphraseAction::CancelOrDelete) {
if self.is_empty {
text = "CANCEL";
@ -183,6 +196,11 @@ impl ChoiceFactoryPassphrase {
menu_item.set_middle_btn(Some(confirm_btn));
}
// Making middle button create LongPress events
if without_release {
menu_item = menu_item.with_middle_action_without_release();
}
if let Some(icon) = icon {
menu_item = menu_item.with_icon(icon);
}
@ -291,6 +309,10 @@ where
self.textbox.delete_last(ctx);
}
fn delete_all_digits(&mut self, ctx: &mut EventCtx) {
self.textbox.clear(ctx);
}
/// Displaying the MENU
fn show_menu_page(&mut self, ctx: &mut EventCtx) {
let menu_choices = ChoiceFactoryPassphrase::new(ChoiceCategory::Menu, self.is_empty());
@ -359,13 +381,18 @@ where
}
}
if let Some(action) = self.choice_page.event(ctx, event) {
if let Some((action, long_press)) = self.choice_page.event(ctx, event) {
match action {
PassphraseAction::CancelOrDelete => {
if self.is_empty() {
return Some(CancelConfirmMsg::Cancelled);
} else {
self.delete_last_digit(ctx);
// Deleting all when long-pressed
if long_press {
self.delete_all_digits(ctx);
} else {
self.delete_last_digit(ctx);
}
self.update_passphrase_dots(ctx);
if self.is_empty() {
// Allowing for DELETE/CANCEL change

@ -27,20 +27,22 @@ const EMPTY_PIN_STR: &str = "_";
const CHOICE_LENGTH: usize = 13;
const NUMBER_START_INDEX: usize = 3;
const CHOICES: [(&str, PinAction, Option<Icon>); CHOICE_LENGTH] = [
("DELETE", PinAction::Delete, Some(theme::ICON_DELETE)),
("SHOW", PinAction::Show, Some(theme::ICON_EYE)),
("ENTER", PinAction::Enter, Some(theme::ICON_TICK)),
("0", PinAction::Digit('0'), None),
("1", PinAction::Digit('1'), None),
("2", PinAction::Digit('2'), None),
("3", PinAction::Digit('3'), None),
("4", PinAction::Digit('4'), None),
("5", PinAction::Digit('5'), None),
("6", PinAction::Digit('6'), None),
("7", PinAction::Digit('7'), None),
("8", PinAction::Digit('8'), None),
("9", PinAction::Digit('9'), None),
/// Text, action, icon, without_release
const CHOICES: [(&str, PinAction, Option<Icon>, bool); CHOICE_LENGTH] = [
// DELETE should be triggerable without release (after long-press)
("DELETE", PinAction::Delete, Some(theme::ICON_DELETE), true),
("SHOW", PinAction::Show, Some(theme::ICON_EYE), false),
("ENTER", PinAction::Enter, Some(theme::ICON_TICK), false),
("0", PinAction::Digit('0'), None, false),
("1", PinAction::Digit('1'), None, false),
("2", PinAction::Digit('2'), None, false),
("3", PinAction::Digit('3'), None, false),
("4", PinAction::Digit('4'), None, false),
("5", PinAction::Digit('5'), None, false),
("6", PinAction::Digit('6'), None, false),
("7", PinAction::Digit('7'), None, false),
("8", PinAction::Digit('8'), None, false),
("9", PinAction::Digit('9'), None, false),
];
fn get_random_digit_position() -> usize {
@ -54,7 +56,7 @@ impl<T: StringType + Clone> ChoiceFactory<T> for ChoiceFactoryPIN {
type Item = ChoiceItem<T>;
fn get(&self, choice_index: usize) -> (Self::Item, Self::Action) {
let (choice_str, action, icon) = CHOICES[choice_index];
let (choice_str, action, icon, without_release) = CHOICES[choice_index];
let mut choice_item = ChoiceItem::new(choice_str, ButtonLayout::default_three_icons());
@ -64,6 +66,11 @@ impl<T: StringType + Clone> ChoiceFactory<T> for ChoiceFactoryPIN {
choice_item.set_middle_btn(Some(confirm_btn));
}
// Making middle button create LongPress events
if without_release {
choice_item = choice_item.with_middle_action_without_release();
}
// Adding icons for appropriate items
if let Some(icon) = icon {
choice_item = choice_item.with_icon(icon);
@ -240,29 +247,34 @@ where
}
}
match self.choice_page.event(ctx, event) {
Some(PinAction::Delete) => {
self.textbox.delete_last(ctx);
self.update(ctx);
None
}
Some(PinAction::Show) => {
self.show_real_pin = true;
self.update(ctx);
None
}
Some(PinAction::Enter) => Some(CancelConfirmMsg::Confirmed),
Some(PinAction::Digit(ch)) if !self.is_full() => {
self.textbox.append(ctx, ch);
// Choosing random digit to be shown next
self.choice_page
.set_page_counter(ctx, get_random_digit_position(), true);
self.show_last_digit = true;
self.update(ctx);
None
if let Some((action, long_press)) = self.choice_page.event(ctx, event) {
match action {
PinAction::Delete => {
// Deleting all when long-pressed
if long_press {
self.textbox.clear(ctx);
} else {
self.textbox.delete_last(ctx);
}
self.update(ctx);
}
PinAction::Show => {
self.show_real_pin = true;
self.update(ctx);
}
PinAction::Enter => return Some(CancelConfirmMsg::Confirmed),
PinAction::Digit(ch) if !self.is_full() => {
self.textbox.append(ctx, ch);
// Choosing random digit to be shown next
self.choice_page
.set_page_counter(ctx, get_random_digit_position(), true);
self.show_last_digit = true;
self.update(ctx);
}
_ => {}
}
_ => None,
}
None
}
fn paint(&mut self) {

@ -112,7 +112,7 @@ where
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
self.choice_page.event(ctx, event)
self.choice_page.event(ctx, event).map(|evt| evt.0)
}
fn paint(&mut self) {

@ -95,7 +95,8 @@ impl<T: StringType + Clone> ChoiceFactory<T> for ChoiceFactoryWordlist {
if choice_index == DELETE_INDEX {
return (
ChoiceItem::new("DELETE", ButtonLayout::arrow_armed_arrow("CONFIRM".into()))
.with_icon(theme::ICON_DELETE),
.with_icon(theme::ICON_DELETE)
.with_middle_action_without_release(),
WordlistAction::Delete,
);
}
@ -156,19 +157,49 @@ where
ChoiceFactoryWordlist::new(self.wordlist_type, self.textbox.content())
}
fn get_last_textbox_letter(&self) -> Option<char> {
self.textbox.content().chars().last()
}
fn get_new_page_counter(&self, new_choices: &ChoiceFactoryWordlist) -> usize {
// Starting at the random position in case of letters and at the beginning in
// case of words.
if self.offer_words {
INITIAL_PAGE_COUNTER
} else {
let choices_count = <ChoiceFactoryWordlist as ChoiceFactory<T>>::count(new_choices);
// There should be always DELETE and at least one letter
assert!(choices_count > 1);
if choices_count == 2 {
// In case there is only DELETE and one letter, starting on that letter
// (regardless of the last letter in the textbox)
return INITIAL_PAGE_COUNTER;
}
// We do not want to end up at the same letter as the last one in the textbox
loop {
let random_position = get_random_position(choices_count);
let current_action =
<ChoiceFactoryWordlist as ChoiceFactory<T>>::get(new_choices, random_position)
.1;
if let WordlistAction::Letter(current_letter) = current_action {
if let Some(last_letter) = self.get_last_textbox_letter() {
if current_letter == last_letter {
// Randomly trying again when the last and current letter match
continue;
}
}
}
break random_position;
}
}
}
/// Updates the whole page.
fn update(&mut self, ctx: &mut EventCtx) {
self.update_chosen_letters(ctx);
let new_choices = self.get_current_choices();
self.offer_words = new_choices.offer_words;
// Starting at the random position in case of letters and at the beginning in
// case of words
let new_page_counter = if self.offer_words {
INITIAL_PAGE_COUNTER
} else {
let choices_count = <ChoiceFactoryWordlist as ChoiceFactory<T>>::count(&new_choices);
get_random_position(choices_count)
};
let new_page_counter = self.get_new_page_counter(&new_choices);
// Not using carousel in case of words, as that looks weird in case
// there is only one word to choose from.
self.choice_page
@ -201,19 +232,25 @@ where
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
match self.choice_page.event(ctx, event) {
Some(WordlistAction::Delete) => {
self.textbox.delete_last(ctx);
self.update(ctx);
}
Some(WordlistAction::Letter(letter)) => {
self.textbox.append(ctx, letter);
self.update(ctx);
}
Some(WordlistAction::Word(word)) => {
return Some(word);
if let Some((action, long_press)) = self.choice_page.event(ctx, event) {
match action {
WordlistAction::Delete => {
// Deleting all when long-pressed
if long_press {
self.textbox.clear(ctx);
} else {
self.textbox.delete_last(ctx);
}
self.update(ctx);
}
WordlistAction::Letter(letter) => {
self.textbox.append(ctx, letter);
self.update(ctx);
}
WordlistAction::Word(word) => {
return Some(word);
}
}
_ => {}
}
None
}

@ -50,7 +50,7 @@ pub use flow::Flow;
pub use flow_pages::{FlowPages, Page};
pub use frame::{Frame, ScrollableContent, ScrollableFrame};
#[cfg(feature = "micropython")]
pub use homescreen::{Homescreen, Lockscreen};
pub use homescreen::{ConfirmHomescreen, Homescreen, Lockscreen};
pub use input_methods::{
number_input::NumberInput,
passphrase::PassphraseEntry,

@ -182,7 +182,7 @@ where
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
ctx.set_page_count(self.page_count());
if let Some(ButtonControllerMsg::Triggered(pos)) = self.buttons.event(ctx, event) {
if let Some(ButtonControllerMsg::Triggered(pos, _)) = self.buttons.event(ctx, event) {
match pos {
ButtonPos::Left => {
if self.has_previous_page() {

@ -5,8 +5,10 @@ use crate::ui::{
geometry::{Alignment2D, Offset, Point, Rect},
};
const MESSAGE_AREA_START: i16 = 26;
const FOOTER_AREA_START: i16 = 40;
const MESSAGE_AREA_START: i16 = 24 + 11;
const MESSAGE_AREA_START_2L: i16 = 24 + 7;
const FOOTER_AREA_START: i16 = MESSAGE_AREA_START + 10;
const FOOTER_AREA_START_2L: i16 = MESSAGE_AREA_START_2L + 10;
const ICON_TOP: i16 = 12;
pub struct ResultScreen<'a> {
@ -53,15 +55,36 @@ impl<'a> Component for ResultScreen<'a> {
fn place(&mut self, bounds: Rect) -> Rect {
self.bg.place(bounds);
self.message_top.place(Rect::new(
Point::new(0, MESSAGE_AREA_START),
Point::new(WIDTH, FOOTER_AREA_START),
));
let bottom_area = Rect::new(Point::new(0, FOOTER_AREA_START), Point::new(WIDTH, HEIGHT));
self.small_pad.place(bottom_area);
self.message_bottom.place(bottom_area);
let h = self.message_bottom.inner().text_height(WIDTH);
if h > 8 {
self.message_top.place(Rect::new(
Point::new(0, MESSAGE_AREA_START_2L),
Point::new(WIDTH, FOOTER_AREA_START_2L),
));
let bottom_area = Rect::new(
Point::new(0, FOOTER_AREA_START_2L),
Point::new(WIDTH, FOOTER_AREA_START_2L + h),
);
self.message_bottom.place(bottom_area);
} else {
self.message_top.place(Rect::new(
Point::new(0, MESSAGE_AREA_START),
Point::new(WIDTH, FOOTER_AREA_START),
));
let bottom_area = Rect::new(
Point::new(0, FOOTER_AREA_START),
Point::new(WIDTH, FOOTER_AREA_START + h),
);
self.message_bottom.place(bottom_area);
}
self.small_pad.place(bottom_area);
bounds
}

@ -134,7 +134,7 @@ where
self.text.event(ctx, event);
self.headline.event(ctx, event);
if let Some(ButtonControllerMsg::Triggered(ButtonPos::Right)) =
if let Some(ButtonControllerMsg::Triggered(ButtonPos::Right, _)) =
self.buttons.event(ctx, event)
{
button_confirmed = true;

@ -58,7 +58,7 @@ where
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
let button_event = self.buttons.event(ctx, event);
if let Some(ButtonControllerMsg::Triggered(pos)) = button_event {
if let Some(ButtonControllerMsg::Triggered(pos, _)) = button_event {
match pos {
ButtonPos::Left => {
return Some(CancelInfoConfirmMsg::Cancelled);

@ -6,8 +6,15 @@ use crate::{
error::Error,
maybe_trace::MaybeTrace,
micropython::{
buffer::StrBuffer, gc::Gc, iter::IterBuf, list::List, map::Map, module::Module, obj::Obj,
qstr::Qstr, util,
buffer::{get_buffer, StrBuffer},
gc::Gc,
iter::IterBuf,
list::List,
map::Map,
module::Module,
obj::Obj,
qstr::Qstr,
util,
},
strutil::StringType,
ui::{
@ -38,9 +45,10 @@ use crate::{
use super::{
component::{
AddressDetails, ButtonActions, ButtonDetails, ButtonLayout, ButtonPage, CancelConfirmMsg,
CancelInfoConfirmMsg, CoinJoinProgress, Flow, FlowPages, Frame, Homescreen, Lockscreen,
NumberInput, Page, PassphraseEntry, PinEntry, Progress, ScrollableContent, ScrollableFrame,
ShareWords, ShowMore, SimpleChoice, WelcomeScreen, WordlistEntry, WordlistType,
CancelInfoConfirmMsg, CoinJoinProgress, ConfirmHomescreen, Flow, FlowPages, Frame,
Homescreen, Lockscreen, NumberInput, Page, PassphraseEntry, PinEntry, Progress,
ScrollableContent, ScrollableFrame, ShareWords, ShowMore, SimpleChoice, WelcomeScreen,
WordlistEntry, WordlistType,
},
constant, theme,
};
@ -242,6 +250,19 @@ where
}
}
impl<'a, T, F> ComponentMsgObj for ConfirmHomescreen<T, F>
where
T: StringType + Clone,
F: Fn() -> &'a [u8],
{
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
match msg {
CancelConfirmMsg::Confirmed => Ok(CONFIRMED.as_obj()),
CancelConfirmMsg::Cancelled => Ok(CANCELLED.as_obj()),
}
}
}
/// Function to create and call a `ButtonPage` dialog based on paginable content
/// (e.g. `Paragraphs` or `FormattedText`).
/// Has optional title (supply empty `StrBuffer` for that) and hold-to-confirm
@ -392,6 +413,24 @@ extern "C" fn new_confirm_properties(n_args: usize, args: *const Obj, kwargs: *m
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_confirm_homescreen(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let data: Obj = kwargs.get(Qstr::MP_QSTR_image)?;
// Layout needs to hold the Obj to play nice with GC. Obj is resolved to &[u8]
// in every paint pass.
// SAFETY: We expect no existing mutable reference. Resulting reference is
// discarded before returning to micropython.
let buffer_func = move || unsafe { unwrap!(get_buffer(data)) };
let obj = LayoutObj::new(ConfirmHomescreen::new(title, buffer_func))?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_confirm_reset_device(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
@ -1587,6 +1626,14 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// """Confirm action."""
Qstr::MP_QSTR_confirm_action => obj_fn_kw!(0, new_confirm_action).as_obj(),
/// def confirm_homescreen(
/// *,
/// title: str,
/// image: bytes,
/// ) -> object:
/// """Confirm homescreen."""
Qstr::MP_QSTR_confirm_homescreen => obj_fn_kw!(0, new_confirm_homescreen).as_obj(),
/// def confirm_blob(
/// *,
/// title: str,

@ -43,8 +43,8 @@ pub const TEXT_MONO_ADDRESS_CHUNKS: TextStyle = TEXT_MONO_DATA
.with_line_spacing(2)
.with_ellipsis_icon(ICON_NEXT_PAGE, -2);
// Chunks for this model, with accounting for some wider characters in MONO font
pub const MONO_CHUNKS: Chunks = Chunks::new(4, 4).with_wider_chars("mMwW");
// Chunks for this model
pub const MONO_CHUNKS: Chunks = Chunks::new(4, 4);
/// Convert Python-side numeric id to a `TextStyle`.
pub fn textstyle_number(num: i32) -> &'static TextStyle {

@ -2,11 +2,11 @@ use crate::ui::{
component::{Child, Component, Event, EventCtx, Label, Pad},
constant::screen,
display::Icon,
geometry::{Insets, Point, Rect},
geometry::{Alignment, Insets, Point, Rect},
model_tt::{
bootloader::theme::{
button_bld, button_bld_menu, BLD_BG, BUTTON_AREA_START, BUTTON_HEIGHT, CONTENT_PADDING,
CORNER_BUTTON_AREA, MENU32, TEXT_NORMAL, TEXT_TITLE, TITLE_AREA,
CORNER_BUTTON_AREA, MENU32, TEXT_NORMAL, TEXT_TITLE, TEXT_WARNING, TITLE_AREA,
},
component::{Button, ButtonMsg::Clicked},
constant::WIDTH,
@ -26,10 +26,11 @@ pub struct Intro<'a> {
menu: Child<Button<&'static str>>,
host: Child<Button<&'static str>>,
text: Child<Label<&'a str>>,
warn: Option<Child<Label<&'a str>>>,
}
impl<'a> Intro<'a> {
pub fn new(title: &'a str, content: &'a str) -> Self {
pub fn new(title: &'a str, content: &'a str, fw_ok: bool) -> Self {
Self {
bg: Pad::with_background(BLD_BG).with_clear(),
title: Child::new(Label::left_aligned(title, TEXT_TITLE).vertically_centered()),
@ -40,6 +41,10 @@ impl<'a> Intro<'a> {
),
host: Child::new(Button::with_text("INSTALL FIRMWARE").styled(button_bld())),
text: Child::new(Label::left_aligned(content, TEXT_NORMAL).vertically_centered()),
warn: (!fw_ok).then_some(Child::new(
Label::new("FIRMWARE CORRUPTED", Alignment::Start, TEXT_WARNING)
.vertically_centered(),
)),
}
}
}
@ -56,10 +61,25 @@ impl<'a> Component for Intro<'a> {
Point::new(CONTENT_PADDING, BUTTON_AREA_START),
Point::new(WIDTH - CONTENT_PADDING, BUTTON_AREA_START + BUTTON_HEIGHT),
));
self.text.place(Rect::new(
Point::new(CONTENT_PADDING, TITLE_AREA.y1 + CONTENT_PADDING),
Point::new(WIDTH - CONTENT_PADDING, BUTTON_AREA_START - CONTENT_PADDING),
));
if self.warn.is_some() {
self.warn.place(Rect::new(
Point::new(CONTENT_PADDING, TITLE_AREA.y1 + CONTENT_PADDING),
Point::new(
WIDTH - CONTENT_PADDING,
TITLE_AREA.y1 + CONTENT_PADDING + 30,
),
));
self.text.place(Rect::new(
Point::new(CONTENT_PADDING, TITLE_AREA.y1 + CONTENT_PADDING + 30),
Point::new(WIDTH - CONTENT_PADDING, BUTTON_AREA_START - CONTENT_PADDING),
));
} else {
self.text.place(Rect::new(
Point::new(CONTENT_PADDING, TITLE_AREA.y1 + CONTENT_PADDING),
Point::new(WIDTH - CONTENT_PADDING, BUTTON_AREA_START - CONTENT_PADDING),
));
}
bounds
}
@ -77,6 +97,7 @@ impl<'a> Component for Intro<'a> {
self.bg.paint();
self.title.paint();
self.text.paint();
self.warn.paint();
self.host.paint();
self.menu.paint();
}

@ -1,15 +1,18 @@
use crate::ui::{
component::{Child, Component, Event, EventCtx, Label, Pad},
constant::{screen, WIDTH},
display::Icon,
geometry::{Insets, Point, Rect},
model_tt::{
bootloader::theme::{
button_bld, button_bld_menu, BLD_BG, BUTTON_HEIGHT, CONTENT_PADDING,
CORNER_BUTTON_AREA, CORNER_BUTTON_TOUCH_EXPANSION, FIRE24, REFRESH24, TEXT_TITLE,
TITLE_AREA, X32,
use crate::{
trezorhal::secbool::{secbool, sectrue},
ui::{
component::{Child, Component, Event, EventCtx, Label, Pad},
constant::{screen, WIDTH},
display::Icon,
geometry::{Insets, Point, Rect},
model_tt::{
bootloader::theme::{
button_bld, button_bld_menu, BLD_BG, BUTTON_HEIGHT, CONTENT_PADDING,
CORNER_BUTTON_AREA, CORNER_BUTTON_TOUCH_EXPANSION, FIRE24, REFRESH24, TEXT_TITLE,
TITLE_AREA, X32,
},
component::{Button, ButtonMsg::Clicked, IconText},
},
component::{Button, ButtonMsg::Clicked, IconText},
},
};
@ -19,9 +22,9 @@ const BUTTON_SPACING: i16 = 8;
#[repr(u32)]
#[derive(Copy, Clone, ToPrimitive)]
pub enum MenuMsg {
Close = 1,
Reboot = 2,
FactoryReset = 3,
Close = 0xAABBCCDD,
Reboot = 0x11223344,
FactoryReset = 0x55667788,
}
pub struct Menu {
@ -33,7 +36,7 @@ pub struct Menu {
}
impl Menu {
pub fn new() -> Self {
pub fn new(firmware_present: secbool) -> Self {
let content_reboot = IconText::new("REBOOT TREZOR", Icon::new(REFRESH24));
let content_reset = IconText::new("FACTORY RESET", Icon::new(FIRE24));
@ -45,7 +48,11 @@ impl Menu {
.styled(button_bld_menu())
.with_expanded_touch_area(Insets::uniform(CORNER_BUTTON_TOUCH_EXPANSION)),
),
reboot: Child::new(Button::with_icon_and_text(content_reboot).styled(button_bld())),
reboot: Child::new(
Button::with_icon_and_text(content_reboot)
.styled(button_bld())
.initially_enabled(sectrue == firmware_present),
),
reset: Child::new(Button::with_icon_and_text(content_reset).styled(button_bld())),
};
instance.bg.clear();

@ -35,7 +35,7 @@ pub mod menu;
pub mod theme;
pub mod welcome;
use crate::ui::model_tt::theme::BLACK;
use crate::{trezorhal::secbool::secbool, ui::model_tt::theme::BLACK};
use confirm::Confirm;
use intro::Intro;
use menu::Menu;
@ -220,8 +220,8 @@ extern "C" fn screen_wipe_confirm() -> u32 {
}
#[no_mangle]
extern "C" fn screen_menu() -> u32 {
run(&mut Menu::new())
extern "C" fn screen_menu(firmware_present: secbool) -> u32 {
run(&mut Menu::new(firmware_present))
}
#[no_mangle]
@ -230,6 +230,7 @@ extern "C" fn screen_intro(
vendor_str: *const cty::c_char,
vendor_str_len: u8,
version: *const cty::c_char,
fw_ok: bool,
) -> u32 {
let vendor = unwrap!(unsafe { from_c_array(vendor_str, vendor_str_len as usize) });
let version = unwrap!(unsafe { from_c_str(version) });
@ -245,7 +246,7 @@ extern "C" fn screen_intro(
unwrap!(version_str.push_str("\nby "));
unwrap!(version_str.push_str(vendor));
let mut frame = Intro::new(title_str.as_str(), version_str.as_str());
let mut frame = Intro::new(title_str.as_str(), version_str.as_str(), fw_ok);
run(&mut frame)
}
@ -315,7 +316,7 @@ extern "C" fn screen_wipe_success() {
&RESULT_WIPE,
Icon::new(CHECK40),
"Trezor reset\nsuccessfully",
RECONNECT_MESSAGE,
Label::centered(RECONNECT_MESSAGE, RESULT_WIPE.title_style()).vertically_centered(),
true,
);
show(&mut frame, true);
@ -327,7 +328,7 @@ extern "C" fn screen_wipe_fail() {
&RESULT_WIPE,
Icon::new(WARNING40),
"Trezor reset was\nnot successful",
RECONNECT_MESSAGE,
Label::centered(RECONNECT_MESSAGE, RESULT_WIPE.title_style()).vertically_centered(),
true,
);
show(&mut frame, true);
@ -357,29 +358,29 @@ extern "C" fn screen_install_fail() {
&RESULT_FW_INSTALL,
Icon::new(WARNING40),
"Firmware installation was not successful",
RECONNECT_MESSAGE,
Label::centered(RECONNECT_MESSAGE, RESULT_FW_INSTALL.title_style()).vertically_centered(),
true,
);
show(&mut frame, true);
}
fn screen_install_success_bld(msg: &'static str, complete_draw: bool) {
fn screen_install_success_bld(msg: &str, complete_draw: bool) {
let mut frame = ResultScreen::new(
&RESULT_FW_INSTALL,
Icon::new(CHECK40),
"Firmware installed\nsuccessfully",
msg,
Label::centered(msg, RESULT_FW_INSTALL.title_style()).vertically_centered(),
complete_draw,
);
show(&mut frame, complete_draw);
}
fn screen_install_success_initial(msg: &'static str, complete_draw: bool) {
fn screen_install_success_initial(msg: &str, complete_draw: bool) {
let mut frame = ResultScreen::new(
&RESULT_INITIAL,
Icon::new(CHECK40),
"Firmware installed\nsuccessfully",
msg,
Label::centered(msg, RESULT_INITIAL.title_style()).vertically_centered(),
complete_draw,
);
show(&mut frame, complete_draw);
@ -387,15 +388,25 @@ fn screen_install_success_initial(msg: &'static str, complete_draw: bool) {
#[no_mangle]
extern "C" fn screen_install_success(
reboot_msg: *const cty::c_char,
restart_seconds: u8,
initial_setup: bool,
complete_draw: bool,
) {
let msg = unwrap!(unsafe { from_c_str(reboot_msg) });
let mut reboot_msg = BootloaderString::new();
if restart_seconds >= 1 {
unwrap!(reboot_msg.push_str("RESTARTING IN "));
// in practice, restart_seconds is 5 or less so this is fine
let seconds_char = b'0' + restart_seconds % 10;
unwrap!(reboot_msg.push(seconds_char as char));
} else {
unwrap!(reboot_msg.push_str(RECONNECT_MESSAGE));
}
if initial_setup {
screen_install_success_initial(msg, complete_draw)
screen_install_success_initial(reboot_msg.as_str(), complete_draw)
} else {
screen_install_success_bld(msg, complete_draw)
screen_install_success_bld(reboot_msg.as_str(), complete_draw)
}
display::refresh();
}

@ -14,6 +14,8 @@ pub const BLD_FG: Color = WHITE;
pub const BLD_WIPE_COLOR: Color = Color::rgb(0xE7, 0x0E, 0x0E);
pub const BLD_WIPE_TEXT_COLOR: Color = WHITE;
pub const BLD_WARN_COLOR: Color = Color::rgb(0xFF, 0x00, 0x00);
pub const BLD_WIPE_BTN_COLOR: Color = WHITE;
pub const BLD_WIPE_BTN_COLOR_ACTIVE: Color = Color::rgb(0xFA, 0xCF, 0xCF);
@ -238,6 +240,13 @@ pub const TEXT_TITLE: TextStyle = TextStyle::new(
);
pub const TEXT_NORMAL: TextStyle = TextStyle::new(Font::NORMAL, BLD_FG, BLD_BG, BLD_FG, BLD_FG);
pub const TEXT_WARNING: TextStyle = TextStyle::new(
Font::BOLD,
BLD_WARN_COLOR,
BLD_BG,
BLD_WARN_COLOR,
BLD_WARN_COLOR,
);
pub const TEXT_FINGERPRINT: TextStyle =
TextStyle::new(Font::NORMAL, BLD_FG, BLD_BG, BLD_FG, BLD_FG)
.with_line_breaking(BreakWordsNoHyphen);

@ -30,7 +30,10 @@ impl<T: AsRef<str>> ErrorScreen<'_, T> {
pub fn new(title: T, message: T, footer: T) -> Self {
let title = Label::centered(title, STYLE.title_style());
let message = Label::centered(message, STYLE.message_style());
let footer = ResultFooter::new(footer, STYLE);
let footer = ResultFooter::new(
Label::centered(footer, STYLE.title_style()).vertically_centered(),
STYLE,
);
Self {
bg: Pad::with_background(FATAL_ERROR_COLOR).with_clear(),

@ -48,10 +48,10 @@ pub struct ResultFooter<'a, T> {
}
impl<'a, T: AsRef<str>> ResultFooter<'a, T> {
pub fn new(text: T, style: &'a ResultStyle) -> Self {
pub fn new(text: Label<T>, style: &'a ResultStyle) -> Self {
Self {
style,
text: Label::centered(text, style.title_style()).vertically_centered(),
text,
area: Rect::zero(),
}
}
@ -101,7 +101,7 @@ pub struct ResultScreen<'a, T> {
style: &'a ResultStyle,
icon: Icon,
message: Child<Label<T>>,
footer: Child<ResultFooter<'a, T>>,
footer: Child<ResultFooter<'a, &'a str>>,
}
impl<'a, T: StringType> ResultScreen<'a, T> {
@ -109,7 +109,7 @@ impl<'a, T: StringType> ResultScreen<'a, T> {
style: &'a ResultStyle,
icon: Icon,
message: T,
footer: T,
footer: Label<&'a str>,
complete_draw: bool,
) -> Self {
let mut instance = Self {
@ -130,13 +130,13 @@ impl<'a, T: StringType> ResultScreen<'a, T> {
}
}
impl<T: StringType> Component for ResultScreen<'_, T> {
impl<'a, T: StringType> Component for ResultScreen<'a, T> {
type Msg = Never;
fn place(&mut self, _bounds: Rect) -> Rect {
self.bg.place(screen());
let (main_area, footer_area) = ResultFooter::<T>::split_bounds();
let (main_area, footer_area) = ResultFooter::<&'a str>::split_bounds();
self.footer_pad.place(footer_area);
self.footer.place(footer_area);

@ -76,6 +76,7 @@ void show_pin_too_many_screen(void);
void hal_delay(uint32_t ms);
uint32_t hal_ticks_ms();
void hal_delay_us(uint16_t delay_us);
void collect_hw_entropy(void);
#define HW_ENTROPY_LEN (12 + 32)

@ -175,8 +175,10 @@ static optiga_result optiga_i2c_write(const uint8_t *data, uint16_t data_size) {
}
if (HAL_OK == i2c_transmit(OPTIGA_I2C_INSTANCE, OPTIGA_ADDRESS,
(uint8_t *)data, data_size, I2C_TIMEOUT)) {
hal_delay_us(1000);
return OPTIGA_SUCCESS;
}
hal_delay_us(1000);
}
return OPTIGA_ERR_I2C_WRITE;
}

@ -46,6 +46,8 @@
#define COLOR_FATAL_ERROR COLOR_BLACK
#endif
uint32_t systick_val_copy = 0;
// from util.s
extern void shutdown_privileged(void);
@ -154,6 +156,17 @@ void __assert_func(const char *file, int line, const char *func,
void hal_delay(uint32_t ms) { HAL_Delay(ms); }
uint32_t hal_ticks_ms() { return HAL_GetTick(); }
void hal_delay_us(uint16_t delay_us) {
uint32_t val = svc_get_systick_val();
uint32_t t = hal_ticks_ms() * 1000 +
(((SystemCoreClock / 1000) - val) / (SystemCoreClock / 1000000));
uint32_t t2 = t;
do {
val = svc_get_systick_val();
t2 = hal_ticks_ms() * 1000 +
(((SystemCoreClock / 1000) - val) / (SystemCoreClock / 1000000));
} while ((t2 - t) < delay_us);
}
// reference RM0090 section 35.12.1 Figure 413
#define USB_OTG_HS_DATA_FIFO_RAM (USB_OTG_HS_PERIPH_BASE + 0x20000U)

@ -171,9 +171,19 @@ HAL_StatusTypeDef i2c_transmit(uint16_t idx, uint8_t addr, uint8_t *data,
uint16_t len, uint32_t timeout) {
return HAL_I2C_Master_Transmit(&i2c_handle[idx], addr, data, len, timeout);
}
HAL_StatusTypeDef i2c_receive(uint16_t idx, uint8_t addr, uint8_t *data,
uint16_t len, uint32_t timeout) {
return HAL_I2C_Master_Receive(&i2c_handle[idx], addr, data, len, timeout);
HAL_StatusTypeDef ret =
HAL_I2C_Master_Receive(&i2c_handle[idx], addr, data, len, timeout);
#ifdef USE_OPTIGA
if (idx == OPTIGA_I2C_INSTANCE) {
// apply GUARD_TIME as specified by the OPTIGA datasheet
// (only applies to the I2C bus to which the OPTIGA is connected)
hal_delay_us(50);
}
#endif
return ret;
}
HAL_StatusTypeDef i2c_mem_write(uint16_t idx, uint8_t addr, uint16_t mem_addr,

@ -11,6 +11,9 @@ void optiga_hal_init(void) {
GPIO_InitStructure.Alternate = 0;
GPIO_InitStructure.Pin = GPIO_PIN_9;
HAL_GPIO_Init(GPIOD, &GPIO_InitStructure);
// perform reset on every initialization
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_9, GPIO_PIN_RESET);
hal_delay(10);
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_9, GPIO_PIN_SET);
// warm reset startup time min 15ms
hal_delay(20);

@ -6,11 +6,15 @@
#define SVC_SHUTDOWN 4
#define SVC_REBOOT_TO_BOOTLOADER 5
#define SVC_REBOOT_COPY_IMAGE_HEADER 6
#define SVC_GET_SYSTICK_VAL 7
#include <string.h>
#include "common.h"
#include "image.h"
// from common.c
extern uint32_t systick_val_copy;
// from util.s
extern void shutdown_privileged(void);
extern void reboot_to_bootloader(void);
@ -87,3 +91,13 @@ static inline void svc_reboot_copy_image_header(const uint8_t *image_address) {
reboot_to_bootloader();
}
}
static inline uint32_t svc_get_systick_val(void) {
if (is_mode_unprivileged() && !is_mode_handler()) {
__asm__ __volatile__("svc %0" ::"i"(SVC_GET_SYSTICK_VAL) : "memory");
return systick_val_copy;
} else {
systick_val_copy = SysTick->VAL;
return systick_val_copy;
}
}

@ -25,7 +25,7 @@
#include "rand.h"
static const uint8_t DEVICE_CERT_CHAIN[] = {
0x30, 0x82, 0x01, 0x90, 0x30, 0x82, 0x01, 0x37, 0xa0, 0x03, 0x02, 0x01,
0x30, 0x82, 0x01, 0x91, 0x30, 0x82, 0x01, 0x37, 0xa0, 0x03, 0x02, 0x01,
0x02, 0x02, 0x04, 0x4e, 0xe2, 0xa5, 0x0f, 0x30, 0x0a, 0x06, 0x08, 0x2a,
0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x30, 0x4f, 0x31, 0x0b, 0x30,
0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x43, 0x5a, 0x31, 0x1e,
@ -49,18 +49,18 @@ static const uint8_t DEVICE_CERT_CHAIN[] = {
0xa3, 0x41, 0x30, 0x3f, 0x30, 0x0e, 0x06, 0x03, 0x55, 0x1d, 0x0f, 0x01,
0x01, 0xff, 0x04, 0x04, 0x03, 0x02, 0x00, 0x80, 0x30, 0x0c, 0x06, 0x03,
0x55, 0x1d, 0x13, 0x01, 0x01, 0xff, 0x04, 0x02, 0x30, 0x00, 0x30, 0x1f,
0x06, 0x03, 0x55, 0x1d, 0x23, 0x04, 0x18, 0x30, 0x16, 0x80, 0x14, 0x0a,
0x80, 0xa4, 0x22, 0xc1, 0x6e, 0x36, 0x94, 0x24, 0xd3, 0xb8, 0x12, 0x67,
0xb5, 0xaa, 0xe6, 0x46, 0x74, 0x74, 0xc8, 0x30, 0x0a, 0x06, 0x08, 0x2a,
0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x03, 0x47, 0x00, 0x30, 0x44,
0x02, 0x20, 0x2f, 0xba, 0xc7, 0xe7, 0x99, 0x5b, 0x0e, 0x00, 0xc2, 0xc2,
0xa7, 0xcb, 0xd8, 0xdd, 0x1d, 0x4e, 0xce, 0xf5, 0x58, 0x75, 0xa2, 0x65,
0xc6, 0x86, 0xd4, 0xa8, 0xd2, 0x80, 0x68, 0x63, 0x5b, 0xf8, 0x02, 0x20,
0x1e, 0xee, 0x62, 0x10, 0x0d, 0x0c, 0x97, 0x5a, 0x96, 0x34, 0x6f, 0x14,
0xef, 0xe2, 0x6b, 0xc9, 0x3b, 0x00, 0xba, 0x30, 0x28, 0x9c, 0xe3, 0x7c,
0xef, 0x17, 0x89, 0xbc, 0xc0, 0x68, 0xba, 0xb9, 0x30, 0x82, 0x01, 0xe0,
0x30, 0x82, 0x01, 0x85, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x05, 0x00,
0x94, 0x4e, 0x25, 0x7c, 0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce,
0x06, 0x03, 0x55, 0x1d, 0x23, 0x04, 0x18, 0x30, 0x16, 0x80, 0x14, 0x67,
0xc5, 0xd4, 0xe7, 0xf0, 0x8f, 0x91, 0xb6, 0xe7, 0x48, 0xdf, 0x42, 0xbf,
0x9f, 0x74, 0x1f, 0x43, 0xd2, 0x73, 0x75, 0x30, 0x0a, 0x06, 0x08, 0x2a,
0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x03, 0x48, 0x00, 0x30, 0x45,
0x02, 0x21, 0x00, 0x8e, 0xd1, 0x92, 0xc5, 0xe5, 0xcd, 0xf4, 0xb3, 0x43,
0x07, 0x20, 0x15, 0x2a, 0x4d, 0xbe, 0x18, 0x7e, 0xaf, 0xa9, 0x45, 0x1e,
0x01, 0x29, 0x53, 0xa7, 0xec, 0x17, 0x16, 0x4e, 0xe7, 0x49, 0xd0, 0x02,
0x20, 0x09, 0xd4, 0xdd, 0x11, 0x49, 0xd9, 0xa0, 0x28, 0xc1, 0xcc, 0x5d,
0x6c, 0xca, 0x01, 0xd2, 0xe6, 0x19, 0x47, 0xa4, 0xad, 0xb3, 0x0d, 0xac,
0xe4, 0x30, 0xc8, 0x36, 0x8b, 0x34, 0x46, 0x6e, 0x79, 0x30, 0x82, 0x01,
0xde, 0x30, 0x82, 0x01, 0x84, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x04,
0x48, 0xf1, 0xf6, 0xc5, 0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce,
0x3d, 0x04, 0x03, 0x02, 0x30, 0x54, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03,
0x55, 0x04, 0x06, 0x13, 0x02, 0x43, 0x5a, 0x31, 0x1e, 0x30, 0x1c, 0x06,
0x03, 0x55, 0x04, 0x0a, 0x0c, 0x15, 0x54, 0x72, 0x65, 0x7a, 0x6f, 0x72,
@ -80,25 +80,25 @@ static const uint8_t DEVICE_CERT_CHAIN[] = {
0x75, 0x66, 0x61, 0x63, 0x74, 0x75, 0x72, 0x69, 0x6e, 0x67, 0x20, 0x43,
0x41, 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d,
0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07,
0x03, 0x42, 0x00, 0x04, 0x9b, 0xf0, 0x76, 0x0c, 0xd7, 0x55, 0x5e, 0xfc,
0x5b, 0x01, 0xe6, 0x4c, 0xe9, 0x46, 0x06, 0xcd, 0xee, 0xb4, 0x8a, 0x4f,
0x91, 0x96, 0xf4, 0x54, 0xbc, 0x2a, 0x01, 0x41, 0xb3, 0x31, 0x88, 0x2d,
0x06, 0x8b, 0x4b, 0x6e, 0x63, 0x79, 0x13, 0xdd, 0x22, 0x06, 0x54, 0xe2,
0x8f, 0xde, 0x3c, 0x44, 0x21, 0x41, 0xf9, 0x53, 0xb3, 0xe3, 0x6a, 0xd9,
0xa5, 0x75, 0x19, 0x00, 0x71, 0x19, 0xd9, 0xc9, 0xa3, 0x47, 0x30, 0x45,
0x03, 0x42, 0x00, 0x04, 0xba, 0x60, 0x84, 0xcb, 0x9f, 0xba, 0x7c, 0x86,
0xd5, 0xd5, 0xa8, 0x61, 0x08, 0xa9, 0x1d, 0x55, 0xa2, 0x70, 0x56, 0xda,
0x4e, 0xab, 0xbe, 0xdd, 0xe8, 0x8a, 0x95, 0xe1, 0xca, 0xe8, 0xbc, 0xe3,
0x62, 0x08, 0x89, 0x16, 0x7a, 0xaf, 0x7f, 0x2d, 0xb1, 0x66, 0x99, 0x8f,
0x95, 0x09, 0x84, 0xaa, 0x19, 0x5e, 0x86, 0x8f, 0x96, 0xe2, 0x28, 0x03,
0xc3, 0xcd, 0x99, 0x1b, 0xe3, 0x1d, 0x39, 0xe7, 0xa3, 0x47, 0x30, 0x45,
0x30, 0x0e, 0x06, 0x03, 0x55, 0x1d, 0x0f, 0x01, 0x01, 0xff, 0x04, 0x04,
0x03, 0x02, 0x02, 0x04, 0x30, 0x12, 0x06, 0x03, 0x55, 0x1d, 0x13, 0x01,
0x01, 0xff, 0x04, 0x08, 0x30, 0x06, 0x01, 0x01, 0xff, 0x02, 0x01, 0x00,
0x30, 0x1f, 0x06, 0x03, 0x55, 0x1d, 0x23, 0x04, 0x18, 0x30, 0x16, 0x80,
0x14, 0x28, 0xb2, 0x02, 0xf8, 0xf9, 0xc7, 0x8a, 0x74, 0xe8, 0xc1, 0x52,
0xbb, 0xfb, 0x43, 0x3d, 0x99, 0xd0, 0xca, 0x03, 0xef, 0x30, 0x0a, 0x06,
0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x03, 0x49, 0x00,
0x30, 0x46, 0x02, 0x21, 0x00, 0x9b, 0x60, 0xfb, 0x5f, 0xc7, 0xee, 0x07,
0x00, 0x31, 0x2d, 0x40, 0x2d, 0xcb, 0xe0, 0x77, 0xe4, 0x81, 0x82, 0xbb,
0x94, 0x4b, 0xb1, 0xb3, 0x30, 0x79, 0xdd, 0xa0, 0xb6, 0xca, 0x9a, 0xb5,
0xb7, 0x02, 0x21, 0x00, 0x81, 0x5d, 0xd9, 0x76, 0xab, 0xec, 0xb9, 0xb3,
0xa8, 0x0f, 0x5b, 0x86, 0xb4, 0x49, 0x4f, 0xef, 0x94, 0xf2, 0xe0, 0xc1,
0xf2, 0xc4, 0x5d, 0xe6, 0xec, 0x5f, 0x89, 0x5c, 0xa5, 0x6f, 0x04, 0x8b};
0x14, 0xc8, 0xb5, 0xb2, 0x43, 0xb3, 0x30, 0x43, 0xcc, 0x08, 0xb4, 0xdc,
0x3a, 0x72, 0xa1, 0xde, 0xcd, 0xcf, 0xd7, 0xea, 0xdb, 0x30, 0x0a, 0x06,
0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x03, 0x48, 0x00,
0x30, 0x45, 0x02, 0x20, 0x02, 0xc1, 0x02, 0x0a, 0xb3, 0xc6, 0x4e, 0xd9,
0xe6, 0x58, 0xfd, 0xf8, 0x70, 0x93, 0x72, 0xc9, 0xe0, 0x53, 0x82, 0xde,
0x4e, 0x58, 0x75, 0x80, 0xc8, 0xba, 0xc4, 0x2f, 0x43, 0x78, 0x4a, 0xd9,
0x02, 0x21, 0x00, 0x99, 0x00, 0x98, 0x1c, 0xbc, 0x68, 0xae, 0xb0, 0x6d,
0x3e, 0xa9, 0x11, 0x94, 0x8d, 0x63, 0x11, 0xd6, 0xf6, 0x94, 0x40, 0x3a,
0xbb, 0xbb, 0x65, 0x9e, 0x5a, 0xf5, 0x2b, 0xf3, 0x2e, 0x33, 0xc4};
int optiga_sign(uint8_t index, const uint8_t *digest, size_t digest_size,
uint8_t *signature, size_t max_sig_size, size_t *sig_size) {

@ -0,0 +1,26 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
void optiga_hal_init(void) {
// nothing to do
}
void optiga_reset(void) {
// nothing to do
}

@ -29,6 +29,15 @@ def confirm_action(
"""Confirm action."""
# rust/src/ui/model_tr/layout.rs
def confirm_homescreen(
*,
title: str,
image: bytes,
) -> object:
"""Confirm homescreen."""
# rust/src/ui/model_tr/layout.rs
def confirm_blob(
*,

@ -98,3 +98,4 @@ MODEL: str
INTERNAL_MODEL: str
EMULATOR: bool
BITCOIN_ONLY: bool
UI_LAYOUT: str

@ -39,6 +39,8 @@ def generate(env):
is_t2b1 = env["TREZOR_MODEL"] == "R"
backlight = env["backlight"]
optiga = env["optiga"]
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
sed_scripts = " ".join(
[
@ -46,6 +48,8 @@ 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\.UI_LAYOUT == \"TT\"/{layout_tt}/g'",
rf"-e 's/utils\.UI_LAYOUT == \"TR\"/{layout_tr}/g'",
r"-e 's/if TYPE_CHECKING/if False/'",
r"-e 's/import typing/# \0/'",
r"-e '/from typing import (/,/^\s*)/ {s/^/# /}'",

@ -173,18 +173,18 @@ trezor.ui.layouts.tr.recovery
import trezor.ui.layouts.tr.recovery
trezor.ui.layouts.tr.reset
import trezor.ui.layouts.tr.reset
trezor.ui.layouts.tt_v2
import trezor.ui.layouts.tt_v2
trezor.ui.layouts.tt_v2.fido
import trezor.ui.layouts.tt_v2.fido
trezor.ui.layouts.tt_v2.homescreen
import trezor.ui.layouts.tt_v2.homescreen
trezor.ui.layouts.tt_v2.progress
import trezor.ui.layouts.tt_v2.progress
trezor.ui.layouts.tt_v2.recovery
import trezor.ui.layouts.tt_v2.recovery
trezor.ui.layouts.tt_v2.reset
import trezor.ui.layouts.tt_v2.reset
trezor.ui.layouts.tt
import trezor.ui.layouts.tt
trezor.ui.layouts.tt.fido
import trezor.ui.layouts.tt.fido
trezor.ui.layouts.tt.homescreen
import trezor.ui.layouts.tt.homescreen
trezor.ui.layouts.tt.progress
import trezor.ui.layouts.tt.progress
trezor.ui.layouts.tt.recovery
import trezor.ui.layouts.tt.recovery
trezor.ui.layouts.tt.reset
import trezor.ui.layouts.tt.reset
trezor.ui.style
import trezor.ui.style
trezor.utils

@ -80,6 +80,7 @@ def get_features() -> Features:
Capability.Crypto,
Capability.Shamir,
Capability.ShamirGroups,
Capability.PassphraseEntry,
]
else:
f.capabilities = [

@ -26,8 +26,13 @@ async def get_address(msg: BinanceGetAddress, keychain: Keychain) -> BinanceAddr
pubkey = node.public_key()
address = address_from_public_key(pubkey, HRP)
if msg.show_display:
from . import PATTERN, SLIP44_ID
await show_address(
address, path=paths.address_n_to_str(address_n), chunkify=bool(msg.chunkify)
address,
path=paths.address_n_to_str(address_n),
account=paths.get_account_name("BNB", address_n, PATTERN, SLIP44_ID),
chunkify=bool(msg.chunkify),
)
return BinanceAddress(address=address)

@ -24,7 +24,13 @@ async def get_public_key(
pubkey = node.public_key()
if msg.show_display:
from . import PATTERN, SLIP44_ID
path = paths.address_n_to_str(msg.address_n)
await show_pubkey(hexlify(pubkey).decode(), path=path)
await show_pubkey(
hexlify(pubkey).decode(),
account=paths.get_account_name("BNB", msg.address_n, PATTERN, SLIP44_ID),
path=path,
)
return BinancePublicKey(public_key=pubkey)

@ -372,3 +372,46 @@ def address_n_to_str(address_n: Iterable[int]) -> str:
def unharden(item: int) -> int:
return item ^ (item & HARDENED)
def get_account_name(
coin: str, address_n: Bip32Path, pattern: str | Sequence[str], slip44_id: int
) -> str | None:
account_num = _get_account_num(address_n, pattern, slip44_id)
if account_num is None:
return None
return f"{coin} #{account_num}"
def _get_account_num(
address_n: Bip32Path, pattern: str | Sequence[str], slip44_id: int
) -> int | None:
if isinstance(pattern, str):
pattern = [pattern]
# Trying all possible patterns - at least one should match
for patt in pattern:
try:
return _get_account_num_single(address_n, patt, slip44_id)
except ValueError:
pass
# This function should not raise
return None
def _get_account_num_single(address_n: Bip32Path, pattern: str, slip44_id: int) -> int:
# Validating address_n is compatible with pattern
if not PathSchema.parse(pattern, slip44_id).match(address_n):
raise ValueError
account_pos = pattern.find("/account")
if account_pos >= 0:
i = pattern.count("/", 0, account_pos)
num = address_n[i]
if is_hardened(num):
return unharden(num) + 1
else:
return num + 1
else:
raise ValueError

@ -26,6 +26,9 @@ async def get_public_key(msg: EosGetPublicKey, keychain: Keychain) -> EosPublicK
wif = public_key_to_wif(public_key)
if msg.show_display:
from . import PATTERN, SLIP44_ID
path = paths.address_n_to_str(msg.address_n)
await require_get_public_key(wif, path)
account = paths.get_account_name("EOS", msg.address_n, PATTERN, SLIP44_ID)
await require_get_public_key(wif, path, account)
return EosPublicKey(wif_public_key=wif, raw_public_key=public_key)

@ -1,7 +1,9 @@
async def require_get_public_key(public_key: str, path: str) -> None:
async def require_get_public_key(
public_key: str, path: str, account: str | None
) -> None:
from trezor.ui.layouts import show_pubkey
await show_pubkey(public_key, path=path)
await show_pubkey(public_key, path=path, account=account)
async def require_sign_tx(num_actions: int) -> None:

@ -32,8 +32,14 @@ async def get_address(
address = address_from_bytes(node.ethereum_pubkeyhash(), defs.network)
if msg.show_display:
slip44_id = address_n[1] # it depends on the network (ETH vs ETC...)
await show_address(
address, path=paths.address_n_to_str(address_n), chunkify=bool(msg.chunkify)
address,
path=paths.address_n_to_str(address_n),
account=paths.get_account_name(
"ETH", address_n, PATTERNS_ADDRESS, slip44_id
),
chunkify=bool(msg.chunkify),
)
return EthereumAddress(address=address)

@ -3,7 +3,7 @@ from typing import TYPE_CHECKING
import trezorui2
from trezor import utils
from trezor.enums import ButtonRequestType
from trezor.ui.layouts import confirm_action, confirm_homescreen, confirm_single
from trezor.ui.layouts import confirm_action
from trezor.wire import DataError
if TYPE_CHECKING:
@ -151,6 +151,8 @@ async def apply_settings(msg: ApplySettings) -> Success:
async def _require_confirm_change_homescreen(homescreen: bytes) -> None:
from trezor.ui.layouts import confirm_homescreen
if homescreen == b"":
await confirm_action(
"set_homescreen",
@ -159,12 +161,12 @@ async def _require_confirm_change_homescreen(homescreen: bytes) -> None:
br_code=BRT_PROTECT_CALL,
)
else:
await confirm_homescreen(
homescreen,
)
await confirm_homescreen(homescreen)
async def _require_confirm_change_label(label: str) -> None:
from trezor.ui.layouts import confirm_single
await confirm_single(
"set_label",
"Device name",

@ -22,10 +22,11 @@ async def get_address(msg: MoneroGetAddress, keychain: Keychain) -> MoneroAddres
account = msg.account # local_cache_attribute
minor = msg.minor # local_cache_attribute
payment_id = msg.payment_id # local_cache_attribute
address_n = msg.address_n # local_cache_attribute
await paths.validate_path(keychain, msg.address_n)
await paths.validate_path(keychain, address_n)
creds = misc.get_creds(keychain, msg.address_n, msg.network_type)
creds = misc.get_creds(keychain, address_n, msg.network_type)
addr = creds.address
have_subaddress = (
@ -65,10 +66,13 @@ async def get_address(msg: MoneroGetAddress, keychain: Keychain) -> MoneroAddres
)
if msg.show_display:
from . import PATTERN, SLIP44_ID
await show_address(
addr,
address_qr="monero:" + addr,
path=paths.address_n_to_str(msg.address_n),
path=paths.address_n_to_str(address_n),
account=paths.get_account_name("XMR", msg.address_n, PATTERN, SLIP44_ID),
chunkify=bool(msg.chunkify),
)

@ -15,7 +15,7 @@ async def get_address(msg: NEMGetAddress, keychain: Keychain) -> NEMAddress:
from trezor.messages import NEMAddress
from trezor.ui.layouts import show_address
from apps.common.paths import address_n_to_str, validate_path
from apps.common import paths
from .helpers import check_path, get_network_str
from .validators import validate_network
@ -24,16 +24,19 @@ async def get_address(msg: NEMGetAddress, keychain: Keychain) -> NEMAddress:
network = msg.network # local_cache_attribute
validate_network(network)
await validate_path(keychain, address_n, check_path(address_n, network))
await paths.validate_path(keychain, address_n, check_path(address_n, network))
node = keychain.derive(address_n)
address = node.nem_address(network)
if msg.show_display:
from . import PATTERNS, SLIP44_ID
await show_address(
address,
case_sensitive=False,
path=address_n_to_str(address_n),
path=paths.address_n_to_str(address_n),
account=paths.get_account_name("NEM", msg.address_n, PATTERNS, SLIP44_ID),
network=get_network_str(network),
chunkify=bool(msg.chunkify),
)

@ -18,16 +18,21 @@ async def get_address(msg: RippleGetAddress, keychain: Keychain) -> RippleAddres
from .helpers import address_from_public_key
await paths.validate_path(keychain, msg.address_n)
address_n = msg.address_n # local_cache_attribute
node = keychain.derive(msg.address_n)
await paths.validate_path(keychain, address_n)
node = keychain.derive(address_n)
pubkey = node.public_key()
address = address_from_public_key(pubkey)
if msg.show_display:
from . import PATTERN, SLIP44_ID
await show_address(
address,
path=paths.address_n_to_str(msg.address_n),
path=paths.address_n_to_str(address_n),
account=paths.get_account_name("XRP", msg.address_n, PATTERN, SLIP44_ID),
chunkify=bool(msg.chunkify),
)

@ -17,16 +17,23 @@ async def get_address(msg: StellarGetAddress, keychain: Keychain) -> StellarAddr
from . import helpers
await paths.validate_path(keychain, msg.address_n)
address_n = msg.address_n # local_cache_attribute
node = keychain.derive(msg.address_n)
await paths.validate_path(keychain, address_n)
node = keychain.derive(address_n)
pubkey = seed.remove_ed25519_prefix(node.public_key())
address = helpers.address_from_public_key(pubkey)
if msg.show_display:
path = paths.address_n_to_str(msg.address_n)
from . import PATTERN, SLIP44_ID
await show_address(
address, case_sensitive=False, path=path, chunkify=bool(msg.chunkify)
address,
case_sensitive=False,
path=paths.address_n_to_str(address_n),
account=paths.get_account_name("XLM", msg.address_n, PATTERN, SLIP44_ID),
chunkify=bool(msg.chunkify),
)
return StellarAddress(address=address)

@ -20,18 +20,23 @@ async def get_address(msg: TezosGetAddress, keychain: Keychain) -> TezosAddress:
from . import helpers
await paths.validate_path(keychain, msg.address_n)
address_n = msg.address_n # local_cache_attribute
node = keychain.derive(msg.address_n)
await paths.validate_path(keychain, address_n)
node = keychain.derive(address_n)
pk = seed.remove_ed25519_prefix(node.public_key())
pkh = hashlib.blake2b(pk, outlen=helpers.PUBLIC_KEY_HASH_SIZE).digest()
address = helpers.base58_encode_check(pkh, helpers.TEZOS_ED25519_ADDRESS_PREFIX)
if msg.show_display:
from . import PATTERNS, SLIP44_ID
await show_address(
address,
path=paths.address_n_to_str(msg.address_n),
path=paths.address_n_to_str(address_n),
account=paths.get_account_name("XTZ", address_n, PATTERNS, SLIP44_ID),
chunkify=bool(msg.chunkify),
)

@ -26,6 +26,10 @@ async def get_public_key(msg: TezosGetPublicKey, keychain: Keychain) -> TezosPub
pk_prefixed = helpers.base58_encode_check(pk, helpers.TEZOS_PUBLICKEY_PREFIX)
if msg.show_display:
await show_pubkey(pk_prefixed)
from . import PATTERNS, SLIP44_ID
account = paths.get_account_name("XTZ", msg.address_n, PATTERNS, SLIP44_ID)
path = paths.address_n_to_str(msg.address_n)
await show_pubkey(pk_prefixed, account=account, path=path)
return TezosPublicKey(public_key=pk_prefixed)

@ -4,9 +4,9 @@ from .common import * # noqa: F401,F403
# NOTE: using any import magic probably causes mypy not to check equivalence of
# layout type signatures across models
if utils.INTERNAL_MODEL in ("T1B1", "T2B1"):
if utils.UI_LAYOUT == "TR":
from .tr import * # noqa: F401,F403
elif utils.INTERNAL_MODEL in ("T2T1", "D001"):
from .tt_v2 import * # noqa: F401,F403
elif utils.UI_LAYOUT == "TT":
from .tt import * # noqa: F401,F403
else:
raise ValueError("Unknown Trezor model")
raise ValueError("Unknown layout")

@ -1,6 +1,6 @@
from trezor import utils
if utils.INTERNAL_MODEL in ("T2T1", "D001"):
from .tt_v2.fido import * # noqa: F401,F403
elif utils.INTERNAL_MODEL in ("T2B1",):
if utils.UI_LAYOUT == "TT":
from .tt.fido import * # noqa: F401,F403
elif utils.UI_LAYOUT == "TR":
from .tr.fido import * # noqa: F401,F403

@ -1,6 +1,6 @@
from trezor import utils
if utils.INTERNAL_MODEL in ("T2T1", "D001"):
from .tt_v2.homescreen import * # noqa: F401,F403
elif utils.INTERNAL_MODEL in ("T2B1",):
if utils.UI_LAYOUT == "TT":
from .tt.homescreen import * # noqa: F401,F403
elif utils.UI_LAYOUT == "TR":
from .tr.homescreen import * # noqa: F401,F403

@ -1,6 +1,6 @@
from trezor import utils
if utils.INTERNAL_MODEL in ("T2T1", "D001"):
from .tt_v2.progress import * # noqa: F401,F403
elif utils.INTERNAL_MODEL in ("T2B1",):
if utils.UI_LAYOUT == "TT":
from .tt.progress import * # noqa: F401,F403
elif utils.UI_LAYOUT == "TR":
from .tr.progress import * # noqa: F401,F403

@ -1,6 +1,6 @@
from trezor import utils
if utils.INTERNAL_MODEL in ("T2T1", "D001"):
from .tt_v2.recovery import * # noqa: F401,F403
elif utils.INTERNAL_MODEL in ("T2B1",):
if utils.UI_LAYOUT == "TT":
from .tt.recovery import * # noqa: F401,F403
elif utils.UI_LAYOUT == "TR":
from .tr.recovery import * # noqa: F401,F403

@ -1,6 +1,6 @@
from trezor import utils
if utils.INTERNAL_MODEL in ("T2T1", "D001"):
from .tt_v2.reset import * # noqa: F401,F403
elif utils.INTERNAL_MODEL in ("T2B1",):
if utils.UI_LAYOUT == "TT":
from .tt.reset import * # noqa: F401,F403
elif utils.UI_LAYOUT == "TR":
from .tr.reset import * # noqa: F401,F403

@ -443,12 +443,17 @@ async def confirm_path_warning(
async def confirm_homescreen(
image: bytes,
) -> None:
# TODO: show homescreen preview?
await confirm_action(
"set_homescreen",
"Set homescreen",
description="Do you really want to set new homescreen image?",
br_code=ButtonRequestType.ProtectCall,
await raise_if_not_confirmed(
interact(
RustLayout(
trezorui2.confirm_homescreen(
title="CHANGE HOMESCREEN?",
image=image,
)
),
"set_homesreen",
ButtonRequestType.ProtectCall,
)
)

@ -6,6 +6,7 @@ from trezorutils import ( # noqa: F401
INTERNAL_MODEL,
MODEL,
SCM_REVISION,
UI_LAYOUT,
USE_BACKLIGHT,
USE_OPTIGA,
USE_SD_CARD,

@ -649,6 +649,15 @@ class DebugLink:
physical_button=messages.DebugPhysicalButton.MIDDLE_BTN, wait=wait
)
def press_middle_htc(
self, hold_ms: int, extra_ms: int = 200
) -> Optional[LayoutContent]:
return self.press_htc(
button=messages.DebugPhysicalButton.MIDDLE_BTN,
hold_ms=hold_ms,
extra_ms=extra_ms,
)
@overload
def press_right(self) -> None:
...
@ -664,10 +673,19 @@ class DebugLink:
def press_right_htc(
self, hold_ms: int, extra_ms: int = 200
) -> Optional[LayoutContent]:
return self.press_htc(
button=messages.DebugPhysicalButton.RIGHT_BTN,
hold_ms=hold_ms,
extra_ms=extra_ms,
)
def press_htc(
self, button: messages.DebugPhysicalButton, hold_ms: int, extra_ms: int = 200
) -> Optional[LayoutContent]:
hold_ms = hold_ms + extra_ms # safety margin
result = self.input(
physical_button=messages.DebugPhysicalButton.RIGHT_BTN,
physical_button=button,
hold_ms=hold_ms,
)
# sleeping little longer for UI to update

@ -70,6 +70,7 @@ def navigate_to_action_and_press(
wanted_action: str,
all_actions: list[str],
is_carousel: bool = True,
hold_ms: int = 0,
) -> None:
"""Navigate to the button with certain action and press it"""
# Orient
@ -99,8 +100,11 @@ def navigate_to_action_and_press(
is_carousel=is_carousel,
)
# Press
debug.press_middle(wait=True)
# Press or hold
if hold_ms:
debug.press_middle_htc(1000)
else:
debug.press_middle(wait=True)
def _get_action_index(wanted_action: str, all_actions: list[str]) -> int:

@ -169,6 +169,18 @@ def _delete_pin(debug: "DebugLink", digits_to_delete: int, check: bool = True) -
assert before[:-digits_to_delete] == after
def _delete_all(debug: "DebugLink", check: bool = True) -> None:
"""Navigate to "DELETE" and hold it until all digits are deleted"""
if debug.model == "T":
debug.click_hold(buttons.pin_passphrase_grid(9), hold_ms=1500)
elif debug.model == "R":
navigate_to_action_and_press(debug, "DELETE", TR_PIN_ACTIONS, hold_ms=1000)
if check:
after = debug.read_layout().pin()
assert after == ""
def _cancel_pin(debug: "DebugLink") -> None:
"""Navigate to "CANCEL" and press it"""
# It is the same button as DELETE
@ -224,6 +236,17 @@ def test_pin_long_delete(device_handler: "BackgroundDeviceHandler"):
_input_see_confirm(debug, PIN24[-10:])
@pytest.mark.setup_client(pin=PIN4)
def test_pin_delete_hold(device_handler: "BackgroundDeviceHandler"):
with prepare(device_handler) as debug:
_input_pin(debug, PIN4)
_see_pin(debug)
_delete_all(debug)
_input_see_confirm(debug, PIN4)
@pytest.mark.setup_client(pin=PIN60[:50])
def test_pin_longer_than_max(device_handler: "BackgroundDeviceHandler"):
with prepare(device_handler) as debug:

@ -20,26 +20,41 @@ from trezorlib.binance import get_address
from trezorlib.debuglink import TrezorClientDebugLink as Client
from trezorlib.tools import parse_path
from ...input_flows import InputFlowShowAddressQRCode
pytestmark = [
pytest.mark.altcoin,
pytest.mark.binance,
pytest.mark.skip_t1, # T1 support is not planned
pytest.mark.setup_client(
mnemonic="offer caution gift cross surge pretty orange during eye soldier popular holiday mention east eight office fashion ill parrot vault rent devote earth cousin"
),
]
BINANCE_ADDRESS_TEST_VECTORS = [
("m/44h/714h/0h/0/0", "bnb1hgm0p7khfk85zpz5v0j8wnej3a90w709vhkdfu"),
("m/44h/714h/0h/0/1", "bnb1egswqkszzfc2uq78zjslc6u2uky4pw46x4rstd"),
]
@pytest.mark.altcoin
@pytest.mark.binance
@pytest.mark.skip_t1 # T1 support is not planned
@pytest.mark.setup_client(
mnemonic="offer caution gift cross surge pretty orange during eye soldier popular holiday mention east eight office fashion ill parrot vault rent devote earth cousin"
)
@pytest.mark.parametrize("chunkify", (True, False))
@pytest.mark.parametrize("path, expected_address", BINANCE_ADDRESS_TEST_VECTORS)
def test_binance_get_address(
client: Client, chunkify: bool, path: str, expected_address: str
):
def test_binance_get_address(client: Client, path: str, expected_address: str):
# data from https://github.com/binance-chain/javascript-sdk/blob/master/__tests__/crypto.test.js#L50
address = get_address(
client, parse_path(path), show_display=True, chunkify=chunkify
)
address = get_address(client, parse_path(path), show_display=True)
assert address == expected_address
@pytest.mark.parametrize("path, expected_address", BINANCE_ADDRESS_TEST_VECTORS)
def test_binance_get_address_chunkify_details(
client: Client, path: str, expected_address: str
):
# data from https://github.com/binance-chain/javascript-sdk/blob/master/__tests__/crypto.test.js#L50
with client:
IF = InputFlowShowAddressQRCode(client)
client.set_input_flow(IF.get())
address = get_address(
client, parse_path(path), show_display=True, chunkify=True
)
assert address == expected_address

@ -21,15 +21,27 @@ from trezorlib.debuglink import TrezorClientDebugLink as Client
from trezorlib.tools import parse_path
from ...common import parametrize_using_common_fixtures
from ...input_flows import InputFlowShowAddressQRCode
pytestmark = [pytest.mark.altcoin, pytest.mark.ethereum]
@parametrize_using_common_fixtures("ethereum/getaddress.json")
@pytest.mark.parametrize("chunkify", (True, False))
def test_getaddress(client: Client, chunkify: bool, parameters, result):
def test_getaddress(client: Client, parameters, result):
address_n = parse_path(parameters["path"])
assert (
ethereum.get_address(client, address_n, show_display=True, chunkify=chunkify)
== result["address"]
ethereum.get_address(client, address_n, show_display=True) == result["address"]
)
@pytest.mark.skip_t1("No input flow for T1")
@parametrize_using_common_fixtures("ethereum/getaddress.json")
def test_getaddress_chunkify_details(client: Client, parameters, result):
with client:
IF = InputFlowShowAddressQRCode(client)
client.set_input_flow(IF.get())
address_n = parse_path(parameters["path"])
assert (
ethereum.get_address(client, address_n, show_display=True, chunkify=True)
== result["address"]
)

@ -21,29 +21,45 @@ from trezorlib.debuglink import TrezorClientDebugLink as Client
from trezorlib.tools import parse_path
from ...common import MNEMONIC12
from ...input_flows import InputFlowShowAddressQRCode
TEST_VECTORS = [
(
"m/44h/128h/0h",
b"4Ahp23WfMrMFK3wYL2hLWQFGt87ZTeRkufS6JoQZu6MEFDokAQeGWmu9MA3GFq1yVLSJQbKJqVAn9F9DLYGpRzRAEXqAXKM",
),
(
"m/44h/128h/1h",
b"44iAazhoAkv5a5RqLNVyh82a1n3ceNggmN4Ho7bUBJ14WkEVR8uFTe9f7v5rNnJ2kEbVXxfXiRzsD5Jtc6NvBi4D6WNHPie",
),
(
"m/44h/128h/2h",
b"47ejhmbZ4wHUhXaqA4b7PN667oPMkokf4ZkNdWrMSPy9TNaLVr7vLqVUQHh2MnmaAEiyrvLsX8xUf99q3j1iAeMV8YvSFcH",
),
]
@pytest.mark.altcoin
@pytest.mark.monero
@pytest.mark.skip_t1
@pytest.mark.setup_client(mnemonic=MNEMONIC12)
@pytest.mark.parametrize("chunkify", (True, False))
def test_monero_getaddress(client: Client, chunkify: bool):
assert (
monero.get_address(
client, parse_path("m/44h/128h/0h"), show_display=True, chunkify=chunkify
)
== b"4Ahp23WfMrMFK3wYL2hLWQFGt87ZTeRkufS6JoQZu6MEFDokAQeGWmu9MA3GFq1yVLSJQbKJqVAn9F9DLYGpRzRAEXqAXKM"
)
assert (
monero.get_address(
client, parse_path("m/44h/128h/1h"), show_display=True, chunkify=chunkify
)
== b"44iAazhoAkv5a5RqLNVyh82a1n3ceNggmN4Ho7bUBJ14WkEVR8uFTe9f7v5rNnJ2kEbVXxfXiRzsD5Jtc6NvBi4D6WNHPie"
)
assert (
monero.get_address(
client, parse_path("m/44h/128h/2h"), show_display=True, chunkify=chunkify
pytestmark = [
pytest.mark.altcoin,
pytest.mark.monero,
pytest.mark.skip_t1,
pytest.mark.setup_client(mnemonic=MNEMONIC12),
]
@pytest.mark.parametrize("path, expected_address", TEST_VECTORS)
def test_monero_getaddress(client: Client, path: str, expected_address: bytes):
address = monero.get_address(client, parse_path(path), show_display=True)
assert address == expected_address
@pytest.mark.parametrize("path, expected_address", TEST_VECTORS)
def test_monero_getaddress_chunkify_details(
client: Client, path: str, expected_address: bytes
):
with client:
IF = InputFlowShowAddressQRCode(client)
client.set_input_flow(IF.get())
address = monero.get_address(
client, parse_path(path), show_display=True, chunkify=True
)
== b"47ejhmbZ4wHUhXaqA4b7PN667oPMkokf4ZkNdWrMSPy9TNaLVr7vLqVUQHh2MnmaAEiyrvLsX8xUf99q3j1iAeMV8YvSFcH"
)
assert address == expected_address

@ -20,6 +20,8 @@ from trezorlib.debuglink import TrezorClientDebugLink as Client
from trezorlib.ripple import get_address
from trezorlib.tools import parse_path
from ...input_flows import InputFlowShowAddressQRCode
CUSTOM_MNEMONIC = (
"armed bundle pudding lazy strategy impulse where identify "
"submit weekend physical antenna flight social acoustic absurd "
@ -32,15 +34,31 @@ pytestmark = [
pytest.mark.skip_t1, # T1 support is not planned
]
# data from https://iancoleman.io/bip39/
TEST_VECTORS = [
("m/44h/144h/0h/0/0", "rNaqKtKrMSwpwZSzRckPf7S96DkimjkF4H"),
("m/44h/144h/0h/0/1", "rBKz5MC2iXdoS3XgnNSYmF69K1Yo4NS3Ws"),
("m/44h/144h/1h/0/0", "rJX2KwzaLJDyFhhtXKi3htaLfaUH2tptEX"),
]
def test_ripple_get_address(client: Client):
# data from https://iancoleman.io/bip39/
address = get_address(client, parse_path("m/44h/144h/0h/0/0"))
assert address == "rNaqKtKrMSwpwZSzRckPf7S96DkimjkF4H"
address = get_address(client, parse_path("m/44h/144h/0h/0/1"))
assert address == "rBKz5MC2iXdoS3XgnNSYmF69K1Yo4NS3Ws"
address = get_address(client, parse_path("m/44h/144h/1h/0/0"))
assert address == "rJX2KwzaLJDyFhhtXKi3htaLfaUH2tptEX"
@pytest.mark.parametrize("path, expected_address", TEST_VECTORS)
def test_ripple_get_address(client: Client, path: str, expected_address: str):
address = get_address(client, parse_path(path), show_display=True)
assert address == expected_address
@pytest.mark.parametrize("path, expected_address", TEST_VECTORS)
def test_ripple_get_address_chunkify_details(
client: Client, path: str, expected_address: str
):
with client:
IF = InputFlowShowAddressQRCode(client)
client.set_input_flow(IF.get())
address = get_address(
client, parse_path(path), show_display=True, chunkify=True
)
assert address == expected_address
@pytest.mark.setup_client(mnemonic=CUSTOM_MNEMONIC)

@ -59,6 +59,12 @@ from trezorlib.debuglink import TrezorClientDebugLink as Client
from trezorlib.tools import parse_path
from ...common import parametrize_using_common_fixtures
from ...input_flows import InputFlowShowAddressQRCode
pytestmark = [
pytest.mark.altcoin,
pytest.mark.stellar,
]
def parameters_to_proto(parameters):
@ -80,8 +86,6 @@ def parameters_to_proto(parameters):
return tx, operations
@pytest.mark.altcoin
@pytest.mark.stellar
@parametrize_using_common_fixtures("stellar/sign_tx.json")
def test_sign_tx(client: Client, parameters, result):
tx, operations = parameters_to_proto(parameters)
@ -92,8 +96,6 @@ def test_sign_tx(client: Client, parameters, result):
assert b64encode(response.signature).decode() == result["signature"]
@pytest.mark.altcoin
@pytest.mark.stellar
@parametrize_using_common_fixtures("stellar/sign_tx.json")
@pytest.mark.skipif(not stellar.HAVE_STELLAR_SDK, reason="requires Stellar SDK")
def test_xdr(parameters, result):
@ -110,13 +112,21 @@ def test_xdr(parameters, result):
assert expected == actual
@pytest.mark.altcoin
@pytest.mark.stellar
@pytest.mark.parametrize("chunkify", (True, False))
@parametrize_using_common_fixtures("stellar/get_address.json")
def test_get_address(client: Client, chunkify: bool, parameters, result):
def test_get_address(client: Client, parameters, result):
address_n = parse_path(parameters["path"])
address = stellar.get_address(
client, address_n, show_display=True, chunkify=chunkify
)
address = stellar.get_address(client, address_n, show_display=True)
assert address == result["address"]
@pytest.mark.skip_t1("No input flow for T1")
@parametrize_using_common_fixtures("stellar/get_address.json")
def test_get_address_chunkify_details(client: Client, parameters, result):
with client:
IF = InputFlowShowAddressQRCode(client)
client.set_input_flow(IF.get())
address_n = parse_path(parameters["path"])
address = stellar.get_address(
client, address_n, show_display=True, chunkify=True
)
assert address == result["address"]

@ -12,7 +12,7 @@ from ..common import compact_size
pytestmark = [pytest.mark.skip_t1, pytest.mark.skip_t2]
ROOT_PUBLIC_KEY = bytes.fromhex(
"04626d58aca84f0fcb52ea63f0eb08de1067b8d406574a715d5e7928f4b67f113a00fb5c5918e74d2327311946c446b242c20fe7347482999bdc1e229b94e27d96"
"047f77368dea2d4d61e989f474a56723c3212dacf8a808d8795595ef38441427c4389bc454f02089d7f08b873005e4c28d432468997871c0bf286fd3861e21e96a"
)

@ -69,11 +69,11 @@ def test_busy_expiry(client: Client):
_assert_busy(client, True)
# Hasn't expired yet.
time.sleep(1.4)
time.sleep(0.1)
_assert_busy(client, True)
# Wait for it to expire. Add 400ms tolerance to account for CI slowness.
time.sleep(0.5)
# Wait for it to expire. Add some tolerance to account for CI/hardware slowness.
time.sleep(4.0)
# Check that the device is no longer busy.
# Also needs to come back to Homescreen (for UI tests).

@ -20,16 +20,34 @@ from trezorlib.debuglink import TrezorClientDebugLink as Client
from trezorlib.tezos import get_address
from trezorlib.tools import parse_path
from ...input_flows import InputFlowShowAddressQRCode
@pytest.mark.altcoin
@pytest.mark.tezos
@pytest.mark.skip_t1
@pytest.mark.parametrize("chunkify", (True, False))
def test_tezos_get_address(client: Client, chunkify: bool):
path = parse_path("m/44h/1729h/0h")
address = get_address(client, path, show_display=True, chunkify=chunkify)
assert address == "tz1Kef7BSg6fo75jk37WkKRYSnJDs69KVqt9"
path = parse_path("m/44h/1729h/1h")
address = get_address(client, path, show_display=True, chunkify=chunkify)
assert address == "tz1ekQapZCX4AXxTJhJZhroDKDYLHDHegvm1"
pytestmark = [
pytest.mark.altcoin,
pytest.mark.tezos,
pytest.mark.skip_t1,
]
TEST_VECTORS = [
("m/44h/1729h/0h", "tz1Kef7BSg6fo75jk37WkKRYSnJDs69KVqt9"),
("m/44h/1729h/1h", "tz1ekQapZCX4AXxTJhJZhroDKDYLHDHegvm1"),
]
@pytest.mark.parametrize("path, expected_address", TEST_VECTORS)
def test_tezos_get_address(client: Client, path: str, expected_address: str):
address = get_address(client, parse_path(path), show_display=True)
assert address == expected_address
@pytest.mark.parametrize("path, expected_address", TEST_VECTORS)
def test_tezos_get_address_chunkify_details(
client: Client, path: str, expected_address: str
):
with client:
IF = InputFlowShowAddressQRCode(client)
client.set_input_flow(IF.get())
address = get_address(
client, parse_path(path), show_display=True, chunkify=True
)
assert address == expected_address

@ -273,7 +273,15 @@ class InputFlowShowAddressQRCode(InputFlowBase):
self.debug.press_yes()
def input_flow_tr(self) -> BRGeneratorType:
yield
# Find out the page-length of the address
br = yield
if br.pages is not None:
address_swipes = br.pages - 1
else:
address_swipes = 0
for _ in range(address_swipes):
self.debug.press_right()
# Go into details
self.debug.press_right()
# Go through details and back
@ -281,6 +289,8 @@ class InputFlowShowAddressQRCode(InputFlowBase):
self.debug.press_left()
self.debug.press_left()
# Confirm
for _ in range(address_swipes):
self.debug.press_right()
self.debug.press_middle()

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save