Merge branch 'master' into release/23.09

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

@ -349,10 +349,10 @@ combine: ## combine boardloader + bootloader + prodtest into one combined image
$(PRODTEST_BUILD_DIR)/combined.bin $(PRODTEST_BUILD_DIR)/combined.bin
upload: ## upload firmware using trezorctl 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 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 coverage: ## generate coverage report
./tools/coverage-report ./tools/coverage-report

@ -136,6 +136,7 @@ if TREZOR_MODEL in ('R', ):
] ]
SOURCE_TREZORHAL += [ SOURCE_TREZORHAL += [
'embed/trezorhal/unix/secret.c', 'embed/trezorhal/unix/secret.c',
'embed/trezorhal/unix/optiga_hal.c',
] ]
SOURCE_UNIX = [ SOURCE_UNIX = [
@ -206,7 +207,7 @@ env.Replace(
'TREZOR_EMULATOR', 'TREZOR_EMULATOR',
CPU_MODEL, CPU_MODEL,
'HW_MODEL=' + MODEL_AS_NUMBER, '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_MODEL_'+TREZOR_MODEL,
'TREZOR_BOARD=\\"boards/board-unix.h\\"', 'TREZOR_BOARD=\\"boards/board-unix.h\\"',
'MCU_TYPE='+CPU_MODEL, '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) 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.Tool('micropython')
env.Replace( env.Replace(
@ -430,6 +445,7 @@ env.Replace(
'FIRMWARE', 'FIRMWARE',
'TREZOR_MODEL_'+TREZOR_MODEL, 'TREZOR_MODEL_'+TREZOR_MODEL,
'USE_HAL_DRIVER', 'USE_HAL_DRIVER',
UI_LAYOUT,
] + CPPDEFINES_MOD + CPPDEFINES_HAL, ] + CPPDEFINES_MOD + CPPDEFINES_HAL,
ASFLAGS=env.get('ENV')['CPU_ASFLAGS'], ASFLAGS=env.get('ENV')['CPU_ASFLAGS'],
ASPPFLAGS='$CFLAGS $CCFLAGS', ASPPFLAGS='$CFLAGS $CCFLAGS',
@ -518,7 +534,7 @@ if FROZEN:
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/*.py', SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/*.py',
exclude=[ exclude=[
SOURCE_PY_DIR + 'trezor/sdcard.py', 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/crypto/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/*.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', SOURCE_PY_DIR + 'trezor/ui/layouts/fido.py',
] if not EVERYTHING else [] ] if not EVERYTHING else []
)) ))
if TREZOR_MODEL in ('T', 'DISC1'): if UI_LAYOUT == 'UI_LAYOUT_TT':
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/*.py', SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt/*.py',
exclude=[ exclude=[
SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/fido.py', SOURCE_PY_DIR + 'trezor/ui/layouts/tt/fido.py',
] if not EVERYTHING else [] ] 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', SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tr/*.py',
exclude=[ exclude=[
SOURCE_PY_DIR + 'trezor/ui/layouts/tr/fido.py', SOURCE_PY_DIR + 'trezor/ui/layouts/tr/fido.py',
] if not EVERYTHING else [] ] if not EVERYTHING else []
)) ))
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 + 'trezor/wire/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'storage/*.py', SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'storage/*.py',
exclude=[ exclude=[
SOURCE_PY_DIR + 'storage/sd_salt.py', 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')) 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', SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/common/*.py',
exclude=[ exclude=[
SOURCE_PY_DIR + 'apps/common/sdcard.py', 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/debug/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/homescreen/*.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/homescreen/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/management/*.py', SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/management/*.py',
exclude=[ exclude=[
SOURCE_PY_DIR + 'apps/management/sd_protect.py', 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/management/*/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/misc/*.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/misc/*.py'))
@ -645,7 +661,8 @@ if FROZEN:
source_dir=SOURCE_PY_DIR, source_dir=SOURCE_PY_DIR,
bitcoin_only=BITCOIN_ONLY, bitcoin_only=BITCOIN_ONLY,
backlight='backlight' in FEATURES_AVAILABLE, backlight='backlight' in FEATURES_AVAILABLE,
optiga='optiga' in FEATURES_AVAILABLE optiga='optiga' in FEATURES_AVAILABLE,
ui_layout=UI_LAYOUT,
) )
source_mpyc = env.FrozenCFile( source_mpyc = env.FrozenCFile(
@ -681,13 +698,7 @@ def cargo_build():
else: else:
profile = '' profile = ''
# T1 does not have its own Rust feature, it shares it with TR features = ['micropython', 'protobuf', ui_layout_feature]
if TREZOR_MODEL in ('1', 'R'):
model_feature = 'model_tr'
else:
model_feature = 'model_tt'
features = ['micropython', 'protobuf', model_feature]
if BITCOIN_ONLY == '1': if BITCOIN_ONLY == '1':
features.append('bitcoin_only') features.append('bitcoin_only')
features.append('ui') features.append('ui')

@ -387,7 +387,6 @@ SOURCE_UNIX = [
if TREZOR_MODEL in ('T', 'R'): if TREZOR_MODEL in ('T', 'R'):
SOURCE_UNIX += [ SOURCE_UNIX += [
'embed/trezorhal/unix/sbu.c', 'embed/trezorhal/unix/sbu.c',
'embed/trezorhal/unix/sdcard.c',
] ]
if TREZOR_MODEL == 'R': 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)) 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.Tool('micropython')
env.Replace( env.Replace(
@ -508,6 +525,7 @@ env.Replace(
'TREZOR_BOARD=\\"boards/board-unix.h\\"', 'TREZOR_BOARD=\\"boards/board-unix.h\\"',
'MCU_TYPE='+CPU_MODEL, 'MCU_TYPE='+CPU_MODEL,
('MP_CONFIGFILE', '\\"embed/unix/mpconfigport.h\\"'), ('MP_CONFIGFILE', '\\"embed/unix/mpconfigport.h\\"'),
UI_LAYOUT,
] + CPPDEFINES_MOD, ] + CPPDEFINES_MOD,
ASPPFLAGS='$CFLAGS $CCFLAGS', ) ASPPFLAGS='$CFLAGS $CCFLAGS', )
@ -599,7 +617,7 @@ if FROZEN:
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/*.py', SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/*.py',
exclude=[ exclude=[
SOURCE_PY_DIR + 'trezor/sdcard.py', 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/crypto/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/*.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', SOURCE_PY_DIR + 'trezor/ui/layouts/fido.py',
] if not EVERYTHING else [] ] if not EVERYTHING else []
)) ))
if TREZOR_MODEL in ('T',): if UI_LAYOUT == 'UI_LAYOUT_TT':
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/*.py', SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt/*.py',
exclude=[ exclude=[
SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/fido.py', SOURCE_PY_DIR + 'trezor/ui/layouts/tt/fido.py',
] if not EVERYTHING else [] ] 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', SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tr/*.py',
exclude=[ exclude=[
SOURCE_PY_DIR + 'trezor/ui/layouts/tr/fido.py', SOURCE_PY_DIR + 'trezor/ui/layouts/tr/fido.py',
] if not EVERYTHING else [] ] if not EVERYTHING else []
)) ))
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 + 'trezor/wire/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'storage/*.py', SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'storage/*.py',
exclude=[ exclude=[
SOURCE_PY_DIR + 'storage/sd_salt.py', 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')) 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', SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/common/*.py',
exclude=[ exclude=[
SOURCE_PY_DIR + 'apps/common/sdcard.py', 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/debug/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/homescreen/*.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/homescreen/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/management/*.py', SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/management/*.py',
exclude=[ exclude=[
SOURCE_PY_DIR + 'apps/management/sd_protect.py', 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', SOURCE_PY_DIR + 'apps/management/authenticate_device.py',
] if TREZOR_MODEL not in ('R',) else []) ] if TREZOR_MODEL not in ('R',) else [])
) )
@ -728,7 +746,8 @@ if FROZEN:
source_dir=SOURCE_PY_DIR, source_dir=SOURCE_PY_DIR,
bitcoin_only=BITCOIN_ONLY, bitcoin_only=BITCOIN_ONLY,
backlight=TREZOR_MODEL in ('T',), backlight=TREZOR_MODEL in ('T',),
optiga=TREZOR_MODEL in ('R',) optiga=TREZOR_MODEL in ('R',),
ui_layout=UI_LAYOUT,
) )
source_mpyc = env.FrozenCFile( source_mpyc = env.FrozenCFile(
@ -764,9 +783,7 @@ RUST_LIB = 'trezor_lib'
RUST_LIBPATH = f'{RUST_LIBDIR}/lib{RUST_LIB}.a' RUST_LIBPATH = f'{RUST_LIBDIR}/lib{RUST_LIB}.a'
def cargo_build(): def cargo_build():
# T1 does not have its own Rust feature, it shares it with TR features = ['micropython', 'protobuf', ui_layout_feature]
model_feature = 'model_tr' if TREZOR_MODEL == '1' else f'model_t{TREZOR_MODEL.lower()}'
features = ['micropython', 'protobuf', model_feature]
if BITCOIN_ONLY == '1': if BITCOIN_ONLY == '1':
features.append('bitcoin_only') features.append('bitcoin_only')
features.append('ui') 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(); } void ui_screen_welcome(void) { screen_welcome(); }
uint32_t ui_screen_intro(const vendor_header *const vhdr, 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 bld_ver[32];
char ver_str[64]; char ver_str[64];
format_ver("%d.%d.%d", VERSION_UINT32, bld_ver, sizeof(bld_ver)); format_ver("%d.%d.%d", VERSION_UINT32, bld_ver, sizeof(bld_ver));
format_ver("%d.%d.%d", hdr->version, ver_str, sizeof(ver_str)); 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 // install UI
@ -242,17 +244,7 @@ void ui_screen_wipe_progress(int pos, int len) {
// done UI // done UI
void ui_screen_done(uint8_t restart_seconds, secbool full_redraw) { void ui_screen_done(uint8_t restart_seconds, secbool full_redraw) {
const char *str; screen_install_success(restart_seconds, initial_setup, full_redraw);
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);
} }
void ui_screen_boot_empty(bool fading) { screen_boot_empty(fading); } void ui_screen_boot_empty(bool fading) { screen_boot_empty(fading); }

@ -31,6 +31,7 @@ typedef enum {
SCREEN_WIPE_CONFIRM = 2, SCREEN_WIPE_CONFIRM = 2,
SCREEN_FINGER_PRINT = 3, SCREEN_FINGER_PRINT = 3,
SCREEN_WAIT_FOR_HOST = 4, SCREEN_WAIT_FOR_HOST = 4,
SCREEN_WELCOME = 5,
} screen_t; } screen_t;
void ui_screen_boot(const vendor_header* const vhdr, void ui_screen_boot(const vendor_header* const vhdr,
@ -42,9 +43,9 @@ void ui_click(void);
void ui_screen_welcome(void); void ui_screen_welcome(void);
uint32_t ui_screen_intro(const vendor_header* const vhdr, 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, uint32_t ui_screen_install_confirm(const vendor_header* const vhdr,
const image_header* const hdr, const image_header* const hdr,

@ -87,7 +87,8 @@ __attribute__((noreturn)) void jump_to(void *addr) {
"STORAGE WAS ERASED"); "STORAGE WAS ERASED");
} else { } else {
printf("storage was retained\n"); 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_backlight(180);
display_refresh(); display_refresh();

@ -35,6 +35,9 @@
#ifdef USE_I2C #ifdef USE_I2C
#include "i2c.h" #include "i2c.h"
#endif #endif
#ifdef USE_OPTIGA
#include "optiga_hal.h"
#endif
#ifdef USE_TOUCH #ifdef USE_TOUCH
#include "touch.h" #include "touch.h"
#endif #endif
@ -81,11 +84,15 @@ static const uint8_t * const BOOTLOADER_KEYS[] = {
#define USB_IFACE_NUM 0 #define USB_IFACE_NUM 0
typedef enum { typedef enum {
CONTINUE = 0, SHUTDOWN = 0,
RETURN = 1, CONTINUE_TO_FIRMWARE = 0xAABBCCDD,
SHUTDOWN = 2, RETURN_TO_MENU = 0x55667788,
} usb_result_t; } 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) { static void usb_init_all(secbool usb21_landing) {
usb_dev_info_t dev_info = { usb_dev_info_t dev_info = {
.device_class = 0x00, .device_class = 0x00,
@ -164,7 +171,7 @@ static usb_result_t bootloader_usb_loop(const vendor_header *const vhdr,
hal_delay(100); hal_delay(100);
usb_stop(); usb_stop();
usb_deinit(); usb_deinit();
return RETURN; return RETURN_TO_MENU;
} }
ui_screen_wipe(); ui_screen_wipe();
r = process_msg_WipeDevice(USB_IFACE_NUM, msg_size, buf); 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); hal_delay(100);
usb_stop(); usb_stop();
usb_deinit(); usb_deinit();
return RETURN; return RETURN_TO_MENU;
} else if (r == 0) { // last chunk received } else if (r == 0) { // last chunk received
ui_screen_install_progress_upload(1000); ui_screen_install_progress_upload(1000);
ui_screen_done(4, sectrue); ui_screen_done(4, sectrue);
@ -213,7 +220,7 @@ static usb_result_t bootloader_usb_loop(const vendor_header *const vhdr,
usb_stop(); usb_stop();
usb_deinit(); usb_deinit();
ui_screen_boot_empty(true); ui_screen_boot_empty(true);
return CONTINUE; return CONTINUE_TO_FIRMWARE;
} }
break; break;
case MessageType_MessageType_GetFeatures: case MessageType_MessageType_GetFeatures:
@ -227,7 +234,7 @@ static usb_result_t bootloader_usb_loop(const vendor_header *const vhdr,
hal_delay(100); hal_delay(100);
usb_stop(); usb_stop();
usb_deinit(); usb_deinit();
return RETURN; return RETURN_TO_MENU;
} }
process_msg_UnlockBootloader(USB_IFACE_NUM, msg_size, buf); process_msg_UnlockBootloader(USB_IFACE_NUM, msg_size, buf);
screen_unlock_bootloader_success(); screen_unlock_bootloader_success();
@ -294,6 +301,79 @@ static void check_bootloader_version(void) {
#endif #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 #ifndef TREZOR_EMULATOR
int main(void) { int main(void) {
// grab "stay in bootloader" flag as soon as possible // grab "stay in bootloader" flag as soon as possible
@ -315,41 +395,55 @@ int bootloader_main(void) {
mpu_config_bootloader(); 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; const image_header *hdr = NULL;
vendor_header vhdr; 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) { // detect whether the device contains a valid firmware
firmware_present = check_vendor_header_keys(&vhdr); 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) { if (sectrue == vhdr_keys_ok) {
firmware_present = check_vendor_header_lock(&vhdr); vhdr_lock_ok = check_vendor_header_lock(&vhdr);
} }
if (sectrue == firmware_present) { if (sectrue == vhdr_lock_ok) {
hdr = read_image_header( hdr = read_image_header(
(const uint8_t *)(size_t)(FIRMWARE_START + vhdr.hdrlen), (const uint8_t *)(size_t)(FIRMWARE_START + vhdr.hdrlen),
FIRMWARE_IMAGE_MAGIC, FIRMWARE_IMAGE_MAXSIZE); FIRMWARE_IMAGE_MAGIC, FIRMWARE_IMAGE_MAXSIZE);
if (hdr != (const image_header *)(size_t)(FIRMWARE_START + vhdr.hdrlen)) { if (hdr == (const image_header *)(size_t)(FIRMWARE_START + vhdr.hdrlen)) {
firmware_present = secfalse; img_hdr_ok = sectrue;
} }
} }
if (sectrue == firmware_present) { if (sectrue == img_hdr_ok) {
firmware_present = check_image_model(hdr); model_ok = check_image_model(hdr);
} }
if (sectrue == firmware_present) { if (sectrue == model_ok) {
firmware_present = header_present =
check_image_header_sig(hdr, vhdr.vsig_m, vhdr.vsig_n, vhdr.vpub); 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( firmware_present = check_image_contents(
hdr, IMAGE_HEADER_SIZE + vhdr.hdrlen, &FIRMWARE_AREA); hdr, IMAGE_HEADER_SIZE + vhdr.hdrlen, &FIRMWARE_AREA);
firmware_present_backup = firmware_present;
} }
#if defined TREZOR_MODEL_T #if defined TREZOR_MODEL_T
@ -361,6 +455,10 @@ int bootloader_main(void) {
i2c_init(); i2c_init();
#endif #endif
#ifdef USE_OPTIGA
optiga_hal_init();
#endif
#ifdef USE_TOUCH #ifdef USE_TOUCH
touch_power_on(); touch_power_on();
touch_init(); touch_init();
@ -390,6 +488,9 @@ int bootloader_main(void) {
stay_in_bootloader = sectrue; 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 // delay to detect touch or skip if we know we are staying in bootloader
// anyway // anyway
uint32_t touched = 0; uint32_t touched = 0;
@ -414,54 +515,64 @@ int bootloader_main(void) {
} }
#endif #endif
// start the bootloader if no or broken firmware found ... ensure(dont_optimize_out_true * (firmware_present == firmware_present_backup),
if (firmware_present != sectrue) { NULL);
#ifdef TREZOR_EMULATOR
// wait a bit so that the empty lock icon is visible // start the bootloader ...
// (on a real device, we are waiting for touch init which takes longer) // ... if user touched the screen on start
hal_delay(400); // ... or we have stay_in_bootloader flag to force it
#endif // ... or there is no valid firmware
// ignore stay in bootloader if (touched || stay_in_bootloader == sectrue || firmware_present != sectrue) {
stay_in_bootloader = secfalse; screen_t screen;
touched = false; 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 #ifndef USE_BACKLIGHT
hal_delay(1500); hal_delay(1500);
#else #else
// backlight fading takes some time so the explicit delay here is shorter // backlight fading takes some time so the explicit delay here is
hal_delay(1000); // shorter
hal_delay(1000);
#endif #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) { 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; uint32_t ui_result = 0;
switch (screen) { 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: case SCREEN_INTRO:
ui_result = ui_screen_intro(&vhdr, hdr); ui_result = ui_screen_intro(&vhdr, hdr, firmware_present);
if (ui_result == 1) { if (ui_result == 1) {
screen = SCREEN_MENU; screen = SCREEN_MENU;
} }
@ -470,15 +581,16 @@ int bootloader_main(void) {
} }
break; break;
case SCREEN_MENU: case SCREEN_MENU:
ui_result = ui_screen_menu(); ui_result = ui_screen_menu(firmware_present);
if (ui_result == 1) { // exit menu if (ui_result == 0xAABBCCDD) { // exit menu
screen = SCREEN_INTRO; screen = SCREEN_INTRO;
} }
if (ui_result == 2) { // reboot if (ui_result == 0x11223344) { // reboot
ui_screen_boot_empty(true); 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; screen = SCREEN_WIPE_CONFIRM;
} }
break; break;
@ -503,10 +615,11 @@ int bootloader_main(void) {
case SCREEN_WAIT_FOR_HOST: case SCREEN_WAIT_FOR_HOST:
screen_connect(); screen_connect();
switch (bootloader_usb_loop(&vhdr, hdr)) { switch (bootloader_usb_loop(&vhdr, hdr)) {
case CONTINUE: case CONTINUE_TO_FIRMWARE:
continue_to_firmware = true; continue_to_firmware = sectrue;
continue_to_firmware_backup = sectrue;
break; break;
case RETURN: case RETURN_TO_MENU:
screen = SCREEN_INTRO; screen = SCREEN_INTRO;
break; break;
case SHUTDOWN: case SHUTDOWN:
@ -520,75 +633,29 @@ int bootloader_main(void) {
break; 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; break;
} }
} }
} }
ensure(read_vendor_header((const uint8_t *)FIRMWARE_START, &vhdr), ensure(dont_optimize_out_true * (firmware_present == firmware_present_backup),
"Firmware is corrupted"); NULL);
if (sectrue == firmware_present) {
ensure(check_vendor_header_keys(&vhdr), "Firmware is corrupted"); firmware_jump_fn = real_jump_to_firmware;
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_compatible_settings(); firmware_jump_fn();
// 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);
return 0; return 0;
} }

@ -295,6 +295,7 @@ STATIC mp_obj_str_t mod_trezorutils_model_name_obj = {
/// INTERNAL_MODEL: str /// INTERNAL_MODEL: str
/// EMULATOR: bool /// EMULATOR: bool
/// BITCOIN_ONLY: bool /// BITCOIN_ONLY: bool
/// UI_LAYOUT: str
STATIC const mp_rom_map_elem_t mp_module_trezorutils_globals_table[] = { STATIC const mp_rom_map_elem_t mp_module_trezorutils_globals_table[] = {
{MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_trezorutils)}, {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 #else
{MP_ROM_QSTR(MP_QSTR_BITCOIN_ONLY), mp_const_false}, {MP_ROM_QSTR(MP_QSTR_BITCOIN_ONLY), mp_const_false},
#endif #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, STATIC MP_DEFINE_CONST_DICT(mp_module_trezorutils_globals,

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

@ -1,6 +1,6 @@
#define VERSION_MAJOR 2 #define VERSION_MAJOR 2
#define VERSION_MINOR 6 #define VERSION_MINOR 6
#define VERSION_PATCH 1 #define VERSION_PATCH 2
#define VERSION_BUILD 0 #define VERSION_BUILD 0
#define FIX_VERSION_MAJOR 2 #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 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 // - 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_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_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_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_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_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_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_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 }; /* ( */ 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_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_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_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 }; /* 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 }; /* 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 }; /* 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 }; /* 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 }; /* 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 }; /* 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 }; /* 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 }; /* 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 }; /* 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 }; /* 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 }; /* 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 }; /* 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 }; /* 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 }; /* 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 }; /* 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_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_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_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 }; /* ` */ 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 }; /* 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 }; /* 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 }; /* 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 }; /* 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 }; /* 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 }; /* 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 }; /* 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 }; /* 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 }; /* 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 }; /* 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 }; /* 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 }; /* 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 }; /* 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 }; /* 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); bool initial_setup);
void screen_wipe_progress(int16_t progress, bool initialize); void screen_wipe_progress(int16_t progress, bool initialize);
uint32_t screen_intro(const char* bld_version_str, const char* vendor_str, uint32_t screen_intro(const char* bld_version_str, const char* vendor_str,
uint8_t vendor_str_len, const char* version_str); uint8_t vendor_str_len, const char* version_str,
uint32_t screen_menu(void); bool fw_ok);
uint32_t screen_menu(secbool firmware_present);
void screen_connect(void); void screen_connect(void);
void screen_fatal_error_rust(const char* title, const char* msg, void screen_fatal_error_rust(const char* title, const char* msg,
const char* footer); const char* footer);
void screen_wipe_success(void); void screen_wipe_success(void);
void screen_wipe_fail(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); bool complete_draw);
uint32_t screen_install_fail(void); uint32_t screen_install_fail(void);
void screen_welcome_model(void); void screen_welcome_model(void);

@ -9,7 +9,7 @@ pub mod dma2d;
mod ffi; mod ffi;
pub mod io; pub mod io;
pub mod random; pub mod random;
#[cfg(feature = "model_tr")] #[cfg(feature = "rgb_led")]
pub mod rgb_led; pub mod rgb_led;
pub mod slip39; pub mod slip39;
pub mod storage; pub mod storage;
@ -18,6 +18,8 @@ pub mod uzlib;
pub mod wordlist; pub mod wordlist;
pub mod buffers; pub mod buffers;
pub mod secbool;
#[cfg(not(feature = "micropython"))] #[cfg(not(feature = "micropython"))]
pub mod time; 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, pub chunk_size: usize,
/// How big will be the space between chunks (in pixels). /// How big will be the space between chunks (in pixels).
pub x_offset: i16, pub x_offset: i16,
/// Optional characters that are wider and should be accounted for
pub wider_chars: Option<&'static str>,
} }
impl Chunks { impl Chunks {
@ -68,20 +66,6 @@ impl Chunks {
Chunks { Chunks {
chunk_size, chunk_size,
x_offset, 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) 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 { impl TextLayout {
@ -262,9 +254,21 @@ impl TextLayout {
PageBreaking::CutAndInsertEllipsisBoth PageBreaking::CutAndInsertEllipsisBoth
) && self.continues_from_prev_page ) && self.continues_from_prev_page
{ {
sink.prev_page_ellipsis(*cursor, self);
// Move the cursor to the right, always the same distance // 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() { while !remaining_text.is_empty() {
@ -285,6 +289,20 @@ impl TextLayout {
self.style.chunks, 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 { cursor.x += match self.align {
Alignment::Start => 0, Alignment::Start => 0,
Alignment::Center => (remaining_width - span.advance.x) / 2, Alignment::Center => (remaining_width - span.advance.x) / 2,
@ -598,7 +616,6 @@ impl Span {
let mut span_width = 0; let mut span_width = 0;
let mut found_any_whitespace = false; let mut found_any_whitespace = false;
let mut chunks_wider_chars = 0;
let mut char_indices_iter = text.char_indices().peekable(); let mut char_indices_iter = text.char_indices().peekable();
// Iterating manually because we need a reference to the iterator inside the // 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 let Some(chunkify_config) = chunks {
if i == chunkify_config.chunk_size { if i == chunkify_config.chunk_size {
line.advance.y = 0; line.advance.y = 0;
// Decreasing the offset for each wider character in the chunk line.advance.x += chunkify_config.x_offset;
line.advance.x += chunkify_config.x_offset - chunks_wider_chars;
return line; 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); let msg = self.buttons.event(ctx, event);
if self.showing_info_screen { if self.showing_info_screen {
// Showing the info screen currently - going back with the left button // 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.showing_info_screen = false;
self.update_everything(ctx); self.update_everything(ctx);
}; };
@ -163,11 +163,13 @@ impl<'a> Component for Confirm<'a> {
} else if self.has_info_screen() { } else if self.has_info_screen() {
// Being on the "main" screen but with an info screen available on the right // Being on the "main" screen but with an info screen available on the right
match msg { match msg {
Some(ButtonControllerMsg::Triggered(ButtonPos::Left)) => Some(ConfirmMsg::Cancel), Some(ButtonControllerMsg::Triggered(ButtonPos::Left, _)) => {
Some(ButtonControllerMsg::Triggered(ButtonPos::Middle)) => { Some(ConfirmMsg::Cancel)
}
Some(ButtonControllerMsg::Triggered(ButtonPos::Middle, _)) => {
Some(ConfirmMsg::Confirm) Some(ConfirmMsg::Confirm)
} }
Some(ButtonControllerMsg::Triggered(ButtonPos::Right)) => { Some(ButtonControllerMsg::Triggered(ButtonPos::Right, _)) => {
self.showing_info_screen = true; self.showing_info_screen = true;
self.update_everything(ctx); self.update_everything(ctx);
None None
@ -176,8 +178,10 @@ impl<'a> Component for Confirm<'a> {
} }
} else if self.two_btn_confirm { } else if self.two_btn_confirm {
match msg { match msg {
Some(ButtonControllerMsg::Triggered(ButtonPos::Left)) => Some(ConfirmMsg::Cancel), Some(ButtonControllerMsg::Triggered(ButtonPos::Left, _)) => {
Some(ButtonControllerMsg::Triggered(ButtonPos::Middle)) => { Some(ConfirmMsg::Cancel)
}
Some(ButtonControllerMsg::Triggered(ButtonPos::Middle, _)) => {
Some(ConfirmMsg::Confirm) Some(ConfirmMsg::Confirm)
} }
_ => None, _ => None,
@ -185,8 +189,12 @@ impl<'a> Component for Confirm<'a> {
} else { } else {
// There is just one main screen without info screen // There is just one main screen without info screen
match msg { match msg {
Some(ButtonControllerMsg::Triggered(ButtonPos::Left)) => Some(ConfirmMsg::Cancel), Some(ButtonControllerMsg::Triggered(ButtonPos::Left, _)) => {
Some(ButtonControllerMsg::Triggered(ButtonPos::Right)) => Some(ConfirmMsg::Confirm), Some(ConfirmMsg::Cancel)
}
Some(ButtonControllerMsg::Triggered(ButtonPos::Right, _)) => {
Some(ConfirmMsg::Confirm)
}
_ => None, _ => None,
} }
} }

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

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

@ -21,11 +21,14 @@ mod menu;
mod theme; mod theme;
mod welcome; mod welcome;
use crate::ui::{ use crate::{
constant, trezorhal::secbool::secbool,
constant::HEIGHT, ui::{
geometry::Point, constant,
model_tr::theme::{ICON_ARM_LEFT, ICON_ARM_RIGHT, WHITE}, constant::HEIGHT,
geometry::Point,
model_tr::theme::{ICON_ARM_LEFT, ICON_ARM_RIGHT, WHITE},
},
}; };
use confirm::Confirm; use confirm::Confirm;
use connect::Connect; use connect::Connect;
@ -192,8 +195,8 @@ extern "C" fn screen_unlock_bootloader_success() {
} }
#[no_mangle] #[no_mangle]
extern "C" fn screen_menu(_bld_version: *const cty::c_char) -> u32 { extern "C" fn screen_menu(firmware_present: secbool) -> u32 {
run(&mut Menu::new()) run(&mut Menu::new(firmware_present))
} }
#[no_mangle] #[no_mangle]
@ -202,6 +205,7 @@ extern "C" fn screen_intro(
vendor_str: *const cty::c_char, vendor_str: *const cty::c_char,
vendor_str_len: u8, vendor_str_len: u8,
version: *const cty::c_char, version: *const cty::c_char,
fw_ok: bool,
) -> u32 { ) -> u32 {
let vendor = unwrap!(unsafe { from_c_array(vendor_str, vendor_str_len as usize) }); let vendor = unwrap!(unsafe { from_c_array(vendor_str, vendor_str_len as usize) });
let version = unwrap!(unsafe { from_c_str(version) }); 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("\nby "));
unwrap!(version_str.push_str(vendor)); 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) 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 title = Label::centered("Trezor Reset", theme::TEXT_BOLD).vertically_centered();
let content = 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); let mut frame = ResultScreen::new(BLD_FG, BLD_BG, ICON_SPINNER, title, content, true);
show(&mut frame); show(&mut frame);
@ -339,15 +343,24 @@ extern "C" fn screen_install_fail() {
#[no_mangle] #[no_mangle]
extern "C" fn screen_install_success( extern "C" fn screen_install_success(
reboot_msg: *const cty::c_char, restart_seconds: u8,
_initial_setup: bool, _initial_setup: bool,
complete_draw: 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 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); let mut frame = ResultScreen::new(BLD_FG, BLD_BG, ICON_SPINNER, title, content, complete_draw);
show(&mut frame); show(&mut frame);

@ -204,7 +204,7 @@ where
}; };
let button_event = self.buttons.event(ctx, event); 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() { if self.is_in_subpage() {
match button { match button {
ButtonPos::Left => { ButtonPos::Left => {

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

@ -3,7 +3,7 @@ use super::{
}; };
use crate::{ use crate::{
strutil::StringType, strutil::StringType,
time::Duration, time::{Duration, Instant},
ui::{ ui::{
component::{base::Event, Component, EventCtx, Pad, TimerToken}, component::{base::Event, Component, EventCtx, Pad, TimerToken},
event::{ButtonEvent, PhysicalButton}, event::{ButtonEvent, PhysicalButton},
@ -39,8 +39,13 @@ enum ButtonState {
} }
pub enum ButtonControllerMsg { pub enum ButtonControllerMsg {
/// Button was pressed down.
Pressed(ButtonPos), 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. /// Defines what kind of button should be currently used.
@ -105,6 +110,16 @@ where
{ {
pos: ButtonPos, pos: ButtonPos,
button_type: ButtonType<T>, 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> impl<T> ButtonContainer<T>
@ -114,9 +129,17 @@ where
/// Supplying `None` as `btn_details` marks the button inactive /// Supplying `None` as `btn_details` marks the button inactive
/// (it can be later activated in `set()`). /// (it can be later activated in `set()`).
pub fn new(pos: ButtonPos, btn_details: Option<ButtonDetails<T>>) -> Self { 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 { Self {
pos, pos,
button_type: ButtonType::from_button_details(pos, btn_details), 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. /// Passing `None` as `btn_details` will mark the button as inactive.
pub fn set(&mut self, btn_details: Option<ButtonDetails<T>>, button_area: Rect) { 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 = ButtonType::from_button_details(self.pos, btn_details);
self.button_type.place(button_area); self.button_type.place(button_area);
} }
@ -151,7 +177,15 @@ where
/// hold. /// hold.
pub fn maybe_trigger(&mut self, ctx: &mut EventCtx) -> Option<ButtonControllerMsg> { pub fn maybe_trigger(&mut self, ctx: &mut EventCtx) -> Option<ButtonControllerMsg> {
match self.button_type { 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); self.hold_ended(ctx);
None None
@ -169,6 +203,27 @@ where
false 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. /// Registering hold event.
pub fn hold_started(&mut self, ctx: &mut EventCtx) { pub fn hold_started(&mut self, ctx: &mut EventCtx) {
if let ButtonType::HoldToConfirm(htc) = &mut self.button_type { if let ButtonType::HoldToConfirm(htc) = &mut self.button_type {
@ -283,18 +338,63 @@ where
if self.left_btn.htc_got_triggered(ctx, event) { if self.left_btn.htc_got_triggered(ctx, event) {
self.state = ButtonState::HTCNeedsRelease(PhysicalButton::Left); self.state = ButtonState::HTCNeedsRelease(PhysicalButton::Left);
self.set_pressed(ctx, false, false, false); 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) { } else if self.middle_btn.htc_got_triggered(ctx, event) {
self.state = ButtonState::Nothing; self.state = ButtonState::Nothing;
self.set_pressed(ctx, false, false, false); 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) { } else if self.right_btn.htc_got_triggered(ctx, event) {
self.state = ButtonState::HTCNeedsRelease(PhysicalButton::Right); self.state = ButtonState::HTCNeedsRelease(PhysicalButton::Right);
self.set_pressed(ctx, false, false, false); self.set_pressed(ctx, false, false, false);
return Some(ButtonControllerMsg::Triggered(ButtonPos::Right)); return Some(ButtonControllerMsg::Triggered(ButtonPos::Right, true));
} }
None 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> impl<T> Component for ButtonController<T>
@ -325,11 +425,13 @@ where
match which { match which {
// ▼ * // ▼ *
PhysicalButton::Left => { PhysicalButton::Left => {
self.got_pressed(ctx, ButtonPos::Left);
self.left_btn.hold_started(ctx); self.left_btn.hold_started(ctx);
Some(ButtonControllerMsg::Pressed(ButtonPos::Left)) Some(ButtonControllerMsg::Pressed(ButtonPos::Left))
} }
// * ▼ // * ▼
PhysicalButton::Right => { PhysicalButton::Right => {
self.got_pressed(ctx, ButtonPos::Right);
self.right_btn.hold_started(ctx); self.right_btn.hold_started(ctx);
Some(ButtonControllerMsg::Pressed(ButtonPos::Right)) Some(ButtonControllerMsg::Pressed(ButtonPos::Right))
} }
@ -369,6 +471,7 @@ where
return None; return None;
} }
} }
self.got_pressed(ctx, ButtonPos::Middle);
self.middle_hold_started(ctx); self.middle_hold_started(ctx);
( (
// ↓ ↓ // ↓ ↓
@ -399,7 +502,7 @@ where
// ▲ * | * ▲ // ▲ * | * ▲
ButtonEvent::ButtonReleased(b) if b != which_up => { 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 { 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::Left);
ignore_btn_delay.make_button_clickable(ButtonPos::Right); ignore_btn_delay.make_button_clickable(ButtonPos::Right);
@ -447,6 +550,9 @@ where
if let Some(ignore_btn_delay) = &mut self.ignore_btn_delay { if let Some(ignore_btn_delay) = &mut self.ignore_btn_delay {
ignore_btn_delay.handle_timer_token(token); 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) self.handle_htc_expiration(ctx, event)
} }
_ => None, _ => None,
@ -544,6 +650,13 @@ impl IgnoreButtonDelay {
self.right_clickable_timer = None; 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 /// Component allowing for automatically moving through items (e.g. Choice

@ -256,7 +256,7 @@ where
// Do something when a button was triggered // Do something when a button was triggered
// and we have some action connected with it // 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, // When there is a previous or next screen in the current flow,
// handle that first and in case it triggers, then do not continue // handle that first and in case it triggers, then do not continue
if self.event_consumed_by_current_choice(ctx, pos) { if self.event_consumed_by_current_choice(ctx, pos) {

@ -12,7 +12,7 @@ use crate::{
use super::{ use super::{
super::constant, common::display_center, theme, ButtonController, ButtonControllerMsg, super::constant, common::display_center, theme, ButtonController, ButtonControllerMsg,
ButtonLayout, ButtonLayout, ButtonPos, CancelConfirmMsg,
}; };
const AREA: Rect = constant::screen(); const AREA: Rect = constant::screen();
@ -144,7 +144,7 @@ where
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> { fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
Self::event_usb(self, ctx, event); Self::event_usb(self, ctx, event);
// HTC press of any button will lock the device // 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(()); return Some(());
} }
None None
@ -202,7 +202,7 @@ where
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> { fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
// Press of any button will unlock the device // 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(()); return Some(());
} }
None 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 // DEBUG-ONLY SECTION BELOW
#[cfg(feature = "ui_debug")] #[cfg(feature = "ui_debug")]
@ -243,3 +307,14 @@ where
t.child("label", &self.label); 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> { fn btn_layout(&self) -> ButtonLayout<T> {
ButtonLayout::default_three_icons() 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 /// Interface for a specific component efficiently giving
@ -130,7 +136,7 @@ where
/// Need to update the initial button layout. /// Need to update the initial button layout.
pub fn with_initial_page_counter(mut self, page_counter: usize) -> Self { pub fn with_initial_page_counter(mut self, page_counter: usize) -> Self {
self.page_counter = page_counter; 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( self.buttons = Child::new(
ButtonController::new(initial_btn_layout) ButtonController::new(initial_btn_layout)
.with_ignore_btn_delay(constant::IGNORE_OTHER_BTN_MS), .with_ignore_btn_delay(constant::IGNORE_OTHER_BTN_MS),
@ -235,7 +241,7 @@ where
} }
// Getting the remaining left and right areas. // 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); let (left_area, _center_area, right_area) = center_row_area.split_center(center_width);
// Possibly drawing on the left side. // Possibly drawing on the left side.
@ -277,14 +283,23 @@ where
} }
/// Getting the choice on the current index /// 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) 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. /// Display the current choice in the middle.
fn show_current_choice(&mut self, area: Rect) { fn show_current_choice(&mut self, area: Rect) {
self.get_current_choice() self.get_current_item()
.0
.paint_center(area, self.inverse_selected_item); .paint_center(area, self.inverse_selected_item);
// Color inversion is just one-time thing. // Color inversion is just one-time thing.
@ -406,7 +421,7 @@ where
/// If defined in the current choice, setting their text, /// If defined in the current choice, setting their text,
/// whether they are long-pressed, and painting them. /// whether they are long-pressed, and painting them.
fn set_buttons(&mut self, ctx: &mut EventCtx) { 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| { self.buttons.mutate(ctx, |ctx, buttons| {
buttons.set(btn_layout); buttons.set(btn_layout);
// When user holds one of the buttons, highlighting it. // When user holds one of the buttons, highlighting it.
@ -474,7 +489,7 @@ where
F: ChoiceFactory<T, Action = A>, F: ChoiceFactory<T, Action = A>,
T: StringType + Clone, T: StringType + Clone,
{ {
type Msg = A; type Msg = (A, bool);
fn place(&mut self, bounds: Rect) -> Rect { fn place(&mut self, bounds: Rect) -> Rect {
let (content_area, button_area) = bounds.split_bottom(theme::BUTTON_HEIGHT); 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 // Stopping the automatic movement when the released button is the same as the
// direction we were moving, or when the pressed button is the // direction we were moving, or when the pressed button is the
// opposite one (user does middle-click). // 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) || matches!(button_event, Some(ButtonControllerMsg::Pressed(pos)) if pos != moving_direction)
{ {
self.holding_mover.stop_moving(); self.holding_mover.stop_moving();
@ -534,7 +549,7 @@ where
} }
// There was a legitimate button event - doing some action // 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 { match pos {
ButtonPos::Left => { ButtonPos::Left => {
// Clicked BACK. Decrease the page counter. // Clicked BACK. Decrease the page counter.
@ -547,12 +562,24 @@ where
self.move_right(ctx); self.move_right(ctx);
} }
ButtonPos::Middle => { ButtonPos::Middle => {
// Clicked SELECT. Send current choice index // Clicked SELECT. Send current choice index with information about long-press
self.clear_and_repaint(ctx); 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 // The middle button was pressed, highlighting the current choice by color
// inversion. // inversion.
if let Some(ButtonControllerMsg::Pressed(ButtonPos::Middle)) = button_event { if let Some(ButtonControllerMsg::Pressed(ButtonPos::Middle)) = button_event {

@ -19,6 +19,7 @@ pub struct ChoiceItem<T: StringType> {
icon: Option<Icon>, icon: Option<Icon>,
btn_layout: ButtonLayout<T>, btn_layout: ButtonLayout<T>,
font: Font, font: Font,
middle_action_without_release: bool,
} }
impl<T: StringType> ChoiceItem<T> { impl<T: StringType> ChoiceItem<T> {
@ -28,6 +29,7 @@ impl<T: StringType> ChoiceItem<T> {
icon: None, icon: None,
btn_layout, btn_layout,
font: theme::FONT_CHOICE_ITEMS, font: theme::FONT_CHOICE_ITEMS,
middle_action_without_release: false,
} }
} }
@ -43,6 +45,15 @@ impl<T: StringType> ChoiceItem<T> {
self 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. /// Setting left button.
pub fn set_left_btn(&mut self, btn_left: Option<ButtonDetails<T>>) { pub fn set_left_btn(&mut self, btn_left: Option<ButtonDetails<T>>) {
self.btn_layout.btn_left = btn_left; self.btn_layout.btn_left = btn_left;
@ -117,6 +128,11 @@ where
fn btn_layout(&self) -> ButtonLayout<T> { fn btn_layout(&self) -> ButtonLayout<T> {
self.btn_layout.clone() 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) { 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> { 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) { fn paint(&mut self) {

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

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

@ -112,7 +112,7 @@ where
} }
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> { 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) { fn paint(&mut self) {

@ -95,7 +95,8 @@ impl<T: StringType + Clone> ChoiceFactory<T> for ChoiceFactoryWordlist {
if choice_index == DELETE_INDEX { if choice_index == DELETE_INDEX {
return ( return (
ChoiceItem::new("DELETE", ButtonLayout::arrow_armed_arrow("CONFIRM".into())) 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, WordlistAction::Delete,
); );
} }
@ -156,19 +157,49 @@ where
ChoiceFactoryWordlist::new(self.wordlist_type, self.textbox.content()) 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. /// Updates the whole page.
fn update(&mut self, ctx: &mut EventCtx) { fn update(&mut self, ctx: &mut EventCtx) {
self.update_chosen_letters(ctx); self.update_chosen_letters(ctx);
let new_choices = self.get_current_choices(); let new_choices = self.get_current_choices();
self.offer_words = new_choices.offer_words; self.offer_words = new_choices.offer_words;
// Starting at the random position in case of letters and at the beginning in let new_page_counter = self.get_new_page_counter(&new_choices);
// 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)
};
// Not using carousel in case of words, as that looks weird in case // Not using carousel in case of words, as that looks weird in case
// there is only one word to choose from. // there is only one word to choose from.
self.choice_page self.choice_page
@ -201,19 +232,25 @@ where
} }
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> { fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
match self.choice_page.event(ctx, event) { if let Some((action, long_press)) = self.choice_page.event(ctx, event) {
Some(WordlistAction::Delete) => { match action {
self.textbox.delete_last(ctx); WordlistAction::Delete => {
self.update(ctx); // Deleting all when long-pressed
} if long_press {
Some(WordlistAction::Letter(letter)) => { self.textbox.clear(ctx);
self.textbox.append(ctx, letter); } else {
self.update(ctx); self.textbox.delete_last(ctx);
} }
Some(WordlistAction::Word(word)) => { self.update(ctx);
return Some(word); }
WordlistAction::Letter(letter) => {
self.textbox.append(ctx, letter);
self.update(ctx);
}
WordlistAction::Word(word) => {
return Some(word);
}
} }
_ => {}
} }
None None
} }

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

@ -182,7 +182,7 @@ where
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> { fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
ctx.set_page_count(self.page_count()); 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 { match pos {
ButtonPos::Left => { ButtonPos::Left => {
if self.has_previous_page() { if self.has_previous_page() {

@ -5,8 +5,10 @@ use crate::ui::{
geometry::{Alignment2D, Offset, Point, Rect}, geometry::{Alignment2D, Offset, Point, Rect},
}; };
const MESSAGE_AREA_START: i16 = 26; const MESSAGE_AREA_START: i16 = 24 + 11;
const FOOTER_AREA_START: i16 = 40; 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; const ICON_TOP: i16 = 12;
pub struct ResultScreen<'a> { pub struct ResultScreen<'a> {
@ -53,15 +55,36 @@ impl<'a> Component for ResultScreen<'a> {
fn place(&mut self, bounds: Rect) -> Rect { fn place(&mut self, bounds: Rect) -> Rect {
self.bg.place(bounds); 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)); 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); 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 bounds
} }

@ -134,7 +134,7 @@ where
self.text.event(ctx, event); self.text.event(ctx, event);
self.headline.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) self.buttons.event(ctx, event)
{ {
button_confirmed = true; button_confirmed = true;

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

@ -6,8 +6,15 @@ use crate::{
error::Error, error::Error,
maybe_trace::MaybeTrace, maybe_trace::MaybeTrace,
micropython::{ micropython::{
buffer::StrBuffer, gc::Gc, iter::IterBuf, list::List, map::Map, module::Module, obj::Obj, buffer::{get_buffer, StrBuffer},
qstr::Qstr, util, gc::Gc,
iter::IterBuf,
list::List,
map::Map,
module::Module,
obj::Obj,
qstr::Qstr,
util,
}, },
strutil::StringType, strutil::StringType,
ui::{ ui::{
@ -38,9 +45,10 @@ use crate::{
use super::{ use super::{
component::{ component::{
AddressDetails, ButtonActions, ButtonDetails, ButtonLayout, ButtonPage, CancelConfirmMsg, AddressDetails, ButtonActions, ButtonDetails, ButtonLayout, ButtonPage, CancelConfirmMsg,
CancelInfoConfirmMsg, CoinJoinProgress, Flow, FlowPages, Frame, Homescreen, Lockscreen, CancelInfoConfirmMsg, CoinJoinProgress, ConfirmHomescreen, Flow, FlowPages, Frame,
NumberInput, Page, PassphraseEntry, PinEntry, Progress, ScrollableContent, ScrollableFrame, Homescreen, Lockscreen, NumberInput, Page, PassphraseEntry, PinEntry, Progress,
ShareWords, ShowMore, SimpleChoice, WelcomeScreen, WordlistEntry, WordlistType, ScrollableContent, ScrollableFrame, ShareWords, ShowMore, SimpleChoice, WelcomeScreen,
WordlistEntry, WordlistType,
}, },
constant, theme, 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 /// Function to create and call a `ButtonPage` dialog based on paginable content
/// (e.g. `Paragraphs` or `FormattedText`). /// (e.g. `Paragraphs` or `FormattedText`).
/// Has optional title (supply empty `StrBuffer` for that) and hold-to-confirm /// 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) } 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 { 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 block = move |_args: &[Obj], kwargs: &Map| {
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; 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.""" /// """Confirm action."""
Qstr::MP_QSTR_confirm_action => obj_fn_kw!(0, new_confirm_action).as_obj(), 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( /// def confirm_blob(
/// *, /// *,
/// title: str, /// title: str,

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

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

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

@ -35,7 +35,7 @@ pub mod menu;
pub mod theme; pub mod theme;
pub mod welcome; pub mod welcome;
use crate::ui::model_tt::theme::BLACK; use crate::{trezorhal::secbool::secbool, ui::model_tt::theme::BLACK};
use confirm::Confirm; use confirm::Confirm;
use intro::Intro; use intro::Intro;
use menu::Menu; use menu::Menu;
@ -220,8 +220,8 @@ extern "C" fn screen_wipe_confirm() -> u32 {
} }
#[no_mangle] #[no_mangle]
extern "C" fn screen_menu() -> u32 { extern "C" fn screen_menu(firmware_present: secbool) -> u32 {
run(&mut Menu::new()) run(&mut Menu::new(firmware_present))
} }
#[no_mangle] #[no_mangle]
@ -230,6 +230,7 @@ extern "C" fn screen_intro(
vendor_str: *const cty::c_char, vendor_str: *const cty::c_char,
vendor_str_len: u8, vendor_str_len: u8,
version: *const cty::c_char, version: *const cty::c_char,
fw_ok: bool,
) -> u32 { ) -> u32 {
let vendor = unwrap!(unsafe { from_c_array(vendor_str, vendor_str_len as usize) }); let vendor = unwrap!(unsafe { from_c_array(vendor_str, vendor_str_len as usize) });
let version = unwrap!(unsafe { from_c_str(version) }); 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("\nby "));
unwrap!(version_str.push_str(vendor)); 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) run(&mut frame)
} }
@ -315,7 +316,7 @@ extern "C" fn screen_wipe_success() {
&RESULT_WIPE, &RESULT_WIPE,
Icon::new(CHECK40), Icon::new(CHECK40),
"Trezor reset\nsuccessfully", "Trezor reset\nsuccessfully",
RECONNECT_MESSAGE, Label::centered(RECONNECT_MESSAGE, RESULT_WIPE.title_style()).vertically_centered(),
true, true,
); );
show(&mut frame, true); show(&mut frame, true);
@ -327,7 +328,7 @@ extern "C" fn screen_wipe_fail() {
&RESULT_WIPE, &RESULT_WIPE,
Icon::new(WARNING40), Icon::new(WARNING40),
"Trezor reset was\nnot successful", "Trezor reset was\nnot successful",
RECONNECT_MESSAGE, Label::centered(RECONNECT_MESSAGE, RESULT_WIPE.title_style()).vertically_centered(),
true, true,
); );
show(&mut frame, true); show(&mut frame, true);
@ -357,29 +358,29 @@ extern "C" fn screen_install_fail() {
&RESULT_FW_INSTALL, &RESULT_FW_INSTALL,
Icon::new(WARNING40), Icon::new(WARNING40),
"Firmware installation was not successful", "Firmware installation was not successful",
RECONNECT_MESSAGE, Label::centered(RECONNECT_MESSAGE, RESULT_FW_INSTALL.title_style()).vertically_centered(),
true, true,
); );
show(&mut frame, 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( let mut frame = ResultScreen::new(
&RESULT_FW_INSTALL, &RESULT_FW_INSTALL,
Icon::new(CHECK40), Icon::new(CHECK40),
"Firmware installed\nsuccessfully", "Firmware installed\nsuccessfully",
msg, Label::centered(msg, RESULT_FW_INSTALL.title_style()).vertically_centered(),
complete_draw, complete_draw,
); );
show(&mut frame, 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( let mut frame = ResultScreen::new(
&RESULT_INITIAL, &RESULT_INITIAL,
Icon::new(CHECK40), Icon::new(CHECK40),
"Firmware installed\nsuccessfully", "Firmware installed\nsuccessfully",
msg, Label::centered(msg, RESULT_INITIAL.title_style()).vertically_centered(),
complete_draw, complete_draw,
); );
show(&mut frame, complete_draw); show(&mut frame, complete_draw);
@ -387,15 +388,25 @@ fn screen_install_success_initial(msg: &'static str, complete_draw: bool) {
#[no_mangle] #[no_mangle]
extern "C" fn screen_install_success( extern "C" fn screen_install_success(
reboot_msg: *const cty::c_char, restart_seconds: u8,
initial_setup: bool, initial_setup: bool,
complete_draw: 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 { if initial_setup {
screen_install_success_initial(msg, complete_draw) screen_install_success_initial(reboot_msg.as_str(), complete_draw)
} else { } else {
screen_install_success_bld(msg, complete_draw) screen_install_success_bld(reboot_msg.as_str(), complete_draw)
} }
display::refresh(); 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_COLOR: Color = Color::rgb(0xE7, 0x0E, 0x0E);
pub const BLD_WIPE_TEXT_COLOR: Color = WHITE; 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: Color = WHITE;
pub const BLD_WIPE_BTN_COLOR_ACTIVE: Color = Color::rgb(0xFA, 0xCF, 0xCF); 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_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 = pub const TEXT_FINGERPRINT: TextStyle =
TextStyle::new(Font::NORMAL, BLD_FG, BLD_BG, BLD_FG, BLD_FG) TextStyle::new(Font::NORMAL, BLD_FG, BLD_BG, BLD_FG, BLD_FG)
.with_line_breaking(BreakWordsNoHyphen); .with_line_breaking(BreakWordsNoHyphen);

@ -30,7 +30,10 @@ impl<T: AsRef<str>> ErrorScreen<'_, T> {
pub fn new(title: T, message: T, footer: T) -> Self { pub fn new(title: T, message: T, footer: T) -> Self {
let title = Label::centered(title, STYLE.title_style()); let title = Label::centered(title, STYLE.title_style());
let message = Label::centered(message, STYLE.message_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 { Self {
bg: Pad::with_background(FATAL_ERROR_COLOR).with_clear(), 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> { 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 { Self {
style, style,
text: Label::centered(text, style.title_style()).vertically_centered(), text,
area: Rect::zero(), area: Rect::zero(),
} }
} }
@ -101,7 +101,7 @@ pub struct ResultScreen<'a, T> {
style: &'a ResultStyle, style: &'a ResultStyle,
icon: Icon, icon: Icon,
message: Child<Label<T>>, message: Child<Label<T>>,
footer: Child<ResultFooter<'a, T>>, footer: Child<ResultFooter<'a, &'a str>>,
} }
impl<'a, T: StringType> ResultScreen<'a, T> { impl<'a, T: StringType> ResultScreen<'a, T> {
@ -109,7 +109,7 @@ impl<'a, T: StringType> ResultScreen<'a, T> {
style: &'a ResultStyle, style: &'a ResultStyle,
icon: Icon, icon: Icon,
message: T, message: T,
footer: T, footer: Label<&'a str>,
complete_draw: bool, complete_draw: bool,
) -> Self { ) -> Self {
let mut instance = 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; type Msg = Never;
fn place(&mut self, _bounds: Rect) -> Rect { fn place(&mut self, _bounds: Rect) -> Rect {
self.bg.place(screen()); 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_pad.place(footer_area);
self.footer.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); void hal_delay(uint32_t ms);
uint32_t hal_ticks_ms(); uint32_t hal_ticks_ms();
void hal_delay_us(uint16_t delay_us);
void collect_hw_entropy(void); void collect_hw_entropy(void);
#define HW_ENTROPY_LEN (12 + 32) #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, if (HAL_OK == i2c_transmit(OPTIGA_I2C_INSTANCE, OPTIGA_ADDRESS,
(uint8_t *)data, data_size, I2C_TIMEOUT)) { (uint8_t *)data, data_size, I2C_TIMEOUT)) {
hal_delay_us(1000);
return OPTIGA_SUCCESS; return OPTIGA_SUCCESS;
} }
hal_delay_us(1000);
} }
return OPTIGA_ERR_I2C_WRITE; return OPTIGA_ERR_I2C_WRITE;
} }

@ -46,6 +46,8 @@
#define COLOR_FATAL_ERROR COLOR_BLACK #define COLOR_FATAL_ERROR COLOR_BLACK
#endif #endif
uint32_t systick_val_copy = 0;
// from util.s // from util.s
extern void shutdown_privileged(void); 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); } void hal_delay(uint32_t ms) { HAL_Delay(ms); }
uint32_t hal_ticks_ms() { return HAL_GetTick(); } 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 // reference RM0090 section 35.12.1 Figure 413
#define USB_OTG_HS_DATA_FIFO_RAM (USB_OTG_HS_PERIPH_BASE + 0x20000U) #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) { uint16_t len, uint32_t timeout) {
return HAL_I2C_Master_Transmit(&i2c_handle[idx], addr, data, len, 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, HAL_StatusTypeDef i2c_receive(uint16_t idx, uint8_t addr, uint8_t *data,
uint16_t len, uint32_t timeout) { 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, 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.Alternate = 0;
GPIO_InitStructure.Pin = GPIO_PIN_9; GPIO_InitStructure.Pin = GPIO_PIN_9;
HAL_GPIO_Init(GPIOD, &GPIO_InitStructure); 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); HAL_GPIO_WritePin(GPIOD, GPIO_PIN_9, GPIO_PIN_SET);
// warm reset startup time min 15ms // warm reset startup time min 15ms
hal_delay(20); hal_delay(20);

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

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

@ -39,6 +39,8 @@ def generate(env):
is_t2b1 = env["TREZOR_MODEL"] == "R" is_t2b1 = env["TREZOR_MODEL"] == "R"
backlight = env["backlight"] backlight = env["backlight"]
optiga = env["optiga"] 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 interim = f"{target[:-4]}.i" # replace .mpy with .i
sed_scripts = " ".join( sed_scripts = " ".join(
[ [
@ -46,6 +48,8 @@ def generate(env):
rf"-e 's/utils\.BITCOIN_ONLY/{btc_only}/g'", rf"-e 's/utils\.BITCOIN_ONLY/{btc_only}/g'",
rf"-e 's/utils\.USE_BACKLIGHT/{backlight}/g'", rf"-e 's/utils\.USE_BACKLIGHT/{backlight}/g'",
rf"-e 's/utils\.USE_OPTIGA/{optiga}/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/if TYPE_CHECKING/if False/'",
r"-e 's/import typing/# \0/'", r"-e 's/import typing/# \0/'",
r"-e '/from typing import (/,/^\s*)/ {s/^/# /}'", r"-e '/from typing import (/,/^\s*)/ {s/^/# /}'",

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

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

@ -26,8 +26,13 @@ async def get_address(msg: BinanceGetAddress, keychain: Keychain) -> BinanceAddr
pubkey = node.public_key() pubkey = node.public_key()
address = address_from_public_key(pubkey, HRP) address = address_from_public_key(pubkey, HRP)
if msg.show_display: if msg.show_display:
from . import PATTERN, SLIP44_ID
await show_address( 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) return BinanceAddress(address=address)

@ -24,7 +24,13 @@ async def get_public_key(
pubkey = node.public_key() pubkey = node.public_key()
if msg.show_display: if msg.show_display:
from . import PATTERN, SLIP44_ID
path = paths.address_n_to_str(msg.address_n) 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) return BinancePublicKey(public_key=pubkey)

@ -372,3 +372,46 @@ def address_n_to_str(address_n: Iterable[int]) -> str:
def unharden(item: int) -> int: def unharden(item: int) -> int:
return item ^ (item & HARDENED) 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) wif = public_key_to_wif(public_key)
if msg.show_display: if msg.show_display:
from . import PATTERN, SLIP44_ID
path = paths.address_n_to_str(msg.address_n) 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) 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 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: 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) address = address_from_bytes(node.ethereum_pubkeyhash(), defs.network)
if msg.show_display: if msg.show_display:
slip44_id = address_n[1] # it depends on the network (ETH vs ETC...)
await show_address( 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) return EthereumAddress(address=address)

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

@ -22,10 +22,11 @@ async def get_address(msg: MoneroGetAddress, keychain: Keychain) -> MoneroAddres
account = msg.account # local_cache_attribute account = msg.account # local_cache_attribute
minor = msg.minor # local_cache_attribute minor = msg.minor # local_cache_attribute
payment_id = msg.payment_id # 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 addr = creds.address
have_subaddress = ( have_subaddress = (
@ -65,10 +66,13 @@ async def get_address(msg: MoneroGetAddress, keychain: Keychain) -> MoneroAddres
) )
if msg.show_display: if msg.show_display:
from . import PATTERN, SLIP44_ID
await show_address( await show_address(
addr, addr,
address_qr="monero:" + 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), chunkify=bool(msg.chunkify),
) )

@ -15,7 +15,7 @@ async def get_address(msg: NEMGetAddress, keychain: Keychain) -> NEMAddress:
from trezor.messages import NEMAddress from trezor.messages import NEMAddress
from trezor.ui.layouts import show_address 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 .helpers import check_path, get_network_str
from .validators import validate_network from .validators import validate_network
@ -24,16 +24,19 @@ async def get_address(msg: NEMGetAddress, keychain: Keychain) -> NEMAddress:
network = msg.network # local_cache_attribute network = msg.network # local_cache_attribute
validate_network(network) 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) node = keychain.derive(address_n)
address = node.nem_address(network) address = node.nem_address(network)
if msg.show_display: if msg.show_display:
from . import PATTERNS, SLIP44_ID
await show_address( await show_address(
address, address,
case_sensitive=False, 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), network=get_network_str(network),
chunkify=bool(msg.chunkify), chunkify=bool(msg.chunkify),
) )

@ -18,16 +18,21 @@ async def get_address(msg: RippleGetAddress, keychain: Keychain) -> RippleAddres
from .helpers import address_from_public_key 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() pubkey = node.public_key()
address = address_from_public_key(pubkey) address = address_from_public_key(pubkey)
if msg.show_display: if msg.show_display:
from . import PATTERN, SLIP44_ID
await show_address( await show_address(
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), chunkify=bool(msg.chunkify),
) )

@ -17,16 +17,23 @@ async def get_address(msg: StellarGetAddress, keychain: Keychain) -> StellarAddr
from . import helpers 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()) pubkey = seed.remove_ed25519_prefix(node.public_key())
address = helpers.address_from_public_key(pubkey) address = helpers.address_from_public_key(pubkey)
if msg.show_display: if msg.show_display:
path = paths.address_n_to_str(msg.address_n) from . import PATTERN, SLIP44_ID
await show_address( 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) return StellarAddress(address=address)

@ -20,18 +20,23 @@ async def get_address(msg: TezosGetAddress, keychain: Keychain) -> TezosAddress:
from . import helpers 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()) pk = seed.remove_ed25519_prefix(node.public_key())
pkh = hashlib.blake2b(pk, outlen=helpers.PUBLIC_KEY_HASH_SIZE).digest() pkh = hashlib.blake2b(pk, outlen=helpers.PUBLIC_KEY_HASH_SIZE).digest()
address = helpers.base58_encode_check(pkh, helpers.TEZOS_ED25519_ADDRESS_PREFIX) address = helpers.base58_encode_check(pkh, helpers.TEZOS_ED25519_ADDRESS_PREFIX)
if msg.show_display: if msg.show_display:
from . import PATTERNS, SLIP44_ID
await show_address( await show_address(
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), 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) pk_prefixed = helpers.base58_encode_check(pk, helpers.TEZOS_PUBLICKEY_PREFIX)
if msg.show_display: 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) 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 # NOTE: using any import magic probably causes mypy not to check equivalence of
# layout type signatures across models # layout type signatures across models
if utils.INTERNAL_MODEL in ("T1B1", "T2B1"): if utils.UI_LAYOUT == "TR":
from .tr import * # noqa: F401,F403 from .tr import * # noqa: F401,F403
elif utils.INTERNAL_MODEL in ("T2T1", "D001"): elif utils.UI_LAYOUT == "TT":
from .tt_v2 import * # noqa: F401,F403 from .tt import * # noqa: F401,F403
else: else:
raise ValueError("Unknown Trezor model") raise ValueError("Unknown layout")

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

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

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

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

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

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

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

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

@ -70,6 +70,7 @@ def navigate_to_action_and_press(
wanted_action: str, wanted_action: str,
all_actions: list[str], all_actions: list[str],
is_carousel: bool = True, is_carousel: bool = True,
hold_ms: int = 0,
) -> None: ) -> None:
"""Navigate to the button with certain action and press it""" """Navigate to the button with certain action and press it"""
# Orient # Orient
@ -99,8 +100,11 @@ def navigate_to_action_and_press(
is_carousel=is_carousel, is_carousel=is_carousel,
) )
# Press # Press or hold
debug.press_middle(wait=True) 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: 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 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: def _cancel_pin(debug: "DebugLink") -> None:
"""Navigate to "CANCEL" and press it""" """Navigate to "CANCEL" and press it"""
# It is the same button as DELETE # 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:]) _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]) @pytest.mark.setup_client(pin=PIN60[:50])
def test_pin_longer_than_max(device_handler: "BackgroundDeviceHandler"): def test_pin_longer_than_max(device_handler: "BackgroundDeviceHandler"):
with prepare(device_handler) as debug: 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.debuglink import TrezorClientDebugLink as Client
from trezorlib.tools import parse_path 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 = [ BINANCE_ADDRESS_TEST_VECTORS = [
("m/44h/714h/0h/0/0", "bnb1hgm0p7khfk85zpz5v0j8wnej3a90w709vhkdfu"), ("m/44h/714h/0h/0/0", "bnb1hgm0p7khfk85zpz5v0j8wnej3a90w709vhkdfu"),
("m/44h/714h/0h/0/1", "bnb1egswqkszzfc2uq78zjslc6u2uky4pw46x4rstd"), ("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) @pytest.mark.parametrize("path, expected_address", BINANCE_ADDRESS_TEST_VECTORS)
def test_binance_get_address( def test_binance_get_address(client: Client, path: str, expected_address: str):
client: Client, chunkify: bool, path: str, expected_address: str
):
# data from https://github.com/binance-chain/javascript-sdk/blob/master/__tests__/crypto.test.js#L50 # data from https://github.com/binance-chain/javascript-sdk/blob/master/__tests__/crypto.test.js#L50
address = get_address( address = get_address(client, parse_path(path), show_display=True)
client, parse_path(path), show_display=True, chunkify=chunkify
)
assert address == expected_address 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 trezorlib.tools import parse_path
from ...common import parametrize_using_common_fixtures from ...common import parametrize_using_common_fixtures
from ...input_flows import InputFlowShowAddressQRCode
pytestmark = [pytest.mark.altcoin, pytest.mark.ethereum] pytestmark = [pytest.mark.altcoin, pytest.mark.ethereum]
@parametrize_using_common_fixtures("ethereum/getaddress.json") @parametrize_using_common_fixtures("ethereum/getaddress.json")
@pytest.mark.parametrize("chunkify", (True, False)) def test_getaddress(client: Client, parameters, result):
def test_getaddress(client: Client, chunkify: bool, parameters, result):
address_n = parse_path(parameters["path"]) address_n = parse_path(parameters["path"])
assert ( assert (
ethereum.get_address(client, address_n, show_display=True, chunkify=chunkify) ethereum.get_address(client, address_n, show_display=True) == result["address"]
== 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 trezorlib.tools import parse_path
from ...common import MNEMONIC12 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 pytestmark = [
@pytest.mark.monero pytest.mark.altcoin,
@pytest.mark.skip_t1 pytest.mark.monero,
@pytest.mark.setup_client(mnemonic=MNEMONIC12) pytest.mark.skip_t1,
@pytest.mark.parametrize("chunkify", (True, False)) pytest.mark.setup_client(mnemonic=MNEMONIC12),
def test_monero_getaddress(client: Client, chunkify: bool): ]
assert (
monero.get_address(
client, parse_path("m/44h/128h/0h"), show_display=True, chunkify=chunkify @pytest.mark.parametrize("path, expected_address", TEST_VECTORS)
) def test_monero_getaddress(client: Client, path: str, expected_address: bytes):
== b"4Ahp23WfMrMFK3wYL2hLWQFGt87ZTeRkufS6JoQZu6MEFDokAQeGWmu9MA3GFq1yVLSJQbKJqVAn9F9DLYGpRzRAEXqAXKM" address = monero.get_address(client, parse_path(path), show_display=True)
) assert address == expected_address
assert (
monero.get_address(
client, parse_path("m/44h/128h/1h"), show_display=True, chunkify=chunkify @pytest.mark.parametrize("path, expected_address", TEST_VECTORS)
) def test_monero_getaddress_chunkify_details(
== b"44iAazhoAkv5a5RqLNVyh82a1n3ceNggmN4Ho7bUBJ14WkEVR8uFTe9f7v5rNnJ2kEbVXxfXiRzsD5Jtc6NvBi4D6WNHPie" client: Client, path: str, expected_address: bytes
) ):
assert ( with client:
monero.get_address( IF = InputFlowShowAddressQRCode(client)
client, parse_path("m/44h/128h/2h"), show_display=True, chunkify=chunkify 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.ripple import get_address
from trezorlib.tools import parse_path from trezorlib.tools import parse_path
from ...input_flows import InputFlowShowAddressQRCode
CUSTOM_MNEMONIC = ( CUSTOM_MNEMONIC = (
"armed bundle pudding lazy strategy impulse where identify " "armed bundle pudding lazy strategy impulse where identify "
"submit weekend physical antenna flight social acoustic absurd " "submit weekend physical antenna flight social acoustic absurd "
@ -32,15 +34,31 @@ pytestmark = [
pytest.mark.skip_t1, # T1 support is not planned 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/ @pytest.mark.parametrize("path, expected_address", TEST_VECTORS)
address = get_address(client, parse_path("m/44h/144h/0h/0/0")) def test_ripple_get_address(client: Client, path: str, expected_address: str):
assert address == "rNaqKtKrMSwpwZSzRckPf7S96DkimjkF4H" address = get_address(client, parse_path(path), show_display=True)
address = get_address(client, parse_path("m/44h/144h/0h/0/1")) assert address == expected_address
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_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) @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 trezorlib.tools import parse_path
from ...common import parametrize_using_common_fixtures from ...common import parametrize_using_common_fixtures
from ...input_flows import InputFlowShowAddressQRCode
pytestmark = [
pytest.mark.altcoin,
pytest.mark.stellar,
]
def parameters_to_proto(parameters): def parameters_to_proto(parameters):
@ -80,8 +86,6 @@ def parameters_to_proto(parameters):
return tx, operations return tx, operations
@pytest.mark.altcoin
@pytest.mark.stellar
@parametrize_using_common_fixtures("stellar/sign_tx.json") @parametrize_using_common_fixtures("stellar/sign_tx.json")
def test_sign_tx(client: Client, parameters, result): def test_sign_tx(client: Client, parameters, result):
tx, operations = parameters_to_proto(parameters) 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"] assert b64encode(response.signature).decode() == result["signature"]
@pytest.mark.altcoin
@pytest.mark.stellar
@parametrize_using_common_fixtures("stellar/sign_tx.json") @parametrize_using_common_fixtures("stellar/sign_tx.json")
@pytest.mark.skipif(not stellar.HAVE_STELLAR_SDK, reason="requires Stellar SDK") @pytest.mark.skipif(not stellar.HAVE_STELLAR_SDK, reason="requires Stellar SDK")
def test_xdr(parameters, result): def test_xdr(parameters, result):
@ -110,13 +112,21 @@ def test_xdr(parameters, result):
assert expected == actual assert expected == actual
@pytest.mark.altcoin
@pytest.mark.stellar
@pytest.mark.parametrize("chunkify", (True, False))
@parametrize_using_common_fixtures("stellar/get_address.json") @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_n = parse_path(parameters["path"])
address = stellar.get_address( address = stellar.get_address(client, address_n, show_display=True)
client, address_n, show_display=True, chunkify=chunkify
)
assert address == result["address"] 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] pytestmark = [pytest.mark.skip_t1, pytest.mark.skip_t2]
ROOT_PUBLIC_KEY = bytes.fromhex( ROOT_PUBLIC_KEY = bytes.fromhex(
"04626d58aca84f0fcb52ea63f0eb08de1067b8d406574a715d5e7928f4b67f113a00fb5c5918e74d2327311946c446b242c20fe7347482999bdc1e229b94e27d96" "047f77368dea2d4d61e989f474a56723c3212dacf8a808d8795595ef38441427c4389bc454f02089d7f08b873005e4c28d432468997871c0bf286fd3861e21e96a"
) )

@ -69,11 +69,11 @@ def test_busy_expiry(client: Client):
_assert_busy(client, True) _assert_busy(client, True)
# Hasn't expired yet. # Hasn't expired yet.
time.sleep(1.4) time.sleep(0.1)
_assert_busy(client, True) _assert_busy(client, True)
# Wait for it to expire. Add 400ms tolerance to account for CI slowness. # Wait for it to expire. Add some tolerance to account for CI/hardware slowness.
time.sleep(0.5) time.sleep(4.0)
# Check that the device is no longer busy. # Check that the device is no longer busy.
# Also needs to come back to Homescreen (for UI tests). # 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.tezos import get_address
from trezorlib.tools import parse_path from trezorlib.tools import parse_path
from ...input_flows import InputFlowShowAddressQRCode
@pytest.mark.altcoin pytestmark = [
@pytest.mark.tezos pytest.mark.altcoin,
@pytest.mark.skip_t1 pytest.mark.tezos,
@pytest.mark.parametrize("chunkify", (True, False)) pytest.mark.skip_t1,
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) TEST_VECTORS = [
assert address == "tz1Kef7BSg6fo75jk37WkKRYSnJDs69KVqt9" ("m/44h/1729h/0h", "tz1Kef7BSg6fo75jk37WkKRYSnJDs69KVqt9"),
("m/44h/1729h/1h", "tz1ekQapZCX4AXxTJhJZhroDKDYLHDHegvm1"),
path = parse_path("m/44h/1729h/1h") ]
address = get_address(client, path, show_display=True, chunkify=chunkify)
assert address == "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() self.debug.press_yes()
def input_flow_tr(self) -> BRGeneratorType: 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 # Go into details
self.debug.press_right() self.debug.press_right()
# Go through details and back # Go through details and back
@ -281,6 +289,8 @@ class InputFlowShowAddressQRCode(InputFlowBase):
self.debug.press_left() self.debug.press_left()
self.debug.press_left() self.debug.press_left()
# Confirm # Confirm
for _ in range(address_swipes):
self.debug.press_right()
self.debug.press_middle() self.debug.press_middle()

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