From b9e0928a3341c47cdc8d91fb8b0497d7b43ff92f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Baptiste=20B=C3=A9drune?= Date: Wed, 27 Oct 2021 17:33:14 +0200 Subject: [PATCH 01/65] Fix compilation warnings --- Makefile | 2 +- src/bolos_ux_common.h | 3 +- src/bolos_ux_nanos.c | 8 +-- src/bolos_ux_nanos.h | 4 -- src/bolos_ux_nanos_keyboard.c | 2 +- src/bolos_ux_nanox.c | 2 +- src/bolos_ux_nanox_keyboard.c | 40 ++++++------- src/main.c | 45 ++++----------- src/nanos_enter_phrase.c | 14 ++--- src/nanox_enter_phrase.c | 38 ++++++------- src_ux_common/bolos_ux_common_bip39.h | 1 - src_ux_common/bolos_ux_onboarding_electrum.c | 30 +--------- .../bolos_ux_onboarding_seed_bip39.c | 57 +++---------------- 13 files changed, 76 insertions(+), 170 deletions(-) diff --git a/Makefile b/Makefile index a9b5cb57..3bbf43f4 100755 --- a/Makefile +++ b/Makefile @@ -90,7 +90,7 @@ $(info GCCPATH is not set: arm-none-eabi-* will be used from PATH) endif CC := $(CLANGPATH)clang -CFLAGS += -O3 -Os +CFLAGS += -O3 -Os -Wshadow -Wformat AS := $(GCCPATH)arm-none-eabi-gcc AFLAGS += diff --git a/src/bolos_ux_common.h b/src/bolos_ux_common.h index a70023ab..41cfd110 100644 --- a/src/bolos_ux_common.h +++ b/src/bolos_ux_common.h @@ -113,6 +113,8 @@ void screen_settings_passphrase_temporary_1_init(void); void screen_settings_passphrase_type_and_review_init(unsigned int kind); void screen_settings_erase_all_init(void); +void screen_common_keyboard_init(unsigned int stack_slot, unsigned int current_element, unsigned int nb_elements, keyboard_callback_t callback); + #define COMMON_KEYBOARD_INDEX_UNCHANGED (-1UL) #if defined(TARGET_NANOX) || defined(TARGET_NANOS2) @@ -149,7 +151,6 @@ void screen_consent_set_interval(unsigned int interval_ms); void screen_common_pin_init(unsigned int stack_slot, pin_callback_t end_callback); -void screen_common_keyboard_init(unsigned int stack_slot, unsigned int current_element, unsigned int nb_elements, keyboard_callback_t callback); void screen_keyboard_init(char* buffer, unsigned int maxsize, appmain_t validation_callback); void debug(unsigned int id, unsigned char* msg); diff --git a/src/bolos_ux_nanos.c b/src/bolos_ux_nanos.c index 5e9ef190..b06d5b0f 100644 --- a/src/bolos_ux_nanos.c +++ b/src/bolos_ux_nanos.c @@ -38,11 +38,11 @@ unsigned short io_timeout(unsigned short last_timeout) { return 1; } -const unsigned char const C_app_empty_colors[] = { +const unsigned char C_app_empty_colors[] = { 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, }; -const unsigned char const C_app_empty_bitmap[] = { +const unsigned char C_app_empty_bitmap[] = { // color index table 0x01, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, @@ -67,7 +67,7 @@ void screen_prepare_masked_icon(unsigned char *icon_bitmap, // prepare the icon_details content icon_details->bpp = C_app_empty_bitmap[0]; // use color table from the const - icon_details->colors = (unsigned int *)C_app_empty_colors; + icon_details->colors = (const unsigned int *) C_app_empty_colors; icon_details->bitmap = bitmap; // when first color of the bitmap is not 0, then, must inverse the icon's @@ -93,7 +93,7 @@ void screen_prepare_masked_icon(unsigned char *icon_bitmap, } void io_seproxyhal_display(const bagl_element_t *element) { - io_seproxyhal_display_default((bagl_element_t *)element); + io_seproxyhal_display_default(element); } void bolos_ux_hslider3_init(unsigned int total_count) { diff --git a/src/bolos_ux_nanos.h b/src/bolos_ux_nanos.h index 02a3fac0..860a75dd 100644 --- a/src/bolos_ux_nanos.h +++ b/src/bolos_ux_nanos.h @@ -250,10 +250,6 @@ unsigned int bolos_ux_electrum_new_mnemonic(unsigned int version, unsigned int bolos_ux_electrum_mnemonic_check(unsigned int version, unsigned char *mnemonic, unsigned int mnemonicLength); -void bolos_ux_electrum_mnemonic_to_seed(unsigned char *mnemonic, - unsigned int mnemonicLength, - unsigned char *seed); - #endif /** diff --git a/src/bolos_ux_nanos_keyboard.c b/src/bolos_ux_nanos_keyboard.c index 101d7b23..a3fd8c5e 100644 --- a/src/bolos_ux_nanos_keyboard.c +++ b/src/bolos_ux_nanos_keyboard.c @@ -70,7 +70,7 @@ const bagl_element_t screen_common_keyboard_elements[] = { const bagl_element_t* screen_common_keyboard_before_element_display_callback(const bagl_element_t* element) { const bagl_element_t* e; // copy element to be displayed - os_memmove(&G_ux.tmp_element, PIC(element), sizeof(G_ux.tmp_element)); + memcpy(&G_ux.tmp_element, PIC(element), sizeof(G_ux.tmp_element)); switch (element->component.userid) { diff --git a/src/bolos_ux_nanox.c b/src/bolos_ux_nanox.c index 5dfe95ae..5003aeb2 100644 --- a/src/bolos_ux_nanox.c +++ b/src/bolos_ux_nanox.c @@ -50,7 +50,7 @@ void screen_keyboard_validate_entered_text(void) { // last keycode is \n G_ux_params.u.keyboard.keycode = '\n'; // copy output text - os_memmove(G_ux_params.u.keyboard.entered_text, G_bolos_ux_context.keyboard_user_buffer, sizeof(G_ux_params.u.keyboard.entered_text)); + memcpy(G_ux_params.u.keyboard.entered_text, G_bolos_ux_context.keyboard_user_buffer, sizeof(G_ux_params.u.keyboard.entered_text)); // pop the screen ux_stack_pop(); ux_stack_redisplay(); diff --git a/src/bolos_ux_nanox_keyboard.c b/src/bolos_ux_nanox_keyboard.c index 66858721..c394dd54 100644 --- a/src/bolos_ux_nanox_keyboard.c +++ b/src/bolos_ux_nanox_keyboard.c @@ -19,39 +19,39 @@ const bagl_element_t screen_common_keyboard_elements[] = { // erase - {{BAGL_RECTANGLE , 0x00, 0, 0, 128, 64, 0, 0, BAGL_FILL, 0x000000, 0xFFFFFF, 0, 0}, NULL, 0, 0, 0, NULL, NULL, NULL}, + {{BAGL_RECTANGLE , 0x00, 0, 0, 128, 64, 0, 0, BAGL_FILL, 0x000000, 0xFFFFFF, 0, 0}, NULL}, // title - {{BAGL_LABELINE , 0x04, 0, 20, 128, 32-5, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_REGULAR_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL, 0, 0, 0, NULL, NULL, NULL }, + {{BAGL_LABELINE , 0x04, 0, 20, 128, 32-5, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_REGULAR_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL}, // typed word - {{BAGL_LABELINE , 0x10, 128/2-12/2-40, 48+5, 14, 14, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL, 0, 0, 0, NULL, NULL, NULL }, - {{BAGL_LABELINE , 0x11, 128/2-12/2-30, 48+5, 14, 14, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL, 0, 0, 0, NULL, NULL, NULL }, - {{BAGL_LABELINE , 0x12, 128/2-12/2-20, 48+5, 14, 14, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL, 0, 0, 0, NULL, NULL, NULL }, - {{BAGL_LABELINE , 0x13, 128/2-12/2-10, 48+5, 14, 14, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL, 0, 0, 0, NULL, NULL, NULL }, - {{BAGL_LABELINE , 0x14, 128/2-12/2 , 48+5, 14, 14, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL, 0, 0, 0, NULL, NULL, NULL }, - {{BAGL_LABELINE , 0x15, 128/2-12/2+10, 48+5, 14, 14, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL, 0, 0, 0, NULL, NULL, NULL }, - {{BAGL_LABELINE , 0x16, 128/2-12/2+20, 48+5, 14, 14, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL, 0, 0, 0, NULL, NULL, NULL }, - {{BAGL_LABELINE , 0x17, 128/2-12/2+30, 48+5, 14, 14, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL, 0, 0, 0, NULL, NULL, NULL }, - {{BAGL_LABELINE , 0x18, 128/2-12/2+40, 48+5, 14, 14, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL, 0, 0, 0, NULL, NULL, NULL }, + {{BAGL_LABELINE , 0x10, 128/2-12/2-40, 48+5, 14, 14, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL}, + {{BAGL_LABELINE , 0x11, 128/2-12/2-30, 48+5, 14, 14, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL}, + {{BAGL_LABELINE , 0x12, 128/2-12/2-20, 48+5, 14, 14, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL}, + {{BAGL_LABELINE , 0x13, 128/2-12/2-10, 48+5, 14, 14, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL}, + {{BAGL_LABELINE , 0x14, 128/2-12/2 , 48+5, 14, 14, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL}, + {{BAGL_LABELINE , 0x15, 128/2-12/2+10, 48+5, 14, 14, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL}, + {{BAGL_LABELINE , 0x16, 128/2-12/2+20, 48+5, 14, 14, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL}, + {{BAGL_LABELINE , 0x17, 128/2-12/2+30, 48+5, 14, 14, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL}, + {{BAGL_LABELINE , 0x18, 128/2-12/2+40, 48+5, 14, 14, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL}, // slider elements - {{BAGL_LABELINE , 0x01, 29, 36, 14, 13, 0, 0, BAGL_FILL, 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_REGULAR_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL, 0, 0, 0, NULL, NULL, NULL }, - {{BAGL_LINE , 0x06, 48, 32, 4, 1, 0, 0, 0 , 0xFFFFFF, 0x000000, 0, 0 }, NULL, 0, 0, 0, NULL, NULL, NULL }, - {{BAGL_RECTANGLE , 0x00, 57, 36-10, 14, 14, 0, 4, BAGL_FILL, 0xFFFFFF, 0x000000, 0, 0}, NULL, 0, 0, 0, NULL, NULL, NULL}, - {{BAGL_LABELINE , 0x02, 58, 36, 12, 13, 0, 0, BAGL_FILL, 0x000000, 0xFFFFFF, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL, 0, 0, 0, NULL, NULL, NULL }, - {{BAGL_LINE , 0x07, 76, 32, 4, 1, 0, 0, 0 , 0xFFFFFF, 0x000000, 0, 0 }, NULL, 0, 0, 0, NULL, NULL, NULL }, - {{BAGL_LABELINE , 0x03, 85, 36, 14, 13, 0, 0, BAGL_FILL, 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_REGULAR_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL, 0, 0, 0, NULL, NULL, NULL }, + {{BAGL_LABELINE , 0x01, 29, 36, 14, 13, 0, 0, BAGL_FILL, 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_REGULAR_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL}, + {{BAGL_LINE , 0x06, 48, 32, 4, 1, 0, 0, 0 , 0xFFFFFF, 0x000000, 0, 0 }, NULL}, + {{BAGL_RECTANGLE , 0x00, 57, 36-10, 14, 14, 0, 4, BAGL_FILL, 0xFFFFFF, 0x000000, 0, 0}, NULL}, + {{BAGL_LABELINE , 0x02, 58, 36, 12, 13, 0, 0, BAGL_FILL, 0x000000, 0xFFFFFF, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL}, + {{BAGL_LINE , 0x07, 76, 32, 4, 1, 0, 0, 0 , 0xFFFFFF, 0x000000, 0, 0 }, NULL}, + {{BAGL_LABELINE , 0x03, 85, 36, 14, 13, 0, 0, BAGL_FILL, 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_REGULAR_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL}, // left/rights icons - {{BAGL_ICON , 0x0A, 2, 28, 4, 7, 0, 0, 0 , 0xFFFFFF, 0x000000, 0, 0 }, &C_icon_left, 0, 0, 0, NULL, NULL, NULL }, - {{BAGL_ICON , 0x0B, 122, 28, 4, 7, 0, 0, 0 , 0xFFFFFF, 0x000000, 0, 0 }, &C_icon_right, 0, 0, 0, NULL, NULL, NULL }, + {{BAGL_ICON , 0x0A, 2, 28, 4, 7, 0, 0, 0 , 0xFFFFFF, 0x000000, 0, 0 }, (const char *) &C_icon_left}, + {{BAGL_ICON , 0x0B, 122, 28, 4, 7, 0, 0, 0 , 0xFFFFFF, 0x000000, 0, 0 }, (const char *) &C_icon_right}, }; const bagl_element_t* screen_common_keyboard_before_element_display_callback(const bagl_element_t* element) { // copy element to be displayed - os_memmove(&G_ux.tmp_element, PIC(element), sizeof(G_ux.tmp_element)); + memcpy(&G_ux.tmp_element, PIC(element), sizeof(G_ux.tmp_element)); switch(element->component.userid) { case 0x01: diff --git a/src/main.c b/src/main.c index 55bad363..ea3d79a8 100644 --- a/src/main.c +++ b/src/main.c @@ -27,7 +27,7 @@ static unsigned int current_text_pos; // parsing cursor in the text to display static unsigned int text_y; // current location of the displayed text // UI currently displayed -extern enum UI_STATE { UI_IDLE, UI_TEXT, UI_APPROVAL }; +enum UI_STATE { UI_IDLE, UI_TEXT, UI_APPROVAL }; extern enum UI_STATE uiState; @@ -38,14 +38,16 @@ extern enum UI_STATE uiState; #include "bolos_ux_nanos.h" #endif -ux_state_t G_ux; -bolos_ux_params_t G_ux_params; - -static unsigned char display_text_part(void); +#ifdef TARGET_NANOX +uint8_t compare_recovery_phrase(void); +#endif -#define MAX_CHARS_PER_LINE 49 +#ifdef TARGET_NANOS +void compare_recovery_phrase(void); +#endif -static char lineBuffer[50]; +ux_state_t G_ux; +bolos_ux_params_t G_ux_params; unsigned short io_exchange_al(unsigned char channel, unsigned short tx_len) { switch (channel & ~(IO_FLAGS)) { @@ -78,42 +80,17 @@ static void sample_main(void) { // next timer callback in 500 ms //UX_CALLBACK_SET_INTERVAL(500); - uint8_t flags; + uint8_t flags = 0; for (;;) { io_exchange(CHANNEL_APDU | flags, 0); flags |= IO_ASYNCH_REPLY; } - -return_to_dashboard: return; } - -// Pick the text elements to display -static unsigned char display_text_part() { - unsigned int i; - WIDE char *text = (char*) G_io_apdu_buffer + 5; - if (text[current_text_pos] == '\0') { - return 0; - } - i = 0; - while ((text[current_text_pos] != 0) && (text[current_text_pos] != '\n') && - (i < MAX_CHARS_PER_LINE)) { - lineBuffer[i++] = text[current_text_pos]; - current_text_pos++; - } - if (text[current_text_pos] == '\n') { - current_text_pos++; - } - lineBuffer[i] = '\0'; - - return 1; -} - - -unsigned char io_event(unsigned char channel) { +unsigned char io_event(unsigned char channel __attribute__((unused))) { // nothing done with the event, throw an error on the transport layer if // needed diff --git a/src/nanos_enter_phrase.c b/src/nanos_enter_phrase.c index b1a6af6b..70089d89 100644 --- a/src/nanos_enter_phrase.c +++ b/src/nanos_enter_phrase.c @@ -16,6 +16,7 @@ ********************************************************************************/ #include "ui.h" +#include "bolos_ux_common.h" #ifdef TARGET_NANOS @@ -268,15 +269,14 @@ void compare_recovery_phrase(void) cx_hmac_sha512_t ctx; const char key[] = "Bitcoin seed"; - cx_hmac_sha512_init(&ctx, key, sizeof(key)); - cx_hmac(&ctx,CX_LAST, buffer, 64, buffer, 64); + cx_hmac_sha512_init(&ctx, (const uint8_t *) key, sizeof(key)); + cx_hmac((cx_hmac_t *) &ctx,CX_LAST, buffer, 64, buffer, 64); //PRINTF("Root key from input:\n%.*H\n", 64, buffer); // get rootkey from device's seed uint8_t buffer_device[64]; - const unsigned int path = ""; - os_perso_derive_node_bip32(CX_CURVE_256K1, path, 0, buffer_device, buffer_device+32); + os_perso_derive_node_bip32(CX_CURVE_256K1, NULL, 0, buffer_device, buffer_device+32); //PRINTF("Root key from device: \n%.*H\n", 64, buffer_device); // compare both rootkey @@ -347,7 +347,7 @@ void screen_onboarding_4_restore_word_validate(void) { G_bolos_ux_context.onboarding_step++; if (G_bolos_ux_context.onboarding_step == G_bolos_ux_context.onboarding_kind) { - unsigned char valid; + unsigned int valid; #ifdef HAVE_ELECTRUM // if we've entered all the words, then check the phrase @@ -455,7 +455,7 @@ void screen_onboarding_4_restore_word_init(unsigned int action) { G_bolos_ux_context.onboarding_step = 0; // flush the words first - os_memset(G_bolos_ux_context.words_buffer, 0, sizeof(G_bolos_ux_context.words_buffer)); + memset(G_bolos_ux_context.words_buffer, 0, sizeof(G_bolos_ux_context.words_buffer)); G_bolos_ux_context.words_buffer_length = 0; break; @@ -464,7 +464,7 @@ void screen_onboarding_4_restore_word_init(unsigned int action) { break; } - os_memset(G_ux.string_buffer, 0, sizeof(G_ux.string_buffer)); + memset(G_ux.string_buffer, 0, sizeof(G_ux.string_buffer)); // offset 0: the display buffer for various placement // offset 16: the entered stem for the current word restoration // offset 32: array of next letters possible after the current word's stem in the dictionary (word completion possibilities) diff --git a/src/nanox_enter_phrase.c b/src/nanox_enter_phrase.c index 739e599d..8ffadc59 100644 --- a/src/nanox_enter_phrase.c +++ b/src/nanox_enter_phrase.c @@ -8,7 +8,7 @@ #include "os_io_seproxyhal.h" #include "string.h" -#include "bolos_ux_common.h" +#include "ui.h" #include "glyphs.h" @@ -18,30 +18,29 @@ const bagl_element_t* screen_onboarding_4_restore_word_before_element_display_ca // show intro const bagl_element_t screen_onboarding_4_restore_word_intro_elements[] = { // erase - {{BAGL_RECTANGLE , 0x00, 0, 0, 128, 64, 0, 0, BAGL_FILL, 0x000000, 0xFFFFFF, 0, 0}, NULL, 0, 0, 0, NULL, NULL, NULL}, + {{BAGL_RECTANGLE , 0x00, 0, 0, 128, 64, 0, 0, BAGL_FILL, 0x000000, 0xFFFFFF, 0, 0}, NULL}, - {{BAGL_LABELINE , 0x31, 0, 12, 128, 32, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_REGULAR_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, "Enter first letters", 0, 0, 0, NULL, NULL, NULL }, - {{BAGL_LABELINE , 0x32, 0, 12, 128, 32, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_REGULAR_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, "Next, enter letters", 0, 0, 0, NULL, NULL, NULL }, - {{BAGL_LABELINE , 0x33, 0, 12, 128, 32, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_REGULAR_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, "Finally, enter letters", 0, 0, 0, NULL, NULL, NULL }, - {{BAGL_LABELINE , 0x30, 0, 26, 128, 32, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_REGULAR_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, G_ux.string_buffer, 0, 0, 0, NULL, NULL, NULL }, + {{BAGL_LABELINE , 0x31, 0, 12, 128, 32, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_REGULAR_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, "Enter first letters"}, + {{BAGL_LABELINE , 0x32, 0, 12, 128, 32, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_REGULAR_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, "Next, enter letters"}, + {{BAGL_LABELINE , 0x33, 0, 12, 128, 32, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_REGULAR_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, "Finally, enter letters"}, + {{BAGL_LABELINE , 0x30, 0, 26, 128, 32, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_REGULAR_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, G_ux.string_buffer}, }; // word selection + word clear const bagl_element_t screen_onboarding_4_restore_word_select_elements[] = { // erase - {{BAGL_RECTANGLE , 0x00, 0, 0, 128, 64, 0, 0, BAGL_FILL, 0x000000, 0xFFFFFF, 0, 0}, NULL, 0, 0, 0, NULL, NULL, NULL}, + {{BAGL_RECTANGLE , 0x00, 0, 0, 128, 64, 0, 0, BAGL_FILL, 0x000000, 0xFFFFFF, 0, 0}, NULL}, - //{{BAGL_LABELINE , 0x25, 0, 18, 128, 32, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_REGULAR_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, G_ux.string_buffer, 0, 0, 0, NULL, NULL, NULL }, - {{BAGL_LABELINE , 0x21, 0, 29, 128, 32, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_REGULAR_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, G_ux.string_buffer, 0, 0, 0, NULL, NULL, NULL }, - {{BAGL_LABELINE , 0x20, 0, 43, 128, 32, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, G_ux.string_buffer, 0, 0, 0, NULL, NULL, NULL }, + {{BAGL_LABELINE , 0x21, 0, 29, 128, 32, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_REGULAR_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, G_ux.string_buffer}, + {{BAGL_LABELINE , 0x20, 0, 43, 128, 32, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, G_ux.string_buffer}, - {{BAGL_ICON , 0x24, (128-14)/2, 17, 14, 14, 0, 0, 0 , 0xFFFFFF, 0x000000, 0, 0 }, &C_icon_clear, 0, 0, 0, NULL, NULL, NULL }, - {{BAGL_LABELINE , 0x24, 0, 43, 128, 32, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, "Clear word", 0, 0, 0, NULL, NULL, NULL }, + {{BAGL_ICON , 0x24, (128-14)/2, 17, 14, 14, 0, 0, 0 , 0xFFFFFF, 0x000000, 0, 0 }, (const char *) &C_icon_clear}, + {{BAGL_LABELINE , 0x24, 0, 43, 128, 32, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, "Clear word"}, // left/rights icons - {{BAGL_ICON , 0x22, 2, 28, 4, 7, 0, 0, 0 , 0xFFFFFF, 0x000000, 0, 0 }, (const char*)&C_icon_left, 0, 0, 0, NULL, NULL, NULL }, - {{BAGL_ICON , 0x23, 122, 28, 4, 7, 0, 0, 0 , 0xFFFFFF, 0x000000, 0, 0 }, (const char*)&C_icon_right, 0, 0, 0, NULL, NULL, NULL }, + {{BAGL_ICON , 0x22, 2, 28, 4, 7, 0, 0, 0 , 0xFFFFFF, 0x000000, 0, 0 }, (const char*)&C_icon_left}, + {{BAGL_ICON , 0x23, 122, 28, 4, 7, 0, 0, 0 , 0xFFFFFF, 0x000000, 0, 0 }, (const char*)&C_icon_right}, }; UX_STEP_NOCB( @@ -327,15 +326,14 @@ uint8_t compare_recovery_phrase(void) cx_hmac_sha512_t ctx; const char key[] = "Bitcoin seed"; - cx_hmac_sha512_init(&ctx, key, sizeof(key)); - cx_hmac(&ctx,CX_LAST, buffer, 64, buffer, 64); + cx_hmac_sha512_init(&ctx, (const uint8_t *) key, sizeof(key)); + cx_hmac((cx_hmac_t *) &ctx,CX_LAST, buffer, 64, buffer, 64); //PRINTF("Root key from input:\n%.*H\n", 64, buffer); // get rootkey from device's seed uint8_t buffer_device[64]; - const unsigned int path = ""; - os_perso_derive_node_bip32(CX_CURVE_256K1, path, 0, buffer_device, buffer_device+32); + os_perso_derive_node_bip32(CX_CURVE_256K1, NULL, 0, buffer_device, buffer_device+32); //PRINTF("Root key from device: \n%.*H\n", 64, buffer_device); // compare both rootkey @@ -446,11 +444,11 @@ void screen_onboarding_4_restore_word_init(unsigned int firstWord) { G_bolos_ux_context.onboarding_step = 0; // flush the words first - os_memset(G_bolos_ux_context.words_buffer, 0, sizeof(G_bolos_ux_context.words_buffer)); + memset(G_bolos_ux_context.words_buffer, 0, sizeof(G_bolos_ux_context.words_buffer)); G_bolos_ux_context.words_buffer_length = 0; } - os_memset(G_ux.string_buffer, 0, sizeof(G_ux.string_buffer)); + memset(G_ux.string_buffer, 0, sizeof(G_ux.string_buffer)); // offset 0: the display buffer for various placement // offset 16: the entered stem for the current word restoration // offset 32: array of next letters possible after the current word's stem in the dictionary (word completion possibilities) diff --git a/src_ux_common/bolos_ux_common_bip39.h b/src_ux_common/bolos_ux_common_bip39.h index 72520891..425a9e8b 100755 --- a/src_ux_common/bolos_ux_common_bip39.h +++ b/src_ux_common/bolos_ux_common_bip39.h @@ -30,7 +30,6 @@ unsigned int bolos_ux_bip39_get_word_next_letters_starting_with(unsigned char* p unsigned int bolos_ux_electrum_new_mnemonic(unsigned int version, unsigned char *out, unsigned int outLength); unsigned int bolos_ux_electrum_mnemonic_check(unsigned int version, unsigned char *mnemonic, unsigned int mnemonicLength); -void bolos_ux_electrum_mnemonic_to_seed(unsigned char *mnemonic, unsigned int mnemonicLength, unsigned char *seed); #endif diff --git a/src_ux_common/bolos_ux_onboarding_electrum.c b/src_ux_common/bolos_ux_onboarding_electrum.c index 5ed0ab25..1f70f56d 100755 --- a/src_ux_common/bolos_ux_onboarding_electrum.c +++ b/src_ux_common/bolos_ux_onboarding_electrum.c @@ -7,8 +7,6 @@ #include "bolos_ux_common.h" -extern unsigned int bolos_ux_mnemonic_to_seed_hash_length128(unsigned char *mnemonic, unsigned int mnemonicLength); - int cx_math_shiftr_11(unsigned char *r, unsigned int len) { unsigned int j,b11; b11 = r[len-1] | ((r[len-2]&7)<<8); @@ -22,24 +20,11 @@ int cx_math_shiftr_11(unsigned char *r, unsigned int len) { return b11; } -#if 0 -void cx_math_pushr_11(unsigned char *r, unsigned int len, unsigned int b11) { - unsigned int j; - - for (j = 0; j>5); - } - r[len-2] = (r[len-1]<<3) | (b11>>8); - r[len-1] = b11 & 0xFF; - -} -#endif - -unsigned int bolos_ux_electrum_mnemonic_encode(unsigned char *seed17, unsigned char *out, unsigned int outLength) { +static unsigned int bolos_ux_electrum_mnemonic_encode(const uint8_t *seed17, uint8_t *out, size_t outLength) { unsigned char tmp[17]; unsigned int i; unsigned int offset = 0; - os_memmove(tmp, seed17, sizeof(tmp)); + memcpy(tmp, seed17, sizeof(tmp)); for (i=0; i<12; i++) { unsigned char wordLength; unsigned int idx = cx_math_shiftr_11(tmp, sizeof(tmp)); @@ -47,7 +32,7 @@ unsigned int bolos_ux_electrum_mnemonic_encode(unsigned char *seed17, unsigned c if ((offset + wordLength) > outLength) { THROW (INVALID_PARAMETER); } - os_memmove(out + offset, BIP39_WORDLIST + BIP39_WORDLIST_OFFSETS[idx], wordLength); + memcpy(out + offset, BIP39_WORDLIST + BIP39_WORDLIST_OFFSETS[idx], wordLength); offset += wordLength; if (i < 11) { if (offset > outLength) { @@ -92,13 +77,4 @@ unsigned int bolos_ux_electrum_mnemonic_check(unsigned int version, unsigned cha return (tmp[0] == version); } -void bolos_ux_electrum_mnemonic_to_seed(unsigned char *mnemonic, unsigned int mnemonicLength, unsigned char *seed) { - unsigned char passphrase[ELECTRUM_MNEMONIC_LENGTH + 4]; - mnemonicLength = bolos_ux_mnemonic_to_seed_hash_length128(mnemonic, mnemonicLength); - - os_memmove(passphrase, ELECTRUM_MNEMONIC, ELECTRUM_MNEMONIC_LENGTH); - cx_pbkdf2_sha512(mnemonic, mnemonicLength, passphrase, ELECTRUM_MNEMONIC_LENGTH + 4/*for round index, set in pbkdf2*/, ELECTRUM_PBKDF2_ROUNDS, seed, 64); -} - - #endif diff --git a/src_ux_common/bolos_ux_onboarding_seed_bip39.c b/src_ux_common/bolos_ux_onboarding_seed_bip39.c index cba837aa..3c0aacf3 100755 --- a/src_ux_common/bolos_ux_onboarding_seed_bip39.c +++ b/src_ux_common/bolos_ux_onboarding_seed_bip39.c @@ -16,10 +16,10 @@ unsigned int bolos_ux_mnemonic_from_data(unsigned char *in, unsigned int inLengt cx_hash_sha256(in, inLength, bits, 32); bits[inLength] = bits[0]; - os_memmove(bits, in, inLength); + memcpy(bits, in, inLength); offset = 0; for (i = 0; i < mlen; i++) { - unsigned char wordLength; + size_t wordLength; idx = 0; for (j = 0; j < 11; j++) { idx <<= 1; @@ -29,7 +29,7 @@ unsigned int bolos_ux_mnemonic_from_data(unsigned char *in, unsigned int inLengt if ((offset + wordLength) > outLength) { THROW (INVALID_PARAMETER); } - os_memmove(out + offset, BIP39_WORDLIST + BIP39_WORDLIST_OFFSETS[idx], wordLength); + memcpy(out + offset, BIP39_WORDLIST + BIP39_WORDLIST_OFFSETS[idx], wordLength); offset += wordLength; if (i < mlen - 1) { if (offset > outLength) { @@ -41,25 +41,6 @@ unsigned int bolos_ux_mnemonic_from_data(unsigned char *in, unsigned int inLengt return offset; } -#if 0 -unsigned int bolos_ux_mnemonic_indexes_to_words(unsigned char *indexes, unsigned char *words) { - unsigned char i; - unsigned int offset = 0; - for (i=0; i<24; i++) { - unsigned char wordLength; - unsigned int idx = ((*indexes) << 8) | (*(indexes + 1)); - wordLength = BIP39_WORDLIST_OFFSETS[idx + 1] - BIP39_WORDLIST_OFFSETS[idx]; - os_memmove(words + offset, BIP39_WORDLIST + BIP39_WORDLIST_OFFSETS[idx], wordLength); - offset += wordLength; - if (i != 23) { - words[offset++] = ' '; - } - indexes += 2; - } - return offset; -} -#endif - // separated function to lower the stack usage when jumping into pbkdf algorithm unsigned int bolos_ux_mnemonic_to_seed_hash_length128(unsigned char *mnemonic, unsigned int mnemonicLength) { if (mnemonicLength > 128) { @@ -74,7 +55,7 @@ void bolos_ux_mnemonic_to_seed(unsigned char *mnemonic, unsigned int mnemonicLen unsigned char passphrase[BIP39_MNEMONIC_LENGTH + 4]; mnemonicLength = bolos_ux_mnemonic_to_seed_hash_length128(mnemonic, mnemonicLength); - os_memmove(passphrase, BIP39_MNEMONIC, BIP39_MNEMONIC_LENGTH); + memcpy(passphrase, BIP39_MNEMONIC, BIP39_MNEMONIC_LENGTH); cx_pbkdf2_sha512(mnemonic, mnemonicLength, passphrase, BIP39_MNEMONIC_LENGTH, BIP39_PBKDF2_ROUNDS, seed, 64); // what happen to the second block for a very short seed ? @@ -104,19 +85,6 @@ unsigned int bolos_ux_get_word_ptr(unsigned char ** word, unsigned int max_lengt return word_length; } -#if 0 -unsigned char bolos_ux_word_check(unsigned char *word, unsigned int wordLength) { - unsigned int i; - for (i=0; i Date: Mon, 3 Oct 2022 11:50:16 +0200 Subject: [PATCH 02/65] [ci][add] Basic CI checks --- .clang-format | 20 +++++++++ .github/workflows/ci-workflow.yml | 60 +++++++++++++++++++++++++++ .github/workflows/codeql-workflow.yml | 42 +++++++++++++++++++ .github/workflows/lint-workflow.yml | 41 ++++++++++++++++++ .gitignore | 32 ++++++++++++++ 5 files changed, 195 insertions(+) create mode 100644 .clang-format create mode 100644 .github/workflows/ci-workflow.yml create mode 100644 .github/workflows/codeql-workflow.yml create mode 100644 .github/workflows/lint-workflow.yml create mode 100644 .gitignore diff --git a/.clang-format b/.clang-format new file mode 100644 index 00000000..b4abfa06 --- /dev/null +++ b/.clang-format @@ -0,0 +1,20 @@ +--- +BasedOnStyle: Google +IndentWidth: 4 +--- +Language: Cpp +ColumnLimit: 100 +PointerAlignment: Right +AlignAfterOpenBracket: Align +AlignConsecutiveMacros: true +AllowAllParametersOfDeclarationOnNextLine: false +SortIncludes: false +SpaceAfterCStyleCast: true +AllowShortCaseLabelsOnASingleLine: false +AllowAllArgumentsOnNextLine: false +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortBlocksOnASingleLine: Never +AllowShortFunctionsOnASingleLine: None +BinPackArguments: false +BinPackParameters: false +--- diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml new file mode 100644 index 00000000..fbcc72e2 --- /dev/null +++ b/.github/workflows/ci-workflow.yml @@ -0,0 +1,60 @@ +name: Compilation & tests + +on: + workflow_dispatch: + push: + branches: + - master + - develop + pull_request: + +jobs: + nano_build: + name: Build application for NanoS, X and S+ + strategy: + matrix: + include: + - SDK: "$NANOS_SDK" + model: nanos + - SDK: "$NANOX_SDK" + model: nanox + - SDK: "$NANOSP_SDK" + model: nanosp + runs-on: ubuntu-latest + container: + image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder:latest + steps: + - name: Clone + uses: actions/checkout@v2 + + - name: Build + run: | + git config --global --add safe.directory "$GITHUB_WORKSPACE" + make BOLOS_SDK=${{ matrix.SDK }} + mv bin/app.elf "bin/${{ matrix.model }}.elf" + - name: Upload app binary + uses: actions/upload-artifact@v2 + with: + name: apps + path: bin/*.elf + + job_scan_build: + name: Clang Static Analyzer + runs-on: ubuntu-latest + container: + image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder:latest + steps: + - name: Clone + uses: actions/checkout@v2 + + - name: Build with Clang Static Analyzer + run: | + make clean + scan-build --use-cc=clang -analyze-headers -enable-checker security -enable-checker unix -enable-checker valist -o scan-build --status-bugs make default + + - name: Upload scan result + uses: actions/upload-artifact@v2 + if: failure() + with: + name: scan-build + path: scan-build diff --git a/.github/workflows/codeql-workflow.yml b/.github/workflows/codeql-workflow.yml new file mode 100644 index 00000000..197a7cd3 --- /dev/null +++ b/.github/workflows/codeql-workflow.yml @@ -0,0 +1,42 @@ +name: "CodeQL" + +on: + push: + branches: + - master + - develop + pull_request: + # Excluded path: add the paths you want to ignore instead of deleting the workflow + paths-ignore: + - '.github/workflows/*.yml' + - 'tests/*' + +jobs: + analyse: + name: Analyse + strategy: + matrix: + sdk: [ "$NANOS_SDK", "$NANOX_SDK", "$NANOSP_SDK" ] + #'cpp' covers C and C++ + language: [ 'cpp' ] + runs-on: ubuntu-latest + container: + image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder:latest + + steps: + - name: Clone + uses: actions/checkout@v3 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + queries: security-and-quality + + # CodeQL will create the database during the compilation + - name: Build + run: | + make BOLOS_SDK=${{ matrix.sdk }} + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/lint-workflow.yml b/.github/workflows/lint-workflow.yml new file mode 100644 index 00000000..d04a315d --- /dev/null +++ b/.github/workflows/lint-workflow.yml @@ -0,0 +1,41 @@ +name: Code style check + +on: + workflow_dispatch: + push: + branches: + - master + pull_request: + branches: + - master + - develop + +jobs: + job_lint: + name: Lint + runs-on: ubuntu-latest + + steps: + - name: Clone + uses: actions/checkout@v2 + + - name: Lint + uses: DoozyX/clang-format-lint-action@v0.11 + with: + source: './src' + extensions: 'h,c' + clangFormatVersion: 11 + + misspell: + name: Check misspellings + runs-on: ubuntu-latest + steps: + - name: Clone + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Check misspellings + uses: codespell-project/actions-codespell@2391250ab05295bddd51e36a8c6295edb6343b0e + with: + builtin: clear,rare + check_filenames: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..d50062f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +# Compilation of Ledger's app +src/glyphs.c +src/glyphs.h +bin/ +debug/ +dep/ +obj/ +elfs/ + +# Unit tests and code coverage +unit-tests/build/ +unit-tests/coverage/ +unit-tests/coverage.info + +# Fuzzing +fuzzing/build/ + + +# Editors +.idea/ + +# Python +*.pyc[cod] +*.egg +__pycache__/ +*.egg-info/ +.eggs/ +.python-version + +# Doxygen +doc/html +doc/latex From 8b12dace2e61bb2bb64da3ed59e943b1a9b5edc7 Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Mon, 3 Oct 2022 16:10:06 +0200 Subject: [PATCH 03/65] [fix] Enabling NanoS+ compilation --- Makefile | 66 +++++++++++++++++++---------------- src/bolos_ux_common.h | 2 +- src/bolos_ux_nanox.c | 39 ++++++++++----------- src/bolos_ux_nanox_keyboard.c | 2 +- src/main.c | 2 +- src/nanox_enter_phrase.c | 24 ++++++------- src/ui.c | 14 ++++---- 7 files changed, 76 insertions(+), 73 deletions(-) diff --git a/Makefile b/Makefile index 3bbf43f4..5bf5d84b 100755 --- a/Makefile +++ b/Makefile @@ -25,76 +25,80 @@ all: default # Main app configuration APPNAME = "Recovery Check" -APPVERSION = 1.0.7 +APPVERSION_M = 1 +APPVERSION_N = 1 +APPVERSION_P = 0 +APPVERSION = "$(APPVERSION_M).$(APPVERSION_N).$(APPVERSION_P)" + APP_LOAD_PARAMS = --appFlags 0x210 $(COMMON_LOAD_PARAMS) --apdu --curve secp256k1 --path "" ifeq ($(TARGET_NAME),TARGET_NANOS) - ICONNAME=nanos_app_recovery_check.gif + ICONNAME=nanos_app_recovery_check.gif else - ICONNAME=nanox_app_recovery_check.gif + ICONNAME=nanox_app_recovery_check.gif endif # Build configuration +DEFINES += APPNAME=\"$(APPNAME)\" DEFINES += APPVERSION=\"$(APPVERSION)\" - +DEFINES += LEDGER_MAJOR_VERSION=$(APPVERSION_M) +DEFINES += LEDGER_MINOR_VERSION=$(APPVERSION_N) +DEFINES += LEDGER_PATCH_VERSION=$(APPVERSION_P) DEFINES += OS_IO_SEPROXYHAL IO_SEPROXYHAL_BUFFER_SIZE_B=128 +DEFINES += UNUSED\(x\)=\(void\)x DEFINES += HAVE_BAGL HAVE_SPRINTF DEFINES += BOLOS_APP_ICON_SIZE_B=\(9+32\) #DEFINES += HAVE_ELECTRUM DEFINES += IO_USB_MAX_ENDPOINTS=4 IO_HID_EP_LENGTH=64 -DEFINES += HAVE_UX_FLOW +DEFINES += HAVE_UX_FLOW ifneq ($(TARGET_NAME),TARGET_NANOS) -DEFINES += HAVE_GLO096 HAVE_BOLOS_UX -DEFINES += HAVE_BAGL BAGL_WIDTH=128 BAGL_HEIGHT=64 -DEFINES += HAVE_BAGL_ELLIPSIS # long label truncation feature -DEFINES += HAVE_BAGL_FONT_OPEN_SANS_REGULAR_11PX -DEFINES += HAVE_BAGL_FONT_OPEN_SANS_EXTRABOLD_11PX -DEFINES += HAVE_BAGL_FONT_OPEN_SANS_LIGHT_16PX -DEFINES += HAVE_KEYBOARD_UX + DEFINES += HAVE_GLO096 HAVE_BOLOS_UX + DEFINES += HAVE_BAGL BAGL_WIDTH=128 BAGL_HEIGHT=64 + DEFINES += HAVE_BAGL_ELLIPSIS # long label truncation feature + DEFINES += HAVE_BAGL_FONT_OPEN_SANS_REGULAR_11PX + DEFINES += HAVE_BAGL_FONT_OPEN_SANS_EXTRABOLD_11PX + DEFINES += HAVE_BAGL_FONT_OPEN_SANS_LIGHT_16PX + DEFINES += HAVE_KEYBOARD_UX endif -# Enabling debug PRINTF + DEBUG = 0 ifneq ($(DEBUG),0) - - DEFINES += HAVE_BOLOS_NOT_SHUFFLED_RESTORE - - ifeq ($(TARGET_NAME),TARGET_NANOS) - DEFINES += HAVE_PRINTF PRINTF=screen_printf - else - DEFINES += HAVE_PRINTF PRINTF=mcu_usb_printf - endif + DEFINES += HAVE_PRINTF + ifeq ($(TARGET_NAME),TARGET_NANOS) + DEFINES += PRINTF=screen_printf + else + DEFINES += PRINTF=mcu_usb_printf + endif else - DEFINES += PRINTF\(...\)= + DEFINES += PRINTF\(...\)= endif ############## # Compiler # ############## ifneq ($(BOLOS_ENV),) -$(info BOLOS_ENV=$(BOLOS_ENV)) -CLANGPATH := $(BOLOS_ENV)/clang-arm-fropi/bin/ -GCCPATH := $(BOLOS_ENV)/gcc-arm-none-eabi-5_3-2016q1/bin/ + $(info BOLOS_ENV=$(BOLOS_ENV)) + CLANGPATH := $(BOLOS_ENV)/clang-arm-fropi/bin/ + GCCPATH := $(BOLOS_ENV)/gcc-arm-none-eabi-5_3-2016q1/bin/ else -$(info BOLOS_ENV is not set: falling back to CLANGPATH and GCCPATH) + $(info BOLOS_ENV is not set: falling back to CLANGPATH and GCCPATH) endif ifeq ($(CLANGPATH),) -$(info CLANGPATH is not set: clang will be used from PATH) + $(info CLANGPATH is not set: clang will be used from PATH) endif ifeq ($(GCCPATH),) -$(info GCCPATH is not set: arm-none-eabi-* will be used from PATH) + $(info GCCPATH is not set: arm-none-eabi-* will be used from PATH) endif CC := $(CLANGPATH)clang CFLAGS += -O3 -Os -Wshadow -Wformat - AS := $(GCCPATH)arm-none-eabi-gcc AFLAGS += - LD := $(GCCPATH)arm-none-eabi-gcc LDFLAGS += -O3 -Os LDLIBS += -lm -lgcc -lc @@ -105,7 +109,7 @@ include $(BOLOS_SDK)/Makefile.glyphs APP_SOURCE_PATH += src src_ux_common ifneq ($(TARGET_NAME),TARGET_NANOS) -SDK_SOURCE_PATH += lib_ux + SDK_SOURCE_PATH += lib_ux endif # Main rules diff --git a/src/bolos_ux_common.h b/src/bolos_ux_common.h index 41cfd110..7df31398 100644 --- a/src/bolos_ux_common.h +++ b/src/bolos_ux_common.h @@ -162,6 +162,6 @@ void screen_boot_delay_init(void); void settings_general_version(appmain_t end_callback); void settings_general_regulatory(appmain_t end_callback); void settings_general_serial(appmain_t end_callback); -#endif //TARGET_NANOX +#endif //TARGET_NANOX || TARGET_NANOS2 #endif // OS_IO_SEPROXYHAL diff --git a/src/bolos_ux_nanox.c b/src/bolos_ux_nanox.c index 5003aeb2..cc9562d1 100644 --- a/src/bolos_ux_nanox.c +++ b/src/bolos_ux_nanox.c @@ -2,13 +2,13 @@ #if defined(TARGET_NANOX) || defined(TARGET_NANOS2) -#include "cx.h" -#include "ux.h" +#include +#include -#include "os_io_seproxyhal.h" -#include "string.h" +#include +#include -#include "bolos_ux_common.h" +#include #include "glyphs.h" #ifdef OS_IO_SEPROXYHAL @@ -25,11 +25,11 @@ unsigned short io_timeout(unsigned short last_timeout) { void screen_hex_identifier_string_buffer (const unsigned char * buffer, unsigned int total) { - SPRINTF(G_ux.string_buffer, - "%.*H...%.*H", - BOLOS_UX_HASH_LENGTH/2, - buffer, - BOLOS_UX_HASH_LENGTH/2, + SPRINTF(G_ux.string_buffer, + "%.*H...%.*H", + BOLOS_UX_HASH_LENGTH/2, + buffer, + BOLOS_UX_HASH_LENGTH/2, buffer +total -BOLOS_UX_HASH_LENGTH/2); @@ -58,7 +58,7 @@ void screen_keyboard_validate_entered_text(void) { void bolos_ux_hslider3_init(unsigned int total_count) { - G_bolos_ux_context.hslider3_total = total_count; + G_bolos_ux_context.hslider3_total = total_count; switch (total_count) { case 0: G_bolos_ux_context.hslider3_before = BOLOS_UX_HSLIDER3_NONE; @@ -103,14 +103,14 @@ void bolos_ux_hslider3_next(void) { case 2: switch(G_bolos_ux_context.hslider3_current) { case 0: - G_bolos_ux_context.hslider3_before = 0; + G_bolos_ux_context.hslider3_before = 0; G_bolos_ux_context.hslider3_current = 1; - G_bolos_ux_context.hslider3_after = BOLOS_UX_HSLIDER3_NONE; + G_bolos_ux_context.hslider3_after = BOLOS_UX_HSLIDER3_NONE; break; case 1: G_bolos_ux_context.hslider3_before = BOLOS_UX_HSLIDER3_NONE; G_bolos_ux_context.hslider3_current = 0; - G_bolos_ux_context.hslider3_after = 1; + G_bolos_ux_context.hslider3_after = 1; break; } break; @@ -130,14 +130,14 @@ void bolos_ux_hslider3_previous(void) { case 2: switch(G_bolos_ux_context.hslider3_current) { case 0: - G_bolos_ux_context.hslider3_before = 0; + G_bolos_ux_context.hslider3_before = 0; G_bolos_ux_context.hslider3_current = 1; - G_bolos_ux_context.hslider3_after = BOLOS_UX_HSLIDER3_NONE; + G_bolos_ux_context.hslider3_after = BOLOS_UX_HSLIDER3_NONE; break; case 1: G_bolos_ux_context.hslider3_before = BOLOS_UX_HSLIDER3_NONE; G_bolos_ux_context.hslider3_current = 0; - G_bolos_ux_context.hslider3_after = 1; + G_bolos_ux_context.hslider3_after = 1; break; } break; @@ -175,14 +175,14 @@ unsigned int screen_consent_ticker(unsigned int ignored) { void screen_consent_set_interval(unsigned int interval_ms) { G_ux.stack[0].ticker_value = interval_ms; - G_ux.stack[0].ticker_interval = interval_ms; + G_ux.stack[0].ticker_interval = interval_ms; } void screen_consent_ticker_init(unsigned int number_of_steps, unsigned int interval_ms, unsigned int check_pin_to_confirm) { UNUSED(check_pin_to_confirm); // register action callbacks G_ux.stack[0].ticker_value = interval_ms; - G_ux.stack[0].ticker_interval = interval_ms; + G_ux.stack[0].ticker_interval = interval_ms; G_ux.stack[0].ticker_callback = screen_consent_ticker; /*if (!check_pin_to_confirm || ! os_perso_isonboarded() == BOLOS_UX_OK) { managed at bolos task level */ G_ux.stack[0].button_push_callback = screen_consent_button; @@ -200,4 +200,3 @@ void screen_consent_ticker_init(unsigned int number_of_steps, unsigned int inter #endif // OS_IO_SEPROXYHAL #endif - diff --git a/src/bolos_ux_nanox_keyboard.c b/src/bolos_ux_nanox_keyboard.c index c394dd54..0184881c 100644 --- a/src/bolos_ux_nanox_keyboard.c +++ b/src/bolos_ux_nanox_keyboard.c @@ -150,4 +150,4 @@ void screen_common_keyboard_init(unsigned int stack_slot, unsigned int current_e #endif -#endif \ No newline at end of file +#endif diff --git a/src/main.c b/src/main.c index ea3d79a8..2a3c1ecf 100644 --- a/src/main.c +++ b/src/main.c @@ -38,7 +38,7 @@ extern enum UI_STATE uiState; #include "bolos_ux_nanos.h" #endif -#ifdef TARGET_NANOX +#if defined(TARGET_NANOX) || defined(TARGET_NANOS2) uint8_t compare_recovery_phrase(void); #endif diff --git a/src/nanox_enter_phrase.c b/src/nanox_enter_phrase.c index 8ffadc59..307f6304 100644 --- a/src/nanox_enter_phrase.c +++ b/src/nanox_enter_phrase.c @@ -114,8 +114,8 @@ unsigned int screen_onboarding_4_restore_word_select_button(unsigned int button_ const bagl_element_t* screen_onboarding_4_restore_word_keyboard_callback(unsigned int event, unsigned int value); void screen_onboarding_4_restore_word_display_auto_complete(void) { - unsigned int auto_complete_count = bolos_ux_bip39_get_word_next_letters_starting_with((unsigned char*)G_ux.string_buffer+16, - strlen(G_ux.string_buffer+16), + unsigned int auto_complete_count = bolos_ux_bip39_get_word_next_letters_starting_with((unsigned char*)G_ux.string_buffer+16, + strlen(G_ux.string_buffer+16), (unsigned char*)G_ux.string_buffer+32); // prepare title of the common keyboard component, after the list of possible letters snprintf(G_ux.string_buffer+32+auto_complete_count+1, sizeof(G_ux.string_buffer)-32-auto_complete_count-1, "Enter word #%d", G_bolos_ux_context.onboarding_step + 1); @@ -123,18 +123,18 @@ void screen_onboarding_4_restore_word_display_auto_complete(void) { G_ux.string_buffer[32+auto_complete_count]= '\b'; G_bolos_ux_context.common_label = G_ux.string_buffer+32+auto_complete_count+1; // display added letter and refresh slider - screen_common_keyboard_init(0, + screen_common_keyboard_init(0, #ifdef HAVE_BOLOS_NOT_SHUFFLED_RESTORE - 0, /*always restart from the first element in the list*/ + 0, /*always restart from the first element in the list*/ #else // HAVE_BOLOS_NOT_SHUFFLED_RESTORE (strlen(G_ux.string_buffer+16)?0:cx_rng_u8()%auto_complete_count), /* start from a random element in the list for the word start letter, else keep the order */ #endif // HAVE_BOLOS_NOT_SHUFFLED_RESTORE // recompute alphabet and set the number of elements in the keyboard auto_complete_count - + (strlen(G_ux.string_buffer+16)?1:0) /* backspace if a stem is already entered, else no backspace */, + + (strlen(G_ux.string_buffer+16)?1:0) /* backspace if a stem is already entered, else no backspace */, screen_onboarding_4_restore_word_keyboard_callback); // append the special backspace to allow for easier dispatch in the keyboard callback - ((unsigned char*)(G_ux.string_buffer+32))[auto_complete_count] = '\b'; + ((unsigned char*)(G_ux.string_buffer+32))[auto_complete_count] = '\b'; } void screen_onboarding_4_restore_word_display_word_selection(void) { @@ -144,7 +144,7 @@ void screen_onboarding_4_restore_word_display_word_selection(void) { G_ux.stack[0].element_arrays[0].element_array_count = ARRAYLEN(screen_onboarding_4_restore_word_select_elements); G_ux.stack[0].element_arrays_count = 1; G_ux.stack[0].screen_before_element_display_callback = screen_onboarding_4_restore_word_before_element_display_callback; - ux_stack_display(0); + ux_stack_display(0); } const bagl_element_t* screen_onboarding_4_restore_word_keyboard_callback(unsigned int event, unsigned int value) { @@ -317,7 +317,7 @@ uint8_t compare_recovery_phrase(void) // convert mnemonic to hex-seed uint8_t buffer[64]; - bolos_ux_mnemonic_to_seed((unsigned char *)G_bolos_ux_context.words_buffer, + bolos_ux_mnemonic_to_seed((unsigned char *)G_bolos_ux_context.words_buffer, G_bolos_ux_context.words_buffer_length, buffer); //PRINTF("Input seed:\n %.*H\n", 64, buffer); @@ -349,7 +349,7 @@ void screen_onboarding_4_restore_word_validate(void) { if (G_bolos_ux_context.onboarding_step == G_bolos_ux_context.onboarding_kind) { unsigned char valid; -#ifdef HAVE_ELECTRUM +#ifdef HAVE_ELECTRUM // if we've entered all the words, then check the phrase if (G_bolos_ux_context.onboarding_algorithm == BOLOS_UX_ONBOARDING_ALGORITHM_ELECTRUM) { valid = bolos_ux_electrum_mnemonic_check(ELECTRUM_SEED_PREFIX_STANDARD, (unsigned char*)G_bolos_ux_context.words_buffer, G_bolos_ux_context.words_buffer_length); @@ -359,7 +359,7 @@ void screen_onboarding_4_restore_word_validate(void) { } #else valid = bolos_ux_mnemonic_check((unsigned char*)G_bolos_ux_context.words_buffer, G_bolos_ux_context.words_buffer_length); -#endif +#endif if ( !valid ) { // invalid recovery phrase ux_flow_init(0, ux_wrong_seed_flow, NULL); @@ -367,7 +367,7 @@ void screen_onboarding_4_restore_word_validate(void) { else { // allright, the recovery phrase looks ok, compare it to onboarded seed - //Display loading icon to user + //Display loading icon to user ux_flow_init(0, ux_load_flow, NULL); if(compare_recovery_phrase()){ ux_flow_init(0, ux_succesfull_check_flow, NULL); @@ -468,4 +468,4 @@ void screen_onboarding_4_restore_word_init(unsigned int firstWord) { } -#endif \ No newline at end of file +#endif diff --git a/src/ui.c b/src/ui.c index 422c2638..8d6c93c9 100644 --- a/src/ui.c +++ b/src/ui.c @@ -7,7 +7,7 @@ enum UI_STATE uiState; #if defined(TARGET_NANOS) UX_STEP_VALID( - ux_idle_flow_1_step, + ux_idle_flow_1_step, pbb, screen_onboarding_3_restore_init();, { @@ -16,8 +16,8 @@ UX_STEP_VALID( "recovery phrase", }); UX_STEP_NOCB( - ux_idle_flow_3_step, - bn, + ux_idle_flow_3_step, + bn, { "Version", APPVERSION, @@ -76,7 +76,7 @@ void number_of_words_selector(unsigned int idx) { ////////////////////////////////////////////////////////////////////// UX_STEP_VALID( - ux_instruction_step, + ux_instruction_step, nnn, ux_menulist_init(0, number_of_words_getter, number_of_words_selector), { @@ -92,7 +92,7 @@ UX_FLOW(ux_instruction_flow, ////////////////////////////////////////////////////////////////////// UX_STEP_VALID( - ux_idle_flow_1_step, + ux_idle_flow_1_step, pbb, ux_flow_init(0, ux_instruction_flow, NULL), { @@ -101,8 +101,8 @@ UX_STEP_VALID( "recovery phrase", }); UX_STEP_NOCB( - ux_idle_flow_3_step, - bn, + ux_idle_flow_3_step, + bn, { "Version", APPVERSION, From c11f4381bea40085fd65bcd700e81ca6eaa2209e Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Mon, 3 Oct 2022 17:21:02 +0200 Subject: [PATCH 04/65] [clean] Lint --- src/bolos_ux_common.h | 51 +- src/bolos_ux_nanos.c | 310 +++++++----- src/bolos_ux_nanos.h | 146 +++--- src/bolos_ux_nanos_keyboard.c | 412 +++++++++++----- src/bolos_ux_nanox.c | 277 ++++++----- src/bolos_ux_nanox.h | 307 ++++++------ src/bolos_ux_nanox_keyboard.c | 427 +++++++++++----- src/main.c | 122 +++-- src/nanos_enter_phrase.c | 733 ++++++++++++++------------- src/nanos_pick_phrase_length.c | 90 ++-- src/nanox_enter_phrase.c | 870 ++++++++++++++++++--------------- src/ui.c | 182 +++---- src/ui.h | 1 - 13 files changed, 2211 insertions(+), 1717 deletions(-) diff --git a/src/bolos_ux_common.h b/src/bolos_ux_common.h index 7df31398..e6d62dfd 100644 --- a/src/bolos_ux_common.h +++ b/src/bolos_ux_common.h @@ -6,12 +6,11 @@ #include "string.h" #if defined(TARGET_NANOS) - #include "bolos_ux_nanos.h" +#include "bolos_ux_nanos.h" #elif defined(TARGET_NANOX) || defined(TARGET_NANOS2) - #include "bolos_ux_nanox.h" +#include "bolos_ux_nanox.h" #endif - #include "glyphs.h" #ifdef OS_IO_SEPROXYHAL @@ -20,17 +19,17 @@ //#define COLOR_BG_1 0xF80000 //#define ALWAYS_INVERT -#define KEYCODE_SWITCH '\1' +#define KEYCODE_SWITCH '\1' #define KEYCODE_BACKSPACE '\r' #ifndef SPRINTF // avoid typing the size each time -#define SPRINTF(strbuf, ...) snprintf((char*)(strbuf), sizeof(strbuf), __VA_ARGS__) +#define SPRINTF(strbuf, ...) snprintf((char*) (strbuf), sizeof(strbuf), __VA_ARGS__) #endif -#define ONBOARDING_CONFIRM_WORD_COUNT 24 +#define ONBOARDING_CONFIRM_WORD_COUNT 24 #define ONBOARDING_WORD_COMPLETION_MAX_ITEMS 8 -#define BOLOS_UX_HASH_LENGTH 4 // as on the blue +#define BOLOS_UX_HASH_LENGTH 4 // as on the blue #define CONSENT_INTERVAL_MS 3000 @@ -40,7 +39,7 @@ extern const unsigned char hex_digits[]; unsigned char rng_u8_modulo(unsigned char modulo); -void screen_hex_identifier_string_buffer (const unsigned char * buffer, unsigned int total); +void screen_hex_identifier_string_buffer(const unsigned char* buffer, unsigned int total); // common code for all screens // reset the screen asynch display machine @@ -51,7 +50,7 @@ void screen_state_init(unsigned int stack_slot); void screen_display_init(unsigned int stack_slot); // request display of the element (taking care of calling screen displayed preprocessors) -void screen_display_element(const bagl_element_t * element); +void screen_display_element(const bagl_element_t* element); // prepare to return the exit code after the next display asynch ack is received (0d 00 00) void screen_return_after_displayed_touched_element(unsigned int exit_code); @@ -92,7 +91,7 @@ void screen_onboarding_4_confirm_init(unsigned int feilword); void screen_onboarding_3_restore_init(void); #define RESTORE_WORD_ACTION_REENTER_WORD 0 -#define RESTORE_WORD_ACTION_FIRST_WORD 1 +#define RESTORE_WORD_ACTION_FIRST_WORD 1 void screen_onboarding_4_restore_word_init(unsigned int action); void screen_onboarding_5_passphrase_init(void); @@ -113,13 +112,16 @@ void screen_settings_passphrase_temporary_1_init(void); void screen_settings_passphrase_type_and_review_init(unsigned int kind); void screen_settings_erase_all_init(void); -void screen_common_keyboard_init(unsigned int stack_slot, unsigned int current_element, unsigned int nb_elements, keyboard_callback_t callback); +void screen_common_keyboard_init(unsigned int stack_slot, + unsigned int current_element, + unsigned int nb_elements, + keyboard_callback_t callback); #define COMMON_KEYBOARD_INDEX_UNCHANGED (-1UL) #if defined(TARGET_NANOX) || defined(TARGET_NANOS2) // to be included into all flow that needs to go back to the dashboard -extern const ux_flow_step_t ux_ob_goto_dashboard_step; +extern const ux_flow_step_t ux_ob_goto_dashboard_step; void ux_pairing_init(void); void ux_pairing_deinit(void); @@ -134,19 +136,30 @@ void screen_processing_init(void); void screen_prepare_app_name(unsigned int appidx); -bagl_icon_details_t* screen_prepare_bitmap_14x14(ux_icon_buffer_t* icon_buffer, const unsigned char* icon_bitmap, unsigned int masked_inverted); -void screen_prepare_app_icon_14x14(ux_icon_buffer_t* icon_buffer, const unsigned char* icon_bitmap, unsigned int icon_bitmap_length, unsigned int masked_inverted); -void screen_prepare_app_icon(ux_icon_buffer_t* icon_buffer, unsigned char* icon_bitmap_buffer, unsigned int icon_bitmap_buffer_length, unsigned int appidx, unsigned int inverted); +bagl_icon_details_t* screen_prepare_bitmap_14x14(ux_icon_buffer_t* icon_buffer, + const unsigned char* icon_bitmap, + unsigned int masked_inverted); +void screen_prepare_app_icon_14x14(ux_icon_buffer_t* icon_buffer, + const unsigned char* icon_bitmap, + unsigned int icon_bitmap_length, + unsigned int masked_inverted); +void screen_prepare_app_icon(ux_icon_buffer_t* icon_buffer, + unsigned char* icon_bitmap_buffer, + unsigned int icon_bitmap_buffer_length, + unsigned int appidx, + unsigned int inverted); void screen_lock(void); #ifdef BOLOS_OS_UPGRADER void screen_os_upgrader(void); -#endif // BOLOS_OS_UPGRADER +#endif // BOLOS_OS_UPGRADER unsigned int screen_consent_button(unsigned int button_mask, unsigned int button_mask_counter); unsigned int screen_consent_ticker(unsigned int ignored); -void screen_consent_ticker_init(unsigned int number_of_steps, unsigned int interval_ms, unsigned int check_pin_to_confirm); +void screen_consent_ticker_init(unsigned int number_of_steps, + unsigned int interval_ms, + unsigned int check_pin_to_confirm); void screen_consent_set_interval(unsigned int interval_ms); void screen_common_pin_init(unsigned int stack_slot, pin_callback_t end_callback); @@ -162,6 +175,6 @@ void screen_boot_delay_init(void); void settings_general_version(appmain_t end_callback); void settings_general_regulatory(appmain_t end_callback); void settings_general_serial(appmain_t end_callback); -#endif //TARGET_NANOX || TARGET_NANOS2 +#endif // TARGET_NANOX || TARGET_NANOS2 -#endif // OS_IO_SEPROXYHAL +#endif // OS_IO_SEPROXYHAL diff --git a/src/bolos_ux_nanos.c b/src/bolos_ux_nanos.c index b06d5b0f..78cc0ddf 100644 --- a/src/bolos_ux_nanos.c +++ b/src/bolos_ux_nanos.c @@ -1,19 +1,19 @@ /******************************************************************************* -* Ledger Blue - Secure firmware -* (c) 2016, 2017 Ledger -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -********************************************************************************/ + * Ledger Blue - Secure firmware + * (c) 2016, 2017 Ledger + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ********************************************************************************/ #include "os.h" @@ -39,28 +39,70 @@ unsigned short io_timeout(unsigned short last_timeout) { } const unsigned char C_app_empty_colors[] = { - 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, }; const unsigned char C_app_empty_bitmap[] = { // color index table - 0x01, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, + 0x01, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, // icon mask - 0x00, 0x00, 0xF0, 0x0F, 0xFC, 0x3F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, - 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, - 0xFC, 0x3F, 0xFC, 0x3F, 0xF0, 0x0F, 0x00, 0x00, + 0x00, + 0x00, + 0xF0, + 0x0F, + 0xFC, + 0x3F, + 0xFC, + 0x3F, + 0xFE, + 0x7F, + 0xFE, + 0x7F, + 0xFE, + 0x7F, + 0xFE, + 0x7F, + 0xFE, + 0x7F, + 0xFE, + 0x7F, + 0xFE, + 0x7F, + 0xFE, + 0x7F, + 0xFC, + 0x3F, + 0xFC, + 0x3F, + 0xF0, + 0x0F, + 0x00, + 0x00, }; // prepare the app icon as if it was a icon_detail_t encoded structure in the // string_buffer -void screen_prepare_masked_icon(unsigned char *icon_bitmap, - unsigned int icon_bitmap_length) { +void screen_prepare_masked_icon(unsigned char *icon_bitmap, unsigned int icon_bitmap_length) { unsigned int i, inversemode; - bagl_icon_details_t *icon_details = - (bagl_icon_details_t *)G_bolos_ux_context.string_buffer; - unsigned char *bitmap = (unsigned char *)G_bolos_ux_context.string_buffer + - sizeof(bagl_icon_details_t); + bagl_icon_details_t *icon_details = (bagl_icon_details_t *) G_bolos_ux_context.string_buffer; + unsigned char *bitmap = + (unsigned char *) G_bolos_ux_context.string_buffer + sizeof(bagl_icon_details_t); icon_details->width = 16; icon_details->height = 16; @@ -74,13 +116,11 @@ void screen_prepare_masked_icon(unsigned char *icon_bitmap, // bit to // match the C_app_empty_bitmap bit value inversemode = 0; - if (icon_bitmap[1] != 0 || icon_bitmap[2] != 0 || icon_bitmap[3] != 0 || - icon_bitmap[4] != 0) { + if (icon_bitmap[1] != 0 || icon_bitmap[2] != 0 || icon_bitmap[3] != 0 || icon_bitmap[4] != 0) { inversemode = 1; } - for (i = 1 + 8; i < sizeof(C_app_empty_bitmap) && i < icon_bitmap_length; - i++) { + for (i = 1 + 8; i < sizeof(C_app_empty_bitmap) && i < icon_bitmap_length; i++) { if (inversemode) { bitmap[i - 1 - 8] = C_app_empty_bitmap[i] & (~icon_bitmap[i]); } else { @@ -99,27 +139,27 @@ void io_seproxyhal_display(const bagl_element_t *element) { void bolos_ux_hslider3_init(unsigned int total_count) { G_bolos_ux_context.hslider3_total = total_count; switch (total_count) { - case 0: - G_bolos_ux_context.hslider3_before = BOLOS_UX_HSLIDER3_NONE; - G_bolos_ux_context.hslider3_current = BOLOS_UX_HSLIDER3_NONE; - G_bolos_ux_context.hslider3_after = BOLOS_UX_HSLIDER3_NONE; - break; - case 1: - G_bolos_ux_context.hslider3_before = BOLOS_UX_HSLIDER3_NONE; - G_bolos_ux_context.hslider3_current = 0; - G_bolos_ux_context.hslider3_after = BOLOS_UX_HSLIDER3_NONE; - break; - case 2: - G_bolos_ux_context.hslider3_before = BOLOS_UX_HSLIDER3_NONE; - // G_bolos_ux_context.hslider3_before = 1; // full rotate - G_bolos_ux_context.hslider3_current = 0; - G_bolos_ux_context.hslider3_after = 1; - break; - default: - G_bolos_ux_context.hslider3_before = total_count - 1; - G_bolos_ux_context.hslider3_current = 0; - G_bolos_ux_context.hslider3_after = 1; - break; + case 0: + G_bolos_ux_context.hslider3_before = BOLOS_UX_HSLIDER3_NONE; + G_bolos_ux_context.hslider3_current = BOLOS_UX_HSLIDER3_NONE; + G_bolos_ux_context.hslider3_after = BOLOS_UX_HSLIDER3_NONE; + break; + case 1: + G_bolos_ux_context.hslider3_before = BOLOS_UX_HSLIDER3_NONE; + G_bolos_ux_context.hslider3_current = 0; + G_bolos_ux_context.hslider3_after = BOLOS_UX_HSLIDER3_NONE; + break; + case 2: + G_bolos_ux_context.hslider3_before = BOLOS_UX_HSLIDER3_NONE; + // G_bolos_ux_context.hslider3_before = 1; // full rotate + G_bolos_ux_context.hslider3_current = 0; + G_bolos_ux_context.hslider3_after = 1; + break; + default: + G_bolos_ux_context.hslider3_before = total_count - 1; + G_bolos_ux_context.hslider3_current = 0; + G_bolos_ux_context.hslider3_after = 1; + break; } } @@ -135,82 +175,132 @@ void bolos_ux_hslider3_set_current(unsigned int current) { void bolos_ux_hslider3_next(void) { switch (G_bolos_ux_context.hslider3_total) { - case 0: - case 1: - break; - case 2: - switch (G_bolos_ux_context.hslider3_current) { case 0: - G_bolos_ux_context.hslider3_before = 0; - G_bolos_ux_context.hslider3_current = 1; - G_bolos_ux_context.hslider3_after = BOLOS_UX_HSLIDER3_NONE; - break; case 1: - G_bolos_ux_context.hslider3_before = BOLOS_UX_HSLIDER3_NONE; - G_bolos_ux_context.hslider3_current = 0; - G_bolos_ux_context.hslider3_after = 1; break; - } - break; - default: - G_bolos_ux_context.hslider3_before = - G_bolos_ux_context.hslider3_current; - G_bolos_ux_context.hslider3_current = G_bolos_ux_context.hslider3_after; - G_bolos_ux_context.hslider3_after = - (G_bolos_ux_context.hslider3_after + 1) % - G_bolos_ux_context.hslider3_total; - break; + case 2: + switch (G_bolos_ux_context.hslider3_current) { + case 0: + G_bolos_ux_context.hslider3_before = 0; + G_bolos_ux_context.hslider3_current = 1; + G_bolos_ux_context.hslider3_after = BOLOS_UX_HSLIDER3_NONE; + break; + case 1: + G_bolos_ux_context.hslider3_before = BOLOS_UX_HSLIDER3_NONE; + G_bolos_ux_context.hslider3_current = 0; + G_bolos_ux_context.hslider3_after = 1; + break; + } + break; + default: + G_bolos_ux_context.hslider3_before = G_bolos_ux_context.hslider3_current; + G_bolos_ux_context.hslider3_current = G_bolos_ux_context.hslider3_after; + G_bolos_ux_context.hslider3_after = + (G_bolos_ux_context.hslider3_after + 1) % G_bolos_ux_context.hslider3_total; + break; } } void bolos_ux_hslider3_previous(void) { switch (G_bolos_ux_context.hslider3_total) { - case 0: - case 1: - break; - case 2: - switch (G_bolos_ux_context.hslider3_current) { case 0: - G_bolos_ux_context.hslider3_before = 0; - G_bolos_ux_context.hslider3_current = 1; - G_bolos_ux_context.hslider3_after = BOLOS_UX_HSLIDER3_NONE; - break; case 1: - G_bolos_ux_context.hslider3_before = BOLOS_UX_HSLIDER3_NONE; - G_bolos_ux_context.hslider3_current = 0; - G_bolos_ux_context.hslider3_after = 1; break; - } - break; - default: - G_bolos_ux_context.hslider3_after = G_bolos_ux_context.hslider3_current; - G_bolos_ux_context.hslider3_current = - G_bolos_ux_context.hslider3_before; - G_bolos_ux_context.hslider3_before = - (G_bolos_ux_context.hslider3_before + - G_bolos_ux_context.hslider3_total - 1) % - G_bolos_ux_context.hslider3_total; - break; + case 2: + switch (G_bolos_ux_context.hslider3_current) { + case 0: + G_bolos_ux_context.hslider3_before = 0; + G_bolos_ux_context.hslider3_current = 1; + G_bolos_ux_context.hslider3_after = BOLOS_UX_HSLIDER3_NONE; + break; + case 1: + G_bolos_ux_context.hslider3_before = BOLOS_UX_HSLIDER3_NONE; + G_bolos_ux_context.hslider3_current = 0; + G_bolos_ux_context.hslider3_after = 1; + break; + } + break; + default: + G_bolos_ux_context.hslider3_after = G_bolos_ux_context.hslider3_current; + G_bolos_ux_context.hslider3_current = G_bolos_ux_context.hslider3_before; + G_bolos_ux_context.hslider3_before = + (G_bolos_ux_context.hslider3_before + G_bolos_ux_context.hslider3_total - 1) % + G_bolos_ux_context.hslider3_total; + break; } } const bagl_element_t screen_onboarding_word_list_elements[] = { - // erase - {{BAGL_RECTANGLE , 0x00, 0, 0, 128, 32, 0, 0, BAGL_FILL, 0x000000, 0xFFFFFF, 0, 0}, NULL}, + // erase + {{BAGL_RECTANGLE, 0x00, 0, 0, 128, 32, 0, 0, BAGL_FILL, 0x000000, 0xFFFFFF, 0, 0}, NULL}, - {{BAGL_LABELINE , 0x01, 0, 12, 128, 32, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_REGULAR_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, G_ux.string_buffer }, - {{BAGL_RECTANGLE , 0x02, 32, 16, 64, 14, 0, 4, BAGL_FILL, 0xFFFFFF, 0x000000, 0, 0}, NULL}, - {{BAGL_LABELINE , 0x02, 0, 26, 128, 32, 0, 0, 0 , 0x000000, 0xFFFFFF, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, G_ux.string_buffer }, + {{BAGL_LABELINE, + 0x01, + 0, + 12, + 128, + 32, + 0, + 0, + 0, + 0xFFFFFF, + 0x000000, + BAGL_FONT_OPEN_SANS_REGULAR_11px | BAGL_FONT_ALIGNMENT_CENTER, + 0}, + G_ux.string_buffer}, + {{BAGL_RECTANGLE, 0x02, 32, 16, 64, 14, 0, 4, BAGL_FILL, 0xFFFFFF, 0x000000, 0, 0}, NULL}, + {{BAGL_LABELINE, + 0x02, + 0, + 26, + 128, + 32, + 0, + 0, + 0, + 0x000000, + 0xFFFFFF, + BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, + 0}, + G_ux.string_buffer}, - // left/rights icons - {{BAGL_ICON , 0x03, 3, 12, 4, 7, 0, 0, 0 , 0xFFFFFF, 0x000000, 0, 0}, (const char*)&C_icon_left }, - {{BAGL_ICON , 0x04, 121, 12, 4, 7, 0, 0, 0 , 0xFFFFFF, 0x000000, 0, 0}, (const char*)&C_icon_right }, + // left/rights icons + {{BAGL_ICON, 0x03, 3, 12, 4, 7, 0, 0, 0, 0xFFFFFF, 0x000000, 0, 0}, + (const char *) &C_icon_left}, + {{BAGL_ICON, 0x04, 121, 12, 4, 7, 0, 0, 0, 0xFFFFFF, 0x000000, 0, 0}, + (const char *) &C_icon_right}, - // supplementary static entry - {{BAGL_ICON , 0x05, 16, 9, 14, 14, 0, 0, 0 , 0xFFFFFF, 0x000000, 0, 0}, (const char*)&C_icon_back }, - {{BAGL_LABELINE , 0x05, 41, 12, 128, 32, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px, 0 }, "Restart from" }, - {{BAGL_LABELINE , 0x06, 41, 26, 128, 32, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px, 0 }, G_ux.string_buffer }, + // supplementary static entry + {{BAGL_ICON, 0x05, 16, 9, 14, 14, 0, 0, 0, 0xFFFFFF, 0x000000, 0, 0}, + (const char *) &C_icon_back}, + {{BAGL_LABELINE, + 0x05, + 41, + 12, + 128, + 32, + 0, + 0, + 0, + 0xFFFFFF, + 0x000000, + BAGL_FONT_OPEN_SANS_EXTRABOLD_11px, + 0}, + "Restart from"}, + {{BAGL_LABELINE, + 0x06, + 41, + 26, + 128, + 32, + 0, + 0, + 0, + 0xFFFFFF, + 0x000000, + BAGL_FONT_OPEN_SANS_EXTRABOLD_11px, + 0}, + G_ux.string_buffer}, }; - -#endif +#endif diff --git a/src/bolos_ux_nanos.h b/src/bolos_ux_nanos.h index 860a75dd..3a78131d 100644 --- a/src/bolos_ux_nanos.h +++ b/src/bolos_ux_nanos.h @@ -1,19 +1,19 @@ /******************************************************************************* -* Ledger Blue - Secure firmware -* (c) 2016, 2017 Ledger -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -********************************************************************************/ + * Ledger Blue - Secure firmware + * (c) 2016, 2017 Ledger + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ********************************************************************************/ #ifndef BOLOS_UX_H #define BOLOS_UX_H @@ -26,26 +26,25 @@ typedef unsigned int (*callback_t)(unsigned int); -#define KEYBOARD_ITEM_VALIDATED \ - 1 // callback is called with the entered item index, tmp_element is - // precharged with element to be displayed and using the common string - // buffer as string parameter -#define KEYBOARD_RENDER_ITEM \ - 2 // callback is called the element index, tmp_element is precharged with - // element to be displayed and using the common string buffer as string - // parameter -#define KEYBOARD_RENDER_WORD \ - 3 // callback is called with a -1 when requesting complete word, or the char - // index else, returnin 0 implies no char is to be displayed -typedef const bagl_element_t *(*keyboard_callback_t)(unsigned int event, - unsigned int value); +#define KEYBOARD_ITEM_VALIDATED \ + 1 // callback is called with the entered item index, tmp_element is + // precharged with element to be displayed and using the common string + // buffer as string parameter +#define KEYBOARD_RENDER_ITEM \ + 2 // callback is called the element index, tmp_element is precharged with + // element to be displayed and using the common string buffer as string + // parameter +#define KEYBOARD_RENDER_WORD \ + 3 // callback is called with a -1 when requesting complete word, or the char + // index else, returnin 0 implies no char is to be displayed +typedef const bagl_element_t *(*keyboard_callback_t)(unsigned int event, unsigned int value); // bolos ux context (not mandatory if redesigning a bolos ux) typedef struct bolos_ux_context { #define STATE_UNINITIALIZED 0 #ifndef STATE_INITIALIZED #define STATE_INITIALIZED 0xB01055E5UL -#endif // STATE_INITIALIZED +#endif // STATE_INITIALIZED unsigned int state; // unified arrays @@ -55,8 +54,8 @@ typedef struct bolos_ux_context { struct { const bagl_element_t *element_array; unsigned int element_array_count; - } element_arrays[2]; // not more than 2 arrays of elements are in use - // for any screen + } element_arrays[2]; // not more than 2 arrays of elements are in use + // for any screen unsigned int element_arrays_count; unsigned int element_index; @@ -75,8 +74,7 @@ typedef struct bolos_ux_context { // [onboarding/dashboard/settings] | [pin] | [help] | [screensaver] } screen_stack[4]; - unsigned int - screen_stack_count; // initialized @0 by the bolos ux initialize + unsigned int screen_stack_count; // initialized @0 by the bolos ux initialize // a screen pop occured, the underlaying screen must optimize its drawing as // we've probably trashed the whole screen unsigned int screen_redraw; @@ -84,8 +82,8 @@ typedef struct bolos_ux_context { unsigned int ms; unsigned int setting_auto_lock_delay_ms; -#define IS_SETTING_PRE_POWER_OFF() \ - (G_bolos_ux_context.setting_auto_lock_delay_ms != -1UL && \ +#define IS_SETTING_PRE_POWER_OFF() \ + (G_bolos_ux_context.setting_auto_lock_delay_ms != -1UL && \ G_bolos_ux_context.setting_auto_lock_delay_ms != 0) #define INACTIVITY_MS_AUTO_LOCK (G_bolos_ux_context.setting_auto_lock_delay_ms) unsigned int ms_last_activity; @@ -101,11 +99,11 @@ typedef struct bolos_ux_context { unsigned int last_ux_id; -#define BOLOS_UX_ONBOARDING_NEW 1 -#define BOLOS_UX_ONBOARDING_NEW_12 12 -#define BOLOS_UX_ONBOARDING_NEW_18 18 -#define BOLOS_UX_ONBOARDING_NEW_24 24 -#define BOLOS_UX_ONBOARDING_RESTORE 2 +#define BOLOS_UX_ONBOARDING_NEW 1 +#define BOLOS_UX_ONBOARDING_NEW_12 12 +#define BOLOS_UX_ONBOARDING_NEW_18 18 +#define BOLOS_UX_ONBOARDING_NEW_24 24 +#define BOLOS_UX_ONBOARDING_RESTORE 2 #define BOLOS_UX_ONBOARDING_RESTORE_12 12 #define BOLOS_UX_ONBOARDING_RESTORE_18 18 #define BOLOS_UX_ONBOARDING_RESTORE_24 24 @@ -124,18 +122,16 @@ typedef struct bolos_ux_context { unsigned int words_buffer_length; // after an int to make sure it's aligned - char string_buffer[MAX(64, sizeof(bagl_icon_details_t) + - BOLOS_APP_ICON_SIZE_B - - 1)]; // to store the seed wholy - - char words_buffer[257]; // 128 of words (215 => hashed to 64, or 128) + - // HMAC_LENGTH*2 = 256 + char string_buffer[MAX( + 64, + sizeof(bagl_icon_details_t) + BOLOS_APP_ICON_SIZE_B - 1)]; // to store the seed wholy + char words_buffer[257]; // 128 of words (215 => hashed to 64, or 128) + + // HMAC_LENGTH*2 = 256 #define MAX_PIN_LENGTH 8 #define MIN_PIN_LENGTH 4 - char pin_buffer[MAX_PIN_LENGTH + - 1]; // length prepended for custom pin length + char pin_buffer[MAX_PIN_LENGTH + 1]; // length prepended for custom pin length // filled up during os_ux syscall when called by user or bolos. bolos_ux_params_t parameters; @@ -158,8 +154,8 @@ typedef struct bolos_ux_context { // dashboard last selected item unsigned int dashboard_last_selected; - unsigned int dashboard_redisplayed; // to trigger animation when all - // elements are displayed + unsigned int dashboard_redisplayed; // to trigger animation when all + // elements are displayed // in case autostart is engaged, to avoid starting the app multiple times unsigned int app_auto_started; @@ -190,10 +186,9 @@ void bolos_ux_hslider3_next(void); void bolos_ux_hslider3_previous(void); #define FAST_LIST_THRESHOLD_CS 8 -#define FAST_LIST_ACTION_CS 2 +#define FAST_LIST_ACTION_CS 2 -unsigned int -screen_stack_is_element_array_present(const bagl_element_t *element_array); +unsigned int screen_stack_is_element_array_present(const bagl_element_t *element_array); unsigned int screen_stack_push(void); unsigned int screen_stack_pop(void); void screen_stack_remove(unsigned int stack_slot); @@ -201,14 +196,16 @@ void screen_stack_remove(unsigned int stack_slot); // BIP39 helpers #include "bolos_ux_onboarding_seed_rom_variables.h" -void bolos_ux_pbkdf2(unsigned char *password, unsigned int passwordlen, - unsigned char *salt, unsigned int saltlen, - unsigned int iterations, unsigned char *out, +void bolos_ux_pbkdf2(unsigned char *password, + unsigned int passwordlen, + unsigned char *salt, + unsigned int saltlen, + unsigned int iterations, + unsigned char *out, unsigned int outLength); unsigned char bolos_ux_get_random_bip39_word(unsigned char *word); // return 0 if mnemonic is invalid -unsigned int bolos_ux_mnemonic_check(unsigned char *mnemonic, - unsigned int mnemonicLength); +unsigned int bolos_ux_mnemonic_check(unsigned char *mnemonic, unsigned int mnemonicLength); unsigned char bolos_ux_word_check(unsigned char *word, unsigned int wordLength); unsigned int bolos_ux_get_word_ptr(unsigned char **word, @@ -217,30 +214,26 @@ unsigned int bolos_ux_get_word_ptr(unsigned char **word, // passphrase will be prefixed with "MNEMONIC" from BIP39, the passphrase // content shall start @ 8 -void bolos_ux_mnemonic_to_seed( - unsigned char *mnemonic, unsigned int mnemonicLength, - unsigned char *seed /*, unsigned char *workBuffer*/); -unsigned int bolos_ux_mnemonic_indexes_to_words(unsigned char *indexes, - unsigned char *words); +void bolos_ux_mnemonic_to_seed(unsigned char *mnemonic, + unsigned int mnemonicLength, + unsigned char *seed /*, unsigned char *workBuffer*/); +unsigned int bolos_ux_mnemonic_indexes_to_words(unsigned char *indexes, unsigned char *words); unsigned int bolos_ux_mnemonic_from_data(unsigned char *in, unsigned int inLength, unsigned char *out, unsigned int outLength); -unsigned int -bolos_ux_bip39_get_word_idx_starting_with(unsigned char *prefix, - unsigned int prefixlength); -unsigned int bolos_ux_bip39_idx_strcpy(unsigned int index, - unsigned char *buffer); +unsigned int bolos_ux_bip39_get_word_idx_starting_with(unsigned char *prefix, + unsigned int prefixlength); +unsigned int bolos_ux_bip39_idx_strcpy(unsigned int index, unsigned char *buffer); unsigned int bolos_ux_bip39_idx_startswith(unsigned int idx, unsigned char *prefix, unsigned int prefixlength); -unsigned int -bolos_ux_bip39_get_word_count_starting_with(unsigned char *prefix, - unsigned int prefixlength); -unsigned int bolos_ux_bip39_get_word_next_letters_starting_with( - unsigned char *prefix, unsigned int prefixlength, - unsigned char *next_letters_buffer); +unsigned int bolos_ux_bip39_get_word_count_starting_with(unsigned char *prefix, + unsigned int prefixlength); +unsigned int bolos_ux_bip39_get_word_next_letters_starting_with(unsigned char *prefix, + unsigned int prefixlength, + unsigned char *next_letters_buffer); #ifdef HAVE_ELECTRUM @@ -260,7 +253,6 @@ void bolos_ux_main(void); extern const bagl_element_t screen_onboarding_word_list_elements[9]; +#endif // HAVE_BOLOS_UX -#endif // HAVE_BOLOS_UX - -#endif // BOLOS_UX_H +#endif // BOLOS_UX_H diff --git a/src/bolos_ux_nanos_keyboard.c b/src/bolos_ux_nanos_keyboard.c index a3fd8c5e..50e0b927 100644 --- a/src/bolos_ux_nanos_keyboard.c +++ b/src/bolos_ux_nanos_keyboard.c @@ -1,19 +1,19 @@ /******************************************************************************* -* Ledger Blue - Secure firmware -* (c) 2016, 2017 Ledger -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -********************************************************************************/ + * Ledger Blue - Secure firmware + * (c) 2016, 2017 Ledger + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ********************************************************************************/ #include "os.h" #ifdef TARGET_NANOS @@ -33,147 +33,319 @@ const bagl_element_t screen_common_keyboard_elements[] = { // erase - {{BAGL_RECTANGLE , 0x00, 0, 0, 128, 32, 0, 0, BAGL_FILL, 0x000000, 0xFFFFFF, 0, 0}, NULL}, + {{BAGL_RECTANGLE, 0x00, 0, 0, 128, 32, 0, 0, BAGL_FILL, 0x000000, 0xFFFFFF, 0, 0}, NULL}, // typed word - {{BAGL_RECTANGLE , 0x00, 18, 18, 110-18, 14, 0, 4, BAGL_FILL, 0xFFFFFF, 0x000000, 0, 0}, NULL}, - {{BAGL_LABELINE , 0x10, 128/2-12/2-40, 28, 12, 12, 0, 0, 0 , 0x000000, 0xFFFFFF, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL }, - {{BAGL_LABELINE , 0x11, 128/2-12/2-30, 28, 12, 12, 0, 0, 0 , 0x000000, 0xFFFFFF, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL }, - {{BAGL_LABELINE , 0x12, 128/2-12/2-20, 28, 12, 12, 0, 0, 0 , 0x000000, 0xFFFFFF, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL }, - {{BAGL_LABELINE , 0x13, 128/2-12/2-10, 28, 12, 12, 0, 0, 0 , 0x000000, 0xFFFFFF, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL }, - {{BAGL_LABELINE , 0x14, 128/2-12/2 , 28, 12, 12, 0, 0, 0 , 0x000000, 0xFFFFFF, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL }, - {{BAGL_LABELINE , 0x15, 128/2-12/2+10, 28, 12, 12, 0, 0, 0 , 0x000000, 0xFFFFFF, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL }, - {{BAGL_LABELINE , 0x16, 128/2-12/2+20, 28, 12, 12, 0, 0, 0 , 0x000000, 0xFFFFFF, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL }, - {{BAGL_LABELINE , 0x17, 128/2-12/2+30, 28, 12, 12, 0, 0, 0 , 0x000000, 0xFFFFFF, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL }, - {{BAGL_LABELINE , 0x18, 128/2-12/2+40, 28, 12, 12, 0, 0, 0 , 0x000000, 0xFFFFFF, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL }, + {{BAGL_RECTANGLE, 0x00, 18, 18, 110 - 18, 14, 0, 4, BAGL_FILL, 0xFFFFFF, 0x000000, 0, 0}, NULL}, + {{BAGL_LABELINE, + 0x10, + 128 / 2 - 12 / 2 - 40, + 28, + 12, + 12, + 0, + 0, + 0, + 0x000000, + 0xFFFFFF, + BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, + 0}, + NULL}, + {{BAGL_LABELINE, + 0x11, + 128 / 2 - 12 / 2 - 30, + 28, + 12, + 12, + 0, + 0, + 0, + 0x000000, + 0xFFFFFF, + BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, + 0}, + NULL}, + {{BAGL_LABELINE, + 0x12, + 128 / 2 - 12 / 2 - 20, + 28, + 12, + 12, + 0, + 0, + 0, + 0x000000, + 0xFFFFFF, + BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, + 0}, + NULL}, + {{BAGL_LABELINE, + 0x13, + 128 / 2 - 12 / 2 - 10, + 28, + 12, + 12, + 0, + 0, + 0, + 0x000000, + 0xFFFFFF, + BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, + 0}, + NULL}, + {{BAGL_LABELINE, + 0x14, + 128 / 2 - 12 / 2, + 28, + 12, + 12, + 0, + 0, + 0, + 0x000000, + 0xFFFFFF, + BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, + 0}, + NULL}, + {{BAGL_LABELINE, + 0x15, + 128 / 2 - 12 / 2 + 10, + 28, + 12, + 12, + 0, + 0, + 0, + 0x000000, + 0xFFFFFF, + BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, + 0}, + NULL}, + {{BAGL_LABELINE, + 0x16, + 128 / 2 - 12 / 2 + 20, + 28, + 12, + 12, + 0, + 0, + 0, + 0x000000, + 0xFFFFFF, + BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, + 0}, + NULL}, + {{BAGL_LABELINE, + 0x17, + 128 / 2 - 12 / 2 + 30, + 28, + 12, + 12, + 0, + 0, + 0, + 0x000000, + 0xFFFFFF, + BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, + 0}, + NULL}, + {{BAGL_LABELINE, + 0x18, + 128 / 2 - 12 / 2 + 40, + 28, + 12, + 12, + 0, + 0, + 0, + 0x000000, + 0xFFFFFF, + BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, + 0}, + NULL}, // slider elements - {{BAGL_LINE , 0x06, 46, 8, 3, 1, 0, 0, 0 , 0xFFFFFF, 0x000000, 0, 0 }, NULL }, - {{BAGL_LINE , 0x07, 79, 8, 3, 1, 0, 0, 0 , 0xFFFFFF, 0x000000, 0, 0 }, NULL }, - - // previous element - {{BAGL_LABELINE , 0x01, 26, 12, 14, 13, 0, 0, BAGL_FILL, 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_REGULAR_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL }, - - // current item - {{BAGL_RECTANGLE , 0x22, 57, 12-10, 14, 14, 0, 4, BAGL_FILL, 0xFFFFFF, 0x000000, 0, 0}, NULL}, - {{BAGL_LABELINE , 0x02, 58, 12, 12, 13, 0, 0, BAGL_FILL, 0x000000, 0xFFFFFF, BAGL_FONT_OPEN_SANS_REGULAR_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL }, - //{{BAGL_LABELINE , 0x02, 57, 12, 14, 13, 0, 4, BAGL_FILL, 0x000000, 0xFFFFFF, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER|BAGL_FONT_ALIGNMENT_MIDDLE, 0 }, NULL }, - - // next element - {{BAGL_LABELINE , 0x03, 88, 12, 14, 13, 0, 0, BAGL_FILL, 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_REGULAR_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL }, + {{BAGL_LINE, 0x06, 46, 8, 3, 1, 0, 0, 0, 0xFFFFFF, 0x000000, 0, 0}, NULL}, + {{BAGL_LINE, 0x07, 79, 8, 3, 1, 0, 0, 0, 0xFFFFFF, 0x000000, 0, 0}, NULL}, + + // previous element + {{BAGL_LABELINE, + 0x01, + 26, + 12, + 14, + 13, + 0, + 0, + BAGL_FILL, + 0xFFFFFF, + 0x000000, + BAGL_FONT_OPEN_SANS_REGULAR_11px | BAGL_FONT_ALIGNMENT_CENTER, + 0}, + NULL}, + + // current item + {{BAGL_RECTANGLE, 0x22, 57, 12 - 10, 14, 14, 0, 4, BAGL_FILL, 0xFFFFFF, 0x000000, 0, 0}, NULL}, + {{BAGL_LABELINE, + 0x02, + 58, + 12, + 12, + 13, + 0, + 0, + BAGL_FILL, + 0x000000, + 0xFFFFFF, + BAGL_FONT_OPEN_SANS_REGULAR_11px | BAGL_FONT_ALIGNMENT_CENTER, + 0}, + NULL}, + // {{BAGL_LABELINE , 0x02, 57, 12, 14, 13, 0, 4, BAGL_FILL, 0x000000, + // 0xFFFFFF, + // BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER|BAGL_FONT_ALIGNMENT_MIDDLE, 0 + // }, + // NULL }, + + // next element + {{BAGL_LABELINE, + 0x03, + 88, + 12, + 14, + 13, + 0, + 0, + BAGL_FILL, + 0xFFFFFF, + 0x000000, + BAGL_FONT_OPEN_SANS_REGULAR_11px | BAGL_FONT_ALIGNMENT_CENTER, + 0}, + NULL}, // left/rights icons - {{BAGL_ICON , 0x00, 3, 12, 4, 7, 0, 0, 0 , 0xFFFFFF, 0x000000, 0, 0}, (const char*)&C_icon_left }, - {{BAGL_ICON , 0x00, 121, 12, 4, 7, 0, 0, 0 , 0xFFFFFF, 0x000000, 0, 0}, (const char*)&C_icon_right }, + {{BAGL_ICON, 0x00, 3, 12, 4, 7, 0, 0, 0, 0xFFFFFF, 0x000000, 0, 0}, (const char*) &C_icon_left}, + {{BAGL_ICON, 0x00, 121, 12, 4, 7, 0, 0, 0, 0xFFFFFF, 0x000000, 0, 0}, + (const char*) &C_icon_right}, }; -const bagl_element_t* screen_common_keyboard_before_element_display_callback(const bagl_element_t* element) { - const bagl_element_t* e; +const bagl_element_t* screen_common_keyboard_before_element_display_callback( + const bagl_element_t* element) { + const bagl_element_t* e; // copy element to be displayed - memcpy(&G_ux.tmp_element, PIC(element), sizeof(G_ux.tmp_element)); - + memcpy(&G_ux.tmp_element, PIC(element), sizeof(G_ux.tmp_element)); switch (element->component.userid) { - case 0x01: - if (G_bolos_ux_context.hslider3_before == BOLOS_UX_HSLIDER3_NONE) { - return 0; - } - return G_bolos_ux_context.keyboard_callback(KEYBOARD_RENDER_ITEM, G_bolos_ux_context.hslider3_before); - - // current item (both line and invert rectangle) - case 0x22: - case 0x02: - e = G_bolos_ux_context.keyboard_callback(KEYBOARD_RENDER_ITEM, G_bolos_ux_context.hslider3_current); - - // if the current component to display is not TEXT, then don't display the invert rectangle, to - // avoid graphic glitch - if (element->component.userid == 0x22) { - if (e->component.type == BAGL_ICON) { - return NULL; - } - } - return e; - break; - - case 0x03: - if (G_bolos_ux_context.hslider3_after == BOLOS_UX_HSLIDER3_NONE) { - return 0; - } - return G_bolos_ux_context.keyboard_callback(KEYBOARD_RENDER_ITEM, G_bolos_ux_context.hslider3_after); - - case 0x06: - if (G_bolos_ux_context.hslider3_before == BOLOS_UX_HSLIDER3_NONE) { - return 0; // don't display - } - break; - - case 0x07: - if (G_bolos_ux_context.hslider3_after == BOLOS_UX_HSLIDER3_NONE) { - return 0; // don't display - } - break; - - default: - if (element->component.userid & 0x10) { - // request the xieth word char - return G_bolos_ux_context.keyboard_callback(KEYBOARD_RENDER_WORD, element->component.userid&0x0F); - } - break; + case 0x01: + if (G_bolos_ux_context.hslider3_before == BOLOS_UX_HSLIDER3_NONE) { + return 0; + } + return G_bolos_ux_context.keyboard_callback(KEYBOARD_RENDER_ITEM, + G_bolos_ux_context.hslider3_before); + + // current item (both line and invert rectangle) + case 0x22: + case 0x02: + e = G_bolos_ux_context.keyboard_callback(KEYBOARD_RENDER_ITEM, + G_bolos_ux_context.hslider3_current); + + // if the current component to display is not TEXT, then don't display the invert + // rectangle, to avoid graphic glitch + if (element->component.userid == 0x22) { + if (e->component.type == BAGL_ICON) { + return NULL; + } + } + return e; + break; + + case 0x03: + if (G_bolos_ux_context.hslider3_after == BOLOS_UX_HSLIDER3_NONE) { + return 0; + } + return G_bolos_ux_context.keyboard_callback(KEYBOARD_RENDER_ITEM, + G_bolos_ux_context.hslider3_after); + + case 0x06: + if (G_bolos_ux_context.hslider3_before == BOLOS_UX_HSLIDER3_NONE) { + return 0; // don't display + } + break; + + case 0x07: + if (G_bolos_ux_context.hslider3_after == BOLOS_UX_HSLIDER3_NONE) { + return 0; // don't display + } + break; + + default: + if (element->component.userid & 0x10) { + // request the xieth word char + return G_bolos_ux_context.keyboard_callback(KEYBOARD_RENDER_WORD, + element->component.userid & 0x0F); + } + break; } // return the probably modded element by the callback function - return &G_ux.tmp_element; + return &G_ux.tmp_element; } -unsigned int screen_common_keyboard_button(unsigned int button_mask, unsigned int button_mask_counter) { +unsigned int screen_common_keyboard_button(unsigned int button_mask, + unsigned int button_mask_counter) { UNUSED(button_mask_counter); switch (button_mask) { - case BUTTON_EVT_RELEASED|BUTTON_LEFT|BUTTON_RIGHT: // validate current digit - - // validate the item, and if accepted, then redisplay current screen, else don't draw anything - if (G_bolos_ux_context.keyboard_callback(KEYBOARD_ITEM_VALIDATED, G_bolos_ux_context.hslider3_current)) { + case BUTTON_EVT_RELEASED | BUTTON_LEFT | BUTTON_RIGHT: // validate current digit + + // validate the item, and if accepted, then redisplay current screen, else don't draw + // anything + if (G_bolos_ux_context.keyboard_callback(KEYBOARD_ITEM_VALIDATED, + G_bolos_ux_context.hslider3_current)) { + goto redraw; + } + break; + + case BUTTON_EVT_FAST | BUTTON_LEFT: + case BUTTON_EVT_RELEASED | BUTTON_LEFT: + bolos_ux_hslider3_previous(); goto redraw; - } - break; - - case BUTTON_EVT_FAST | BUTTON_LEFT: - case BUTTON_EVT_RELEASED | BUTTON_LEFT: - bolos_ux_hslider3_previous(); - goto redraw; - case BUTTON_EVT_FAST | BUTTON_RIGHT: - case BUTTON_EVT_RELEASED | BUTTON_RIGHT: - bolos_ux_hslider3_next(); + case BUTTON_EVT_FAST | BUTTON_RIGHT: + case BUTTON_EVT_RELEASED | BUTTON_RIGHT: + bolos_ux_hslider3_next(); - redraw: - ux_stack_display(G_ux.stack_count-1); - break; + redraw: + ux_stack_display(G_ux.stack_count - 1); + break; } return 1; } -void screen_common_keyboard_init(unsigned int stack_slot, unsigned int current_element, unsigned int nb_elements, keyboard_callback_t callback) { +void screen_common_keyboard_init(unsigned int stack_slot, + unsigned int current_element, + unsigned int nb_elements, + keyboard_callback_t callback) { unsigned int current = G_bolos_ux_context.hslider3_current; - ux_stack_init(stack_slot); + ux_stack_init(stack_slot); // initiate the rotating modulo iterator bolos_ux_hslider3_init(nb_elements); if (current_element == COMMON_KEYBOARD_INDEX_UNCHANGED) { bolos_ux_hslider3_set_current(current); - } - else { + } else { bolos_ux_hslider3_set_current(current_element); } - G_ux.stack[stack_slot].element_arrays[0].element_array = screen_common_keyboard_elements; - G_ux.stack[stack_slot].element_arrays[0].element_array_count = ARRAYLEN(screen_common_keyboard_elements); - G_ux.stack[stack_slot].element_arrays_count = 1; - G_ux.stack[stack_slot].screen_before_element_display_callback = screen_common_keyboard_before_element_display_callback; // used for each screen of the validate pin flow - G_ux.stack[stack_slot].button_push_callback = screen_common_keyboard_button; + G_ux.stack[stack_slot].element_arrays[0].element_array = screen_common_keyboard_elements; + G_ux.stack[stack_slot].element_arrays[0].element_array_count = + ARRAYLEN(screen_common_keyboard_elements); + G_ux.stack[stack_slot].element_arrays_count = 1; + G_ux.stack[stack_slot].screen_before_element_display_callback = + screen_common_keyboard_before_element_display_callback; // used for each screen of the + // validate pin flow + G_ux.stack[stack_slot].button_push_callback = screen_common_keyboard_button; G_bolos_ux_context.keyboard_callback = callback; - ux_stack_display(stack_slot); - + ux_stack_display(stack_slot); } #endif diff --git a/src/bolos_ux_nanox.c b/src/bolos_ux_nanox.c index cc9562d1..7a0e499b 100644 --- a/src/bolos_ux_nanox.c +++ b/src/bolos_ux_nanox.c @@ -15,188 +15,185 @@ bolos_ux_context_t G_bolos_ux_context; - - unsigned short io_timeout(unsigned short last_timeout) { - UNUSED(last_timeout); - // infinite timeout - return 1; + UNUSED(last_timeout); + // infinite timeout + return 1; } - -void screen_hex_identifier_string_buffer (const unsigned char * buffer, unsigned int total) { - SPRINTF(G_ux.string_buffer, +void screen_hex_identifier_string_buffer(const unsigned char* buffer, unsigned int total) { + SPRINTF(G_ux.string_buffer, "%.*H...%.*H", - BOLOS_UX_HASH_LENGTH/2, + BOLOS_UX_HASH_LENGTH / 2, buffer, - BOLOS_UX_HASH_LENGTH/2, - buffer - +total - -BOLOS_UX_HASH_LENGTH/2); + BOLOS_UX_HASH_LENGTH / 2, + buffer + total - BOLOS_UX_HASH_LENGTH / 2); } - - - - - -void io_seproxyhal_display(const bagl_element_t * element) { - io_seproxyhal_display_default((bagl_element_t*)element); +void io_seproxyhal_display(const bagl_element_t* element) { + io_seproxyhal_display_default((bagl_element_t*) element); } void screen_keyboard_validate_entered_text(void) { - // we're returning the typed text - G_ux.exit_code = BOLOS_UX_OK; - // last keycode is \n - G_ux_params.u.keyboard.keycode = '\n'; - // copy output text - memcpy(G_ux_params.u.keyboard.entered_text, G_bolos_ux_context.keyboard_user_buffer, sizeof(G_ux_params.u.keyboard.entered_text)); - // pop the screen - ux_stack_pop(); - ux_stack_redisplay(); + // we're returning the typed text + G_ux.exit_code = BOLOS_UX_OK; + // last keycode is \n + G_ux_params.u.keyboard.keycode = '\n'; + // copy output text + memcpy(G_ux_params.u.keyboard.entered_text, + G_bolos_ux_context.keyboard_user_buffer, + sizeof(G_ux_params.u.keyboard.entered_text)); + // pop the screen + ux_stack_pop(); + ux_stack_redisplay(); } - void bolos_ux_hslider3_init(unsigned int total_count) { - G_bolos_ux_context.hslider3_total = total_count; - switch (total_count) { - case 0: - G_bolos_ux_context.hslider3_before = BOLOS_UX_HSLIDER3_NONE; - G_bolos_ux_context.hslider3_current = BOLOS_UX_HSLIDER3_NONE; - G_bolos_ux_context.hslider3_after = BOLOS_UX_HSLIDER3_NONE; - break; - case 1: - G_bolos_ux_context.hslider3_before = BOLOS_UX_HSLIDER3_NONE; - G_bolos_ux_context.hslider3_current = 0; - G_bolos_ux_context.hslider3_after = BOLOS_UX_HSLIDER3_NONE; - break; - case 2: - G_bolos_ux_context.hslider3_before = BOLOS_UX_HSLIDER3_NONE; - //G_bolos_ux_context.hslider3_before = 1; // full rotate - G_bolos_ux_context.hslider3_current = 0; - G_bolos_ux_context.hslider3_after = 1; - break; - default: - G_bolos_ux_context.hslider3_before = total_count-1; - G_bolos_ux_context.hslider3_current = 0; - G_bolos_ux_context.hslider3_after = 1; - break; - } + G_bolos_ux_context.hslider3_total = total_count; + switch (total_count) { + case 0: + G_bolos_ux_context.hslider3_before = BOLOS_UX_HSLIDER3_NONE; + G_bolos_ux_context.hslider3_current = BOLOS_UX_HSLIDER3_NONE; + G_bolos_ux_context.hslider3_after = BOLOS_UX_HSLIDER3_NONE; + break; + case 1: + G_bolos_ux_context.hslider3_before = BOLOS_UX_HSLIDER3_NONE; + G_bolos_ux_context.hslider3_current = 0; + G_bolos_ux_context.hslider3_after = BOLOS_UX_HSLIDER3_NONE; + break; + case 2: + G_bolos_ux_context.hslider3_before = BOLOS_UX_HSLIDER3_NONE; + // G_bolos_ux_context.hslider3_before = 1; // full rotate + G_bolos_ux_context.hslider3_current = 0; + G_bolos_ux_context.hslider3_after = 1; + break; + default: + G_bolos_ux_context.hslider3_before = total_count - 1; + G_bolos_ux_context.hslider3_current = 0; + G_bolos_ux_context.hslider3_after = 1; + break; + } } void bolos_ux_hslider3_set_current(unsigned int current) { - // index is reachable ? - if (G_bolos_ux_context.hslider3_total > current) { - // reach it - while (G_bolos_ux_context.hslider3_current != current) { - bolos_ux_hslider3_next(); + // index is reachable ? + if (G_bolos_ux_context.hslider3_total > current) { + // reach it + while (G_bolos_ux_context.hslider3_current != current) { + bolos_ux_hslider3_next(); + } } - } } - void bolos_ux_hslider3_next(void) { - switch (G_bolos_ux_context.hslider3_total) { - case 0: - case 1: - break; - case 2: - switch(G_bolos_ux_context.hslider3_current) { + switch (G_bolos_ux_context.hslider3_total) { case 0: - G_bolos_ux_context.hslider3_before = 0; - G_bolos_ux_context.hslider3_current = 1; - G_bolos_ux_context.hslider3_after = BOLOS_UX_HSLIDER3_NONE; - break; case 1: - G_bolos_ux_context.hslider3_before = BOLOS_UX_HSLIDER3_NONE; - G_bolos_ux_context.hslider3_current = 0; - G_bolos_ux_context.hslider3_after = 1; - break; - } - break; - default: - G_bolos_ux_context.hslider3_before = G_bolos_ux_context.hslider3_current; - G_bolos_ux_context.hslider3_current = G_bolos_ux_context.hslider3_after; - G_bolos_ux_context.hslider3_after = (G_bolos_ux_context.hslider3_after+1)%G_bolos_ux_context.hslider3_total; - break; - } + break; + case 2: + switch (G_bolos_ux_context.hslider3_current) { + case 0: + G_bolos_ux_context.hslider3_before = 0; + G_bolos_ux_context.hslider3_current = 1; + G_bolos_ux_context.hslider3_after = BOLOS_UX_HSLIDER3_NONE; + break; + case 1: + G_bolos_ux_context.hslider3_before = BOLOS_UX_HSLIDER3_NONE; + G_bolos_ux_context.hslider3_current = 0; + G_bolos_ux_context.hslider3_after = 1; + break; + } + break; + default: + G_bolos_ux_context.hslider3_before = G_bolos_ux_context.hslider3_current; + G_bolos_ux_context.hslider3_current = G_bolos_ux_context.hslider3_after; + G_bolos_ux_context.hslider3_after = + (G_bolos_ux_context.hslider3_after + 1) % G_bolos_ux_context.hslider3_total; + break; + } } void bolos_ux_hslider3_previous(void) { - switch (G_bolos_ux_context.hslider3_total) { - case 0: - case 1: - break; - case 2: - switch(G_bolos_ux_context.hslider3_current) { + switch (G_bolos_ux_context.hslider3_total) { case 0: - G_bolos_ux_context.hslider3_before = 0; - G_bolos_ux_context.hslider3_current = 1; - G_bolos_ux_context.hslider3_after = BOLOS_UX_HSLIDER3_NONE; - break; case 1: - G_bolos_ux_context.hslider3_before = BOLOS_UX_HSLIDER3_NONE; - G_bolos_ux_context.hslider3_current = 0; - G_bolos_ux_context.hslider3_after = 1; - break; - } - break; - default: - G_bolos_ux_context.hslider3_after = G_bolos_ux_context.hslider3_current; - G_bolos_ux_context.hslider3_current = G_bolos_ux_context.hslider3_before; - G_bolos_ux_context.hslider3_before = (G_bolos_ux_context.hslider3_before+G_bolos_ux_context.hslider3_total-1)%G_bolos_ux_context.hslider3_total; - break; - } + break; + case 2: + switch (G_bolos_ux_context.hslider3_current) { + case 0: + G_bolos_ux_context.hslider3_before = 0; + G_bolos_ux_context.hslider3_current = 1; + G_bolos_ux_context.hslider3_after = BOLOS_UX_HSLIDER3_NONE; + break; + case 1: + G_bolos_ux_context.hslider3_before = BOLOS_UX_HSLIDER3_NONE; + G_bolos_ux_context.hslider3_current = 0; + G_bolos_ux_context.hslider3_after = 1; + break; + } + break; + default: + G_bolos_ux_context.hslider3_after = G_bolos_ux_context.hslider3_current; + G_bolos_ux_context.hslider3_current = G_bolos_ux_context.hslider3_before; + G_bolos_ux_context.hslider3_before = + (G_bolos_ux_context.hslider3_before + G_bolos_ux_context.hslider3_total - 1) % + G_bolos_ux_context.hslider3_total; + break; + } } unsigned int screen_consent_button(unsigned int button_mask, unsigned int button_mask_counter) { - UNUSED(button_mask_counter); - switch(button_mask) { - case BUTTON_EVT_RELEASED|BUTTON_LEFT: - G_ux.exit_code = BOLOS_UX_CANCEL; - break; - case BUTTON_EVT_RELEASED|BUTTON_RIGHT: - G_ux.exit_code = BOLOS_UX_OK; - break; - } - return 0; + UNUSED(button_mask_counter); + switch (button_mask) { + case BUTTON_EVT_RELEASED | BUTTON_LEFT: + G_ux.exit_code = BOLOS_UX_CANCEL; + break; + case BUTTON_EVT_RELEASED | BUTTON_RIGHT: + G_ux.exit_code = BOLOS_UX_OK; + break; + } + return 0; } unsigned int screen_consent_ticker(unsigned int ignored) { - UNUSED(ignored); + UNUSED(ignored); - // prepare displaying next screen - G_bolos_ux_context.onboarding_index=(G_bolos_ux_context.onboarding_index+1)%G_bolos_ux_context.onboarding_step; + // prepare displaying next screen + G_bolos_ux_context.onboarding_index = + (G_bolos_ux_context.onboarding_index + 1) % G_bolos_ux_context.onboarding_step; - // redisplay taking into account the new counter - ux_stack_display(0); - return 0; + // redisplay taking into account the new counter + ux_stack_display(0); + return 0; } void screen_consent_set_interval(unsigned int interval_ms) { - G_ux.stack[0].ticker_value = interval_ms; - G_ux.stack[0].ticker_interval = interval_ms; + G_ux.stack[0].ticker_value = interval_ms; + G_ux.stack[0].ticker_interval = interval_ms; } -void screen_consent_ticker_init(unsigned int number_of_steps, unsigned int interval_ms, unsigned int check_pin_to_confirm) { - UNUSED(check_pin_to_confirm); - // register action callbacks - G_ux.stack[0].ticker_value = interval_ms; - G_ux.stack[0].ticker_interval = interval_ms; - G_ux.stack[0].ticker_callback = screen_consent_ticker; - /*if (!check_pin_to_confirm || ! os_perso_isonboarded() == BOLOS_UX_OK) { managed at bolos task level */ +void screen_consent_ticker_init(unsigned int number_of_steps, + unsigned int interval_ms, + unsigned int check_pin_to_confirm) { + UNUSED(check_pin_to_confirm); + // register action callbacks + G_ux.stack[0].ticker_value = interval_ms; + G_ux.stack[0].ticker_interval = interval_ms; + G_ux.stack[0].ticker_callback = screen_consent_ticker; + /*if (!check_pin_to_confirm || ! os_perso_isonboarded() == BOLOS_UX_OK) { managed at bolos task + * level */ G_ux.stack[0].button_push_callback = screen_consent_button; - /*} - else { - G_ux.stack[0].button_push_callback = screen_consent_button_with_final_pin; - }*/ - - // start displaying - G_bolos_ux_context.onboarding_index = number_of_steps-1; - G_bolos_ux_context.onboarding_step = number_of_steps; - screen_consent_ticker(0); + /*} + else { + G_ux.stack[0].button_push_callback = screen_consent_button_with_final_pin; + }*/ + + // start displaying + G_bolos_ux_context.onboarding_index = number_of_steps - 1; + G_bolos_ux_context.onboarding_step = number_of_steps; + screen_consent_ticker(0); } -#endif // OS_IO_SEPROXYHAL +#endif // OS_IO_SEPROXYHAL #endif diff --git a/src/bolos_ux_nanox.h b/src/bolos_ux_nanox.h index f34bbcf5..d901720e 100644 --- a/src/bolos_ux_nanox.h +++ b/src/bolos_ux_nanox.h @@ -8,189 +8,200 @@ #include "ux.h" -typedef unsigned int (*pin_callback_t) (unsigned char* pin_buffer, unsigned int pin_length); - -#define KEYBOARD_ITEM_VALIDATED 1 // callback is called with the entered item index, tmp_element is precharged with element to be displayed and using the common string buffer as string parameter -#define KEYBOARD_RENDER_ITEM 2 // callback is called the element index, tmp_element is precharged with element to be displayed and using the common string buffer as string parameter -#define KEYBOARD_RENDER_WORD 3 // callback is called with a -1 when requesting complete word, or the char index else, returnin 0 implies no char is to be displayed -typedef const bagl_element_t* (*keyboard_callback_t) (unsigned int event, unsigned int value); - - -#define ICON_14x14_SIZE_B (UPPER_ALIGN(GLYPH_dashboard_mask_WIDTH*GLYPH_dashboard_mask_HEIGHT, 8, unsigned int) / 8) +typedef unsigned int (*pin_callback_t)(unsigned char* pin_buffer, unsigned int pin_length); + +#define KEYBOARD_ITEM_VALIDATED \ + 1 // callback is called with the entered item index, tmp_element is precharged with element to + // be displayed and using the common string buffer as string parameter +#define KEYBOARD_RENDER_ITEM \ + 2 // callback is called the element index, tmp_element is precharged with element to be + // displayed and using the common string buffer as string parameter +#define KEYBOARD_RENDER_WORD \ + 3 // callback is called with a -1 when requesting complete word, or the char index else, + // returnin 0 implies no char is to be displayed +typedef const bagl_element_t* (*keyboard_callback_t)(unsigned int event, unsigned int value); + +#define ICON_14x14_SIZE_B \ + (UPPER_ALIGN(GLYPH_dashboard_mask_WIDTH * GLYPH_dashboard_mask_HEIGHT, 8, unsigned int) / 8) typedef struct { - bagl_icon_details_t details; - unsigned char bitmap[ICON_14x14_SIZE_B]; + bagl_icon_details_t details; + unsigned char bitmap[ICON_14x14_SIZE_B]; } ux_icon_buffer_t; #define APPLICATION_MAXCOUNT 100 // bolos ux context (not mandatory if redesigning a bolos ux) typedef struct bolos_ux_context { - - #define STATE_UNINITIALIZED 0 +#define STATE_UNINITIALIZED 0 #ifndef STATE_INITIALIZED - #define STATE_INITIALIZED 0xB01055E5UL -#endif // STATE_INITIALIZED - unsigned int state; +#define STATE_INITIALIZED 0xB01055E5UL +#endif // STATE_INITIALIZED + unsigned int state; - // a screen pop occured, the underlaying screen must optimize its drawing as we've probably trashed the whole screen - unsigned int screen_redraw; + // a screen pop occured, the underlaying screen must optimize its drawing as we've probably + // trashed the whole screen + unsigned int screen_redraw; #define BATTERY_CHECK_INTERVAL_MS 5000 - unsigned int ms_last_batt_check; + unsigned int ms_last_batt_check; + + unsigned int ms; + unsigned int setting_auto_lock_delay_ms; + unsigned int setting_power_off_delay_ms; - unsigned int ms; - unsigned int setting_auto_lock_delay_ms; - unsigned int setting_power_off_delay_ms; - #define AUTO_LOCK_DEFAULT 300000 #define POWER_OFF_DEFAULT 600000 -#define IS_SETTING_PRE_POWER_OFF() (G_bolos_ux_context.setting_auto_lock_delay_ms != -1UL && G_bolos_ux_context.setting_auto_lock_delay_ms != 0) -#define IS_SETTING_POWER_OFF() (G_bolos_ux_context.setting_power_off_delay_ms != -1UL && G_bolos_ux_context.setting_power_off_delay_ms != 0) -#define INACTIVITY_MS_AUTO_LOCK (G_bolos_ux_context.setting_auto_lock_delay_ms) - #define INACTIVITY_MS_POWER_OFF (/*G_bolos_ux_context.setting_auto_lock_delay_ms+*/G_bolos_ux_context.setting_power_off_delay_ms) - unsigned int ms_last_activity; - unsigned int ms_last_activity_saver; - unsigned int ms_last_activity_pwroff; - - unsigned int last_button_state; - - // control center display push both buttons delay +#define IS_SETTING_PRE_POWER_OFF() \ + (G_bolos_ux_context.setting_auto_lock_delay_ms != -1UL && \ + G_bolos_ux_context.setting_auto_lock_delay_ms != 0) +#define IS_SETTING_POWER_OFF() \ + (G_bolos_ux_context.setting_power_off_delay_ms != -1UL && \ + G_bolos_ux_context.setting_power_off_delay_ms != 0) +#define INACTIVITY_MS_AUTO_LOCK (G_bolos_ux_context.setting_auto_lock_delay_ms) +#define INACTIVITY_MS_POWER_OFF \ + (/*G_bolos_ux_context.setting_auto_lock_delay_ms+*/ G_bolos_ux_context \ + .setting_power_off_delay_ms) + unsigned int ms_last_activity; + unsigned int ms_last_activity_saver; + unsigned int ms_last_activity_pwroff; + + unsigned int last_button_state; + + // control center display push both buttons delay #define BOLOS_CC_PERIOD_MS 1500UL - unsigned int ms_cc_start; + unsigned int ms_cc_start; - enum { - INACTIVITY_NONE, - INACTIVITY_LOCK, - INACTIVITY_SAVER, - } inactivity_state; + enum { + INACTIVITY_NONE, + INACTIVITY_LOCK, + INACTIVITY_SAVER, + } inactivity_state; - unsigned int control_center_entering; + unsigned int control_center_entering; - unsigned int last_ux_id; + unsigned int last_ux_id; -#define BOLOS_UX_ONBOARDING_NEW 1 -#define BOLOS_UX_ONBOARDING_NEW_12 12 -#define BOLOS_UX_ONBOARDING_NEW_18 18 -#define BOLOS_UX_ONBOARDING_NEW_24 24 -#define BOLOS_UX_ONBOARDING_RESTORE 2 +#define BOLOS_UX_ONBOARDING_NEW 1 +#define BOLOS_UX_ONBOARDING_NEW_12 12 +#define BOLOS_UX_ONBOARDING_NEW_18 18 +#define BOLOS_UX_ONBOARDING_NEW_24 24 +#define BOLOS_UX_ONBOARDING_RESTORE 2 #define BOLOS_UX_ONBOARDING_RESTORE_12 12 #define BOLOS_UX_ONBOARDING_RESTORE_18 18 #define BOLOS_UX_ONBOARDING_RESTORE_24 24 - unsigned int onboarding_kind; + unsigned int onboarding_kind; #ifdef HAVE_ELECTRUM - unsigned int onboarding_algorithm; + unsigned int onboarding_algorithm; #endif - unsigned int onboarding_step; - unsigned int onboarding_index; - unsigned int onboarding_words_checked; - unsigned int onboarding_words_are_valid; - unsigned int onboarding_step_checked_inc; - unsigned int onboarding_step_checked; - + unsigned int onboarding_step; + unsigned int onboarding_index; + unsigned int onboarding_words_checked; + unsigned int onboarding_words_are_valid; + unsigned int onboarding_step_checked_inc; + unsigned int onboarding_step_checked; - - unsigned int words_buffer_length; - // 128 of words (215 => hashed to 64, or 128) + HMAC_LENGTH*2 = 256 + unsigned int words_buffer_length; + // 128 of words (215 => hashed to 64, or 128) + HMAC_LENGTH*2 = 256 #define WORDS_BUFFER_MAX_SIZE_B 257 - char words_buffer[WORDS_BUFFER_MAX_SIZE_B]; + char words_buffer[WORDS_BUFFER_MAX_SIZE_B]; #define MAX_PIN_LENGTH 8 #define MIN_PIN_LENGTH 4 - pin_callback_t pin_end_callback; - char pin_buffer[MAX_PIN_LENGTH+1]; // length prepended for custom pin length - char pin_digit_buffer; // digit to be displayed + pin_callback_t pin_end_callback; + char pin_buffer[MAX_PIN_LENGTH + 1]; // length prepended for custom pin length + char pin_digit_buffer; // digit to be displayed - appmain_t flow_end_callback; + appmain_t flow_end_callback; - // unsigned int settings_index; - // unsigned int settings_value; + // unsigned int settings_index; + // unsigned int settings_value; - bagl_element_t saver_element; - // int saver_step_x; - // int saver_step_y; + bagl_element_t saver_element; + // int saver_step_x; + // int saver_step_y; #define SAVER_ELEMENTS 8 - struct { - int step_x; - int step_y; - int x; - int y; - } savers[SAVER_ELEMENTS]; - - // label line for common PIN and common keyboard screen (displayed over the entry) - const char* common_label; - - unsigned int pairing_stack_slot_plus_1; - - // slider management / menu list management - unsigned int hslider3_before; - unsigned int hslider3_current; - unsigned int hslider3_after; - unsigned int hslider3_total; - - keyboard_callback_t keyboard_callback; - unsigned char keyboard_user_buffer[BOLOS_UX_KEYBOARD_TEXT_BUFFER_SIZE+1]; - - unsigned int last_installed_os_index; - - unsigned int control_center_enabled; - unsigned int control_center_current; - unsigned int control_center_total; - unsigned char* control_center_mapping; - unsigned char control_center_string[MAX(128,sizeof(bagl_icon_details_t)-1)]; - - unsigned int dashboard_current; // index of the currently selected icon on the dashboard - unsigned int dashboard_previous; // index of the previously selected icon on the dashboard - unsigned int dashboard_first; // index of the first item on the top line displayed on the dashboard - unsigned int dashboard_total; - unsigned char dashboard_sorted_app_os_idx[APPLICATION_MAXCOUNT]; - unsigned int dashboard_last_selected; - //unsigned int dashboard_redisplayed; // to trigger animation when all elements are displayed - //unsigned int dashboard_no_apps_displayed; // when the message saying no apps installed has been displayed or not - // in case autostart is engaged, to avoid starting the app multiple times - unsigned int app_auto_started; - - unsigned int overlay_refresh; - unsigned int battery_percentage; - unsigned int status_batt_level; - unsigned int status_flags; - unsigned int batt_low_displayed; - unsigned int batt_critical_displayed; - - -#define BATTERY_FULL_CHARGE_MV 4200 // 100% -#define BATTERY_SUFFICIENT_CHARGE_MV 3840 // 40% -#define BATTERY_LOW_LEVEL_MV 3750 // 25% -#define BATTERY_CRITICAL_LEVEL_MV 3460 // 10% -#define BATTERY_AUTO_POWER_OFF_LEVEL_MV 3200 // 0% + struct { + int step_x; + int step_y; + int x; + int y; + } savers[SAVER_ELEMENTS]; + + // label line for common PIN and common keyboard screen (displayed over the entry) + const char* common_label; + + unsigned int pairing_stack_slot_plus_1; + + // slider management / menu list management + unsigned int hslider3_before; + unsigned int hslider3_current; + unsigned int hslider3_after; + unsigned int hslider3_total; + + keyboard_callback_t keyboard_callback; + unsigned char keyboard_user_buffer[BOLOS_UX_KEYBOARD_TEXT_BUFFER_SIZE + 1]; + + unsigned int last_installed_os_index; + + unsigned int control_center_enabled; + unsigned int control_center_current; + unsigned int control_center_total; + unsigned char* control_center_mapping; + unsigned char control_center_string[MAX(128, sizeof(bagl_icon_details_t) - 1)]; + + unsigned int dashboard_current; // index of the currently selected icon on the dashboard + unsigned int dashboard_previous; // index of the previously selected icon on the dashboard + unsigned int + dashboard_first; // index of the first item on the top line displayed on the dashboard + unsigned int dashboard_total; + unsigned char dashboard_sorted_app_os_idx[APPLICATION_MAXCOUNT]; + unsigned int dashboard_last_selected; + // unsigned int dashboard_redisplayed; // to trigger animation when all elements are displayed + // unsigned int dashboard_no_apps_displayed; // when the message saying no apps installed has + // been displayed or not + // in case autostart is engaged, to avoid starting the app multiple times + unsigned int app_auto_started; + + unsigned int overlay_refresh; + unsigned int battery_percentage; + unsigned int status_batt_level; + unsigned int status_flags; + unsigned int batt_low_displayed; + unsigned int batt_critical_displayed; + +#define BATTERY_FULL_CHARGE_MV 4200 // 100% +#define BATTERY_SUFFICIENT_CHARGE_MV 3840 // 40% +#define BATTERY_LOW_LEVEL_MV 3750 // 25% +#define BATTERY_CRITICAL_LEVEL_MV 3460 // 10% +#define BATTERY_AUTO_POWER_OFF_LEVEL_MV 3200 // 0% #define BATTERY_FULL_CHARGE_PERCENT 95 #define BATTERY_SUFFICIENT_CHARGE_PERCENT 40 #define BATTERY_LOW_LEVEL_PERCENT 25 #define BATTERY_CRITICAL_LEVEL_PERCENT 10 -#define BATTERY_AUTO_POWER_OFF_LEVEL_PERCENT 2 +#define BATTERY_AUTO_POWER_OFF_LEVEL_PERCENT 2 - // to return to the current context after help screens have been validated - appmain_t help_ended_callback; - unsigned int help_screen_idx; + // to return to the current context after help screens have been validated + appmain_t help_ended_callback; + unsigned int help_screen_idx; - // detect stack/global variable overlap - // have a zero byte to avoid buffer overflow from strings in the ux (we never know) + // detect stack/global variable overlap + // have a zero byte to avoid buffer overflow from strings in the ux (we never know) #define CANARY_MAGIC 0x7600E9AB - unsigned int canary; + unsigned int canary; - // for CheckSeed app only - uint8_t input_seed_is_identical; - uint8_t processing; + // for CheckSeed app only + uint8_t input_seed_is_identical; + uint8_t processing; } bolos_ux_context_t; extern bolos_ux_context_t G_bolos_ux_context; -void ux_stack_display_elements(ux_stack_slot_t* slot); // not to be displayed in the SDK +void ux_stack_display_elements(ux_stack_slot_t* slot); // not to be displayed in the SDK // update before, current, after index for horizontal slider with 3 positions // slider distinguish handling from the data, to be more generic :) @@ -204,27 +215,23 @@ void bolos_ux_hslider3_previous(void); #define FAST_LIST_ACTION_CS 2 /** - * Bolos system app internal UX entry point (could be overriden by a further loaded BOLOS_UX application) + * Bolos system app internal UX entry point (could be overriden by a further loaded BOLOS_UX + * application) */ void bolos_ux_main(void); #include "bolos_ux_common_bip39.h" +#define UX_FLOW_AFTER_PIN(stepname, stackslot, callback) \ + UX_STEP_INIT(stepname, NULL, NULL, { \ + /* invalidate pin and display pin lock */ \ + screen_modal_validate_pin_init(); \ + /* display processing screen on slot 0 (pin modal has been pushed over) */ \ + /* after display then perform */ \ + ux_stack_init(stackslot); \ + G_ux.stack[stackslot].displayed_callback = callback; \ + }); -#define UX_FLOW_AFTER_PIN(stepname, stackslot, callback) \ - UX_STEP_INIT( \ - stepname, \ - NULL, \ - NULL, \ - { \ - /* invalidate pin and display pin lock */ \ - screen_modal_validate_pin_init(); \ - /* display processing screen on slot 0 (pin modal has been pushed over) */ \ - /* after display then perform */ \ - ux_stack_init(stackslot); \ - G_ux.stack[stackslot].displayed_callback = callback; \ - }); - -#endif // HAVE_BOLOS_UX +#endif // HAVE_BOLOS_UX -#endif // BOLOS_UX_H +#endif // BOLOS_UX_H diff --git a/src/bolos_ux_nanox_keyboard.c b/src/bolos_ux_nanox_keyboard.c index 0184881c..2b931b73 100644 --- a/src/bolos_ux_nanox_keyboard.c +++ b/src/bolos_ux_nanox_keyboard.c @@ -15,137 +15,318 @@ #ifdef OS_IO_SEPROXYHAL - const bagl_element_t screen_common_keyboard_elements[] = { - // erase - {{BAGL_RECTANGLE , 0x00, 0, 0, 128, 64, 0, 0, BAGL_FILL, 0x000000, 0xFFFFFF, 0, 0}, NULL}, - - // title - {{BAGL_LABELINE , 0x04, 0, 20, 128, 32-5, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_REGULAR_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL}, - - // typed word - {{BAGL_LABELINE , 0x10, 128/2-12/2-40, 48+5, 14, 14, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL}, - {{BAGL_LABELINE , 0x11, 128/2-12/2-30, 48+5, 14, 14, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL}, - {{BAGL_LABELINE , 0x12, 128/2-12/2-20, 48+5, 14, 14, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL}, - {{BAGL_LABELINE , 0x13, 128/2-12/2-10, 48+5, 14, 14, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL}, - {{BAGL_LABELINE , 0x14, 128/2-12/2 , 48+5, 14, 14, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL}, - {{BAGL_LABELINE , 0x15, 128/2-12/2+10, 48+5, 14, 14, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL}, - {{BAGL_LABELINE , 0x16, 128/2-12/2+20, 48+5, 14, 14, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL}, - {{BAGL_LABELINE , 0x17, 128/2-12/2+30, 48+5, 14, 14, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL}, - {{BAGL_LABELINE , 0x18, 128/2-12/2+40, 48+5, 14, 14, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL}, - - // slider elements - {{BAGL_LABELINE , 0x01, 29, 36, 14, 13, 0, 0, BAGL_FILL, 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_REGULAR_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL}, - {{BAGL_LINE , 0x06, 48, 32, 4, 1, 0, 0, 0 , 0xFFFFFF, 0x000000, 0, 0 }, NULL}, - {{BAGL_RECTANGLE , 0x00, 57, 36-10, 14, 14, 0, 4, BAGL_FILL, 0xFFFFFF, 0x000000, 0, 0}, NULL}, - {{BAGL_LABELINE , 0x02, 58, 36, 12, 13, 0, 0, BAGL_FILL, 0x000000, 0xFFFFFF, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL}, - {{BAGL_LINE , 0x07, 76, 32, 4, 1, 0, 0, 0 , 0xFFFFFF, 0x000000, 0, 0 }, NULL}, - {{BAGL_LABELINE , 0x03, 85, 36, 14, 13, 0, 0, BAGL_FILL, 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_REGULAR_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, NULL}, - - // left/rights icons - {{BAGL_ICON , 0x0A, 2, 28, 4, 7, 0, 0, 0 , 0xFFFFFF, 0x000000, 0, 0 }, (const char *) &C_icon_left}, - {{BAGL_ICON , 0x0B, 122, 28, 4, 7, 0, 0, 0 , 0xFFFFFF, 0x000000, 0, 0 }, (const char *) &C_icon_right}, + // erase + {{BAGL_RECTANGLE, 0x00, 0, 0, 128, 64, 0, 0, BAGL_FILL, 0x000000, 0xFFFFFF, 0, 0}, NULL}, + + // title + {{BAGL_LABELINE, + 0x04, + 0, + 20, + 128, + 32 - 5, + 0, + 0, + 0, + 0xFFFFFF, + 0x000000, + BAGL_FONT_OPEN_SANS_REGULAR_11px | BAGL_FONT_ALIGNMENT_CENTER, + 0}, + NULL}, + + // typed word + {{BAGL_LABELINE, + 0x10, + 128 / 2 - 12 / 2 - 40, + 48 + 5, + 14, + 14, + 0, + 0, + 0, + 0xFFFFFF, + 0x000000, + BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, + 0}, + NULL}, + {{BAGL_LABELINE, + 0x11, + 128 / 2 - 12 / 2 - 30, + 48 + 5, + 14, + 14, + 0, + 0, + 0, + 0xFFFFFF, + 0x000000, + BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, + 0}, + NULL}, + {{BAGL_LABELINE, + 0x12, + 128 / 2 - 12 / 2 - 20, + 48 + 5, + 14, + 14, + 0, + 0, + 0, + 0xFFFFFF, + 0x000000, + BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, + 0}, + NULL}, + {{BAGL_LABELINE, + 0x13, + 128 / 2 - 12 / 2 - 10, + 48 + 5, + 14, + 14, + 0, + 0, + 0, + 0xFFFFFF, + 0x000000, + BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, + 0}, + NULL}, + {{BAGL_LABELINE, + 0x14, + 128 / 2 - 12 / 2, + 48 + 5, + 14, + 14, + 0, + 0, + 0, + 0xFFFFFF, + 0x000000, + BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, + 0}, + NULL}, + {{BAGL_LABELINE, + 0x15, + 128 / 2 - 12 / 2 + 10, + 48 + 5, + 14, + 14, + 0, + 0, + 0, + 0xFFFFFF, + 0x000000, + BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, + 0}, + NULL}, + {{BAGL_LABELINE, + 0x16, + 128 / 2 - 12 / 2 + 20, + 48 + 5, + 14, + 14, + 0, + 0, + 0, + 0xFFFFFF, + 0x000000, + BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, + 0}, + NULL}, + {{BAGL_LABELINE, + 0x17, + 128 / 2 - 12 / 2 + 30, + 48 + 5, + 14, + 14, + 0, + 0, + 0, + 0xFFFFFF, + 0x000000, + BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, + 0}, + NULL}, + {{BAGL_LABELINE, + 0x18, + 128 / 2 - 12 / 2 + 40, + 48 + 5, + 14, + 14, + 0, + 0, + 0, + 0xFFFFFF, + 0x000000, + BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, + 0}, + NULL}, + + // slider elements + {{BAGL_LABELINE, + 0x01, + 29, + 36, + 14, + 13, + 0, + 0, + BAGL_FILL, + 0xFFFFFF, + 0x000000, + BAGL_FONT_OPEN_SANS_REGULAR_11px | BAGL_FONT_ALIGNMENT_CENTER, + 0}, + NULL}, + {{BAGL_LINE, 0x06, 48, 32, 4, 1, 0, 0, 0, 0xFFFFFF, 0x000000, 0, 0}, NULL}, + {{BAGL_RECTANGLE, 0x00, 57, 36 - 10, 14, 14, 0, 4, BAGL_FILL, 0xFFFFFF, 0x000000, 0, 0}, NULL}, + {{BAGL_LABELINE, + 0x02, + 58, + 36, + 12, + 13, + 0, + 0, + BAGL_FILL, + 0x000000, + 0xFFFFFF, + BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, + 0}, + NULL}, + {{BAGL_LINE, 0x07, 76, 32, 4, 1, 0, 0, 0, 0xFFFFFF, 0x000000, 0, 0}, NULL}, + {{BAGL_LABELINE, + 0x03, + 85, + 36, + 14, + 13, + 0, + 0, + BAGL_FILL, + 0xFFFFFF, + 0x000000, + BAGL_FONT_OPEN_SANS_REGULAR_11px | BAGL_FONT_ALIGNMENT_CENTER, + 0}, + NULL}, + + // left/rights icons + {{BAGL_ICON, 0x0A, 2, 28, 4, 7, 0, 0, 0, 0xFFFFFF, 0x000000, 0, 0}, (const char*) &C_icon_left}, + {{BAGL_ICON, 0x0B, 122, 28, 4, 7, 0, 0, 0, 0xFFFFFF, 0x000000, 0, 0}, + (const char*) &C_icon_right}, }; -const bagl_element_t* screen_common_keyboard_before_element_display_callback(const bagl_element_t* element) { - - // copy element to be displayed - memcpy(&G_ux.tmp_element, PIC(element), sizeof(G_ux.tmp_element)); - - switch(element->component.userid) { - case 0x01: - if (G_bolos_ux_context.hslider3_before == BOLOS_UX_HSLIDER3_NONE) { - return 0; - } - return G_bolos_ux_context.keyboard_callback(KEYBOARD_RENDER_ITEM, G_bolos_ux_context.hslider3_before); - - case 0x02: - return G_bolos_ux_context.keyboard_callback(KEYBOARD_RENDER_ITEM, G_bolos_ux_context.hslider3_current); - - case 0x03: - if (G_bolos_ux_context.hslider3_after == BOLOS_UX_HSLIDER3_NONE) { - return 0; - } - return G_bolos_ux_context.keyboard_callback(KEYBOARD_RENDER_ITEM, G_bolos_ux_context.hslider3_after); - - case 0x06: - if (G_bolos_ux_context.hslider3_before == BOLOS_UX_HSLIDER3_NONE) { - return 0; // don't display - } - break; - - case 0x04: - // display the title - G_ux.tmp_element.text = G_bolos_ux_context.common_label; - break; - - case 0x07: - if (G_bolos_ux_context.hslider3_after == BOLOS_UX_HSLIDER3_NONE) { - return 0; // don't display - } - break; - - default: - if (element->component.userid & 0x10) { - // request the xieth word char - return G_bolos_ux_context.keyboard_callback(KEYBOARD_RENDER_WORD, element->component.userid&0x0F); - } - break; - } - // return the probably modded element by the callback function - return &G_ux.tmp_element; +const bagl_element_t* screen_common_keyboard_before_element_display_callback( + const bagl_element_t* element) { + // copy element to be displayed + memcpy(&G_ux.tmp_element, PIC(element), sizeof(G_ux.tmp_element)); + + switch (element->component.userid) { + case 0x01: + if (G_bolos_ux_context.hslider3_before == BOLOS_UX_HSLIDER3_NONE) { + return 0; + } + return G_bolos_ux_context.keyboard_callback(KEYBOARD_RENDER_ITEM, + G_bolos_ux_context.hslider3_before); + + case 0x02: + return G_bolos_ux_context.keyboard_callback(KEYBOARD_RENDER_ITEM, + G_bolos_ux_context.hslider3_current); + + case 0x03: + if (G_bolos_ux_context.hslider3_after == BOLOS_UX_HSLIDER3_NONE) { + return 0; + } + return G_bolos_ux_context.keyboard_callback(KEYBOARD_RENDER_ITEM, + G_bolos_ux_context.hslider3_after); + + case 0x06: + if (G_bolos_ux_context.hslider3_before == BOLOS_UX_HSLIDER3_NONE) { + return 0; // don't display + } + break; + + case 0x04: + // display the title + G_ux.tmp_element.text = G_bolos_ux_context.common_label; + break; + + case 0x07: + if (G_bolos_ux_context.hslider3_after == BOLOS_UX_HSLIDER3_NONE) { + return 0; // don't display + } + break; + + default: + if (element->component.userid & 0x10) { + // request the xieth word char + return G_bolos_ux_context.keyboard_callback(KEYBOARD_RENDER_WORD, + element->component.userid & 0x0F); + } + break; + } + // return the probably modded element by the callback function + return &G_ux.tmp_element; } -unsigned int screen_common_keyboard_button(unsigned int button_mask, unsigned int button_mask_counter) { - UNUSED(button_mask_counter); - - switch(button_mask) { - case BUTTON_EVT_RELEASED|BUTTON_LEFT|BUTTON_RIGHT: // validate current digit - - // validate the item, and if accepted, then redisplay current screen, else don't draw anything - if (G_bolos_ux_context.keyboard_callback(KEYBOARD_ITEM_VALIDATED, G_bolos_ux_context.hslider3_current)) { - goto redraw; - } - break; - - case BUTTON_EVT_FAST|BUTTON_LEFT: - case BUTTON_EVT_RELEASED|BUTTON_LEFT: - bolos_ux_hslider3_previous(); - goto redraw; - - case BUTTON_EVT_FAST|BUTTON_RIGHT: - case BUTTON_EVT_RELEASED|BUTTON_RIGHT: - bolos_ux_hslider3_next(); - - redraw: - ux_stack_display(G_ux.stack_count-1); - break; - } - return 1; +unsigned int screen_common_keyboard_button(unsigned int button_mask, + unsigned int button_mask_counter) { + UNUSED(button_mask_counter); + + switch (button_mask) { + case BUTTON_EVT_RELEASED | BUTTON_LEFT | BUTTON_RIGHT: // validate current digit + + // validate the item, and if accepted, then redisplay current screen, else don't draw + // anything + if (G_bolos_ux_context.keyboard_callback(KEYBOARD_ITEM_VALIDATED, + G_bolos_ux_context.hslider3_current)) { + goto redraw; + } + break; + + case BUTTON_EVT_FAST | BUTTON_LEFT: + case BUTTON_EVT_RELEASED | BUTTON_LEFT: + bolos_ux_hslider3_previous(); + goto redraw; + + case BUTTON_EVT_FAST | BUTTON_RIGHT: + case BUTTON_EVT_RELEASED | BUTTON_RIGHT: + bolos_ux_hslider3_next(); + + redraw: + ux_stack_display(G_ux.stack_count - 1); + break; + } + return 1; } -void screen_common_keyboard_init(unsigned int stack_slot, unsigned int current_element, unsigned int nb_elements, keyboard_callback_t callback) { - unsigned int current = G_bolos_ux_context.hslider3_current; - ux_stack_init(stack_slot); - - // initiate the rotating modulo iterator - bolos_ux_hslider3_init(nb_elements); - if (current_element == COMMON_KEYBOARD_INDEX_UNCHANGED) { - bolos_ux_hslider3_set_current(current); - } - else { - bolos_ux_hslider3_set_current(current_element); - } - - G_ux.stack[stack_slot].element_arrays[0].element_array = screen_common_keyboard_elements; - G_ux.stack[stack_slot].element_arrays[0].element_array_count = ARRAYLEN(screen_common_keyboard_elements); - G_ux.stack[stack_slot].element_arrays_count = 1; - G_ux.stack[stack_slot].screen_before_element_display_callback = screen_common_keyboard_before_element_display_callback; // used for each screen of the validate pin flow - G_ux.stack[stack_slot].button_push_callback = screen_common_keyboard_button; - G_bolos_ux_context.keyboard_callback = callback; - - ux_stack_display(stack_slot); +void screen_common_keyboard_init(unsigned int stack_slot, + unsigned int current_element, + unsigned int nb_elements, + keyboard_callback_t callback) { + unsigned int current = G_bolos_ux_context.hslider3_current; + ux_stack_init(stack_slot); + + // initiate the rotating modulo iterator + bolos_ux_hslider3_init(nb_elements); + if (current_element == COMMON_KEYBOARD_INDEX_UNCHANGED) { + bolos_ux_hslider3_set_current(current); + } else { + bolos_ux_hslider3_set_current(current_element); + } + + G_ux.stack[stack_slot].element_arrays[0].element_array = screen_common_keyboard_elements; + G_ux.stack[stack_slot].element_arrays[0].element_array_count = + ARRAYLEN(screen_common_keyboard_elements); + G_ux.stack[stack_slot].element_arrays_count = 1; + G_ux.stack[stack_slot].screen_before_element_display_callback = + screen_common_keyboard_before_element_display_callback; // used for each screen of the + // validate pin flow + G_ux.stack[stack_slot].button_push_callback = screen_common_keyboard_button; + G_bolos_ux_context.keyboard_callback = callback; + + ux_stack_display(stack_slot); } #endif diff --git a/src/main.c b/src/main.c index 2a3c1ecf..d24d3aee 100644 --- a/src/main.c +++ b/src/main.c @@ -1,19 +1,19 @@ /******************************************************************************* -* Ledger Blue -* (c) 2016 Ledger -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -********************************************************************************/ + * Ledger Blue + * (c) 2016 Ledger + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ********************************************************************************/ #include "os.h" #include "cx.h" @@ -23,8 +23,8 @@ unsigned char G_io_seproxyhal_spi_buffer[IO_SEPROXYHAL_BUFFER_SIZE_B]; -static unsigned int current_text_pos; // parsing cursor in the text to display -static unsigned int text_y; // current location of the displayed text +static unsigned int current_text_pos; // parsing cursor in the text to display +static unsigned int text_y; // current location of the displayed text // UI currently displayed enum UI_STATE { UI_IDLE, UI_TEXT, UI_APPROVAL }; @@ -51,34 +51,32 @@ bolos_ux_params_t G_ux_params; unsigned short io_exchange_al(unsigned char channel, unsigned short tx_len) { switch (channel & ~(IO_FLAGS)) { - case CHANNEL_KEYBOARD: - break; - - // multiplexed io exchange over a SPI channel and TLV encapsulated protocol - case CHANNEL_SPI: - if (tx_len) { - io_seproxyhal_spi_send(G_io_apdu_buffer, tx_len); - - if (channel & IO_RESET_AFTER_REPLIED) { - reset(); + case CHANNEL_KEYBOARD: + break; + + // multiplexed io exchange over a SPI channel and TLV encapsulated protocol + case CHANNEL_SPI: + if (tx_len) { + io_seproxyhal_spi_send(G_io_apdu_buffer, tx_len); + + if (channel & IO_RESET_AFTER_REPLIED) { + reset(); + } + return 0; // nothing received from the master so far (it's a tx + // transaction) + } else { + return io_seproxyhal_spi_recv(G_io_apdu_buffer, sizeof(G_io_apdu_buffer), 0); } - return 0; // nothing received from the master so far (it's a tx - // transaction) - } else { - return io_seproxyhal_spi_recv(G_io_apdu_buffer, - sizeof(G_io_apdu_buffer), 0); - } - default: - THROW(INVALID_PARAMETER); + default: + THROW(INVALID_PARAMETER); } return 0; } static void sample_main(void) { - // next timer callback in 500 ms - //UX_CALLBACK_SET_INTERVAL(500); + // UX_CALLBACK_SET_INTERVAL(500); uint8_t flags = 0; @@ -96,46 +94,44 @@ unsigned char io_event(unsigned char channel __attribute__((unused))) { // can't have more than one tag in the reply, not supported yet. switch (G_io_seproxyhal_spi_buffer[0]) { - case SEPROXYHAL_TAG_FINGER_EVENT: - UX_FINGER_EVENT(G_io_seproxyhal_spi_buffer); - break; + case SEPROXYHAL_TAG_FINGER_EVENT: + UX_FINGER_EVENT(G_io_seproxyhal_spi_buffer); + break; - case SEPROXYHAL_TAG_BUTTON_PUSH_EVENT: // for Nano S - UX_BUTTON_PUSH_EVENT(G_io_seproxyhal_spi_buffer); - break; + case SEPROXYHAL_TAG_BUTTON_PUSH_EVENT: // for Nano S + UX_BUTTON_PUSH_EVENT(G_io_seproxyhal_spi_buffer); + break; - case SEPROXYHAL_TAG_DISPLAY_PROCESSED_EVENT: + case SEPROXYHAL_TAG_DISPLAY_PROCESSED_EVENT: #if defined(TARGET_NANOS) - if ((uiState == UI_TEXT) && - (os_seph_features() & SEPROXYHAL_TAG_SESSION_START_EVENT_FEATURE_SCREEN_BIG)) { + if ((uiState == UI_TEXT) && + (os_seph_features() & SEPROXYHAL_TAG_SESSION_START_EVENT_FEATURE_SCREEN_BIG)) { UX_REDISPLAY(); + } else { + if (G_bolos_ux_context.processing == 1) { + UX_DISPLAYED_EVENT(compare_recovery_phrase();); + } else { + UX_DISPLAYED_EVENT(); + } } - else { - if(G_bolos_ux_context.processing == 1){ - UX_DISPLAYED_EVENT(compare_recovery_phrase();); - } - else { - UX_DISPLAYED_EVENT(); - } - } #elif defined(TARGET_NANOX) || defined(TARGET_NANOS2) - UX_DISPLAYED_EVENT({}); + UX_DISPLAYED_EVENT({}); #endif - break; + break; - case SEPROXYHAL_TAG_TICKER_EVENT: + case SEPROXYHAL_TAG_TICKER_EVENT: UX_TICKER_EVENT(G_io_seproxyhal_spi_buffer, { // defaulty retrig very soon (will be overriden during // stepper_prepro) UX_CALLBACK_SET_INTERVAL(500); UX_REDISPLAY(); }); - break; + break; - // unknown events are acknowledged - default: - UX_DEFAULT_EVENT(); - break; + // unknown events are acknowledged + default: + UX_DEFAULT_EVENT(); + break; } // close the event if not done previously (by a display or whatever) @@ -163,7 +159,7 @@ __attribute__((section(".boot"))) int main(void) { BEGIN_TRY { TRY { io_seproxyhal_init(); - //ui_idle(); + // ui_idle(); ui_idle_init(); sample_main(); } diff --git a/src/nanos_enter_phrase.c b/src/nanos_enter_phrase.c index 70089d89..478e1abb 100644 --- a/src/nanos_enter_phrase.c +++ b/src/nanos_enter_phrase.c @@ -1,258 +1,228 @@ /******************************************************************************* -* Ledger Blue - Secure firmware -* (c) 2016, 2017 Ledger -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -********************************************************************************/ + * Ledger Blue - Secure firmware + * (c) 2016, 2017 Ledger + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ********************************************************************************/ #include "ui.h" #include "bolos_ux_common.h" #ifdef TARGET_NANOS - // allow to edit back any entered word #define RESTORE_WORD_MAX_BACKWARD_STEPS 24 - -const bagl_element_t* screen_onboarding_4_restore_word_before_element_display_callback(const bagl_element_t* element); +const bagl_element_t* screen_onboarding_4_restore_word_before_element_display_callback( + const bagl_element_t* element); void screen_onboarding_4_restore_word_display_auto_complete(void); -UX_STEP_CB( - restore_4_intro_1, - nn, - screen_onboarding_4_restore_word_display_auto_complete();, - { - "Enter", - G_ux.string_buffer - }); - -UX_FLOW( - restore_4_intro, - &restore_4_intro_1 - ); - -UX_STEP_CB( - restore_4_invalid_1, - pbb, - screen_onboarding_3_restore_init();, - { - &C_icon_warning, - "Recovery", - "phrase invalid" - }); - -UX_FLOW( - restore_4_invalid, - &restore_4_invalid_1); - -UX_STEP_NOCB( - ux_nomatch_step_1, - pbb, - { - &C_icon_warning, - "Phrase", - "doesn't match" - }); -UX_STEP_NOCB( - ux_nomatch_step_2, - nn, - { - "Check length", - "order and spelling", - }); -UX_STEP_VALID( - ux_nomatch_step_3, - pb, - ui_idle_init(), - { - &C_icon_back_x, - "Return to menu" - }); - -UX_FLOW( - flow_final_nomatch, - &ux_nomatch_step_1, - &ux_nomatch_step_2, - &ux_nomatch_step_3 -); - -UX_STEP_CB( - step_final_match, - pbb, - os_sched_exit(0);, - { - &C_icon_validate_14, - "Phrase", - "is correct" - }); +UX_STEP_CB(restore_4_intro_1, nn, screen_onboarding_4_restore_word_display_auto_complete(); + , {"Enter", G_ux.string_buffer}); -UX_FLOW(flow_final_match, &step_final_match); +UX_FLOW(restore_4_intro, &restore_4_intro_1); + +UX_STEP_CB(restore_4_invalid_1, pbb, screen_onboarding_3_restore_init(); + , {&C_icon_warning, "Recovery", "phrase invalid"}); + +UX_FLOW(restore_4_invalid, &restore_4_invalid_1); +UX_STEP_NOCB(ux_nomatch_step_1, pbb, {&C_icon_warning, "Phrase", "doesn't match"}); +UX_STEP_NOCB(ux_nomatch_step_2, + nn, + { + "Check length", + "order and spelling", + }); +UX_STEP_VALID(ux_nomatch_step_3, pb, ui_idle_init(), {&C_icon_back_x, "Return to menu"}); + +UX_FLOW(flow_final_nomatch, &ux_nomatch_step_1, &ux_nomatch_step_2, &ux_nomatch_step_3); + +UX_STEP_CB(step_final_match, pbb, os_sched_exit(0);, {&C_icon_validate_14, "Phrase", "is correct"}); + +UX_FLOW(flow_final_match, &step_final_match); void screen_processing_postinit(unsigned int stack_slot) { - // ensure when pin is modal over the processing screen( at end of install) the processing screen gives back the hand to the calling code - G_ux.stack[stack_slot].exit_code_after_elements_displayed = BOLOS_UX_OK; + // ensure when pin is modal over the processing screen( at end of install) the processing screen + // gives back the hand to the calling code + G_ux.stack[stack_slot].exit_code_after_elements_displayed = BOLOS_UX_OK; } -UX_STEP_NOCB_POSTINIT( - processing_step, - pb, - screen_processing_postinit(stack_slot), - { - &C_icon_loader, - "Processing", - }); +UX_STEP_NOCB_POSTINIT(processing_step, + pb, + screen_processing_postinit(stack_slot), + { + &C_icon_loader, + "Processing", + }); -UX_FLOW(processing_flow, - &processing_step); +UX_FLOW(processing_flow, &processing_step); void screen_processing_init(void) { - ux_flow_init(0, processing_flow, NULL); + ux_flow_init(0, processing_flow, NULL); } -unsigned int screen_onboarding_4_restore_word_select_button(unsigned int button_mask, unsigned int button_mask_counter); +unsigned int screen_onboarding_4_restore_word_select_button(unsigned int button_mask, + unsigned int button_mask_counter); -#define ITEMS (G_ux.string_buffer+32) +#define ITEMS (G_ux.string_buffer + 32) -const bagl_element_t* screen_onboarding_4_restore_word_keyboard_callback(unsigned int event, unsigned int value); +const bagl_element_t* screen_onboarding_4_restore_word_keyboard_callback(unsigned int event, + unsigned int value); void screen_onboarding_4_restore_word_display_auto_complete(void) { - unsigned int auto_complete_count = bolos_ux_bip39_get_word_next_letters_starting_with((unsigned char*)G_ux.string_buffer+16, - strlen(G_ux.string_buffer+16), - (unsigned char*)G_ux.string_buffer+32); + unsigned int auto_complete_count = bolos_ux_bip39_get_word_next_letters_starting_with( + (unsigned char*) G_ux.string_buffer + 16, + strlen(G_ux.string_buffer + 16), + (unsigned char*) G_ux.string_buffer + 32); // display added letter and refresh slider - screen_common_keyboard_init(0, - 0, /*always restart from the first element in the list*/ - //(strlen(G_ux.string_buffer+16)?0:cx_rng_u8()%auto_complete_count), /* start from a random element in the list for the word start letter, else keep the order */ + screen_common_keyboard_init( + 0, + 0, /*always restart from the first element in the list*/ + // (strlen(G_ux.string_buffer+16)?0:cx_rng_u8()%auto_complete_count), /* start from a random + // element in the list for the word start letter, else keep the order */ // recompute alphabet and set the number of elements in the keyboard - auto_complete_count - + (strlen(G_ux.string_buffer+16)?1:0) /* backspace if already a stem enetered, else no backspace */, + auto_complete_count + + (strlen(G_ux.string_buffer + 16) + ? 1 + : 0) /* backspace if already a stem enetered, else no backspace */, screen_onboarding_4_restore_word_keyboard_callback); - // append the special backspace to allow for easier dispatch in the keyboard callback - ((unsigned char*)(G_ux.string_buffer+32))[auto_complete_count] = '\b'; + // append the special backspace to allow for easier dispatch in the keyboard callback + ((unsigned char*) (G_ux.string_buffer + 32))[auto_complete_count] = '\b'; } void screen_onboarding_4_restore_word_display_word_selection(void) { - ux_stack_init(0); - G_ux.stack[0].button_push_callback = screen_onboarding_4_restore_word_select_button; - G_ux.stack[0].element_arrays[0].element_array = screen_onboarding_word_list_elements; - G_ux.stack[0].element_arrays[0].element_array_count = sizeof(screen_onboarding_word_list_elements)/sizeof(screen_onboarding_word_list_elements[0]); - G_ux.stack[0].element_arrays_count = 1; - G_ux.stack[0].screen_before_element_display_callback = screen_onboarding_4_restore_word_before_element_display_callback; - ux_stack_display(0); + ux_stack_init(0); + G_ux.stack[0].button_push_callback = screen_onboarding_4_restore_word_select_button; + G_ux.stack[0].element_arrays[0].element_array = screen_onboarding_word_list_elements; + G_ux.stack[0].element_arrays[0].element_array_count = + sizeof(screen_onboarding_word_list_elements) / + sizeof(screen_onboarding_word_list_elements[0]); + G_ux.stack[0].element_arrays_count = 1; + G_ux.stack[0].screen_before_element_display_callback = + screen_onboarding_4_restore_word_before_element_display_callback; + ux_stack_display(0); } -const bagl_element_t* screen_onboarding_4_restore_word_keyboard_callback(unsigned int event, unsigned int value) { - switch(event) { - case KEYBOARD_ITEM_VALIDATED: - // depending on the chosen class, interpret the click - if (G_ux.string_buffer[32+value] == '\b') { - if (strlen(G_ux.string_buffer+16)) { - G_ux.string_buffer[16+strlen(G_ux.string_buffer+16)-1] = 0; - screen_onboarding_4_restore_word_display_auto_complete(); - } - } - else { - // validate next letter of the word - G_ux.string_buffer[16+strlen(G_ux.string_buffer+16)] = G_ux.string_buffer[32+G_bolos_ux_context.hslider3_current]; - - // continue displaying until less than X words matches the stem - G_bolos_ux_context.onboarding_words_checked = bolos_ux_bip39_get_word_count_starting_with((unsigned char*)G_ux.string_buffer+16, strlen(G_ux.string_buffer+16)); - if (G_bolos_ux_context.onboarding_words_checked > ONBOARDING_WORD_COMPLETION_MAX_ITEMS) { - // too much words for slider word completion, await another letter - screen_onboarding_4_restore_word_display_auto_complete(); - } - else { - // always init stem count - // index of the first word matching the stem - G_bolos_ux_context.onboarding_index = bolos_ux_bip39_get_word_idx_starting_with((unsigned char*)G_ux.string_buffer+16, strlen(G_ux.string_buffer+16)); - - // multiple possibilities - // update te slider's possible words - // account for the extra clear word, and clear any previous word items (go back in the onboarding process) - bolos_ux_hslider3_init(G_bolos_ux_context.onboarding_words_checked+MIN(G_bolos_ux_context.onboarding_step+1, RESTORE_WORD_MAX_BACKWARD_STEPS)); - screen_onboarding_4_restore_word_display_word_selection(); - } - return NULL; - } - break; - - case KEYBOARD_RENDER_ITEM: - if (G_ux.string_buffer[32+value] == '\b') { - /* - x:57 w:14 - iconw: 11 - */ - G_ux.tmp_element.component.width = C_icon_backspace.width; - G_ux.tmp_element.component.x += 1 + G_ux.tmp_element.component.width/2 - C_icon_backspace.width/2; - G_ux.tmp_element.component.y = 5; - G_ux.tmp_element.component.height = C_icon_backspace.height; - G_ux.tmp_element.component.type = BAGL_ICON; - G_ux.tmp_element.component.icon_id = 0; - // if current selected, then display the inversed digit (as in a pin digit) - G_ux.tmp_element.text = (const char*)&C_icon_backspace; - } - else { - //G_ux.string_buffer[0] = G_ux.string_buffer[32+value]-'a'+'A'; // render as uppercase, always - G_ux.string_buffer[0] = G_ux.string_buffer[32+value]; // render as lowercase, always - G_ux.string_buffer[1] = 0; - G_ux.tmp_element.text = G_ux.string_buffer; - } - break; - case KEYBOARD_RENDER_WORD: { - unsigned int l = strlen(G_ux.string_buffer+16); - - if (value < 8) { - G_ux.tmp_element.component.x += 5; - // prefix word stem with "%d:" with the current word index - snprintf(G_ux.string_buffer+2, 5, "#%d ", G_bolos_ux_context.onboarding_step + 1); - // ensure font is left aligned - G_ux.tmp_element.text = G_ux.string_buffer; - G_ux.string_buffer[1] = 0; - if (value + ONBOARDING_WORD_COMPLETION_MAX_ITEMS) { + // too much words for slider word completion, await another letter + screen_onboarding_4_restore_word_display_auto_complete(); + } else { + // always init stem count + // index of the first word matching the stem + G_bolos_ux_context.onboarding_index = bolos_ux_bip39_get_word_idx_starting_with( + (unsigned char*) G_ux.string_buffer + 16, + strlen(G_ux.string_buffer + 16)); + + // multiple possibilities + // update te slider's possible words + // account for the extra clear word, and clear any previous word items (go back + // in the onboarding process) + bolos_ux_hslider3_init(G_bolos_ux_context.onboarding_words_checked + + MIN(G_bolos_ux_context.onboarding_step + 1, + RESTORE_WORD_MAX_BACKWARD_STEPS)); + screen_onboarding_4_restore_word_display_word_selection(); } - else { - G_ux.string_buffer[0] = '_'; + return NULL; } - // will never occur on word stem, autocomplete always happen before that - // else { - // // first char is '...' to notify continuing - // if (value == 0) { - // G_ux.string_buffer[0] = '.'; - // G_ux.string_buffer[1] = '.'; - // G_ux.string_buffer[2] = '.'; - // G_ux.string_buffer[3] = 0; - // } - // else { - // G_ux.string_buffer[0] = (G_ux.string_buffer+16+l+1-8)[value]; - // } - // } break; + + case KEYBOARD_RENDER_ITEM: + if (G_ux.string_buffer[32 + value] == '\b') { + /* + x:57 w:14 + iconw: 11 + */ + G_ux.tmp_element.component.width = C_icon_backspace.width; + G_ux.tmp_element.component.x += + 1 + G_ux.tmp_element.component.width / 2 - C_icon_backspace.width / 2; + G_ux.tmp_element.component.y = 5; + G_ux.tmp_element.component.height = C_icon_backspace.height; + G_ux.tmp_element.component.type = BAGL_ICON; + G_ux.tmp_element.component.icon_id = 0; + // if current selected, then display the inversed digit (as in a pin digit) + G_ux.tmp_element.text = (const char*) &C_icon_backspace; + } else { + // G_ux.string_buffer[0] = G_ux.string_buffer[32+value]-'a'+'A'; // render as + // uppercase, always + G_ux.string_buffer[0] = + G_ux.string_buffer[32 + value]; // render as lowercase, always + G_ux.string_buffer[1] = 0; + G_ux.tmp_element.text = G_ux.string_buffer; + } + break; + case KEYBOARD_RENDER_WORD: { + unsigned int l = strlen(G_ux.string_buffer + 16); + + if (value < 8) { + G_ux.tmp_element.component.x += 5; + // prefix word stem with "%d:" with the current word index + snprintf(G_ux.string_buffer + 2, 5, "#%d ", G_bolos_ux_context.onboarding_step + 1); + // ensure font is left aligned + G_ux.tmp_element.text = G_ux.string_buffer; + G_ux.string_buffer[1] = 0; + if (value < strlen(G_ux.string_buffer + 2)) { + G_ux.string_buffer[0] = G_ux.string_buffer[2 + value]; + } else if (value < l + strlen(G_ux.string_buffer + 2)) { + G_ux.string_buffer[0] = + G_ux.string_buffer[16 + value - strlen(G_ux.string_buffer + 2)]; + } else { + G_ux.string_buffer[0] = '_'; + } + // will never occur on word stem, autocomplete always happen before that + // else { + // // first char is '...' to notify continuing + // if (value == 0) { + // G_ux.string_buffer[0] = '.'; + // G_ux.string_buffer[1] = '.'; + // G_ux.string_buffer[2] = '.'; + // G_ux.string_buffer[3] = 0; + // } + // else { + // G_ux.string_buffer[0] = (G_ux.string_buffer+16+l+1-8)[value]; + // } + // } + break; + } + return NULL; } - return NULL; - } } // update element display - return &G_ux.tmp_element; + return &G_ux.tmp_element; } -void compare_recovery_phrase(void) -{ +void compare_recovery_phrase(void) { G_bolos_ux_context.processing = 0; io_seproxyhal_general_status(); @@ -260,219 +230,238 @@ void compare_recovery_phrase(void) // convert mnemonic to hex-seed uint8_t buffer[64]; - bolos_ux_mnemonic_to_seed((unsigned char *)G_bolos_ux_context.words_buffer, - G_bolos_ux_context.words_buffer_length, - buffer); - //PRINTF("Input seed:\n %.*H\n", 64, buffer); + bolos_ux_mnemonic_to_seed((unsigned char*) G_bolos_ux_context.words_buffer, + G_bolos_ux_context.words_buffer_length, + buffer); + // PRINTF("Input seed:\n %.*H\n", 64, buffer); // get rootkey from hex-seed cx_hmac_sha512_t ctx; const char key[] = "Bitcoin seed"; - cx_hmac_sha512_init(&ctx, (const uint8_t *) key, sizeof(key)); - cx_hmac((cx_hmac_t *) &ctx,CX_LAST, buffer, 64, buffer, 64); - //PRINTF("Root key from input:\n%.*H\n", 64, buffer); + cx_hmac_sha512_init(&ctx, (const uint8_t*) key, sizeof(key)); + cx_hmac((cx_hmac_t*) &ctx, CX_LAST, buffer, 64, buffer, 64); + // PRINTF("Root key from input:\n%.*H\n", 64, buffer); // get rootkey from device's seed uint8_t buffer_device[64]; - os_perso_derive_node_bip32(CX_CURVE_256K1, NULL, 0, buffer_device, buffer_device+32); - //PRINTF("Root key from device: \n%.*H\n", 64, buffer_device); + os_perso_derive_node_bip32(CX_CURVE_256K1, NULL, 0, buffer_device, buffer_device + 32); + // PRINTF("Root key from device: \n%.*H\n", 64, buffer_device); // compare both rootkey - if(os_secure_memcmp(buffer, buffer_device, 64)) { + if (os_secure_memcmp(buffer, buffer_device, 64)) { ux_flow_init(0, flow_final_nomatch, NULL); - } - else { + } else { ux_flow_init(0, flow_final_match, NULL); } } +const bagl_element_t* screen_onboarding_4_restore_word_before_element_display_callback( + const bagl_element_t* element) { + switch (element->component.userid) { + case 0x01: + if (G_bolos_ux_context.hslider3_total == 0 || + G_bolos_ux_context.hslider3_current >= + G_bolos_ux_context.onboarding_words_checked) { + return NULL; + } + SPRINTF(G_ux.string_buffer, "Word #%d", G_bolos_ux_context.onboarding_step + 1); + break; -const bagl_element_t* screen_onboarding_4_restore_word_before_element_display_callback(const bagl_element_t* element) { - switch(element->component.userid) { - - case 0x01: - if (G_bolos_ux_context.hslider3_total == 0 || G_bolos_ux_context.hslider3_current >= G_bolos_ux_context.onboarding_words_checked) { - return NULL; - } - SPRINTF(G_ux.string_buffer, "Word #%d", G_bolos_ux_context.onboarding_step + 1); - break; - - case 0x02: - if (G_bolos_ux_context.hslider3_total == 0 || G_bolos_ux_context.hslider3_current >= G_bolos_ux_context.onboarding_words_checked) { - return NULL; - } - // display matching word from the slider's current index - bolos_ux_bip39_idx_strcpy(G_bolos_ux_context.onboarding_index + G_bolos_ux_context.hslider3_current, (unsigned char*)G_ux.string_buffer); - break; - - case 0x03: - // no left button on first item - if (G_bolos_ux_context.hslider3_current == 0) { - return NULL; - } - break; - - case 0x04: - // no right button on last item - if (G_bolos_ux_context.hslider3_total == 0 || G_bolos_ux_context.hslider3_current >= G_bolos_ux_context.hslider3_total-1) { - return NULL; - } - break; - - case 0x05: - if (G_bolos_ux_context.hslider3_total == 0 || G_bolos_ux_context.hslider3_current < G_bolos_ux_context.onboarding_words_checked) { - return NULL; - } - break; - case 0x06: - if (G_bolos_ux_context.hslider3_total == 0 || G_bolos_ux_context.hslider3_current < G_bolos_ux_context.onboarding_words_checked) { - return NULL; - } - SPRINTF(G_ux.string_buffer, "word #%d", G_bolos_ux_context.hslider3_total - G_bolos_ux_context.hslider3_current); - break; - } - - // display other elements only if screen setup, else, only redraw words value - return element; -} + case 0x02: + if (G_bolos_ux_context.hslider3_total == 0 || + G_bolos_ux_context.hslider3_current >= + G_bolos_ux_context.onboarding_words_checked) { + return NULL; + } + // display matching word from the slider's current index + bolos_ux_bip39_idx_strcpy( + G_bolos_ux_context.onboarding_index + G_bolos_ux_context.hslider3_current, + (unsigned char*) G_ux.string_buffer); + break; + + case 0x03: + // no left button on first item + if (G_bolos_ux_context.hslider3_current == 0) { + return NULL; + } + break; + case 0x04: + // no right button on last item + if (G_bolos_ux_context.hslider3_total == 0 || + G_bolos_ux_context.hslider3_current >= G_bolos_ux_context.hslider3_total - 1) { + return NULL; + } + break; + + case 0x05: + if (G_bolos_ux_context.hslider3_total == 0 || + G_bolos_ux_context.hslider3_current < G_bolos_ux_context.onboarding_words_checked) { + return NULL; + } + break; + case 0x06: + if (G_bolos_ux_context.hslider3_total == 0 || + G_bolos_ux_context.hslider3_current < G_bolos_ux_context.onboarding_words_checked) { + return NULL; + } + SPRINTF(G_ux.string_buffer, + "word #%d", + G_bolos_ux_context.hslider3_total - G_bolos_ux_context.hslider3_current); + break; + } + + // display other elements only if screen setup, else, only redraw words value + return element; +} void screen_onboarding_4_restore_word_validate(void) { - bolos_ux_bip39_idx_strcpy(G_bolos_ux_context.onboarding_index + G_bolos_ux_context.hslider3_current, (unsigned char*)(G_bolos_ux_context.words_buffer+G_bolos_ux_context.words_buffer_length)); - G_bolos_ux_context.words_buffer_length=strlen(G_bolos_ux_context.words_buffer); + bolos_ux_bip39_idx_strcpy( + G_bolos_ux_context.onboarding_index + G_bolos_ux_context.hslider3_current, + (unsigned char*) (G_bolos_ux_context.words_buffer + + G_bolos_ux_context.words_buffer_length)); + G_bolos_ux_context.words_buffer_length = strlen(G_bolos_ux_context.words_buffer); // a word has been added G_bolos_ux_context.onboarding_step++; - if (G_bolos_ux_context.onboarding_step == G_bolos_ux_context.onboarding_kind) { + if (G_bolos_ux_context.onboarding_step == G_bolos_ux_context.onboarding_kind) { unsigned int valid; -#ifdef HAVE_ELECTRUM - // if we've entered all the words, then check the phrase - if (G_bolos_ux_context.onboarding_algorithm == BOLOS_UX_ONBOARDING_ALGORITHM_ELECTRUM) { - valid = bolos_ux_electrum_mnemonic_check(ELECTRUM_SEED_PREFIX_STANDARD, (unsigned char*)G_bolos_ux_context.words_buffer, G_bolos_ux_context.words_buffer_length); - } - else { - valid = bolos_ux_mnemonic_check((unsigned char*)G_bolos_ux_context.words_buffer, G_bolos_ux_context.words_buffer_length); - - +#ifdef HAVE_ELECTRUM + // if we've entered all the words, then check the phrase + if (G_bolos_ux_context.onboarding_algorithm == BOLOS_UX_ONBOARDING_ALGORITHM_ELECTRUM) { + valid = + bolos_ux_electrum_mnemonic_check(ELECTRUM_SEED_PREFIX_STANDARD, + (unsigned char*) G_bolos_ux_context.words_buffer, + G_bolos_ux_context.words_buffer_length); + } else { + valid = bolos_ux_mnemonic_check((unsigned char*) G_bolos_ux_context.words_buffer, + G_bolos_ux_context.words_buffer_length); } #else - valid = bolos_ux_mnemonic_check((unsigned char*)G_bolos_ux_context.words_buffer, G_bolos_ux_context.words_buffer_length); -#endif - if ( !valid ) { - ux_flow_init(0, restore_4_invalid, NULL); - } - else { - // allright, the recovery phrase looks ok, finish onboarding - //Display processing warning to user + valid = bolos_ux_mnemonic_check((unsigned char*) G_bolos_ux_context.words_buffer, + G_bolos_ux_context.words_buffer_length); +#endif + if (!valid) { + ux_flow_init(0, restore_4_invalid, NULL); + } else { + // allright, the recovery phrase looks ok, finish onboarding + // Display processing warning to user screen_processing_init(); G_bolos_ux_context.processing = 1; - } - } - else { + } + } else { // add a space before next word - G_bolos_ux_context.words_buffer[G_bolos_ux_context.words_buffer_length++] = ' '; + G_bolos_ux_context.words_buffer[G_bolos_ux_context.words_buffer_length++] = ' '; // enter the next word - /*indexes have been preincremented, it's therefore the next word we're reentering*/ - screen_onboarding_4_restore_word_init(RESTORE_WORD_ACTION_REENTER_WORD); + /*indexes have been preincremented, it's therefore the next word we're reentering*/ + screen_onboarding_4_restore_word_init(RESTORE_WORD_ACTION_REENTER_WORD); } } - -unsigned int screen_onboarding_4_restore_word_select_button(unsigned int button_mask, unsigned int button_mask_counter) { +unsigned int screen_onboarding_4_restore_word_select_button(unsigned int button_mask, + unsigned int button_mask_counter) { UNUSED(button_mask_counter); - switch(button_mask) { - case BUTTON_EVT_FAST | BUTTON_LEFT: - case BUTTON_EVT_RELEASED | BUTTON_LEFT: + switch (button_mask) { + case BUTTON_EVT_FAST | BUTTON_LEFT: + case BUTTON_EVT_RELEASED | BUTTON_LEFT: - // not a rotating list - if (G_bolos_ux_context.hslider3_current == 0) { - return 0; - } - bolos_ux_hslider3_previous(); - goto redraw; - - case BUTTON_EVT_FAST | BUTTON_RIGHT: - case BUTTON_EVT_RELEASED | BUTTON_RIGHT: - // not a rotating list - if (G_bolos_ux_context.hslider3_current == G_bolos_ux_context.hslider3_total-1) { - return 0; - } - bolos_ux_hslider3_next(); + // not a rotating list + if (G_bolos_ux_context.hslider3_current == 0) { + return 0; + } + bolos_ux_hslider3_previous(); + goto redraw; + + case BUTTON_EVT_FAST | BUTTON_RIGHT: + case BUTTON_EVT_RELEASED | BUTTON_RIGHT: + // not a rotating list + if (G_bolos_ux_context.hslider3_current == G_bolos_ux_context.hslider3_total - 1) { + return 0; + } + bolos_ux_hslider3_next(); - redraw: - screen_onboarding_4_restore_word_display_word_selection(); - break; + redraw: + screen_onboarding_4_restore_word_display_word_selection(); + break; - case BUTTON_EVT_RELEASED | BUTTON_LEFT | BUTTON_RIGHT: - if (G_bolos_ux_context.hslider3_current < G_bolos_ux_context.onboarding_words_checked) { - // confirm word, and prepare entering a new one or validate the seed - screen_onboarding_4_restore_word_validate(); - } - else if (G_bolos_ux_context.hslider3_current == G_bolos_ux_context.onboarding_words_checked) { - // clear current word - screen_onboarding_4_restore_word_init(RESTORE_WORD_ACTION_REENTER_WORD); - } - else { - // current word IS NOT already stored. need to wipe one less word - // example selecting word #2: 0 1 2 3 - // prop1 prop2 edit #2 edit #1 - // delete=0 delete=1 - unsigned int word_to_delete = G_bolos_ux_context.hslider3_current - G_bolos_ux_context.onboarding_words_checked; - // remove x words - while (word_to_delete--) { - if (G_bolos_ux_context.onboarding_step && G_bolos_ux_context.words_buffer_length) { - // remove the last space and up to the previous space (but keep the previous space) - do { - G_bolos_ux_context.words_buffer[G_bolos_ux_context.words_buffer_length-1] = 0; - G_bolos_ux_context.words_buffer_length--; -} - // until a previous word exists! - while(G_bolos_ux_context.words_buffer_length && G_bolos_ux_context.words_buffer[G_bolos_ux_context.words_buffer_length-1] != ' '); + case BUTTON_EVT_RELEASED | BUTTON_LEFT | BUTTON_RIGHT: + if (G_bolos_ux_context.hslider3_current < G_bolos_ux_context.onboarding_words_checked) { + // confirm word, and prepare entering a new one or validate the seed + screen_onboarding_4_restore_word_validate(); + } else if (G_bolos_ux_context.hslider3_current == + G_bolos_ux_context.onboarding_words_checked) { + // clear current word + screen_onboarding_4_restore_word_init(RESTORE_WORD_ACTION_REENTER_WORD); + } else { + // current word IS NOT already stored. need to wipe one less word + // example selecting word #2: 0 1 2 3 + // prop1 prop2 edit #2 edit #1 + // delete=0 delete=1 + unsigned int word_to_delete = G_bolos_ux_context.hslider3_current - + G_bolos_ux_context.onboarding_words_checked; + // remove x words + while (word_to_delete--) { + if (G_bolos_ux_context.onboarding_step && + G_bolos_ux_context.words_buffer_length) { + // remove the last space and up to the previous space (but keep the previous + // space) + do { + G_bolos_ux_context + .words_buffer[G_bolos_ux_context.words_buffer_length - 1] = 0; + G_bolos_ux_context.words_buffer_length--; + } + // until a previous word exists! + while (G_bolos_ux_context.words_buffer_length && + G_bolos_ux_context + .words_buffer[G_bolos_ux_context.words_buffer_length - 1] != + ' '); + + // decrement onboarding_step (current word #) + G_bolos_ux_context.onboarding_step--; + } + } + // log_debug(G_bolos_ux_context.words_buffer); - // decrement onboarding_step (current word #) - G_bolos_ux_context.onboarding_step--; - } + // clear previous word + screen_onboarding_4_restore_word_init(RESTORE_WORD_ACTION_REENTER_WORD); + } + break; } - // log_debug(G_bolos_ux_context.words_buffer); - - // clear previous word - screen_onboarding_4_restore_word_init(RESTORE_WORD_ACTION_REENTER_WORD); - } - break; - } return 0; } void screen_onboarding_4_restore_word_init(unsigned int action) { - switch(action) { - case RESTORE_WORD_ACTION_FIRST_WORD: - // start by restore first word (+1 when displayed) - G_bolos_ux_context.onboarding_step = 0; - - // flush the words first - memset(G_bolos_ux_context.words_buffer, 0, sizeof(G_bolos_ux_context.words_buffer)); - G_bolos_ux_context.words_buffer_length = 0; - break; - - case RESTORE_WORD_ACTION_REENTER_WORD: - // don't change anything (the currently edited word has not been copied to the word buffer) - break; + switch (action) { + case RESTORE_WORD_ACTION_FIRST_WORD: + // start by restore first word (+1 when displayed) + G_bolos_ux_context.onboarding_step = 0; + + // flush the words first + memset(G_bolos_ux_context.words_buffer, 0, sizeof(G_bolos_ux_context.words_buffer)); + G_bolos_ux_context.words_buffer_length = 0; + break; + + case RESTORE_WORD_ACTION_REENTER_WORD: + // don't change anything (the currently edited word has not been copied to the word + // buffer) + break; } - memset(G_ux.string_buffer, 0, sizeof(G_ux.string_buffer)); + memset(G_ux.string_buffer, 0, sizeof(G_ux.string_buffer)); // offset 0: the display buffer for various placement // offset 16: the entered stem for the current word restoration - // offset 32: array of next letters possible after the current word's stem in the dictionary (word completion possibilities) + // offset 32: array of next letters possible after the current word's stem in the dictionary + // (word completion possibilities) - // elements to be displayed - SPRINTF(G_ux.string_buffer, "word #%d", G_bolos_ux_context.onboarding_step + 1); + // elements to be displayed + SPRINTF(G_ux.string_buffer, "word #%d", G_bolos_ux_context.onboarding_step + 1); - ux_flow_init(0, restore_4_intro, NULL); + ux_flow_init(0, restore_4_intro, NULL); } #endif diff --git a/src/nanos_pick_phrase_length.c b/src/nanos_pick_phrase_length.c index ad81cc80..0495878f 100644 --- a/src/nanos_pick_phrase_length.c +++ b/src/nanos_pick_phrase_length.c @@ -1,59 +1,51 @@ /******************************************************************************* -* Ledger Blue - Secure firmware -* (c) 2016, 2017 Ledger -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -********************************************************************************/ + * Ledger Blue - Secure firmware + * (c) 2016, 2017 Ledger + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ********************************************************************************/ #include "ui.h" #ifdef TARGET_NANOS -UX_STEP_CB( - restore_3_1_1, - bb, - G_bolos_ux_context.onboarding_kind = 24; screen_onboarding_4_restore_word_init(RESTORE_WORD_ACTION_FIRST_WORD);, - { - "Recovery phrase", - "with 24 words", - }); - -UX_STEP_CB( - restore_3_1_2, - bb, - G_bolos_ux_context.onboarding_kind = 18; screen_onboarding_4_restore_word_init(RESTORE_WORD_ACTION_FIRST_WORD);, - { - "Recovery phrase", - "with 18 words", - }); - -UX_STEP_CB( - restore_3_1_3, - bb, - G_bolos_ux_context.onboarding_kind = 12; screen_onboarding_4_restore_word_init(RESTORE_WORD_ACTION_FIRST_WORD);, - { - "Recovery phrase", - "with 12 words", - }); - -UX_FLOW( - restore_3_1, - &restore_3_1_1, - &restore_3_1_2, - &restore_3_1_3); +UX_STEP_CB(restore_3_1_1, bb, G_bolos_ux_context.onboarding_kind = 24; + screen_onboarding_4_restore_word_init(RESTORE_WORD_ACTION_FIRST_WORD); + , + { + "Recovery phrase", + "with 24 words", + }); + +UX_STEP_CB(restore_3_1_2, bb, G_bolos_ux_context.onboarding_kind = 18; + screen_onboarding_4_restore_word_init(RESTORE_WORD_ACTION_FIRST_WORD); + , + { + "Recovery phrase", + "with 18 words", + }); + +UX_STEP_CB(restore_3_1_3, bb, G_bolos_ux_context.onboarding_kind = 12; + screen_onboarding_4_restore_word_init(RESTORE_WORD_ACTION_FIRST_WORD); + , + { + "Recovery phrase", + "with 12 words", + }); + +UX_FLOW(restore_3_1, &restore_3_1_1, &restore_3_1_2, &restore_3_1_3); void screen_onboarding_3_restore_init(void) { - ux_flow_init(0, restore_3_1, NULL); + ux_flow_init(0, restore_3_1, NULL); } - #endif diff --git a/src/nanox_enter_phrase.c b/src/nanox_enter_phrase.c index 307f6304..1a277181 100644 --- a/src/nanox_enter_phrase.c +++ b/src/nanox_enter_phrase.c @@ -12,460 +12,546 @@ #include "glyphs.h" - -const bagl_element_t* screen_onboarding_4_restore_word_before_element_display_callback(const bagl_element_t* element); +const bagl_element_t* screen_onboarding_4_restore_word_before_element_display_callback( + const bagl_element_t* element); // show intro const bagl_element_t screen_onboarding_4_restore_word_intro_elements[] = { - // erase - {{BAGL_RECTANGLE , 0x00, 0, 0, 128, 64, 0, 0, BAGL_FILL, 0x000000, 0xFFFFFF, 0, 0}, NULL}, - - {{BAGL_LABELINE , 0x31, 0, 12, 128, 32, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_REGULAR_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, "Enter first letters"}, - {{BAGL_LABELINE , 0x32, 0, 12, 128, 32, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_REGULAR_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, "Next, enter letters"}, - {{BAGL_LABELINE , 0x33, 0, 12, 128, 32, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_REGULAR_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, "Finally, enter letters"}, - {{BAGL_LABELINE , 0x30, 0, 26, 128, 32, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_REGULAR_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, G_ux.string_buffer}, + // erase + {{BAGL_RECTANGLE, 0x00, 0, 0, 128, 64, 0, 0, BAGL_FILL, 0x000000, 0xFFFFFF, 0, 0}, NULL}, + + {{BAGL_LABELINE, + 0x31, + 0, + 12, + 128, + 32, + 0, + 0, + 0, + 0xFFFFFF, + 0x000000, + BAGL_FONT_OPEN_SANS_REGULAR_11px | BAGL_FONT_ALIGNMENT_CENTER, + 0}, + "Enter first letters"}, + {{BAGL_LABELINE, + 0x32, + 0, + 12, + 128, + 32, + 0, + 0, + 0, + 0xFFFFFF, + 0x000000, + BAGL_FONT_OPEN_SANS_REGULAR_11px | BAGL_FONT_ALIGNMENT_CENTER, + 0}, + "Next, enter letters"}, + {{BAGL_LABELINE, + 0x33, + 0, + 12, + 128, + 32, + 0, + 0, + 0, + 0xFFFFFF, + 0x000000, + BAGL_FONT_OPEN_SANS_REGULAR_11px | BAGL_FONT_ALIGNMENT_CENTER, + 0}, + "Finally, enter letters"}, + {{BAGL_LABELINE, + 0x30, + 0, + 26, + 128, + 32, + 0, + 0, + 0, + 0xFFFFFF, + 0x000000, + BAGL_FONT_OPEN_SANS_REGULAR_11px | BAGL_FONT_ALIGNMENT_CENTER, + 0}, + G_ux.string_buffer}, }; - // word selection + word clear const bagl_element_t screen_onboarding_4_restore_word_select_elements[] = { - // erase - {{BAGL_RECTANGLE , 0x00, 0, 0, 128, 64, 0, 0, BAGL_FILL, 0x000000, 0xFFFFFF, 0, 0}, NULL}, + // erase + {{BAGL_RECTANGLE, 0x00, 0, 0, 128, 64, 0, 0, BAGL_FILL, 0x000000, 0xFFFFFF, 0, 0}, NULL}, + + {{BAGL_LABELINE, + 0x21, + 0, + 29, + 128, + 32, + 0, + 0, + 0, + 0xFFFFFF, + 0x000000, + BAGL_FONT_OPEN_SANS_REGULAR_11px | BAGL_FONT_ALIGNMENT_CENTER, + 0}, + G_ux.string_buffer}, + {{BAGL_LABELINE, + 0x20, + 0, + 43, + 128, + 32, + 0, + 0, + 0, + 0xFFFFFF, + 0x000000, + BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, + 0}, + G_ux.string_buffer}, + + {{BAGL_ICON, 0x24, (128 - 14) / 2, 17, 14, 14, 0, 0, 0, 0xFFFFFF, 0x000000, 0, 0}, + (const char*) &C_icon_clear}, + {{BAGL_LABELINE, + 0x24, + 0, + 43, + 128, + 32, + 0, + 0, + 0, + 0xFFFFFF, + 0x000000, + BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, + 0}, + "Clear word"}, + + // left/rights icons + {{BAGL_ICON, 0x22, 2, 28, 4, 7, 0, 0, 0, 0xFFFFFF, 0x000000, 0, 0}, (const char*) &C_icon_left}, + {{BAGL_ICON, 0x23, 122, 28, 4, 7, 0, 0, 0, 0xFFFFFF, 0x000000, 0, 0}, + (const char*) &C_icon_right}, +}; - {{BAGL_LABELINE , 0x21, 0, 29, 128, 32, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_REGULAR_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, G_ux.string_buffer}, - {{BAGL_LABELINE , 0x20, 0, 43, 128, 32, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, G_ux.string_buffer}, +UX_STEP_NOCB(ux_load_step, pn, {&C_icon_loader, "Processing"}); +UX_FLOW(ux_load_flow, &ux_load_step); + +UX_STEP_VALID(ux_wrong_seed_step, + pnn, + os_sched_exit(-1), + { + &C_icon_crossmark, + "Recovery", + "phrase invalid", + }); +UX_FLOW(ux_wrong_seed_flow, &ux_wrong_seed_step); + +UX_STEP_NOCB(ux_failed_check_step_1, pbb, {&C_icon_warning, "Phrase", "doesn't match"}); +UX_STEP_NOCB(ux_failed_check_step_2, + nn, + { + "Check length", + "order and spelling", + }); +UX_STEP_VALID(ux_failed_check_step_3, pb, ui_idle_init(), {&C_icon_back_x, "Return to menu"}); +UX_FLOW(ux_failed_check_flow, + &ux_failed_check_step_1, + &ux_failed_check_step_2, + &ux_failed_check_step_3); - {{BAGL_ICON , 0x24, (128-14)/2, 17, 14, 14, 0, 0, 0 , 0xFFFFFF, 0x000000, 0, 0 }, (const char *) &C_icon_clear}, - {{BAGL_LABELINE , 0x24, 0, 43, 128, 32, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, "Clear word"}, +UX_STEP_VALID(ux_success_step, + pbb, + os_sched_exit(-1), + {&C_icon_validate_14, "Phrase", "is correct"}); +UX_FLOW(ux_succesfull_check_flow, &ux_success_step); - // left/rights icons - {{BAGL_ICON , 0x22, 2, 28, 4, 7, 0, 0, 0 , 0xFFFFFF, 0x000000, 0, 0 }, (const char*)&C_icon_left}, - {{BAGL_ICON , 0x23, 122, 28, 4, 7, 0, 0, 0 , 0xFFFFFF, 0x000000, 0, 0 }, (const char*)&C_icon_right}, -}; +unsigned int screen_onboarding_4_restore_word_select_button(unsigned int button_mask, + unsigned int button_mask_counter); -UX_STEP_NOCB( - ux_load_step, - pn, - { - &C_icon_loader, - "Processing" - }); -UX_FLOW(ux_load_flow, - &ux_load_step -); - -UX_STEP_VALID( - ux_wrong_seed_step, - pnn, - os_sched_exit(-1), - { - &C_icon_crossmark, - "Recovery", - "phrase invalid", - }); -UX_FLOW(ux_wrong_seed_flow, - &ux_wrong_seed_step -); - -UX_STEP_NOCB( - ux_failed_check_step_1, - pbb, - { - &C_icon_warning, - "Phrase", - "doesn't match" - }); -UX_STEP_NOCB( - ux_failed_check_step_2, - nn, - { - "Check length", - "order and spelling", - }); -UX_STEP_VALID( - ux_failed_check_step_3, - pb, - ui_idle_init(), - { - &C_icon_back_x, - "Return to menu" - }); -UX_FLOW(ux_failed_check_flow, - &ux_failed_check_step_1, - &ux_failed_check_step_2, - &ux_failed_check_step_3 -); - -UX_STEP_VALID( - ux_success_step, - pbb, - os_sched_exit(-1), - { - &C_icon_validate_14, - "Phrase", - "is correct" - }); -UX_FLOW(ux_succesfull_check_flow, - &ux_success_step -); - -unsigned int screen_onboarding_4_restore_word_select_button(unsigned int button_mask, unsigned int button_mask_counter); - -const bagl_element_t* screen_onboarding_4_restore_word_keyboard_callback(unsigned int event, unsigned int value); +const bagl_element_t* screen_onboarding_4_restore_word_keyboard_callback(unsigned int event, + unsigned int value); void screen_onboarding_4_restore_word_display_auto_complete(void) { - unsigned int auto_complete_count = bolos_ux_bip39_get_word_next_letters_starting_with((unsigned char*)G_ux.string_buffer+16, - strlen(G_ux.string_buffer+16), - (unsigned char*)G_ux.string_buffer+32); - // prepare title of the common keyboard component, after the list of possible letters - snprintf(G_ux.string_buffer+32+auto_complete_count+1, sizeof(G_ux.string_buffer)-32-auto_complete_count-1, "Enter word #%d", G_bolos_ux_context.onboarding_step + 1); - // prepare to display the backspace - G_ux.string_buffer[32+auto_complete_count]= '\b'; - G_bolos_ux_context.common_label = G_ux.string_buffer+32+auto_complete_count+1; - // display added letter and refresh slider - screen_common_keyboard_init(0, + unsigned int auto_complete_count = bolos_ux_bip39_get_word_next_letters_starting_with( + (unsigned char*) G_ux.string_buffer + 16, + strlen(G_ux.string_buffer + 16), + (unsigned char*) G_ux.string_buffer + 32); + // prepare title of the common keyboard component, after the list of possible letters + snprintf(G_ux.string_buffer + 32 + auto_complete_count + 1, + sizeof(G_ux.string_buffer) - 32 - auto_complete_count - 1, + "Enter word #%d", + G_bolos_ux_context.onboarding_step + 1); + // prepare to display the backspace + G_ux.string_buffer[32 + auto_complete_count] = '\b'; + G_bolos_ux_context.common_label = G_ux.string_buffer + 32 + auto_complete_count + 1; + // display added letter and refresh slider + screen_common_keyboard_init( + 0, #ifdef HAVE_BOLOS_NOT_SHUFFLED_RESTORE - 0, /*always restart from the first element in the list*/ -#else // HAVE_BOLOS_NOT_SHUFFLED_RESTORE - (strlen(G_ux.string_buffer+16)?0:cx_rng_u8()%auto_complete_count), /* start from a random element in the list for the word start letter, else keep the order */ -#endif // HAVE_BOLOS_NOT_SHUFFLED_RESTORE - // recompute alphabet and set the number of elements in the keyboard - auto_complete_count - + (strlen(G_ux.string_buffer+16)?1:0) /* backspace if a stem is already entered, else no backspace */, - screen_onboarding_4_restore_word_keyboard_callback); - // append the special backspace to allow for easier dispatch in the keyboard callback - ((unsigned char*)(G_ux.string_buffer+32))[auto_complete_count] = '\b'; + 0, /*always restart from the first element in the list*/ +#else // HAVE_BOLOS_NOT_SHUFFLED_RESTORE + (strlen(G_ux.string_buffer + 16) + ? 0 + : cx_rng_u8() % auto_complete_count), /* start from a random element in the list for + the word start letter, else keep the order */ +#endif // HAVE_BOLOS_NOT_SHUFFLED_RESTORE + // recompute alphabet and set the number of elements in the keyboard + auto_complete_count + + (strlen(G_ux.string_buffer + 16) + ? 1 + : 0) /* backspace if a stem is already entered, else no backspace */, + screen_onboarding_4_restore_word_keyboard_callback); + // append the special backspace to allow for easier dispatch in the keyboard callback + ((unsigned char*) (G_ux.string_buffer + 32))[auto_complete_count] = '\b'; } void screen_onboarding_4_restore_word_display_word_selection(void) { - ux_stack_init(0); - G_ux.stack[0].button_push_callback = screen_onboarding_4_restore_word_select_button; - G_ux.stack[0].element_arrays[0].element_array = screen_onboarding_4_restore_word_select_elements; - G_ux.stack[0].element_arrays[0].element_array_count = ARRAYLEN(screen_onboarding_4_restore_word_select_elements); - G_ux.stack[0].element_arrays_count = 1; - G_ux.stack[0].screen_before_element_display_callback = screen_onboarding_4_restore_word_before_element_display_callback; - ux_stack_display(0); + ux_stack_init(0); + G_ux.stack[0].button_push_callback = screen_onboarding_4_restore_word_select_button; + G_ux.stack[0].element_arrays[0].element_array = + screen_onboarding_4_restore_word_select_elements; + G_ux.stack[0].element_arrays[0].element_array_count = + ARRAYLEN(screen_onboarding_4_restore_word_select_elements); + G_ux.stack[0].element_arrays_count = 1; + G_ux.stack[0].screen_before_element_display_callback = + screen_onboarding_4_restore_word_before_element_display_callback; + ux_stack_display(0); } -const bagl_element_t* screen_onboarding_4_restore_word_keyboard_callback(unsigned int event, unsigned int value) { - switch(event) { - case KEYBOARD_ITEM_VALIDATED: - //depending on the chosen class, interpret the click - if (G_ux.string_buffer[32+value] == '\b') { - if (strlen(G_ux.string_buffer+16)) { - G_ux.string_buffer[16+strlen(G_ux.string_buffer+16)-1] = 0; - screen_onboarding_4_restore_word_display_auto_complete(); +const bagl_element_t* screen_onboarding_4_restore_word_keyboard_callback(unsigned int event, + unsigned int value) { + switch (event) { + case KEYBOARD_ITEM_VALIDATED: + // depending on the chosen class, interpret the click + if (G_ux.string_buffer[32 + value] == '\b') { + if (strlen(G_ux.string_buffer + 16)) { + G_ux.string_buffer[16 + strlen(G_ux.string_buffer + 16) - 1] = 0; + screen_onboarding_4_restore_word_display_auto_complete(); + } + } else { + unsigned int nb_words_matching_stem; + // validate next letter of the word + G_ux.string_buffer[16 + strlen(G_ux.string_buffer + 16)] = + G_ux.string_buffer[32 + G_bolos_ux_context.hslider3_current]; + + // continue displaying until less than X words matches the stem + nb_words_matching_stem = bolos_ux_bip39_get_word_count_starting_with( + (unsigned char*) G_ux.string_buffer + 16, + strlen(G_ux.string_buffer + 16)); + if (nb_words_matching_stem > ONBOARDING_WORD_COMPLETION_MAX_ITEMS) { + // too much words for slider word completion, await another letter + screen_onboarding_4_restore_word_display_auto_complete(); + } else { + // always init stem count + // index of the first word matching the stem + G_bolos_ux_context.onboarding_index = bolos_ux_bip39_get_word_idx_starting_with( + (unsigned char*) G_ux.string_buffer + 16, + strlen(G_ux.string_buffer + 16)); + + // multiple possibilities + // update te slider's possible words + // account for the extra "last" (clear) item + bolos_ux_hslider3_init(nb_words_matching_stem + 1); + screen_onboarding_4_restore_word_display_word_selection(); + } + return NULL; + } + break; + + case KEYBOARD_RENDER_ITEM: + if (G_ux.string_buffer[32 + value] == '\b') { + /* + x:57 w:14 + iconw: 11 + */ + value = 3; + G_ux.tmp_element.component.width = C_icon_backspace.width; + G_ux.tmp_element.component.x += + 1 + G_ux.tmp_element.component.width / 2 - C_icon_backspace.width / 2; + G_ux.tmp_element.component.y -= 7; + // G_ux.tmp_element.component.y = 5; + G_ux.tmp_element.component.height = C_icon_backspace.height; + G_ux.tmp_element.component.type = BAGL_ICON; + G_ux.tmp_element.component.icon_id = 0; + if (G_ux.tmp_element.component.userid == 0x02) { + G_ux.tmp_element.text = (const char*) &C_icon_backspace_invert; + } else { + G_ux.tmp_element.text = (const char*) &C_icon_backspace; + } + } else { + // G_ux.string_buffer[0] = G_ux.string_buffer[32+value]-'a'+'A'; // render as + // uppercase, always + G_ux.string_buffer[0] = + G_ux.string_buffer[32 + value]; // render as lowercase, always + G_ux.string_buffer[1] = 0; + G_ux.tmp_element.text = G_ux.string_buffer; + } + break; + case KEYBOARD_RENDER_WORD: { + unsigned int l = strlen(G_ux.string_buffer + 16); + + if (value < 8) { + G_ux.tmp_element.component.x += 5; + // ensure font is left aligned + G_ux.tmp_element.text = G_ux.string_buffer; + G_ux.string_buffer[1] = 0; + if (l < 8) { + if (l > value) { + G_ux.string_buffer[0] = G_ux.string_buffer[16 + value]; + } else { + G_ux.string_buffer[0] = '_'; + } + } else { + // first char is '...' to notify continuing + if (value == 0) { + G_ux.string_buffer[0] = '.'; + G_ux.string_buffer[1] = '.'; + G_ux.string_buffer[2] = '.'; + G_ux.string_buffer[3] = 0; + } else { + G_ux.string_buffer[0] = (G_ux.string_buffer + 16 + l + 1 - 8)[value]; + } + } + break; + } + return NULL; } - } - else { - unsigned int nb_words_matching_stem; - // validate next letter of the word - G_ux.string_buffer[16+strlen(G_ux.string_buffer+16)] = G_ux.string_buffer[32+G_bolos_ux_context.hslider3_current]; - - // continue displaying until less than X words matches the stem - nb_words_matching_stem = bolos_ux_bip39_get_word_count_starting_with((unsigned char*)G_ux.string_buffer+16, strlen(G_ux.string_buffer+16)); - if (nb_words_matching_stem > ONBOARDING_WORD_COMPLETION_MAX_ITEMS) { - // too much words for slider word completion, await another letter - screen_onboarding_4_restore_word_display_auto_complete(); - } - else { - // always init stem count - // index of the first word matching the stem - G_bolos_ux_context.onboarding_index = bolos_ux_bip39_get_word_idx_starting_with((unsigned char*)G_ux.string_buffer+16, strlen(G_ux.string_buffer+16)); - - // multiple possibilities - // update te slider's possible words - // account for the extra "last" (clear) item - bolos_ux_hslider3_init(nb_words_matching_stem+1); - screen_onboarding_4_restore_word_display_word_selection(); - } - return NULL; - } - break; - - case KEYBOARD_RENDER_ITEM: - if (G_ux.string_buffer[32+value] == '\b') { - /* - x:57 w:14 - iconw: 11 - */ - value = 3; - G_ux.tmp_element.component.width = C_icon_backspace.width; - G_ux.tmp_element.component.x += 1 + G_ux.tmp_element.component.width/2 - C_icon_backspace.width/2; - G_ux.tmp_element.component.y -= 7; - //G_ux.tmp_element.component.y = 5; - G_ux.tmp_element.component.height = C_icon_backspace.height; - G_ux.tmp_element.component.type = BAGL_ICON; - G_ux.tmp_element.component.icon_id = 0; - if (G_ux.tmp_element.component.userid == 0x02) { - G_ux.tmp_element.text = (const char*)&C_icon_backspace_invert; - } - else { - G_ux.tmp_element.text = (const char*)&C_icon_backspace; - } - } - else { - //G_ux.string_buffer[0] = G_ux.string_buffer[32+value]-'a'+'A'; // render as uppercase, always - G_ux.string_buffer[0] = G_ux.string_buffer[32+value]; // render as lowercase, always - G_ux.string_buffer[1] = 0; - G_ux.tmp_element.text = G_ux.string_buffer; - } - break; - case KEYBOARD_RENDER_WORD: { - unsigned int l = strlen(G_ux.string_buffer+16); - - if (value < 8) { - G_ux.tmp_element.component.x += 5; - // ensure font is left aligned - G_ux.tmp_element.text = G_ux.string_buffer; - G_ux.string_buffer[1] = 0; - if (l < 8) { - if (l>value) { - G_ux.string_buffer[0] = G_ux.string_buffer[16+value]; - } - else { - G_ux.string_buffer[0] = '_'; - } - } - else { - // first char is '...' to notify continuing - if (value == 0) { - G_ux.string_buffer[0] = '.'; - G_ux.string_buffer[1] = '.'; - G_ux.string_buffer[2] = '.'; - G_ux.string_buffer[3] = 0; - } - else { - G_ux.string_buffer[0] = (G_ux.string_buffer+16+l+1-8)[value]; - } - } - break; - } - return NULL; } - } - // update element display - return &G_ux.tmp_element; + // update element display + return &G_ux.tmp_element; } -const bagl_element_t* screen_onboarding_4_restore_word_before_element_display_callback(const bagl_element_t* element) { - switch(element->component.userid) { - case 0x30: - // word index - SPRINTF(G_ux.string_buffer, "of word #%d", G_bolos_ux_context.onboarding_step + 1); - break; - - case 0x31: - if (G_bolos_ux_context.onboarding_step != 0) { - return NULL; // don't display - } - break; - case 0x32: - if (G_bolos_ux_context.onboarding_step == 0 || G_bolos_ux_context.onboarding_step == G_bolos_ux_context.onboarding_kind-1) { - return NULL; // don't display - } - break; - case 0x33: - if (G_bolos_ux_context.onboarding_step != G_bolos_ux_context.onboarding_kind-1) { - return NULL; // don't display - } - break; - - case 0x20: - // display matching word from the slider's current index - bolos_ux_bip39_idx_strcpy(G_bolos_ux_context.onboarding_index + G_bolos_ux_context.hslider3_current, (unsigned char*)G_ux.string_buffer); - goto not_on_last_item; - - case 0x21: - SPRINTF(G_ux.string_buffer, "Select word #%d", G_bolos_ux_context.onboarding_step + 1); - goto not_on_last_item; - - case 0x22: - // no left button on first item - if (G_bolos_ux_context.hslider3_current == 0) { - return NULL; - } - break; - not_on_last_item: - case 0x23: - // no right button on last item - if (G_bolos_ux_context.hslider3_total == 0 || G_bolos_ux_context.hslider3_current == G_bolos_ux_context.hslider3_total-1) { - return NULL; - } - break; - - case 0x24: - if (G_bolos_ux_context.hslider3_total == 0 || G_bolos_ux_context.hslider3_current != G_bolos_ux_context.hslider3_total-1) { - return NULL; - } - break; - case 0x25: - // word index - SPRINTF(G_ux.string_buffer, "Enter word #%d", G_bolos_ux_context.onboarding_step + 1); - break; - - } - - // display other elements only if screen setup, else, only redraw words value - return element; +const bagl_element_t* screen_onboarding_4_restore_word_before_element_display_callback( + const bagl_element_t* element) { + switch (element->component.userid) { + case 0x30: + // word index + SPRINTF(G_ux.string_buffer, "of word #%d", G_bolos_ux_context.onboarding_step + 1); + break; + + case 0x31: + if (G_bolos_ux_context.onboarding_step != 0) { + return NULL; // don't display + } + break; + case 0x32: + if (G_bolos_ux_context.onboarding_step == 0 || + G_bolos_ux_context.onboarding_step == G_bolos_ux_context.onboarding_kind - 1) { + return NULL; // don't display + } + break; + case 0x33: + if (G_bolos_ux_context.onboarding_step != G_bolos_ux_context.onboarding_kind - 1) { + return NULL; // don't display + } + break; + + case 0x20: + // display matching word from the slider's current index + bolos_ux_bip39_idx_strcpy( + G_bolos_ux_context.onboarding_index + G_bolos_ux_context.hslider3_current, + (unsigned char*) G_ux.string_buffer); + goto not_on_last_item; + + case 0x21: + SPRINTF(G_ux.string_buffer, "Select word #%d", G_bolos_ux_context.onboarding_step + 1); + goto not_on_last_item; + + case 0x22: + // no left button on first item + if (G_bolos_ux_context.hslider3_current == 0) { + return NULL; + } + break; + not_on_last_item: + case 0x23: + // no right button on last item + if (G_bolos_ux_context.hslider3_total == 0 || + G_bolos_ux_context.hslider3_current == G_bolos_ux_context.hslider3_total - 1) { + return NULL; + } + break; + + case 0x24: + if (G_bolos_ux_context.hslider3_total == 0 || + G_bolos_ux_context.hslider3_current != G_bolos_ux_context.hslider3_total - 1) { + return NULL; + } + break; + case 0x25: + // word index + SPRINTF(G_ux.string_buffer, "Enter word #%d", G_bolos_ux_context.onboarding_step + 1); + break; + } + + // display other elements only if screen setup, else, only redraw words value + return element; } -uint8_t compare_recovery_phrase(void) -{ - //io_seproxyhal_general_status(); +uint8_t compare_recovery_phrase(void) { + // io_seproxyhal_general_status(); // convert mnemonic to hex-seed uint8_t buffer[64]; - bolos_ux_mnemonic_to_seed((unsigned char *)G_bolos_ux_context.words_buffer, - G_bolos_ux_context.words_buffer_length, - buffer); - //PRINTF("Input seed:\n %.*H\n", 64, buffer); + bolos_ux_mnemonic_to_seed((unsigned char*) G_bolos_ux_context.words_buffer, + G_bolos_ux_context.words_buffer_length, + buffer); + // PRINTF("Input seed:\n %.*H\n", 64, buffer); // get rootkey from hex-seed cx_hmac_sha512_t ctx; const char key[] = "Bitcoin seed"; - cx_hmac_sha512_init(&ctx, (const uint8_t *) key, sizeof(key)); - cx_hmac((cx_hmac_t *) &ctx,CX_LAST, buffer, 64, buffer, 64); - //PRINTF("Root key from input:\n%.*H\n", 64, buffer); + cx_hmac_sha512_init(&ctx, (const uint8_t*) key, sizeof(key)); + cx_hmac((cx_hmac_t*) &ctx, CX_LAST, buffer, 64, buffer, 64); + // PRINTF("Root key from input:\n%.*H\n", 64, buffer); // get rootkey from device's seed uint8_t buffer_device[64]; - os_perso_derive_node_bip32(CX_CURVE_256K1, NULL, 0, buffer_device, buffer_device+32); - //PRINTF("Root key from device: \n%.*H\n", 64, buffer_device); + os_perso_derive_node_bip32(CX_CURVE_256K1, NULL, 0, buffer_device, buffer_device + 32); + // PRINTF("Root key from device: \n%.*H\n", 64, buffer_device); // compare both rootkey - return os_secure_memcmp(buffer, buffer_device, 64) ? 0:1; + return os_secure_memcmp(buffer, buffer_device, 64) ? 0 : 1; } void screen_onboarding_4_restore_word_validate(void) { - bolos_ux_bip39_idx_strcpy(G_bolos_ux_context.onboarding_index + G_bolos_ux_context.hslider3_current, (unsigned char*)(G_bolos_ux_context.words_buffer+G_bolos_ux_context.words_buffer_length)); - G_bolos_ux_context.words_buffer_length=strlen(G_bolos_ux_context.words_buffer); + bolos_ux_bip39_idx_strcpy( + G_bolos_ux_context.onboarding_index + G_bolos_ux_context.hslider3_current, + (unsigned char*) (G_bolos_ux_context.words_buffer + + G_bolos_ux_context.words_buffer_length)); + G_bolos_ux_context.words_buffer_length = strlen(G_bolos_ux_context.words_buffer); - // a word has been added - G_bolos_ux_context.onboarding_step++; + // a word has been added + G_bolos_ux_context.onboarding_step++; - if (G_bolos_ux_context.onboarding_step == G_bolos_ux_context.onboarding_kind) { - unsigned char valid; + if (G_bolos_ux_context.onboarding_step == G_bolos_ux_context.onboarding_kind) { + unsigned char valid; #ifdef HAVE_ELECTRUM - // if we've entered all the words, then check the phrase - if (G_bolos_ux_context.onboarding_algorithm == BOLOS_UX_ONBOARDING_ALGORITHM_ELECTRUM) { - valid = bolos_ux_electrum_mnemonic_check(ELECTRUM_SEED_PREFIX_STANDARD, (unsigned char*)G_bolos_ux_context.words_buffer, G_bolos_ux_context.words_buffer_length); - } - else { - valid = bolos_ux_mnemonic_check((unsigned char*)G_bolos_ux_context.words_buffer, G_bolos_ux_context.words_buffer_length); - } + // if we've entered all the words, then check the phrase + if (G_bolos_ux_context.onboarding_algorithm == BOLOS_UX_ONBOARDING_ALGORITHM_ELECTRUM) { + valid = + bolos_ux_electrum_mnemonic_check(ELECTRUM_SEED_PREFIX_STANDARD, + (unsigned char*) G_bolos_ux_context.words_buffer, + G_bolos_ux_context.words_buffer_length); + } else { + valid = bolos_ux_mnemonic_check((unsigned char*) G_bolos_ux_context.words_buffer, + G_bolos_ux_context.words_buffer_length); + } #else - valid = bolos_ux_mnemonic_check((unsigned char*)G_bolos_ux_context.words_buffer, G_bolos_ux_context.words_buffer_length); + valid = bolos_ux_mnemonic_check((unsigned char*) G_bolos_ux_context.words_buffer, + G_bolos_ux_context.words_buffer_length); #endif - if ( !valid ) { - // invalid recovery phrase - ux_flow_init(0, ux_wrong_seed_flow, NULL); - } - else { - // allright, the recovery phrase looks ok, compare it to onboarded seed - - //Display loading icon to user - ux_flow_init(0, ux_load_flow, NULL); - if(compare_recovery_phrase()){ - ux_flow_init(0, ux_succesfull_check_flow, NULL); - } - else{ - ux_flow_init(0, ux_failed_check_flow, NULL); - } + if (!valid) { + // invalid recovery phrase + ux_flow_init(0, ux_wrong_seed_flow, NULL); + } else { + // allright, the recovery phrase looks ok, compare it to onboarded seed + + // Display loading icon to user + ux_flow_init(0, ux_load_flow, NULL); + if (compare_recovery_phrase()) { + ux_flow_init(0, ux_succesfull_check_flow, NULL); + } else { + ux_flow_init(0, ux_failed_check_flow, NULL); + } + } + } else { + // add a space before next word + G_bolos_ux_context.words_buffer[G_bolos_ux_context.words_buffer_length++] = ' '; + + // enter the next word + screen_onboarding_4_restore_word_init(0); } - } - else { - // add a space before next word - G_bolos_ux_context.words_buffer[G_bolos_ux_context.words_buffer_length++] = ' '; - - // enter the next word - screen_onboarding_4_restore_word_init(0); - } } -unsigned int screen_onboarding_4_restore_word_select_button(unsigned int button_mask, unsigned int button_mask_counter) { - UNUSED(button_mask_counter); - switch(button_mask) { - case BUTTON_EVT_FAST|BUTTON_LEFT: - case BUTTON_EVT_RELEASED|BUTTON_LEFT: - - // not a rotating list - if (G_bolos_ux_context.hslider3_current == 0) { - return 0; - } - bolos_ux_hslider3_previous(); - goto redraw; - - case BUTTON_EVT_FAST|BUTTON_RIGHT: - case BUTTON_EVT_RELEASED|BUTTON_RIGHT: - // not a rotating list - if (G_bolos_ux_context.hslider3_current == G_bolos_ux_context.hslider3_total-1) { - return 0; - } - bolos_ux_hslider3_next(); - - redraw: - screen_onboarding_4_restore_word_display_word_selection(); - break; - - case BUTTON_EVT_RELEASED|BUTTON_LEFT|BUTTON_RIGHT: - if (G_bolos_ux_context.hslider3_current == G_bolos_ux_context.hslider3_total-1) { - // clear current word - screen_onboarding_4_restore_word_init(0); - } - else { - // confirm word, and prepare entering a new one or validate the seed - screen_onboarding_4_restore_word_validate(); - } - break; - } - return 0; +unsigned int screen_onboarding_4_restore_word_select_button(unsigned int button_mask, + unsigned int button_mask_counter) { + UNUSED(button_mask_counter); + switch (button_mask) { + case BUTTON_EVT_FAST | BUTTON_LEFT: + case BUTTON_EVT_RELEASED | BUTTON_LEFT: + + // not a rotating list + if (G_bolos_ux_context.hslider3_current == 0) { + return 0; + } + bolos_ux_hslider3_previous(); + goto redraw; + + case BUTTON_EVT_FAST | BUTTON_RIGHT: + case BUTTON_EVT_RELEASED | BUTTON_RIGHT: + // not a rotating list + if (G_bolos_ux_context.hslider3_current == G_bolos_ux_context.hslider3_total - 1) { + return 0; + } + bolos_ux_hslider3_next(); + + redraw: + screen_onboarding_4_restore_word_display_word_selection(); + break; + + case BUTTON_EVT_RELEASED | BUTTON_LEFT | BUTTON_RIGHT: + if (G_bolos_ux_context.hslider3_current == G_bolos_ux_context.hslider3_total - 1) { + // clear current word + screen_onboarding_4_restore_word_init(0); + } else { + // confirm word, and prepare entering a new one or validate the seed + screen_onboarding_4_restore_word_validate(); + } + break; + } + return 0; } -unsigned int screen_onboarding_4_restore_word_intro_button(unsigned int button_mask, unsigned int button_mask_counter) { - UNUSED(button_mask_counter); - switch(button_mask) { - case BUTTON_EVT_RELEASED|BUTTON_LEFT|BUTTON_RIGHT: { - screen_onboarding_4_restore_word_display_auto_complete(); - break; +unsigned int screen_onboarding_4_restore_word_intro_button(unsigned int button_mask, + unsigned int button_mask_counter) { + UNUSED(button_mask_counter); + switch (button_mask) { + case BUTTON_EVT_RELEASED | BUTTON_LEFT | BUTTON_RIGHT: { + screen_onboarding_4_restore_word_display_auto_complete(); + break; + } } - } - return 0; + return 0; } void screen_onboarding_4_restore_word_init(unsigned int firstWord) { - ux_stack_init(0); + ux_stack_init(0); - if (firstWord) { - // start by restore first word (+1 when displayed) - G_bolos_ux_context.onboarding_step = 0; + if (firstWord) { + // start by restore first word (+1 when displayed) + G_bolos_ux_context.onboarding_step = 0; - // flush the words first - memset(G_bolos_ux_context.words_buffer, 0, sizeof(G_bolos_ux_context.words_buffer)); - G_bolos_ux_context.words_buffer_length = 0; - } + // flush the words first + memset(G_bolos_ux_context.words_buffer, 0, sizeof(G_bolos_ux_context.words_buffer)); + G_bolos_ux_context.words_buffer_length = 0; + } - memset(G_ux.string_buffer, 0, sizeof(G_ux.string_buffer)); - // offset 0: the display buffer for various placement - // offset 16: the entered stem for the current word restoration - // offset 32: array of next letters possible after the current word's stem in the dictionary (word completion possibilities) + memset(G_ux.string_buffer, 0, sizeof(G_ux.string_buffer)); + // offset 0: the display buffer for various placement + // offset 16: the entered stem for the current word restoration + // offset 32: array of next letters possible after the current word's stem in the dictionary + // (word completion possibilities) #ifdef RESTORE_INTRO_WORD - G_ux.stack[0].button_push_callback = screen_onboarding_4_restore_word_intro_button; - G_ux.stack[0].screen_before_element_display_callback = screen_onboarding_4_restore_word_before_element_display_callback; - - // elements to be displayed - G_ux.stack[0].element_arrays[0].element_array = screen_onboarding_4_restore_word_intro_elements; - G_ux.stack[0].element_arrays[0].element_array_count = ARRAYLEN(screen_onboarding_4_restore_word_intro_elements); - G_ux.stack[0].element_arrays_count = 1; - ux_stack_display(0); -#else // RESTORE_INTRO_WORD - screen_onboarding_4_restore_word_display_auto_complete(); -#endif // RESTORE_INTRO_WORD + G_ux.stack[0].button_push_callback = screen_onboarding_4_restore_word_intro_button; + G_ux.stack[0].screen_before_element_display_callback = + screen_onboarding_4_restore_word_before_element_display_callback; + + // elements to be displayed + G_ux.stack[0].element_arrays[0].element_array = screen_onboarding_4_restore_word_intro_elements; + G_ux.stack[0].element_arrays[0].element_array_count = + ARRAYLEN(screen_onboarding_4_restore_word_intro_elements); + G_ux.stack[0].element_arrays_count = 1; + ux_stack_display(0); +#else // RESTORE_INTRO_WORD + screen_onboarding_4_restore_word_display_auto_complete(); +#endif // RESTORE_INTRO_WORD } - #endif diff --git a/src/ui.c b/src/ui.c index 8d6c93c9..68a9262b 100644 --- a/src/ui.c +++ b/src/ui.c @@ -6,130 +6,110 @@ enum UI_STATE uiState; #if defined(TARGET_NANOS) -UX_STEP_VALID( - ux_idle_flow_1_step, - pbb, - screen_onboarding_3_restore_init();, - { - &C_badge, - "Check your", - "recovery phrase", - }); -UX_STEP_NOCB( - ux_idle_flow_3_step, - bn, - { - "Version", - APPVERSION, - }); -UX_STEP_VALID( - ux_idle_flow_4_step, - pb, - os_sched_exit(-1), - { - &C_icon_dashboard_x, - "Quit", - }); -UX_FLOW(ux_idle_flow, - &ux_idle_flow_1_step, - &ux_idle_flow_3_step, - &ux_idle_flow_4_step -); +UX_STEP_VALID(ux_idle_flow_1_step, pbb, screen_onboarding_3_restore_init();, + { + &C_badge, + "Check your", + "recovery phrase", + }); +UX_STEP_NOCB(ux_idle_flow_3_step, + bn, + { + "Version", + APPVERSION, + }); +UX_STEP_VALID(ux_idle_flow_4_step, + pb, + os_sched_exit(-1), + { + &C_icon_dashboard_x, + "Quit", + }); +UX_FLOW(ux_idle_flow, &ux_idle_flow_1_step, &ux_idle_flow_3_step, &ux_idle_flow_4_step); #elif defined(TARGET_NANOX) || defined(TARGET_NANOS2) ////////////////////////////////////////////////////////////////////// const char* const number_of_words_getter_values[] = { - "12 words", - "18 words", - "24 words", - "Back", + "12 words", + "18 words", + "24 words", + "Back", }; const char* number_of_words_getter(unsigned int idx) { - if (idx < ARRAYLEN(number_of_words_getter_values)) { - return number_of_words_getter_values[idx]; - } - return NULL; + if (idx < ARRAYLEN(number_of_words_getter_values)) { + return number_of_words_getter_values[idx]; + } + return NULL; } void number_of_words_selector(unsigned int idx) { - switch(idx) { - case 0: - G_bolos_ux_context.onboarding_kind = 12; - screen_onboarding_4_restore_word_init(1 /*entering the first word*/); - break; - case 1: - G_bolos_ux_context.onboarding_kind = 18; - screen_onboarding_4_restore_word_init(1 /*entering the first word*/); - break; - case 2: - G_bolos_ux_context.onboarding_kind = 24; - screen_onboarding_4_restore_word_init(1 /*entering the first word*/); - break; - default: - ui_idle_init(); - } + switch (idx) { + case 0: + G_bolos_ux_context.onboarding_kind = 12; + screen_onboarding_4_restore_word_init(1 /*entering the first word*/); + break; + case 1: + G_bolos_ux_context.onboarding_kind = 18; + screen_onboarding_4_restore_word_init(1 /*entering the first word*/); + break; + case 2: + G_bolos_ux_context.onboarding_kind = 24; + screen_onboarding_4_restore_word_init(1 /*entering the first word*/); + break; + default: + ui_idle_init(); + } } ////////////////////////////////////////////////////////////////////// -UX_STEP_VALID( - ux_instruction_step, - nnn, - ux_menulist_init(0, number_of_words_getter, number_of_words_selector), - { - "Select the number", - "of words written on", - "your Recovery Sheet", - }); +UX_STEP_VALID(ux_instruction_step, + nnn, + ux_menulist_init(0, number_of_words_getter, number_of_words_selector), + { + "Select the number", + "of words written on", + "your Recovery Sheet", + }); -UX_FLOW(ux_instruction_flow, - &ux_instruction_step -); +UX_FLOW(ux_instruction_flow, &ux_instruction_step); ////////////////////////////////////////////////////////////////////// -UX_STEP_VALID( - ux_idle_flow_1_step, - pbb, - ux_flow_init(0, ux_instruction_flow, NULL), - { - &C_badge, - "Check your", - "recovery phrase", - }); -UX_STEP_NOCB( - ux_idle_flow_3_step, - bn, - { - "Version", - APPVERSION, - }); -UX_STEP_VALID( - ux_idle_flow_4_step, - pb, - os_sched_exit(-1), - { - &C_icon_dashboard_x, - "Quit", - }); -UX_FLOW(ux_idle_flow, - &ux_idle_flow_1_step, - &ux_idle_flow_3_step, - &ux_idle_flow_4_step -); +UX_STEP_VALID(ux_idle_flow_1_step, + pbb, + ux_flow_init(0, ux_instruction_flow, NULL), + { + &C_badge, + "Check your", + "recovery phrase", + }); +UX_STEP_NOCB(ux_idle_flow_3_step, + bn, + { + "Version", + APPVERSION, + }); +UX_STEP_VALID(ux_idle_flow_4_step, + pb, + os_sched_exit(-1), + { + &C_icon_dashboard_x, + "Quit", + }); +UX_FLOW(ux_idle_flow, &ux_idle_flow_1_step, &ux_idle_flow_3_step, &ux_idle_flow_4_step); #endif - void ui_idle_init(void) { - uiState = UI_IDLE; + uiState = UI_IDLE; - // reserve a display stack slot if none yet - if(G_ux.stack_count == 0) { - ux_stack_push(); - } - ux_flow_init(0, ux_idle_flow, NULL); + // reserve a display stack slot if none yet + if (G_ux.stack_count == 0) { + ux_stack_push(); + } + ux_flow_init(0, ux_idle_flow, NULL); } diff --git a/src/ui.h b/src/ui.h index b7a1e7f1..e72f8565 100644 --- a/src/ui.h +++ b/src/ui.h @@ -8,7 +8,6 @@ #include "string.h" #include "cx.h" - void ui_idle_init(void); #endif \ No newline at end of file From 40b0bb3d6aab2e678d1ee40feef6fe1a88e0e1cd Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Mon, 3 Oct 2022 17:37:09 +0200 Subject: [PATCH 05/65] [clean] Misspells --- README.md | 2 +- src/bolos_ux_nanos.h | 6 +-- src/bolos_ux_nanox.h | 4 +- src/main.c | 2 +- src/nanos_enter_phrase.c | 6 +-- src/nanox_enter_phrase.c | 4 +- .../bolos_ux_onboarding_seed_bip39.c | 42 +++++++++---------- 7 files changed, 33 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 195bb927..5acfcf80 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # Nano S app to check seed backup -This application invites the user to type his seed on his Nano S, this seed is compared against the onboarded seed, and user is informed wether both seeds are matching or not +This application invites the user to type his seed on his Nano S, this seed is compared against the onboarded seed, and user is informed whether both seeds are matching or not diff --git a/src/bolos_ux_nanos.h b/src/bolos_ux_nanos.h index 3a78131d..71053e45 100644 --- a/src/bolos_ux_nanos.h +++ b/src/bolos_ux_nanos.h @@ -75,7 +75,7 @@ typedef struct bolos_ux_context { } screen_stack[4]; unsigned int screen_stack_count; // initialized @0 by the bolos ux initialize - // a screen pop occured, the underlaying screen must optimize its drawing as + // a screen pop occurred, the underlying screen must optimize its drawing as // we've probably trashed the whole screen unsigned int screen_redraw; @@ -124,7 +124,7 @@ typedef struct bolos_ux_context { // after an int to make sure it's aligned char string_buffer[MAX( 64, - sizeof(bagl_icon_details_t) + BOLOS_APP_ICON_SIZE_B - 1)]; // to store the seed wholy + sizeof(bagl_icon_details_t) + BOLOS_APP_ICON_SIZE_B - 1)]; // to store the seed wholly char words_buffer[257]; // 128 of words (215 => hashed to 64, or 128) + // HMAC_LENGTH*2 = 256 @@ -246,7 +246,7 @@ unsigned int bolos_ux_electrum_mnemonic_check(unsigned int version, #endif /** - * Bolos system app internal UX entry point (could be overriden by a further + * Bolos system app internal UX entry point (could be overridden by a further * loaded BOLOS_UX application) */ void bolos_ux_main(void); diff --git a/src/bolos_ux_nanox.h b/src/bolos_ux_nanox.h index d901720e..12209033 100644 --- a/src/bolos_ux_nanox.h +++ b/src/bolos_ux_nanox.h @@ -39,7 +39,7 @@ typedef struct bolos_ux_context { #endif // STATE_INITIALIZED unsigned int state; - // a screen pop occured, the underlaying screen must optimize its drawing as we've probably + // a screen pop occurred, the underlying screen must optimize its drawing as we've probably // trashed the whole screen unsigned int screen_redraw; @@ -215,7 +215,7 @@ void bolos_ux_hslider3_previous(void); #define FAST_LIST_ACTION_CS 2 /** - * Bolos system app internal UX entry point (could be overriden by a further loaded BOLOS_UX + * Bolos system app internal UX entry point (could be overridden by a further loaded BOLOS_UX * application) */ void bolos_ux_main(void); diff --git a/src/main.c b/src/main.c index d24d3aee..e61853ff 100644 --- a/src/main.c +++ b/src/main.c @@ -121,7 +121,7 @@ unsigned char io_event(unsigned char channel __attribute__((unused))) { case SEPROXYHAL_TAG_TICKER_EVENT: UX_TICKER_EVENT(G_io_seproxyhal_spi_buffer, { - // defaulty retrig very soon (will be overriden during + // default retrig very soon (will be overridden during // stepper_prepro) UX_CALLBACK_SET_INTERVAL(500); UX_REDISPLAY(); diff --git a/src/nanos_enter_phrase.c b/src/nanos_enter_phrase.c index 478e1abb..2e1b1fc5 100644 --- a/src/nanos_enter_phrase.c +++ b/src/nanos_enter_phrase.c @@ -95,7 +95,7 @@ void screen_onboarding_4_restore_word_display_auto_complete(void) { auto_complete_count + (strlen(G_ux.string_buffer + 16) ? 1 - : 0) /* backspace if already a stem enetered, else no backspace */, + : 0) /* backspace if already a stem entered, else no backspace */, screen_onboarding_4_restore_word_keyboard_callback); // append the special backspace to allow for easier dispatch in the keyboard callback ((unsigned char*) (G_ux.string_buffer + 32))[auto_complete_count] = '\b'; @@ -146,7 +146,7 @@ const bagl_element_t* screen_onboarding_4_restore_word_keyboard_callback(unsigne strlen(G_ux.string_buffer + 16)); // multiple possibilities - // update te slider's possible words + // update the slider's possible words // account for the extra clear word, and clear any previous word items (go back // in the onboarding process) bolos_ux_hslider3_init(G_bolos_ux_context.onboarding_words_checked + @@ -348,7 +348,7 @@ void screen_onboarding_4_restore_word_validate(void) { if (!valid) { ux_flow_init(0, restore_4_invalid, NULL); } else { - // allright, the recovery phrase looks ok, finish onboarding + // alright, the recovery phrase looks ok, finish onboarding // Display processing warning to user screen_processing_init(); G_bolos_ux_context.processing = 1; diff --git a/src/nanox_enter_phrase.c b/src/nanox_enter_phrase.c index 1a277181..23c77495 100644 --- a/src/nanox_enter_phrase.c +++ b/src/nanox_enter_phrase.c @@ -251,7 +251,7 @@ const bagl_element_t* screen_onboarding_4_restore_word_keyboard_callback(unsigne strlen(G_ux.string_buffer + 16)); // multiple possibilities - // update te slider's possible words + // update the slider's possible words // account for the extra "last" (clear) item bolos_ux_hslider3_init(nb_words_matching_stem + 1); screen_onboarding_4_restore_word_display_word_selection(); @@ -450,7 +450,7 @@ void screen_onboarding_4_restore_word_validate(void) { // invalid recovery phrase ux_flow_init(0, ux_wrong_seed_flow, NULL); } else { - // allright, the recovery phrase looks ok, compare it to onboarded seed + // alright, the recovery phrase looks ok, compare it to onboarded seed // Display loading icon to user ux_flow_init(0, ux_load_flow, NULL); diff --git a/src_ux_common/bolos_ux_onboarding_seed_bip39.c b/src_ux_common/bolos_ux_onboarding_seed_bip39.c index 3c0aacf3..ae028e2c 100755 --- a/src_ux_common/bolos_ux_onboarding_seed_bip39.c +++ b/src_ux_common/bolos_ux_onboarding_seed_bip39.c @@ -1,11 +1,11 @@ -/* @BANNER@ */ +/* @BANNER@ */ #include "os.h" #include "cx.h" #include "bolos_ux_common.h" -unsigned int bolos_ux_mnemonic_from_data(unsigned char *in, unsigned int inLength, unsigned char *out, unsigned int outLength) { +unsigned int bolos_ux_mnemonic_from_data(unsigned char *in, unsigned int inLength, unsigned char *out, unsigned int outLength) { unsigned char bits[32 + 1]; unsigned int mlen = inLength * 3 / 4; unsigned int i, j, idx, offset; @@ -20,14 +20,14 @@ unsigned int bolos_ux_mnemonic_from_data(unsigned char *in, unsigned int inLengt offset = 0; for (i = 0; i < mlen; i++) { size_t wordLength; - idx = 0; + idx = 0; for (j = 0; j < 11; j++) { idx <<= 1; idx += (bits[(i * 11 + j) / 8] & (1 << (7 - ((i * 11 + j) % 8)))) > 0; } wordLength = BIP39_WORDLIST_OFFSETS[idx + 1] - BIP39_WORDLIST_OFFSETS[idx]; if ((offset + wordLength) > outLength) { - THROW (INVALID_PARAMETER); + THROW (INVALID_PARAMETER); } memcpy(out + offset, BIP39_WORDLIST + BIP39_WORDLIST_OFFSETS[idx], wordLength); offset += wordLength; @@ -47,11 +47,11 @@ unsigned int bolos_ux_mnemonic_to_seed_hash_length128(unsigned char *mnemonic, u cx_hash_sha512(mnemonic, mnemonicLength, mnemonic, 64); // new mnemonic length mnemonicLength = 64; - } + } return mnemonicLength; } -void bolos_ux_mnemonic_to_seed(unsigned char *mnemonic, unsigned int mnemonicLength, unsigned char *seed) { +void bolos_ux_mnemonic_to_seed(unsigned char *mnemonic, unsigned int mnemonicLength, unsigned char *seed) { unsigned char passphrase[BIP39_MNEMONIC_LENGTH + 4]; mnemonicLength = bolos_ux_mnemonic_to_seed_hash_length128(mnemonic, mnemonicLength); @@ -85,11 +85,11 @@ unsigned int bolos_ux_get_word_ptr(unsigned char ** word, unsigned int max_lengt return word_length; } -unsigned int bolos_ux_mnemonic_check(unsigned char *mnemonic, unsigned int mnemonicLength) { +unsigned int bolos_ux_mnemonic_check(unsigned char *mnemonic, unsigned int mnemonicLength) { unsigned int i, n = 0; unsigned int bi; - unsigned char bits[32 + 1]; - unsigned char mask; + unsigned char bits[32 + 1]; + unsigned char mask; for (i=0; i Date: Mon, 3 Oct 2022 18:19:30 +0200 Subject: [PATCH 06/65] [fix] Potential error found by CodeQL --- src_ux_common/bolos_ux_onboarding_seed_bip39.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src_ux_common/bolos_ux_onboarding_seed_bip39.c b/src_ux_common/bolos_ux_onboarding_seed_bip39.c index ae028e2c..700a52eb 100755 --- a/src_ux_common/bolos_ux_onboarding_seed_bip39.c +++ b/src_ux_common/bolos_ux_onboarding_seed_bip39.c @@ -108,7 +108,7 @@ unsigned int bolos_ux_mnemonic_check(unsigned char *mnemonic, unsigned int mnemo unsigned int current_word_size = 0; unsigned int j, k, ki; j = 0; - while (mnemonic[i] != ' ' && i < mnemonicLength) { + while (i < mnemonicLength && mnemonic[i] != ' ') { if (j >= sizeof(current_word)) { return 0; } From 37451dd1e678613365455d37b1b0517dc8e5ba29 Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Tue, 4 Oct 2022 09:44:58 +0200 Subject: [PATCH 07/65] [clean] Lint on 'src_ux_common' too --- .github/workflows/lint-workflow.yml | 2 +- src_ux_common/bolos_ux_common_bip39.h | 51 +- src_ux_common/bolos_ux_onboarding_electrum.c | 130 +- .../bolos_ux_onboarding_seed_bip39.c | 444 +- .../bolos_ux_onboarding_seed_rom_variables.c | 4857 +++-------------- .../bolos_ux_onboarding_seed_rom_variables.h | 24 +- 6 files changed, 1102 insertions(+), 4406 deletions(-) mode change 100755 => 100644 src_ux_common/bolos_ux_common_bip39.h mode change 100755 => 100644 src_ux_common/bolos_ux_onboarding_electrum.c mode change 100755 => 100644 src_ux_common/bolos_ux_onboarding_seed_bip39.c mode change 100755 => 100644 src_ux_common/bolos_ux_onboarding_seed_rom_variables.c mode change 100755 => 100644 src_ux_common/bolos_ux_onboarding_seed_rom_variables.h diff --git a/.github/workflows/lint-workflow.yml b/.github/workflows/lint-workflow.yml index d04a315d..628e60b6 100644 --- a/.github/workflows/lint-workflow.yml +++ b/.github/workflows/lint-workflow.yml @@ -22,7 +22,7 @@ jobs: - name: Lint uses: DoozyX/clang-format-lint-action@v0.11 with: - source: './src' + source: 'src src_ux_common' extensions: 'h,c' clangFormatVersion: 11 diff --git a/src_ux_common/bolos_ux_common_bip39.h b/src_ux_common/bolos_ux_common_bip39.h old mode 100755 new mode 100644 index 425a9e8b..5ffbd0d7 --- a/src_ux_common/bolos_ux_common_bip39.h +++ b/src_ux_common/bolos_ux_common_bip39.h @@ -5,32 +5,53 @@ // BIP39 helpers #include "bolos_ux_onboarding_seed_rom_variables.h" -void bolos_ux_pbkdf2(unsigned char* password, unsigned int passwordlen, unsigned char* salt, unsigned int saltlen, unsigned int iterations, unsigned char* out, unsigned int outLength); +void bolos_ux_pbkdf2(unsigned char *password, + unsigned int passwordlen, + unsigned char *salt, + unsigned int saltlen, + unsigned int iterations, + unsigned char *out, + unsigned int outLength); unsigned char bolos_ux_get_random_bip39_word(unsigned char *word); // return 0 if mnemonic is invalid unsigned int bolos_ux_mnemonic_check(unsigned char *mnemonic, unsigned int mnemonicLength); unsigned char bolos_ux_word_check(unsigned char *word, unsigned int wordLength); -unsigned int bolos_ux_get_word_ptr(unsigned char ** word, unsigned int max_length, unsigned int word_index); +unsigned int bolos_ux_get_word_ptr(unsigned char **word, + unsigned int max_length, + unsigned int word_index); // passphrase will be prefixed with "MNEMONIC" from BIP39, the passphrase content shall start @ 8 -void bolos_ux_mnemonic_to_seed(unsigned char *mnemonic, unsigned int mnemonicLength, unsigned char *seed/*, unsigned char *workBuffer*/); +void bolos_ux_mnemonic_to_seed(unsigned char *mnemonic, + unsigned int mnemonicLength, + unsigned char *seed /*, unsigned char *workBuffer*/); unsigned int bolos_ux_mnemonic_indexes_to_words(unsigned char *indexes, unsigned char *words); -unsigned int bolos_ux_mnemonic_from_data(unsigned char *in, unsigned int inLength, unsigned char *out, unsigned int outLength); - - -unsigned int bolos_ux_bip39_get_word_idx_starting_with(unsigned char* prefix, unsigned int prefixlength); -unsigned int bolos_ux_bip39_idx_strcpy(unsigned int index, unsigned char* buffer); -unsigned int bolos_ux_bip39_idx_startswith(unsigned int idx, unsigned char* prefix, unsigned int prefixlength); -unsigned int bolos_ux_bip39_get_word_count_starting_with(unsigned char* prefix, unsigned int prefixlength); -unsigned int bolos_ux_bip39_get_word_next_letters_starting_with(unsigned char* prefix, unsigned int prefixlength, unsigned char* next_letters_buffer); - +unsigned int bolos_ux_mnemonic_from_data(unsigned char *in, + unsigned int inLength, + unsigned char *out, + unsigned int outLength); + +unsigned int bolos_ux_bip39_get_word_idx_starting_with(unsigned char *prefix, + unsigned int prefixlength); +unsigned int bolos_ux_bip39_idx_strcpy(unsigned int index, unsigned char *buffer); +unsigned int bolos_ux_bip39_idx_startswith(unsigned int idx, + unsigned char *prefix, + unsigned int prefixlength); +unsigned int bolos_ux_bip39_get_word_count_starting_with(unsigned char *prefix, + unsigned int prefixlength); +unsigned int bolos_ux_bip39_get_word_next_letters_starting_with(unsigned char *prefix, + unsigned int prefixlength, + unsigned char *next_letters_buffer); #ifdef HAVE_ELECTRUM -unsigned int bolos_ux_electrum_new_mnemonic(unsigned int version, unsigned char *out, unsigned int outLength); -unsigned int bolos_ux_electrum_mnemonic_check(unsigned int version, unsigned char *mnemonic, unsigned int mnemonicLength); +unsigned int bolos_ux_electrum_new_mnemonic(unsigned int version, + unsigned char *out, + unsigned int outLength); +unsigned int bolos_ux_electrum_mnemonic_check(unsigned int version, + unsigned char *mnemonic, + unsigned int mnemonicLength); #endif -#endif // COMMON_BIP39 +#endif // COMMON_BIP39 diff --git a/src_ux_common/bolos_ux_onboarding_electrum.c b/src_ux_common/bolos_ux_onboarding_electrum.c old mode 100755 new mode 100644 index 1f70f56d..55d5ac5e --- a/src_ux_common/bolos_ux_onboarding_electrum.c +++ b/src_ux_common/bolos_ux_onboarding_electrum.c @@ -1,4 +1,4 @@ -/* @BANNER@ */ +/* @BANNER@ */ #ifdef HAVE_ELECTRUM @@ -7,74 +7,86 @@ #include "bolos_ux_common.h" -int cx_math_shiftr_11(unsigned char *r, unsigned int len) { - unsigned int j,b11; - b11 = r[len-1] | ((r[len-2]&7)<<8); +int cx_math_shiftr_11(unsigned char *r, unsigned int len) { + unsigned int j, b11; + b11 = r[len - 1] | ((r[len - 2] & 7) << 8); - for (j = len-2; j>0; j--) { - r[j+1] = (r[j]>>3) | (r[j-1]<<5); - } - r[1] = r[0]>>3; - r[0] = 0; - - return b11; + for (j = len - 2; j > 0; j--) { + r[j + 1] = (r[j] >> 3) | (r[j - 1] << 5); + } + r[1] = r[0] >> 3; + r[0] = 0; + + return b11; } -static unsigned int bolos_ux_electrum_mnemonic_encode(const uint8_t *seed17, uint8_t *out, size_t outLength) { - unsigned char tmp[17]; - unsigned int i; - unsigned int offset = 0; - memcpy(tmp, seed17, sizeof(tmp)); - for (i=0; i<12; i++) { - unsigned char wordLength; - unsigned int idx = cx_math_shiftr_11(tmp, sizeof(tmp)); - wordLength = BIP39_WORDLIST_OFFSETS[idx + 1] - BIP39_WORDLIST_OFFSETS[idx]; - if ((offset + wordLength) > outLength) { - THROW (INVALID_PARAMETER); - } - memcpy(out + offset, BIP39_WORDLIST + BIP39_WORDLIST_OFFSETS[idx], wordLength); - offset += wordLength; - if (i < 11) { - if (offset > outLength) { - THROW (INVALID_PARAMETER); - } - out[offset++] = ' '; +static unsigned int bolos_ux_electrum_mnemonic_encode(const uint8_t *seed17, + uint8_t *out, + size_t outLength) { + unsigned char tmp[17]; + unsigned int i; + unsigned int offset = 0; + memcpy(tmp, seed17, sizeof(tmp)); + for (i = 0; i < 12; i++) { + unsigned char wordLength; + unsigned int idx = cx_math_shiftr_11(tmp, sizeof(tmp)); + wordLength = BIP39_WORDLIST_OFFSETS[idx + 1] - BIP39_WORDLIST_OFFSETS[idx]; + if ((offset + wordLength) > outLength) { + THROW(INVALID_PARAMETER); + } + memcpy(out + offset, BIP39_WORDLIST + BIP39_WORDLIST_OFFSETS[idx], wordLength); + offset += wordLength; + if (i < 11) { + if (offset > outLength) { + THROW(INVALID_PARAMETER); + } + out[offset++] = ' '; + } } - } - return offset; + return offset; } -unsigned int bolos_ux_electrum_new_mnemonic(unsigned int version, unsigned char *out, unsigned int outLength) { - unsigned char seed[17]; - unsigned int nonce; - unsigned int offset; - // Initialize a proper seed <= 132 bits - for (;;) { - cx_rng(seed, sizeof(seed)); - if (seed[0] < 0x10) { - break; +unsigned int bolos_ux_electrum_new_mnemonic(unsigned int version, + unsigned char *out, + unsigned int outLength) { + unsigned char seed[17]; + unsigned int nonce; + unsigned int offset; + // Initialize a proper seed <= 132 bits + for (;;) { + cx_rng(seed, sizeof(seed)); + if (seed[0] < 0x10) { + break; + } } - } - nonce = (seed[sizeof(seed) - 4] << 24) | (seed[sizeof(seed) - 3] << 16) | (seed[sizeof(seed) - 2] << 8) | (seed[sizeof(seed) - 1]); - // Find a nonce that matches the version - for (;;) { - nonce++; - seed[sizeof(seed) - 4] = (nonce >> 24); - seed[sizeof(seed) - 3] = (nonce >> 16); - seed[sizeof(seed) - 2] = (nonce >> 8); - seed[sizeof(seed) - 1] = nonce; - offset = bolos_ux_electrum_mnemonic_encode(seed, out, outLength); - if (bolos_ux_electrum_mnemonic_check(version, out, offset)) { - break; + nonce = (seed[sizeof(seed) - 4] << 24) | (seed[sizeof(seed) - 3] << 16) | + (seed[sizeof(seed) - 2] << 8) | (seed[sizeof(seed) - 1]); + // Find a nonce that matches the version + for (;;) { + nonce++; + seed[sizeof(seed) - 4] = (nonce >> 24); + seed[sizeof(seed) - 3] = (nonce >> 16); + seed[sizeof(seed) - 2] = (nonce >> 8); + seed[sizeof(seed) - 1] = nonce; + offset = bolos_ux_electrum_mnemonic_encode(seed, out, outLength); + if (bolos_ux_electrum_mnemonic_check(version, out, offset)) { + break; + } } - } - return offset; + return offset; } -unsigned int bolos_ux_electrum_mnemonic_check(unsigned int version, unsigned char *mnemonic, unsigned int mnemonicLength) { - unsigned char tmp[64]; - cx_hmac_sha512(ELECTRUM_SEED_VERSION, ELECTRUM_SEED_VERSION_LENGTH, mnemonic, mnemonicLength, tmp, 64); - return (tmp[0] == version); +unsigned int bolos_ux_electrum_mnemonic_check(unsigned int version, + unsigned char *mnemonic, + unsigned int mnemonicLength) { + unsigned char tmp[64]; + cx_hmac_sha512(ELECTRUM_SEED_VERSION, + ELECTRUM_SEED_VERSION_LENGTH, + mnemonic, + mnemonicLength, + tmp, + 64); + return (tmp[0] == version); } #endif diff --git a/src_ux_common/bolos_ux_onboarding_seed_bip39.c b/src_ux_common/bolos_ux_onboarding_seed_bip39.c old mode 100755 new mode 100644 index 700a52eb..4c5a4b86 --- a/src_ux_common/bolos_ux_onboarding_seed_bip39.c +++ b/src_ux_common/bolos_ux_onboarding_seed_bip39.c @@ -5,258 +5,284 @@ #include "bolos_ux_common.h" -unsigned int bolos_ux_mnemonic_from_data(unsigned char *in, unsigned int inLength, unsigned char *out, unsigned int outLength) { - unsigned char bits[32 + 1]; - unsigned int mlen = inLength * 3 / 4; - unsigned int i, j, idx, offset; +unsigned int bolos_ux_mnemonic_from_data(unsigned char* in, + unsigned int inLength, + unsigned char* out, + unsigned int outLength) { + unsigned char bits[32 + 1]; + unsigned int mlen = inLength * 3 / 4; + unsigned int i, j, idx, offset; - if ((inLength % 4) || (inLength < 16) || (inLength > 32)) { - THROW (INVALID_PARAMETER); - } - cx_hash_sha256(in, inLength, bits, 32); - - bits[inLength] = bits[0]; - memcpy(bits, in, inLength); - offset = 0; - for (i = 0; i < mlen; i++) { - size_t wordLength; - idx = 0; - for (j = 0; j < 11; j++) { - idx <<= 1; - idx += (bits[(i * 11 + j) / 8] & (1 << (7 - ((i * 11 + j) % 8)))) > 0; - } - wordLength = BIP39_WORDLIST_OFFSETS[idx + 1] - BIP39_WORDLIST_OFFSETS[idx]; - if ((offset + wordLength) > outLength) { - THROW (INVALID_PARAMETER); + if ((inLength % 4) || (inLength < 16) || (inLength > 32)) { + THROW(INVALID_PARAMETER); } - memcpy(out + offset, BIP39_WORDLIST + BIP39_WORDLIST_OFFSETS[idx], wordLength); - offset += wordLength; - if (i < mlen - 1) { - if (offset > outLength) { - THROW (INVALID_PARAMETER); - } - out[offset++] = ' '; + cx_hash_sha256(in, inLength, bits, 32); + + bits[inLength] = bits[0]; + memcpy(bits, in, inLength); + offset = 0; + for (i = 0; i < mlen; i++) { + size_t wordLength; + idx = 0; + for (j = 0; j < 11; j++) { + idx <<= 1; + idx += (bits[(i * 11 + j) / 8] & (1 << (7 - ((i * 11 + j) % 8)))) > 0; + } + wordLength = BIP39_WORDLIST_OFFSETS[idx + 1] - BIP39_WORDLIST_OFFSETS[idx]; + if ((offset + wordLength) > outLength) { + THROW(INVALID_PARAMETER); + } + memcpy(out + offset, BIP39_WORDLIST + BIP39_WORDLIST_OFFSETS[idx], wordLength); + offset += wordLength; + if (i < mlen - 1) { + if (offset > outLength) { + THROW(INVALID_PARAMETER); + } + out[offset++] = ' '; + } } - } - return offset; + return offset; } // separated function to lower the stack usage when jumping into pbkdf algorithm -unsigned int bolos_ux_mnemonic_to_seed_hash_length128(unsigned char *mnemonic, unsigned int mnemonicLength) { - if (mnemonicLength > 128) { - cx_hash_sha512(mnemonic, mnemonicLength, mnemonic, 64); - // new mnemonic length - mnemonicLength = 64; - } - return mnemonicLength; +unsigned int bolos_ux_mnemonic_to_seed_hash_length128(unsigned char* mnemonic, + unsigned int mnemonicLength) { + if (mnemonicLength > 128) { + cx_hash_sha512(mnemonic, mnemonicLength, mnemonic, 64); + // new mnemonic length + mnemonicLength = 64; + } + return mnemonicLength; } -void bolos_ux_mnemonic_to_seed(unsigned char *mnemonic, unsigned int mnemonicLength, unsigned char *seed) { - unsigned char passphrase[BIP39_MNEMONIC_LENGTH + 4]; - mnemonicLength = bolos_ux_mnemonic_to_seed_hash_length128(mnemonic, mnemonicLength); +void bolos_ux_mnemonic_to_seed(unsigned char* mnemonic, + unsigned int mnemonicLength, + unsigned char* seed) { + unsigned char passphrase[BIP39_MNEMONIC_LENGTH + 4]; + mnemonicLength = bolos_ux_mnemonic_to_seed_hash_length128(mnemonic, mnemonicLength); - memcpy(passphrase, BIP39_MNEMONIC, BIP39_MNEMONIC_LENGTH); - cx_pbkdf2_sha512(mnemonic, mnemonicLength, passphrase, BIP39_MNEMONIC_LENGTH, BIP39_PBKDF2_ROUNDS, seed, 64); + memcpy(passphrase, BIP39_MNEMONIC, BIP39_MNEMONIC_LENGTH); + cx_pbkdf2_sha512(mnemonic, + mnemonicLength, + passphrase, + BIP39_MNEMONIC_LENGTH, + BIP39_PBKDF2_ROUNDS, + seed, + 64); - // what happen to the second block for a very short seed ? + // what happen to the second block for a very short seed ? } -unsigned int bolos_ux_get_word_ptr(unsigned char ** word, unsigned int max_length, unsigned int word_index) { - unsigned int word_length; +unsigned int bolos_ux_get_word_ptr(unsigned char** word, + unsigned int max_length, + unsigned int word_index) { + unsigned int word_length; - // seek next word - while(word_index--) { - while(*(*word) != ' ' && max_length) { - *word = (*word)+1; - max_length--; + // seek next word + while (word_index--) { + while (*(*word) != ' ' && max_length) { + *word = (*word) + 1; + max_length--; + } + // also skip the space + *word = (*word) + 1; + max_length--; } - // also skip the space - *word = (*word)+1; - max_length--; - } - // seek next word's length - word_length = 0; // could optim by using the smaller word length here (EOS or space as delim here) - while(word_length < max_length && (*word)[word_length] != ' ' && (*word)[word_length] != 0) { - word_length++; - } + // seek next word's length + word_length = + 0; // could optim by using the smaller word length here (EOS or space as delim here) + while (word_length < max_length && (*word)[word_length] != ' ' && (*word)[word_length] != 0) { + word_length++; + } - // word ptr is returned in the parameter - return word_length; + // word ptr is returned in the parameter + return word_length; } -unsigned int bolos_ux_mnemonic_check(unsigned char *mnemonic, unsigned int mnemonicLength) { - unsigned int i, n = 0; - unsigned int bi; - unsigned char bits[32 + 1]; - unsigned char mask; +unsigned int bolos_ux_mnemonic_check(unsigned char* mnemonic, unsigned int mnemonicLength) { + unsigned int i, n = 0; + unsigned int bi; + unsigned char bits[32 + 1]; + unsigned char mask; - for (i=0; i= sizeof(current_word)) { + n++; + if (n != 12 && n != 18 && n != 24) { return 0; - } - current_word[j] = mnemonic[i]; - current_word_size = j; - i++; - j++; - } - if (i < mnemonicLength) { - i++; } - current_word_size++; - for (k=0; k= sizeof(current_word)) { + return 0; + } + current_word[j] = mnemonic[i]; + current_word_size = j; + i++; + j++; + } + if (i < mnemonicLength) { + i++; + } + current_word_size++; + for (k = 0; k < BIP39_WORDLIST_OFFSETS_LENGTH - 1; k++) { + if ((memcmp(current_word, + BIP39_WORDLIST + BIP39_WORDLIST_OFFSETS[k], + current_word_size) == 0) && + ((unsigned int) (BIP39_WORDLIST_OFFSETS[k + 1] - BIP39_WORDLIST_OFFSETS[k]) == + current_word_size)) { + for (ki = 0; ki < 11; ki++) { + if (k & (1 << (10 - ki))) { + bits[bi / 8] |= 1 << (7 - (bi % 8)); + } + bi++; + } + break; + } + } + if (k == (unsigned int) (BIP39_WORDLIST_OFFSETS_LENGTH - 1)) { + return 0; } - break; - } } - if (k == (unsigned int)(BIP39_WORDLIST_OFFSETS_LENGTH - 1)) { - return 0; + if (bi != n * 11) { + return 0; + } + bits[32] = bits[n * 4 / 3]; + cx_hash_sha256(bits, n * 4 / 3, bits, 32); + switch (n) { + case 12: + mask = 0xF0; + break; + case 18: + mask = 0xFC; + break; + default: + mask = 0xFF; + break; + } + if ((bits[0] & mask) != (bits[32] & mask)) { + return 0; } - } - if (bi != n * 11) { - return 0; - } - bits[32] = bits[n * 4 / 3]; - cx_hash_sha256(bits, n * 4 / 3, bits, 32); - switch(n) { - case 12: - mask = 0xF0; - break; - case 18: - mask = 0xFC; - break; - default: - mask = 0xFF; - break; - } - if ((bits[0] & mask) != (bits[32] & mask)) { - return 0; - } - // alright mnemonic is ok - return 1; + // alright mnemonic is ok + return 1; } unsigned int bolos_ux_bip39_idx_strcpy(unsigned int index, unsigned char* buffer) { - if (index < BIP39_WORDLIST_OFFSETS_LENGTH-1 && buffer) { - size_t wordLength = BIP39_WORDLIST_OFFSETS[index + 1] - BIP39_WORDLIST_OFFSETS[index]; - memcpy(buffer, BIP39_WORDLIST + BIP39_WORDLIST_OFFSETS[index], wordLength); - buffer[wordLength] = 0; // EOS - return wordLength; - } - // no word at that index - //buffer[0] = 0; // EOS - return 0; + if (index < BIP39_WORDLIST_OFFSETS_LENGTH - 1 && buffer) { + size_t wordLength = BIP39_WORDLIST_OFFSETS[index + 1] - BIP39_WORDLIST_OFFSETS[index]; + memcpy(buffer, BIP39_WORDLIST + BIP39_WORDLIST_OFFSETS[index], wordLength); + buffer[wordLength] = 0; // EOS + return wordLength; + } + // no word at that index + // buffer[0] = 0; // EOS + return 0; } -unsigned int bolos_ux_bip39_idx_startswith(unsigned int index, unsigned char* prefix, unsigned int prefixlength) { - unsigned int j=0; - if (index < BIP39_WORDLIST_OFFSETS_LENGTH-1) { - while (j < (unsigned int)(BIP39_WORDLIST_OFFSETS[index+1] - BIP39_WORDLIST_OFFSETS[index]) - && BIP39_WORDLIST[BIP39_WORDLIST_OFFSETS[index]+j] == prefix[j]) { - j++; - } - if (j == prefixlength) { - return 1; +unsigned int bolos_ux_bip39_idx_startswith(unsigned int index, + unsigned char* prefix, + unsigned int prefixlength) { + unsigned int j = 0; + if (index < BIP39_WORDLIST_OFFSETS_LENGTH - 1) { + while (j < (unsigned int) (BIP39_WORDLIST_OFFSETS[index + 1] - + BIP39_WORDLIST_OFFSETS[index]) && + BIP39_WORDLIST[BIP39_WORDLIST_OFFSETS[index] + j] == prefix[j]) { + j++; + } + if (j == prefixlength) { + return 1; + } } - } - return 0; + return 0; } -unsigned int bolos_ux_bip39_get_word_idx_starting_with(unsigned char* prefix, unsigned int prefixlength) { - unsigned int i; - for (i = 0 ; i < BIP39_WORDLIST_OFFSETS_LENGTH-1; i++) { - unsigned int j=0; - while (j < (unsigned int)(BIP39_WORDLIST_OFFSETS[i+1] - BIP39_WORDLIST_OFFSETS[i]) - && j < prefixlength && BIP39_WORDLIST[BIP39_WORDLIST_OFFSETS[i]+j] == prefix[j]) { - j++; - } - if (j == prefixlength) { - return i; +unsigned int bolos_ux_bip39_get_word_idx_starting_with(unsigned char* prefix, + unsigned int prefixlength) { + unsigned int i; + for (i = 0; i < BIP39_WORDLIST_OFFSETS_LENGTH - 1; i++) { + unsigned int j = 0; + while (j < (unsigned int) (BIP39_WORDLIST_OFFSETS[i + 1] - BIP39_WORDLIST_OFFSETS[i]) && + j < prefixlength && BIP39_WORDLIST[BIP39_WORDLIST_OFFSETS[i] + j] == prefix[j]) { + j++; + } + if (j == prefixlength) { + return i; + } } - } - // no match, sry - return BIP39_WORDLIST_OFFSETS_LENGTH; + // no match, sry + return BIP39_WORDLIST_OFFSETS_LENGTH; } -unsigned int bolos_ux_bip39_get_word_count_starting_with(unsigned char* prefix, unsigned int prefixlength) { - unsigned int i; - unsigned int count=0; - for (i = 0 ; i < BIP39_WORDLIST_OFFSETS_LENGTH-1; i++) { - unsigned int j=0; - while (j < (unsigned int)(BIP39_WORDLIST_OFFSETS[i+1] - BIP39_WORDLIST_OFFSETS[i]) - && j < prefixlength && BIP39_WORDLIST[BIP39_WORDLIST_OFFSETS[i]+j] == prefix[j]) { - j++; - } - if (j == prefixlength) { - count++; - } - // don't seek till the end, abort when the prefix is not matched anymore - else if (count > 0) { - break; +unsigned int bolos_ux_bip39_get_word_count_starting_with(unsigned char* prefix, + unsigned int prefixlength) { + unsigned int i; + unsigned int count = 0; + for (i = 0; i < BIP39_WORDLIST_OFFSETS_LENGTH - 1; i++) { + unsigned int j = 0; + while (j < (unsigned int) (BIP39_WORDLIST_OFFSETS[i + 1] - BIP39_WORDLIST_OFFSETS[i]) && + j < prefixlength && BIP39_WORDLIST[BIP39_WORDLIST_OFFSETS[i] + j] == prefix[j]) { + j++; + } + if (j == prefixlength) { + count++; + } + // don't seek till the end, abort when the prefix is not matched anymore + else if (count > 0) { + break; + } } - } - // return number of matched word starting with the given prefix - return count; + // return number of matched word starting with the given prefix + return count; } // allocate at most 26 letters for next possibilities // algorithm considers the bip39 words are alphabetically ordered in the wordlist -unsigned int bolos_ux_bip39_get_word_next_letters_starting_with(unsigned char* prefix, unsigned int prefixlength, unsigned char* next_letters_buffer) { - unsigned int i; - unsigned int letter_count=0; - for (i = 0 ; i < BIP39_WORDLIST_OFFSETS_LENGTH-1; i++) { - unsigned int j=0; - while (j < (unsigned int)(BIP39_WORDLIST_OFFSETS[i+1] - BIP39_WORDLIST_OFFSETS[i]) - && j < prefixlength && BIP39_WORDLIST[BIP39_WORDLIST_OFFSETS[i]+j] == prefix[j]) { - j++; - } - if (j == prefixlength) { - if (j < (unsigned int)(BIP39_WORDLIST_OFFSETS[i+1] - BIP39_WORDLIST_OFFSETS[i])) { - // j is inc during previous loop, don't touch it - unsigned char next_letter = BIP39_WORDLIST[BIP39_WORDLIST_OFFSETS[i]+j]; - // add the first next_letter inconditionnally - if (letter_count == 0 ) { - next_letters_buffer[0] = next_letter; - letter_count = 1; +unsigned int bolos_ux_bip39_get_word_next_letters_starting_with( + unsigned char* prefix, + unsigned int prefixlength, + unsigned char* next_letters_buffer) { + unsigned int i; + unsigned int letter_count = 0; + for (i = 0; i < BIP39_WORDLIST_OFFSETS_LENGTH - 1; i++) { + unsigned int j = 0; + while (j < (unsigned int) (BIP39_WORDLIST_OFFSETS[i + 1] - BIP39_WORDLIST_OFFSETS[i]) && + j < prefixlength && BIP39_WORDLIST[BIP39_WORDLIST_OFFSETS[i] + j] == prefix[j]) { + j++; } - // the next_letter is different - else if (next_letters_buffer[0] != next_letter) { - next_letters_buffer++; - next_letters_buffer[0] = next_letter; - letter_count++; + if (j == prefixlength) { + if (j < (unsigned int) (BIP39_WORDLIST_OFFSETS[i + 1] - BIP39_WORDLIST_OFFSETS[i])) { + // j is inc during previous loop, don't touch it + unsigned char next_letter = BIP39_WORDLIST[BIP39_WORDLIST_OFFSETS[i] + j]; + // add the first next_letter inconditionnally + if (letter_count == 0) { + next_letters_buffer[0] = next_letter; + letter_count = 1; + } + // the next_letter is different + else if (next_letters_buffer[0] != next_letter) { + next_letters_buffer++; + next_letters_buffer[0] = next_letter; + letter_count++; + } + } + } + // don't seek till the end, abort when the prefix is not matched anymore + else if (letter_count > 0) { + break; } - } - } - // don't seek till the end, abort when the prefix is not matched anymore - else if (letter_count > 0) { - break; } - } - // return number of matched word starting with the given prefix - return letter_count; + // return number of matched word starting with the given prefix + return letter_count; } diff --git a/src_ux_common/bolos_ux_onboarding_seed_rom_variables.c b/src_ux_common/bolos_ux_onboarding_seed_rom_variables.c old mode 100755 new mode 100644 index 3033b049..6a981f60 --- a/src_ux_common/bolos_ux_onboarding_seed_rom_variables.c +++ b/src_ux_common/bolos_ux_onboarding_seed_rom_variables.c @@ -1,4120 +1,757 @@ -/* @BANNER@ */ +/* @BANNER@ */ unsigned char const BIP39_WORDLIST[] = { -'a','b','a','n','d','o','n', -'a','b','i','l','i','t','y', -'a','b','l','e', -'a','b','o','u','t', -'a','b','o','v','e', -'a','b','s','e','n','t', -'a','b','s','o','r','b', -'a','b','s','t','r','a','c','t', -'a','b','s','u','r','d', -'a','b','u','s','e', -'a','c','c','e','s','s', -'a','c','c','i','d','e','n','t', -'a','c','c','o','u','n','t', -'a','c','c','u','s','e', -'a','c','h','i','e','v','e', -'a','c','i','d', -'a','c','o','u','s','t','i','c', -'a','c','q','u','i','r','e', -'a','c','r','o','s','s', -'a','c','t', -'a','c','t','i','o','n', -'a','c','t','o','r', -'a','c','t','r','e','s','s', -'a','c','t','u','a','l', -'a','d','a','p','t', -'a','d','d', -'a','d','d','i','c','t', -'a','d','d','r','e','s','s', -'a','d','j','u','s','t', -'a','d','m','i','t', -'a','d','u','l','t', -'a','d','v','a','n','c','e', -'a','d','v','i','c','e', -'a','e','r','o','b','i','c', -'a','f','f','a','i','r', -'a','f','f','o','r','d', -'a','f','r','a','i','d', -'a','g','a','i','n', -'a','g','e', -'a','g','e','n','t', -'a','g','r','e','e', -'a','h','e','a','d', -'a','i','m', -'a','i','r', -'a','i','r','p','o','r','t', -'a','i','s','l','e', -'a','l','a','r','m', -'a','l','b','u','m', -'a','l','c','o','h','o','l', -'a','l','e','r','t', -'a','l','i','e','n', -'a','l','l', -'a','l','l','e','y', -'a','l','l','o','w', -'a','l','m','o','s','t', -'a','l','o','n','e', -'a','l','p','h','a', -'a','l','r','e','a','d','y', -'a','l','s','o', -'a','l','t','e','r', -'a','l','w','a','y','s', -'a','m','a','t','e','u','r', -'a','m','a','z','i','n','g', -'a','m','o','n','g', -'a','m','o','u','n','t', -'a','m','u','s','e','d', -'a','n','a','l','y','s','t', -'a','n','c','h','o','r', -'a','n','c','i','e','n','t', -'a','n','g','e','r', -'a','n','g','l','e', -'a','n','g','r','y', -'a','n','i','m','a','l', -'a','n','k','l','e', -'a','n','n','o','u','n','c','e', -'a','n','n','u','a','l', -'a','n','o','t','h','e','r', -'a','n','s','w','e','r', -'a','n','t','e','n','n','a', -'a','n','t','i','q','u','e', -'a','n','x','i','e','t','y', -'a','n','y', -'a','p','a','r','t', -'a','p','o','l','o','g','y', -'a','p','p','e','a','r', -'a','p','p','l','e', -'a','p','p','r','o','v','e', -'a','p','r','i','l', -'a','r','c','h', -'a','r','c','t','i','c', -'a','r','e','a', -'a','r','e','n','a', -'a','r','g','u','e', -'a','r','m', -'a','r','m','e','d', -'a','r','m','o','r', -'a','r','m','y', -'a','r','o','u','n','d', -'a','r','r','a','n','g','e', -'a','r','r','e','s','t', -'a','r','r','i','v','e', -'a','r','r','o','w', -'a','r','t', -'a','r','t','e','f','a','c','t', -'a','r','t','i','s','t', -'a','r','t','w','o','r','k', -'a','s','k', -'a','s','p','e','c','t', -'a','s','s','a','u','l','t', -'a','s','s','e','t', -'a','s','s','i','s','t', -'a','s','s','u','m','e', -'a','s','t','h','m','a', -'a','t','h','l','e','t','e', -'a','t','o','m', -'a','t','t','a','c','k', -'a','t','t','e','n','d', -'a','t','t','i','t','u','d','e', -'a','t','t','r','a','c','t', -'a','u','c','t','i','o','n', -'a','u','d','i','t', -'a','u','g','u','s','t', -'a','u','n','t', -'a','u','t','h','o','r', -'a','u','t','o', -'a','u','t','u','m','n', -'a','v','e','r','a','g','e', -'a','v','o','c','a','d','o', -'a','v','o','i','d', -'a','w','a','k','e', -'a','w','a','r','e', -'a','w','a','y', -'a','w','e','s','o','m','e', -'a','w','f','u','l', -'a','w','k','w','a','r','d', -'a','x','i','s', -'b','a','b','y', -'b','a','c','h','e','l','o','r', -'b','a','c','o','n', -'b','a','d','g','e', -'b','a','g', -'b','a','l','a','n','c','e', -'b','a','l','c','o','n','y', -'b','a','l','l', -'b','a','m','b','o','o', -'b','a','n','a','n','a', -'b','a','n','n','e','r', -'b','a','r', -'b','a','r','e','l','y', -'b','a','r','g','a','i','n', -'b','a','r','r','e','l', -'b','a','s','e', -'b','a','s','i','c', -'b','a','s','k','e','t', -'b','a','t','t','l','e', -'b','e','a','c','h', -'b','e','a','n', -'b','e','a','u','t','y', -'b','e','c','a','u','s','e', -'b','e','c','o','m','e', -'b','e','e','f', -'b','e','f','o','r','e', -'b','e','g','i','n', -'b','e','h','a','v','e', -'b','e','h','i','n','d', -'b','e','l','i','e','v','e', -'b','e','l','o','w', -'b','e','l','t', -'b','e','n','c','h', -'b','e','n','e','f','i','t', -'b','e','s','t', -'b','e','t','r','a','y', -'b','e','t','t','e','r', -'b','e','t','w','e','e','n', -'b','e','y','o','n','d', -'b','i','c','y','c','l','e', -'b','i','d', -'b','i','k','e', -'b','i','n','d', -'b','i','o','l','o','g','y', -'b','i','r','d', -'b','i','r','t','h', -'b','i','t','t','e','r', -'b','l','a','c','k', -'b','l','a','d','e', -'b','l','a','m','e', -'b','l','a','n','k','e','t', -'b','l','a','s','t', -'b','l','e','a','k', -'b','l','e','s','s', -'b','l','i','n','d', -'b','l','o','o','d', -'b','l','o','s','s','o','m', -'b','l','o','u','s','e', -'b','l','u','e', -'b','l','u','r', -'b','l','u','s','h', -'b','o','a','r','d', -'b','o','a','t', -'b','o','d','y', -'b','o','i','l', -'b','o','m','b', -'b','o','n','e', -'b','o','n','u','s', -'b','o','o','k', -'b','o','o','s','t', -'b','o','r','d','e','r', -'b','o','r','i','n','g', -'b','o','r','r','o','w', -'b','o','s','s', -'b','o','t','t','o','m', -'b','o','u','n','c','e', -'b','o','x', -'b','o','y', -'b','r','a','c','k','e','t', -'b','r','a','i','n', -'b','r','a','n','d', -'b','r','a','s','s', -'b','r','a','v','e', -'b','r','e','a','d', -'b','r','e','e','z','e', -'b','r','i','c','k', -'b','r','i','d','g','e', -'b','r','i','e','f', -'b','r','i','g','h','t', -'b','r','i','n','g', -'b','r','i','s','k', -'b','r','o','c','c','o','l','i', -'b','r','o','k','e','n', -'b','r','o','n','z','e', -'b','r','o','o','m', -'b','r','o','t','h','e','r', -'b','r','o','w','n', -'b','r','u','s','h', -'b','u','b','b','l','e', -'b','u','d','d','y', -'b','u','d','g','e','t', -'b','u','f','f','a','l','o', -'b','u','i','l','d', -'b','u','l','b', -'b','u','l','k', -'b','u','l','l','e','t', -'b','u','n','d','l','e', -'b','u','n','k','e','r', -'b','u','r','d','e','n', -'b','u','r','g','e','r', -'b','u','r','s','t', -'b','u','s', -'b','u','s','i','n','e','s','s', -'b','u','s','y', -'b','u','t','t','e','r', -'b','u','y','e','r', -'b','u','z','z', -'c','a','b','b','a','g','e', -'c','a','b','i','n', -'c','a','b','l','e', -'c','a','c','t','u','s', -'c','a','g','e', -'c','a','k','e', -'c','a','l','l', -'c','a','l','m', -'c','a','m','e','r','a', -'c','a','m','p', -'c','a','n', -'c','a','n','a','l', -'c','a','n','c','e','l', -'c','a','n','d','y', -'c','a','n','n','o','n', -'c','a','n','o','e', -'c','a','n','v','a','s', -'c','a','n','y','o','n', -'c','a','p','a','b','l','e', -'c','a','p','i','t','a','l', -'c','a','p','t','a','i','n', -'c','a','r', -'c','a','r','b','o','n', -'c','a','r','d', -'c','a','r','g','o', -'c','a','r','p','e','t', -'c','a','r','r','y', -'c','a','r','t', -'c','a','s','e', -'c','a','s','h', -'c','a','s','i','n','o', -'c','a','s','t','l','e', -'c','a','s','u','a','l', -'c','a','t', -'c','a','t','a','l','o','g', -'c','a','t','c','h', -'c','a','t','e','g','o','r','y', -'c','a','t','t','l','e', -'c','a','u','g','h','t', -'c','a','u','s','e', -'c','a','u','t','i','o','n', -'c','a','v','e', -'c','e','i','l','i','n','g', -'c','e','l','e','r','y', -'c','e','m','e','n','t', -'c','e','n','s','u','s', -'c','e','n','t','u','r','y', -'c','e','r','e','a','l', -'c','e','r','t','a','i','n', -'c','h','a','i','r', -'c','h','a','l','k', -'c','h','a','m','p','i','o','n', -'c','h','a','n','g','e', -'c','h','a','o','s', -'c','h','a','p','t','e','r', -'c','h','a','r','g','e', -'c','h','a','s','e', -'c','h','a','t', -'c','h','e','a','p', -'c','h','e','c','k', -'c','h','e','e','s','e', -'c','h','e','f', -'c','h','e','r','r','y', -'c','h','e','s','t', -'c','h','i','c','k','e','n', -'c','h','i','e','f', -'c','h','i','l','d', -'c','h','i','m','n','e','y', -'c','h','o','i','c','e', -'c','h','o','o','s','e', -'c','h','r','o','n','i','c', -'c','h','u','c','k','l','e', -'c','h','u','n','k', -'c','h','u','r','n', -'c','i','g','a','r', -'c','i','n','n','a','m','o','n', -'c','i','r','c','l','e', -'c','i','t','i','z','e','n', -'c','i','t','y', -'c','i','v','i','l', -'c','l','a','i','m', -'c','l','a','p', -'c','l','a','r','i','f','y', -'c','l','a','w', -'c','l','a','y', -'c','l','e','a','n', -'c','l','e','r','k', -'c','l','e','v','e','r', -'c','l','i','c','k', -'c','l','i','e','n','t', -'c','l','i','f','f', -'c','l','i','m','b', -'c','l','i','n','i','c', -'c','l','i','p', -'c','l','o','c','k', -'c','l','o','g', -'c','l','o','s','e', -'c','l','o','t','h', -'c','l','o','u','d', -'c','l','o','w','n', -'c','l','u','b', -'c','l','u','m','p', -'c','l','u','s','t','e','r', -'c','l','u','t','c','h', -'c','o','a','c','h', -'c','o','a','s','t', -'c','o','c','o','n','u','t', -'c','o','d','e', -'c','o','f','f','e','e', -'c','o','i','l', -'c','o','i','n', -'c','o','l','l','e','c','t', -'c','o','l','o','r', -'c','o','l','u','m','n', -'c','o','m','b','i','n','e', -'c','o','m','e', -'c','o','m','f','o','r','t', -'c','o','m','i','c', -'c','o','m','m','o','n', -'c','o','m','p','a','n','y', -'c','o','n','c','e','r','t', -'c','o','n','d','u','c','t', -'c','o','n','f','i','r','m', -'c','o','n','g','r','e','s','s', -'c','o','n','n','e','c','t', -'c','o','n','s','i','d','e','r', -'c','o','n','t','r','o','l', -'c','o','n','v','i','n','c','e', -'c','o','o','k', -'c','o','o','l', -'c','o','p','p','e','r', -'c','o','p','y', -'c','o','r','a','l', -'c','o','r','e', -'c','o','r','n', -'c','o','r','r','e','c','t', -'c','o','s','t', -'c','o','t','t','o','n', -'c','o','u','c','h', -'c','o','u','n','t','r','y', -'c','o','u','p','l','e', -'c','o','u','r','s','e', -'c','o','u','s','i','n', -'c','o','v','e','r', -'c','o','y','o','t','e', -'c','r','a','c','k', -'c','r','a','d','l','e', -'c','r','a','f','t', -'c','r','a','m', -'c','r','a','n','e', -'c','r','a','s','h', -'c','r','a','t','e','r', -'c','r','a','w','l', -'c','r','a','z','y', -'c','r','e','a','m', -'c','r','e','d','i','t', -'c','r','e','e','k', -'c','r','e','w', -'c','r','i','c','k','e','t', -'c','r','i','m','e', -'c','r','i','s','p', -'c','r','i','t','i','c', -'c','r','o','p', -'c','r','o','s','s', -'c','r','o','u','c','h', -'c','r','o','w','d', -'c','r','u','c','i','a','l', -'c','r','u','e','l', -'c','r','u','i','s','e', -'c','r','u','m','b','l','e', -'c','r','u','n','c','h', -'c','r','u','s','h', -'c','r','y', -'c','r','y','s','t','a','l', -'c','u','b','e', -'c','u','l','t','u','r','e', -'c','u','p', -'c','u','p','b','o','a','r','d', -'c','u','r','i','o','u','s', -'c','u','r','r','e','n','t', -'c','u','r','t','a','i','n', -'c','u','r','v','e', -'c','u','s','h','i','o','n', -'c','u','s','t','o','m', -'c','u','t','e', -'c','y','c','l','e', -'d','a','d', -'d','a','m','a','g','e', -'d','a','m','p', -'d','a','n','c','e', -'d','a','n','g','e','r', -'d','a','r','i','n','g', -'d','a','s','h', -'d','a','u','g','h','t','e','r', -'d','a','w','n', -'d','a','y', -'d','e','a','l', -'d','e','b','a','t','e', -'d','e','b','r','i','s', -'d','e','c','a','d','e', -'d','e','c','e','m','b','e','r', -'d','e','c','i','d','e', -'d','e','c','l','i','n','e', -'d','e','c','o','r','a','t','e', -'d','e','c','r','e','a','s','e', -'d','e','e','r', -'d','e','f','e','n','s','e', -'d','e','f','i','n','e', -'d','e','f','y', -'d','e','g','r','e','e', -'d','e','l','a','y', -'d','e','l','i','v','e','r', -'d','e','m','a','n','d', -'d','e','m','i','s','e', -'d','e','n','i','a','l', -'d','e','n','t','i','s','t', -'d','e','n','y', -'d','e','p','a','r','t', -'d','e','p','e','n','d', -'d','e','p','o','s','i','t', -'d','e','p','t','h', -'d','e','p','u','t','y', -'d','e','r','i','v','e', -'d','e','s','c','r','i','b','e', -'d','e','s','e','r','t', -'d','e','s','i','g','n', -'d','e','s','k', -'d','e','s','p','a','i','r', -'d','e','s','t','r','o','y', -'d','e','t','a','i','l', -'d','e','t','e','c','t', -'d','e','v','e','l','o','p', -'d','e','v','i','c','e', -'d','e','v','o','t','e', -'d','i','a','g','r','a','m', -'d','i','a','l', -'d','i','a','m','o','n','d', -'d','i','a','r','y', -'d','i','c','e', -'d','i','e','s','e','l', -'d','i','e','t', -'d','i','f','f','e','r', -'d','i','g','i','t','a','l', -'d','i','g','n','i','t','y', -'d','i','l','e','m','m','a', -'d','i','n','n','e','r', -'d','i','n','o','s','a','u','r', -'d','i','r','e','c','t', -'d','i','r','t', -'d','i','s','a','g','r','e','e', -'d','i','s','c','o','v','e','r', -'d','i','s','e','a','s','e', -'d','i','s','h', -'d','i','s','m','i','s','s', -'d','i','s','o','r','d','e','r', -'d','i','s','p','l','a','y', -'d','i','s','t','a','n','c','e', -'d','i','v','e','r','t', -'d','i','v','i','d','e', -'d','i','v','o','r','c','e', -'d','i','z','z','y', -'d','o','c','t','o','r', -'d','o','c','u','m','e','n','t', -'d','o','g', -'d','o','l','l', -'d','o','l','p','h','i','n', -'d','o','m','a','i','n', -'d','o','n','a','t','e', -'d','o','n','k','e','y', -'d','o','n','o','r', -'d','o','o','r', -'d','o','s','e', -'d','o','u','b','l','e', -'d','o','v','e', -'d','r','a','f','t', -'d','r','a','g','o','n', -'d','r','a','m','a', -'d','r','a','s','t','i','c', -'d','r','a','w', -'d','r','e','a','m', -'d','r','e','s','s', -'d','r','i','f','t', -'d','r','i','l','l', -'d','r','i','n','k', -'d','r','i','p', -'d','r','i','v','e', -'d','r','o','p', -'d','r','u','m', -'d','r','y', -'d','u','c','k', -'d','u','m','b', -'d','u','n','e', -'d','u','r','i','n','g', -'d','u','s','t', -'d','u','t','c','h', -'d','u','t','y', -'d','w','a','r','f', -'d','y','n','a','m','i','c', -'e','a','g','e','r', -'e','a','g','l','e', -'e','a','r','l','y', -'e','a','r','n', -'e','a','r','t','h', -'e','a','s','i','l','y', -'e','a','s','t', -'e','a','s','y', -'e','c','h','o', -'e','c','o','l','o','g','y', -'e','c','o','n','o','m','y', -'e','d','g','e', -'e','d','i','t', -'e','d','u','c','a','t','e', -'e','f','f','o','r','t', -'e','g','g', -'e','i','g','h','t', -'e','i','t','h','e','r', -'e','l','b','o','w', -'e','l','d','e','r', -'e','l','e','c','t','r','i','c', -'e','l','e','g','a','n','t', -'e','l','e','m','e','n','t', -'e','l','e','p','h','a','n','t', -'e','l','e','v','a','t','o','r', -'e','l','i','t','e', -'e','l','s','e', -'e','m','b','a','r','k', -'e','m','b','o','d','y', -'e','m','b','r','a','c','e', -'e','m','e','r','g','e', -'e','m','o','t','i','o','n', -'e','m','p','l','o','y', -'e','m','p','o','w','e','r', -'e','m','p','t','y', -'e','n','a','b','l','e', -'e','n','a','c','t', -'e','n','d', -'e','n','d','l','e','s','s', -'e','n','d','o','r','s','e', -'e','n','e','m','y', -'e','n','e','r','g','y', -'e','n','f','o','r','c','e', -'e','n','g','a','g','e', -'e','n','g','i','n','e', -'e','n','h','a','n','c','e', -'e','n','j','o','y', -'e','n','l','i','s','t', -'e','n','o','u','g','h', -'e','n','r','i','c','h', -'e','n','r','o','l','l', -'e','n','s','u','r','e', -'e','n','t','e','r', -'e','n','t','i','r','e', -'e','n','t','r','y', -'e','n','v','e','l','o','p','e', -'e','p','i','s','o','d','e', -'e','q','u','a','l', -'e','q','u','i','p', -'e','r','a', -'e','r','a','s','e', -'e','r','o','d','e', -'e','r','o','s','i','o','n', -'e','r','r','o','r', -'e','r','u','p','t', -'e','s','c','a','p','e', -'e','s','s','a','y', -'e','s','s','e','n','c','e', -'e','s','t','a','t','e', -'e','t','e','r','n','a','l', -'e','t','h','i','c','s', -'e','v','i','d','e','n','c','e', -'e','v','i','l', -'e','v','o','k','e', -'e','v','o','l','v','e', -'e','x','a','c','t', -'e','x','a','m','p','l','e', -'e','x','c','e','s','s', -'e','x','c','h','a','n','g','e', -'e','x','c','i','t','e', -'e','x','c','l','u','d','e', -'e','x','c','u','s','e', -'e','x','e','c','u','t','e', -'e','x','e','r','c','i','s','e', -'e','x','h','a','u','s','t', -'e','x','h','i','b','i','t', -'e','x','i','l','e', -'e','x','i','s','t', -'e','x','i','t', -'e','x','o','t','i','c', -'e','x','p','a','n','d', -'e','x','p','e','c','t', -'e','x','p','i','r','e', -'e','x','p','l','a','i','n', -'e','x','p','o','s','e', -'e','x','p','r','e','s','s', -'e','x','t','e','n','d', -'e','x','t','r','a', -'e','y','e', -'e','y','e','b','r','o','w', -'f','a','b','r','i','c', -'f','a','c','e', -'f','a','c','u','l','t','y', -'f','a','d','e', -'f','a','i','n','t', -'f','a','i','t','h', -'f','a','l','l', -'f','a','l','s','e', -'f','a','m','e', -'f','a','m','i','l','y', -'f','a','m','o','u','s', -'f','a','n', -'f','a','n','c','y', -'f','a','n','t','a','s','y', -'f','a','r','m', -'f','a','s','h','i','o','n', -'f','a','t', -'f','a','t','a','l', -'f','a','t','h','e','r', -'f','a','t','i','g','u','e', -'f','a','u','l','t', -'f','a','v','o','r','i','t','e', -'f','e','a','t','u','r','e', -'f','e','b','r','u','a','r','y', -'f','e','d','e','r','a','l', -'f','e','e', -'f','e','e','d', -'f','e','e','l', -'f','e','m','a','l','e', -'f','e','n','c','e', -'f','e','s','t','i','v','a','l', -'f','e','t','c','h', -'f','e','v','e','r', -'f','e','w', -'f','i','b','e','r', -'f','i','c','t','i','o','n', -'f','i','e','l','d', -'f','i','g','u','r','e', -'f','i','l','e', -'f','i','l','m', -'f','i','l','t','e','r', -'f','i','n','a','l', -'f','i','n','d', -'f','i','n','e', -'f','i','n','g','e','r', -'f','i','n','i','s','h', -'f','i','r','e', -'f','i','r','m', -'f','i','r','s','t', -'f','i','s','c','a','l', -'f','i','s','h', -'f','i','t', -'f','i','t','n','e','s','s', -'f','i','x', -'f','l','a','g', -'f','l','a','m','e', -'f','l','a','s','h', -'f','l','a','t', -'f','l','a','v','o','r', -'f','l','e','e', -'f','l','i','g','h','t', -'f','l','i','p', -'f','l','o','a','t', -'f','l','o','c','k', -'f','l','o','o','r', -'f','l','o','w','e','r', -'f','l','u','i','d', -'f','l','u','s','h', -'f','l','y', -'f','o','a','m', -'f','o','c','u','s', -'f','o','g', -'f','o','i','l', -'f','o','l','d', -'f','o','l','l','o','w', -'f','o','o','d', -'f','o','o','t', -'f','o','r','c','e', -'f','o','r','e','s','t', -'f','o','r','g','e','t', -'f','o','r','k', -'f','o','r','t','u','n','e', -'f','o','r','u','m', -'f','o','r','w','a','r','d', -'f','o','s','s','i','l', -'f','o','s','t','e','r', -'f','o','u','n','d', -'f','o','x', -'f','r','a','g','i','l','e', -'f','r','a','m','e', -'f','r','e','q','u','e','n','t', -'f','r','e','s','h', -'f','r','i','e','n','d', -'f','r','i','n','g','e', -'f','r','o','g', -'f','r','o','n','t', -'f','r','o','s','t', -'f','r','o','w','n', -'f','r','o','z','e','n', -'f','r','u','i','t', -'f','u','e','l', -'f','u','n', -'f','u','n','n','y', -'f','u','r','n','a','c','e', -'f','u','r','y', -'f','u','t','u','r','e', -'g','a','d','g','e','t', -'g','a','i','n', -'g','a','l','a','x','y', -'g','a','l','l','e','r','y', -'g','a','m','e', -'g','a','p', -'g','a','r','a','g','e', -'g','a','r','b','a','g','e', -'g','a','r','d','e','n', -'g','a','r','l','i','c', -'g','a','r','m','e','n','t', -'g','a','s', -'g','a','s','p', -'g','a','t','e', -'g','a','t','h','e','r', -'g','a','u','g','e', -'g','a','z','e', -'g','e','n','e','r','a','l', -'g','e','n','i','u','s', -'g','e','n','r','e', -'g','e','n','t','l','e', -'g','e','n','u','i','n','e', -'g','e','s','t','u','r','e', -'g','h','o','s','t', -'g','i','a','n','t', -'g','i','f','t', -'g','i','g','g','l','e', -'g','i','n','g','e','r', -'g','i','r','a','f','f','e', -'g','i','r','l', -'g','i','v','e', -'g','l','a','d', -'g','l','a','n','c','e', -'g','l','a','r','e', -'g','l','a','s','s', -'g','l','i','d','e', -'g','l','i','m','p','s','e', -'g','l','o','b','e', -'g','l','o','o','m', -'g','l','o','r','y', -'g','l','o','v','e', -'g','l','o','w', -'g','l','u','e', -'g','o','a','t', -'g','o','d','d','e','s','s', -'g','o','l','d', -'g','o','o','d', -'g','o','o','s','e', -'g','o','r','i','l','l','a', -'g','o','s','p','e','l', -'g','o','s','s','i','p', -'g','o','v','e','r','n', -'g','o','w','n', -'g','r','a','b', -'g','r','a','c','e', -'g','r','a','i','n', -'g','r','a','n','t', -'g','r','a','p','e', -'g','r','a','s','s', -'g','r','a','v','i','t','y', -'g','r','e','a','t', -'g','r','e','e','n', -'g','r','i','d', -'g','r','i','e','f', -'g','r','i','t', -'g','r','o','c','e','r','y', -'g','r','o','u','p', -'g','r','o','w', -'g','r','u','n','t', -'g','u','a','r','d', -'g','u','e','s','s', -'g','u','i','d','e', -'g','u','i','l','t', -'g','u','i','t','a','r', -'g','u','n', -'g','y','m', -'h','a','b','i','t', -'h','a','i','r', -'h','a','l','f', -'h','a','m','m','e','r', -'h','a','m','s','t','e','r', -'h','a','n','d', -'h','a','p','p','y', -'h','a','r','b','o','r', -'h','a','r','d', -'h','a','r','s','h', -'h','a','r','v','e','s','t', -'h','a','t', -'h','a','v','e', -'h','a','w','k', -'h','a','z','a','r','d', -'h','e','a','d', -'h','e','a','l','t','h', -'h','e','a','r','t', -'h','e','a','v','y', -'h','e','d','g','e','h','o','g', -'h','e','i','g','h','t', -'h','e','l','l','o', -'h','e','l','m','e','t', -'h','e','l','p', -'h','e','n', -'h','e','r','o', -'h','i','d','d','e','n', -'h','i','g','h', -'h','i','l','l', -'h','i','n','t', -'h','i','p', -'h','i','r','e', -'h','i','s','t','o','r','y', -'h','o','b','b','y', -'h','o','c','k','e','y', -'h','o','l','d', -'h','o','l','e', -'h','o','l','i','d','a','y', -'h','o','l','l','o','w', -'h','o','m','e', -'h','o','n','e','y', -'h','o','o','d', -'h','o','p','e', -'h','o','r','n', -'h','o','r','r','o','r', -'h','o','r','s','e', -'h','o','s','p','i','t','a','l', -'h','o','s','t', -'h','o','t','e','l', -'h','o','u','r', -'h','o','v','e','r', -'h','u','b', -'h','u','g','e', -'h','u','m','a','n', -'h','u','m','b','l','e', -'h','u','m','o','r', -'h','u','n','d','r','e','d', -'h','u','n','g','r','y', -'h','u','n','t', -'h','u','r','d','l','e', -'h','u','r','r','y', -'h','u','r','t', -'h','u','s','b','a','n','d', -'h','y','b','r','i','d', -'i','c','e', -'i','c','o','n', -'i','d','e','a', -'i','d','e','n','t','i','f','y', -'i','d','l','e', -'i','g','n','o','r','e', -'i','l','l', -'i','l','l','e','g','a','l', -'i','l','l','n','e','s','s', -'i','m','a','g','e', -'i','m','i','t','a','t','e', -'i','m','m','e','n','s','e', -'i','m','m','u','n','e', -'i','m','p','a','c','t', -'i','m','p','o','s','e', -'i','m','p','r','o','v','e', -'i','m','p','u','l','s','e', -'i','n','c','h', -'i','n','c','l','u','d','e', -'i','n','c','o','m','e', -'i','n','c','r','e','a','s','e', -'i','n','d','e','x', -'i','n','d','i','c','a','t','e', -'i','n','d','o','o','r', -'i','n','d','u','s','t','r','y', -'i','n','f','a','n','t', -'i','n','f','l','i','c','t', -'i','n','f','o','r','m', -'i','n','h','a','l','e', -'i','n','h','e','r','i','t', -'i','n','i','t','i','a','l', -'i','n','j','e','c','t', -'i','n','j','u','r','y', -'i','n','m','a','t','e', -'i','n','n','e','r', -'i','n','n','o','c','e','n','t', -'i','n','p','u','t', -'i','n','q','u','i','r','y', -'i','n','s','a','n','e', -'i','n','s','e','c','t', -'i','n','s','i','d','e', -'i','n','s','p','i','r','e', -'i','n','s','t','a','l','l', -'i','n','t','a','c','t', -'i','n','t','e','r','e','s','t', -'i','n','t','o', -'i','n','v','e','s','t', -'i','n','v','i','t','e', -'i','n','v','o','l','v','e', -'i','r','o','n', -'i','s','l','a','n','d', -'i','s','o','l','a','t','e', -'i','s','s','u','e', -'i','t','e','m', -'i','v','o','r','y', -'j','a','c','k','e','t', -'j','a','g','u','a','r', -'j','a','r', -'j','a','z','z', -'j','e','a','l','o','u','s', -'j','e','a','n','s', -'j','e','l','l','y', -'j','e','w','e','l', -'j','o','b', -'j','o','i','n', -'j','o','k','e', -'j','o','u','r','n','e','y', -'j','o','y', -'j','u','d','g','e', -'j','u','i','c','e', -'j','u','m','p', -'j','u','n','g','l','e', -'j','u','n','i','o','r', -'j','u','n','k', -'j','u','s','t', -'k','a','n','g','a','r','o','o', -'k','e','e','n', -'k','e','e','p', -'k','e','t','c','h','u','p', -'k','e','y', -'k','i','c','k', -'k','i','d', -'k','i','d','n','e','y', -'k','i','n','d', -'k','i','n','g','d','o','m', -'k','i','s','s', -'k','i','t', -'k','i','t','c','h','e','n', -'k','i','t','e', -'k','i','t','t','e','n', -'k','i','w','i', -'k','n','e','e', -'k','n','i','f','e', -'k','n','o','c','k', -'k','n','o','w', -'l','a','b', -'l','a','b','e','l', -'l','a','b','o','r', -'l','a','d','d','e','r', -'l','a','d','y', -'l','a','k','e', -'l','a','m','p', -'l','a','n','g','u','a','g','e', -'l','a','p','t','o','p', -'l','a','r','g','e', -'l','a','t','e','r', -'l','a','t','i','n', -'l','a','u','g','h', -'l','a','u','n','d','r','y', -'l','a','v','a', -'l','a','w', -'l','a','w','n', -'l','a','w','s','u','i','t', -'l','a','y','e','r', -'l','a','z','y', -'l','e','a','d','e','r', -'l','e','a','f', -'l','e','a','r','n', -'l','e','a','v','e', -'l','e','c','t','u','r','e', -'l','e','f','t', -'l','e','g', -'l','e','g','a','l', -'l','e','g','e','n','d', -'l','e','i','s','u','r','e', -'l','e','m','o','n', -'l','e','n','d', -'l','e','n','g','t','h', -'l','e','n','s', -'l','e','o','p','a','r','d', -'l','e','s','s','o','n', -'l','e','t','t','e','r', -'l','e','v','e','l', -'l','i','a','r', -'l','i','b','e','r','t','y', -'l','i','b','r','a','r','y', -'l','i','c','e','n','s','e', -'l','i','f','e', -'l','i','f','t', -'l','i','g','h','t', -'l','i','k','e', -'l','i','m','b', -'l','i','m','i','t', -'l','i','n','k', -'l','i','o','n', -'l','i','q','u','i','d', -'l','i','s','t', -'l','i','t','t','l','e', -'l','i','v','e', -'l','i','z','a','r','d', -'l','o','a','d', -'l','o','a','n', -'l','o','b','s','t','e','r', -'l','o','c','a','l', -'l','o','c','k', -'l','o','g','i','c', -'l','o','n','e','l','y', -'l','o','n','g', -'l','o','o','p', -'l','o','t','t','e','r','y', -'l','o','u','d', -'l','o','u','n','g','e', -'l','o','v','e', -'l','o','y','a','l', -'l','u','c','k','y', -'l','u','g','g','a','g','e', -'l','u','m','b','e','r', -'l','u','n','a','r', -'l','u','n','c','h', -'l','u','x','u','r','y', -'l','y','r','i','c','s', -'m','a','c','h','i','n','e', -'m','a','d', -'m','a','g','i','c', -'m','a','g','n','e','t', -'m','a','i','d', -'m','a','i','l', -'m','a','i','n', -'m','a','j','o','r', -'m','a','k','e', -'m','a','m','m','a','l', -'m','a','n', -'m','a','n','a','g','e', -'m','a','n','d','a','t','e', -'m','a','n','g','o', -'m','a','n','s','i','o','n', -'m','a','n','u','a','l', -'m','a','p','l','e', -'m','a','r','b','l','e', -'m','a','r','c','h', -'m','a','r','g','i','n', -'m','a','r','i','n','e', -'m','a','r','k','e','t', -'m','a','r','r','i','a','g','e', -'m','a','s','k', -'m','a','s','s', -'m','a','s','t','e','r', -'m','a','t','c','h', -'m','a','t','e','r','i','a','l', -'m','a','t','h', -'m','a','t','r','i','x', -'m','a','t','t','e','r', -'m','a','x','i','m','u','m', -'m','a','z','e', -'m','e','a','d','o','w', -'m','e','a','n', -'m','e','a','s','u','r','e', -'m','e','a','t', -'m','e','c','h','a','n','i','c', -'m','e','d','a','l', -'m','e','d','i','a', -'m','e','l','o','d','y', -'m','e','l','t', -'m','e','m','b','e','r', -'m','e','m','o','r','y', -'m','e','n','t','i','o','n', -'m','e','n','u', -'m','e','r','c','y', -'m','e','r','g','e', -'m','e','r','i','t', -'m','e','r','r','y', -'m','e','s','h', -'m','e','s','s','a','g','e', -'m','e','t','a','l', -'m','e','t','h','o','d', -'m','i','d','d','l','e', -'m','i','d','n','i','g','h','t', -'m','i','l','k', -'m','i','l','l','i','o','n', -'m','i','m','i','c', -'m','i','n','d', -'m','i','n','i','m','u','m', -'m','i','n','o','r', -'m','i','n','u','t','e', -'m','i','r','a','c','l','e', -'m','i','r','r','o','r', -'m','i','s','e','r','y', -'m','i','s','s', -'m','i','s','t','a','k','e', -'m','i','x', -'m','i','x','e','d', -'m','i','x','t','u','r','e', -'m','o','b','i','l','e', -'m','o','d','e','l', -'m','o','d','i','f','y', -'m','o','m', -'m','o','m','e','n','t', -'m','o','n','i','t','o','r', -'m','o','n','k','e','y', -'m','o','n','s','t','e','r', -'m','o','n','t','h', -'m','o','o','n', -'m','o','r','a','l', -'m','o','r','e', -'m','o','r','n','i','n','g', -'m','o','s','q','u','i','t','o', -'m','o','t','h','e','r', -'m','o','t','i','o','n', -'m','o','t','o','r', -'m','o','u','n','t','a','i','n', -'m','o','u','s','e', -'m','o','v','e', -'m','o','v','i','e', -'m','u','c','h', -'m','u','f','f','i','n', -'m','u','l','e', -'m','u','l','t','i','p','l','y', -'m','u','s','c','l','e', -'m','u','s','e','u','m', -'m','u','s','h','r','o','o','m', -'m','u','s','i','c', -'m','u','s','t', -'m','u','t','u','a','l', -'m','y','s','e','l','f', -'m','y','s','t','e','r','y', -'m','y','t','h', -'n','a','i','v','e', -'n','a','m','e', -'n','a','p','k','i','n', -'n','a','r','r','o','w', -'n','a','s','t','y', -'n','a','t','i','o','n', -'n','a','t','u','r','e', -'n','e','a','r', -'n','e','c','k', -'n','e','e','d', -'n','e','g','a','t','i','v','e', -'n','e','g','l','e','c','t', -'n','e','i','t','h','e','r', -'n','e','p','h','e','w', -'n','e','r','v','e', -'n','e','s','t', -'n','e','t', -'n','e','t','w','o','r','k', -'n','e','u','t','r','a','l', -'n','e','v','e','r', -'n','e','w','s', -'n','e','x','t', -'n','i','c','e', -'n','i','g','h','t', -'n','o','b','l','e', -'n','o','i','s','e', -'n','o','m','i','n','e','e', -'n','o','o','d','l','e', -'n','o','r','m','a','l', -'n','o','r','t','h', -'n','o','s','e', -'n','o','t','a','b','l','e', -'n','o','t','e', -'n','o','t','h','i','n','g', -'n','o','t','i','c','e', -'n','o','v','e','l', -'n','o','w', -'n','u','c','l','e','a','r', -'n','u','m','b','e','r', -'n','u','r','s','e', -'n','u','t', -'o','a','k', -'o','b','e','y', -'o','b','j','e','c','t', -'o','b','l','i','g','e', -'o','b','s','c','u','r','e', -'o','b','s','e','r','v','e', -'o','b','t','a','i','n', -'o','b','v','i','o','u','s', -'o','c','c','u','r', -'o','c','e','a','n', -'o','c','t','o','b','e','r', -'o','d','o','r', -'o','f','f', -'o','f','f','e','r', -'o','f','f','i','c','e', -'o','f','t','e','n', -'o','i','l', -'o','k','a','y', -'o','l','d', -'o','l','i','v','e', -'o','l','y','m','p','i','c', -'o','m','i','t', -'o','n','c','e', -'o','n','e', -'o','n','i','o','n', -'o','n','l','i','n','e', -'o','n','l','y', -'o','p','e','n', -'o','p','e','r','a', -'o','p','i','n','i','o','n', -'o','p','p','o','s','e', -'o','p','t','i','o','n', -'o','r','a','n','g','e', -'o','r','b','i','t', -'o','r','c','h','a','r','d', -'o','r','d','e','r', -'o','r','d','i','n','a','r','y', -'o','r','g','a','n', -'o','r','i','e','n','t', -'o','r','i','g','i','n','a','l', -'o','r','p','h','a','n', -'o','s','t','r','i','c','h', -'o','t','h','e','r', -'o','u','t','d','o','o','r', -'o','u','t','e','r', -'o','u','t','p','u','t', -'o','u','t','s','i','d','e', -'o','v','a','l', -'o','v','e','n', -'o','v','e','r', -'o','w','n', -'o','w','n','e','r', -'o','x','y','g','e','n', -'o','y','s','t','e','r', -'o','z','o','n','e', -'p','a','c','t', -'p','a','d','d','l','e', -'p','a','g','e', -'p','a','i','r', -'p','a','l','a','c','e', -'p','a','l','m', -'p','a','n','d','a', -'p','a','n','e','l', -'p','a','n','i','c', -'p','a','n','t','h','e','r', -'p','a','p','e','r', -'p','a','r','a','d','e', -'p','a','r','e','n','t', -'p','a','r','k', -'p','a','r','r','o','t', -'p','a','r','t','y', -'p','a','s','s', -'p','a','t','c','h', -'p','a','t','h', -'p','a','t','i','e','n','t', -'p','a','t','r','o','l', -'p','a','t','t','e','r','n', -'p','a','u','s','e', -'p','a','v','e', -'p','a','y','m','e','n','t', -'p','e','a','c','e', -'p','e','a','n','u','t', -'p','e','a','r', -'p','e','a','s','a','n','t', -'p','e','l','i','c','a','n', -'p','e','n', -'p','e','n','a','l','t','y', -'p','e','n','c','i','l', -'p','e','o','p','l','e', -'p','e','p','p','e','r', -'p','e','r','f','e','c','t', -'p','e','r','m','i','t', -'p','e','r','s','o','n', -'p','e','t', -'p','h','o','n','e', -'p','h','o','t','o', -'p','h','r','a','s','e', -'p','h','y','s','i','c','a','l', -'p','i','a','n','o', -'p','i','c','n','i','c', -'p','i','c','t','u','r','e', -'p','i','e','c','e', -'p','i','g', -'p','i','g','e','o','n', -'p','i','l','l', -'p','i','l','o','t', -'p','i','n','k', -'p','i','o','n','e','e','r', -'p','i','p','e', -'p','i','s','t','o','l', -'p','i','t','c','h', -'p','i','z','z','a', -'p','l','a','c','e', -'p','l','a','n','e','t', -'p','l','a','s','t','i','c', -'p','l','a','t','e', -'p','l','a','y', -'p','l','e','a','s','e', -'p','l','e','d','g','e', -'p','l','u','c','k', -'p','l','u','g', -'p','l','u','n','g','e', -'p','o','e','m', -'p','o','e','t', -'p','o','i','n','t', -'p','o','l','a','r', -'p','o','l','e', -'p','o','l','i','c','e', -'p','o','n','d', -'p','o','n','y', -'p','o','o','l', -'p','o','p','u','l','a','r', -'p','o','r','t','i','o','n', -'p','o','s','i','t','i','o','n', -'p','o','s','s','i','b','l','e', -'p','o','s','t', -'p','o','t','a','t','o', -'p','o','t','t','e','r','y', -'p','o','v','e','r','t','y', -'p','o','w','d','e','r', -'p','o','w','e','r', -'p','r','a','c','t','i','c','e', -'p','r','a','i','s','e', -'p','r','e','d','i','c','t', -'p','r','e','f','e','r', -'p','r','e','p','a','r','e', -'p','r','e','s','e','n','t', -'p','r','e','t','t','y', -'p','r','e','v','e','n','t', -'p','r','i','c','e', -'p','r','i','d','e', -'p','r','i','m','a','r','y', -'p','r','i','n','t', -'p','r','i','o','r','i','t','y', -'p','r','i','s','o','n', -'p','r','i','v','a','t','e', -'p','r','i','z','e', -'p','r','o','b','l','e','m', -'p','r','o','c','e','s','s', -'p','r','o','d','u','c','e', -'p','r','o','f','i','t', -'p','r','o','g','r','a','m', -'p','r','o','j','e','c','t', -'p','r','o','m','o','t','e', -'p','r','o','o','f', -'p','r','o','p','e','r','t','y', -'p','r','o','s','p','e','r', -'p','r','o','t','e','c','t', -'p','r','o','u','d', -'p','r','o','v','i','d','e', -'p','u','b','l','i','c', -'p','u','d','d','i','n','g', -'p','u','l','l', -'p','u','l','p', -'p','u','l','s','e', -'p','u','m','p','k','i','n', -'p','u','n','c','h', -'p','u','p','i','l', -'p','u','p','p','y', -'p','u','r','c','h','a','s','e', -'p','u','r','i','t','y', -'p','u','r','p','o','s','e', -'p','u','r','s','e', -'p','u','s','h', -'p','u','t', -'p','u','z','z','l','e', -'p','y','r','a','m','i','d', -'q','u','a','l','i','t','y', -'q','u','a','n','t','u','m', -'q','u','a','r','t','e','r', -'q','u','e','s','t','i','o','n', -'q','u','i','c','k', -'q','u','i','t', -'q','u','i','z', -'q','u','o','t','e', -'r','a','b','b','i','t', -'r','a','c','c','o','o','n', -'r','a','c','e', -'r','a','c','k', -'r','a','d','a','r', -'r','a','d','i','o', -'r','a','i','l', -'r','a','i','n', -'r','a','i','s','e', -'r','a','l','l','y', -'r','a','m','p', -'r','a','n','c','h', -'r','a','n','d','o','m', -'r','a','n','g','e', -'r','a','p','i','d', -'r','a','r','e', -'r','a','t','e', -'r','a','t','h','e','r', -'r','a','v','e','n', -'r','a','w', -'r','a','z','o','r', -'r','e','a','d','y', -'r','e','a','l', -'r','e','a','s','o','n', -'r','e','b','e','l', -'r','e','b','u','i','l','d', -'r','e','c','a','l','l', -'r','e','c','e','i','v','e', -'r','e','c','i','p','e', -'r','e','c','o','r','d', -'r','e','c','y','c','l','e', -'r','e','d','u','c','e', -'r','e','f','l','e','c','t', -'r','e','f','o','r','m', -'r','e','f','u','s','e', -'r','e','g','i','o','n', -'r','e','g','r','e','t', -'r','e','g','u','l','a','r', -'r','e','j','e','c','t', -'r','e','l','a','x', -'r','e','l','e','a','s','e', -'r','e','l','i','e','f', -'r','e','l','y', -'r','e','m','a','i','n', -'r','e','m','e','m','b','e','r', -'r','e','m','i','n','d', -'r','e','m','o','v','e', -'r','e','n','d','e','r', -'r','e','n','e','w', -'r','e','n','t', -'r','e','o','p','e','n', -'r','e','p','a','i','r', -'r','e','p','e','a','t', -'r','e','p','l','a','c','e', -'r','e','p','o','r','t', -'r','e','q','u','i','r','e', -'r','e','s','c','u','e', -'r','e','s','e','m','b','l','e', -'r','e','s','i','s','t', -'r','e','s','o','u','r','c','e', -'r','e','s','p','o','n','s','e', -'r','e','s','u','l','t', -'r','e','t','i','r','e', -'r','e','t','r','e','a','t', -'r','e','t','u','r','n', -'r','e','u','n','i','o','n', -'r','e','v','e','a','l', -'r','e','v','i','e','w', -'r','e','w','a','r','d', -'r','h','y','t','h','m', -'r','i','b', -'r','i','b','b','o','n', -'r','i','c','e', -'r','i','c','h', -'r','i','d','e', -'r','i','d','g','e', -'r','i','f','l','e', -'r','i','g','h','t', -'r','i','g','i','d', -'r','i','n','g', -'r','i','o','t', -'r','i','p','p','l','e', -'r','i','s','k', -'r','i','t','u','a','l', -'r','i','v','a','l', -'r','i','v','e','r', -'r','o','a','d', -'r','o','a','s','t', -'r','o','b','o','t', -'r','o','b','u','s','t', -'r','o','c','k','e','t', -'r','o','m','a','n','c','e', -'r','o','o','f', -'r','o','o','k','i','e', -'r','o','o','m', -'r','o','s','e', -'r','o','t','a','t','e', -'r','o','u','g','h', -'r','o','u','n','d', -'r','o','u','t','e', -'r','o','y','a','l', -'r','u','b','b','e','r', -'r','u','d','e', -'r','u','g', -'r','u','l','e', -'r','u','n', -'r','u','n','w','a','y', -'r','u','r','a','l', -'s','a','d', -'s','a','d','d','l','e', -'s','a','d','n','e','s','s', -'s','a','f','e', -'s','a','i','l', -'s','a','l','a','d', -'s','a','l','m','o','n', -'s','a','l','o','n', -'s','a','l','t', -'s','a','l','u','t','e', -'s','a','m','e', -'s','a','m','p','l','e', -'s','a','n','d', -'s','a','t','i','s','f','y', -'s','a','t','o','s','h','i', -'s','a','u','c','e', -'s','a','u','s','a','g','e', -'s','a','v','e', -'s','a','y', -'s','c','a','l','e', -'s','c','a','n', -'s','c','a','r','e', -'s','c','a','t','t','e','r', -'s','c','e','n','e', -'s','c','h','e','m','e', -'s','c','h','o','o','l', -'s','c','i','e','n','c','e', -'s','c','i','s','s','o','r','s', -'s','c','o','r','p','i','o','n', -'s','c','o','u','t', -'s','c','r','a','p', -'s','c','r','e','e','n', -'s','c','r','i','p','t', -'s','c','r','u','b', -'s','e','a', -'s','e','a','r','c','h', -'s','e','a','s','o','n', -'s','e','a','t', -'s','e','c','o','n','d', -'s','e','c','r','e','t', -'s','e','c','t','i','o','n', -'s','e','c','u','r','i','t','y', -'s','e','e','d', -'s','e','e','k', -'s','e','g','m','e','n','t', -'s','e','l','e','c','t', -'s','e','l','l', -'s','e','m','i','n','a','r', -'s','e','n','i','o','r', -'s','e','n','s','e', -'s','e','n','t','e','n','c','e', -'s','e','r','i','e','s', -'s','e','r','v','i','c','e', -'s','e','s','s','i','o','n', -'s','e','t','t','l','e', -'s','e','t','u','p', -'s','e','v','e','n', -'s','h','a','d','o','w', -'s','h','a','f','t', -'s','h','a','l','l','o','w', -'s','h','a','r','e', -'s','h','e','d', -'s','h','e','l','l', -'s','h','e','r','i','f','f', -'s','h','i','e','l','d', -'s','h','i','f','t', -'s','h','i','n','e', -'s','h','i','p', -'s','h','i','v','e','r', -'s','h','o','c','k', -'s','h','o','e', -'s','h','o','o','t', -'s','h','o','p', -'s','h','o','r','t', -'s','h','o','u','l','d','e','r', -'s','h','o','v','e', -'s','h','r','i','m','p', -'s','h','r','u','g', -'s','h','u','f','f','l','e', -'s','h','y', -'s','i','b','l','i','n','g', -'s','i','c','k', -'s','i','d','e', -'s','i','e','g','e', -'s','i','g','h','t', -'s','i','g','n', -'s','i','l','e','n','t', -'s','i','l','k', -'s','i','l','l','y', -'s','i','l','v','e','r', -'s','i','m','i','l','a','r', -'s','i','m','p','l','e', -'s','i','n','c','e', -'s','i','n','g', -'s','i','r','e','n', -'s','i','s','t','e','r', -'s','i','t','u','a','t','e', -'s','i','x', -'s','i','z','e', -'s','k','a','t','e', -'s','k','e','t','c','h', -'s','k','i', -'s','k','i','l','l', -'s','k','i','n', -'s','k','i','r','t', -'s','k','u','l','l', -'s','l','a','b', -'s','l','a','m', -'s','l','e','e','p', -'s','l','e','n','d','e','r', -'s','l','i','c','e', -'s','l','i','d','e', -'s','l','i','g','h','t', -'s','l','i','m', -'s','l','o','g','a','n', -'s','l','o','t', -'s','l','o','w', -'s','l','u','s','h', -'s','m','a','l','l', -'s','m','a','r','t', -'s','m','i','l','e', -'s','m','o','k','e', -'s','m','o','o','t','h', -'s','n','a','c','k', -'s','n','a','k','e', -'s','n','a','p', -'s','n','i','f','f', -'s','n','o','w', -'s','o','a','p', -'s','o','c','c','e','r', -'s','o','c','i','a','l', -'s','o','c','k', -'s','o','d','a', -'s','o','f','t', -'s','o','l','a','r', -'s','o','l','d','i','e','r', -'s','o','l','i','d', -'s','o','l','u','t','i','o','n', -'s','o','l','v','e', -'s','o','m','e','o','n','e', -'s','o','n','g', -'s','o','o','n', -'s','o','r','r','y', -'s','o','r','t', -'s','o','u','l', -'s','o','u','n','d', -'s','o','u','p', -'s','o','u','r','c','e', -'s','o','u','t','h', -'s','p','a','c','e', -'s','p','a','r','e', -'s','p','a','t','i','a','l', -'s','p','a','w','n', -'s','p','e','a','k', -'s','p','e','c','i','a','l', -'s','p','e','e','d', -'s','p','e','l','l', -'s','p','e','n','d', -'s','p','h','e','r','e', -'s','p','i','c','e', -'s','p','i','d','e','r', -'s','p','i','k','e', -'s','p','i','n', -'s','p','i','r','i','t', -'s','p','l','i','t', -'s','p','o','i','l', -'s','p','o','n','s','o','r', -'s','p','o','o','n', -'s','p','o','r','t', -'s','p','o','t', -'s','p','r','a','y', -'s','p','r','e','a','d', -'s','p','r','i','n','g', -'s','p','y', -'s','q','u','a','r','e', -'s','q','u','e','e','z','e', -'s','q','u','i','r','r','e','l', -'s','t','a','b','l','e', -'s','t','a','d','i','u','m', -'s','t','a','f','f', -'s','t','a','g','e', -'s','t','a','i','r','s', -'s','t','a','m','p', -'s','t','a','n','d', -'s','t','a','r','t', -'s','t','a','t','e', -'s','t','a','y', -'s','t','e','a','k', -'s','t','e','e','l', -'s','t','e','m', -'s','t','e','p', -'s','t','e','r','e','o', -'s','t','i','c','k', -'s','t','i','l','l', -'s','t','i','n','g', -'s','t','o','c','k', -'s','t','o','m','a','c','h', -'s','t','o','n','e', -'s','t','o','o','l', -'s','t','o','r','y', -'s','t','o','v','e', -'s','t','r','a','t','e','g','y', -'s','t','r','e','e','t', -'s','t','r','i','k','e', -'s','t','r','o','n','g', -'s','t','r','u','g','g','l','e', -'s','t','u','d','e','n','t', -'s','t','u','f','f', -'s','t','u','m','b','l','e', -'s','t','y','l','e', -'s','u','b','j','e','c','t', -'s','u','b','m','i','t', -'s','u','b','w','a','y', -'s','u','c','c','e','s','s', -'s','u','c','h', -'s','u','d','d','e','n', -'s','u','f','f','e','r', -'s','u','g','a','r', -'s','u','g','g','e','s','t', -'s','u','i','t', -'s','u','m','m','e','r', -'s','u','n', -'s','u','n','n','y', -'s','u','n','s','e','t', -'s','u','p','e','r', -'s','u','p','p','l','y', -'s','u','p','r','e','m','e', -'s','u','r','e', -'s','u','r','f','a','c','e', -'s','u','r','g','e', -'s','u','r','p','r','i','s','e', -'s','u','r','r','o','u','n','d', -'s','u','r','v','e','y', -'s','u','s','p','e','c','t', -'s','u','s','t','a','i','n', -'s','w','a','l','l','o','w', -'s','w','a','m','p', -'s','w','a','p', -'s','w','a','r','m', -'s','w','e','a','r', -'s','w','e','e','t', -'s','w','i','f','t', -'s','w','i','m', -'s','w','i','n','g', -'s','w','i','t','c','h', -'s','w','o','r','d', -'s','y','m','b','o','l', -'s','y','m','p','t','o','m', -'s','y','r','u','p', -'s','y','s','t','e','m', -'t','a','b','l','e', -'t','a','c','k','l','e', -'t','a','g', -'t','a','i','l', -'t','a','l','e','n','t', -'t','a','l','k', -'t','a','n','k', -'t','a','p','e', -'t','a','r','g','e','t', -'t','a','s','k', -'t','a','s','t','e', -'t','a','t','t','o','o', -'t','a','x','i', -'t','e','a','c','h', -'t','e','a','m', -'t','e','l','l', -'t','e','n', -'t','e','n','a','n','t', -'t','e','n','n','i','s', -'t','e','n','t', -'t','e','r','m', -'t','e','s','t', -'t','e','x','t', -'t','h','a','n','k', -'t','h','a','t', -'t','h','e','m','e', -'t','h','e','n', -'t','h','e','o','r','y', -'t','h','e','r','e', -'t','h','e','y', -'t','h','i','n','g', -'t','h','i','s', -'t','h','o','u','g','h','t', -'t','h','r','e','e', -'t','h','r','i','v','e', -'t','h','r','o','w', -'t','h','u','m','b', -'t','h','u','n','d','e','r', -'t','i','c','k','e','t', -'t','i','d','e', -'t','i','g','e','r', -'t','i','l','t', -'t','i','m','b','e','r', -'t','i','m','e', -'t','i','n','y', -'t','i','p', -'t','i','r','e','d', -'t','i','s','s','u','e', -'t','i','t','l','e', -'t','o','a','s','t', -'t','o','b','a','c','c','o', -'t','o','d','a','y', -'t','o','d','d','l','e','r', -'t','o','e', -'t','o','g','e','t','h','e','r', -'t','o','i','l','e','t', -'t','o','k','e','n', -'t','o','m','a','t','o', -'t','o','m','o','r','r','o','w', -'t','o','n','e', -'t','o','n','g','u','e', -'t','o','n','i','g','h','t', -'t','o','o','l', -'t','o','o','t','h', -'t','o','p', -'t','o','p','i','c', -'t','o','p','p','l','e', -'t','o','r','c','h', -'t','o','r','n','a','d','o', -'t','o','r','t','o','i','s','e', -'t','o','s','s', -'t','o','t','a','l', -'t','o','u','r','i','s','t', -'t','o','w','a','r','d', -'t','o','w','e','r', -'t','o','w','n', -'t','o','y', -'t','r','a','c','k', -'t','r','a','d','e', -'t','r','a','f','f','i','c', -'t','r','a','g','i','c', -'t','r','a','i','n', -'t','r','a','n','s','f','e','r', -'t','r','a','p', -'t','r','a','s','h', -'t','r','a','v','e','l', -'t','r','a','y', -'t','r','e','a','t', -'t','r','e','e', -'t','r','e','n','d', -'t','r','i','a','l', -'t','r','i','b','e', -'t','r','i','c','k', -'t','r','i','g','g','e','r', -'t','r','i','m', -'t','r','i','p', -'t','r','o','p','h','y', -'t','r','o','u','b','l','e', -'t','r','u','c','k', -'t','r','u','e', -'t','r','u','l','y', -'t','r','u','m','p','e','t', -'t','r','u','s','t', -'t','r','u','t','h', -'t','r','y', -'t','u','b','e', -'t','u','i','t','i','o','n', -'t','u','m','b','l','e', -'t','u','n','a', -'t','u','n','n','e','l', -'t','u','r','k','e','y', -'t','u','r','n', -'t','u','r','t','l','e', -'t','w','e','l','v','e', -'t','w','e','n','t','y', -'t','w','i','c','e', -'t','w','i','n', -'t','w','i','s','t', -'t','w','o', -'t','y','p','e', -'t','y','p','i','c','a','l', -'u','g','l','y', -'u','m','b','r','e','l','l','a', -'u','n','a','b','l','e', -'u','n','a','w','a','r','e', -'u','n','c','l','e', -'u','n','c','o','v','e','r', -'u','n','d','e','r', -'u','n','d','o', -'u','n','f','a','i','r', -'u','n','f','o','l','d', -'u','n','h','a','p','p','y', -'u','n','i','f','o','r','m', -'u','n','i','q','u','e', -'u','n','i','t', -'u','n','i','v','e','r','s','e', -'u','n','k','n','o','w','n', -'u','n','l','o','c','k', -'u','n','t','i','l', -'u','n','u','s','u','a','l', -'u','n','v','e','i','l', -'u','p','d','a','t','e', -'u','p','g','r','a','d','e', -'u','p','h','o','l','d', -'u','p','o','n', -'u','p','p','e','r', -'u','p','s','e','t', -'u','r','b','a','n', -'u','r','g','e', -'u','s','a','g','e', -'u','s','e', -'u','s','e','d', -'u','s','e','f','u','l', -'u','s','e','l','e','s','s', -'u','s','u','a','l', -'u','t','i','l','i','t','y', -'v','a','c','a','n','t', -'v','a','c','u','u','m', -'v','a','g','u','e', -'v','a','l','i','d', -'v','a','l','l','e','y', -'v','a','l','v','e', -'v','a','n', -'v','a','n','i','s','h', -'v','a','p','o','r', -'v','a','r','i','o','u','s', -'v','a','s','t', -'v','a','u','l','t', -'v','e','h','i','c','l','e', -'v','e','l','v','e','t', -'v','e','n','d','o','r', -'v','e','n','t','u','r','e', -'v','e','n','u','e', -'v','e','r','b', -'v','e','r','i','f','y', -'v','e','r','s','i','o','n', -'v','e','r','y', -'v','e','s','s','e','l', -'v','e','t','e','r','a','n', -'v','i','a','b','l','e', -'v','i','b','r','a','n','t', -'v','i','c','i','o','u','s', -'v','i','c','t','o','r','y', -'v','i','d','e','o', -'v','i','e','w', -'v','i','l','l','a','g','e', -'v','i','n','t','a','g','e', -'v','i','o','l','i','n', -'v','i','r','t','u','a','l', -'v','i','r','u','s', -'v','i','s','a', -'v','i','s','i','t', -'v','i','s','u','a','l', -'v','i','t','a','l', -'v','i','v','i','d', -'v','o','c','a','l', -'v','o','i','c','e', -'v','o','i','d', -'v','o','l','c','a','n','o', -'v','o','l','u','m','e', -'v','o','t','e', -'v','o','y','a','g','e', -'w','a','g','e', -'w','a','g','o','n', -'w','a','i','t', -'w','a','l','k', -'w','a','l','l', -'w','a','l','n','u','t', -'w','a','n','t', -'w','a','r','f','a','r','e', -'w','a','r','m', -'w','a','r','r','i','o','r', -'w','a','s','h', -'w','a','s','p', -'w','a','s','t','e', -'w','a','t','e','r', -'w','a','v','e', -'w','a','y', -'w','e','a','l','t','h', -'w','e','a','p','o','n', -'w','e','a','r', -'w','e','a','s','e','l', -'w','e','a','t','h','e','r', -'w','e','b', -'w','e','d','d','i','n','g', -'w','e','e','k','e','n','d', -'w','e','i','r','d', -'w','e','l','c','o','m','e', -'w','e','s','t', -'w','e','t', -'w','h','a','l','e', -'w','h','a','t', -'w','h','e','a','t', -'w','h','e','e','l', -'w','h','e','n', -'w','h','e','r','e', -'w','h','i','p', -'w','h','i','s','p','e','r', -'w','i','d','e', -'w','i','d','t','h', -'w','i','f','e', -'w','i','l','d', -'w','i','l','l', -'w','i','n', -'w','i','n','d','o','w', -'w','i','n','e', -'w','i','n','g', -'w','i','n','k', -'w','i','n','n','e','r', -'w','i','n','t','e','r', -'w','i','r','e', -'w','i','s','d','o','m', -'w','i','s','e', -'w','i','s','h', -'w','i','t','n','e','s','s', -'w','o','l','f', -'w','o','m','a','n', -'w','o','n','d','e','r', -'w','o','o','d', -'w','o','o','l', -'w','o','r','d', -'w','o','r','k', -'w','o','r','l','d', -'w','o','r','r','y', -'w','o','r','t','h', -'w','r','a','p', -'w','r','e','c','k', -'w','r','e','s','t','l','e', -'w','r','i','s','t', -'w','r','i','t','e', -'w','r','o','n','g', -'y','a','r','d', -'y','e','a','r', -'y','e','l','l','o','w', -'y','o','u', -'y','o','u','n','g', -'y','o','u','t','h', -'z','e','b','r','a', -'z','e','r','o', -'z','o','n','e', -'z','o','o' -}; + 'a', 'b', 'a', 'n', 'd', 'o', 'n', 'a', 'b', 'i', 'l', 'i', 't', 'y', 'a', 'b', 'l', 'e', 'a', + 'b', 'o', 'u', 't', 'a', 'b', 'o', 'v', 'e', 'a', 'b', 's', 'e', 'n', 't', 'a', 'b', 's', 'o', + 'r', 'b', 'a', 'b', 's', 't', 'r', 'a', 'c', 't', 'a', 'b', 's', 'u', 'r', 'd', 'a', 'b', 'u', + 's', 'e', 'a', 'c', 'c', 'e', 's', 's', 'a', 'c', 'c', 'i', 'd', 'e', 'n', 't', 'a', 'c', 'c', + 'o', 'u', 'n', 't', 'a', 'c', 'c', 'u', 's', 'e', 'a', 'c', 'h', 'i', 'e', 'v', 'e', 'a', 'c', + 'i', 'd', 'a', 'c', 'o', 'u', 's', 't', 'i', 'c', 'a', 'c', 'q', 'u', 'i', 'r', 'e', 'a', 'c', + 'r', 'o', 's', 's', 'a', 'c', 't', 'a', 'c', 't', 'i', 'o', 'n', 'a', 'c', 't', 'o', 'r', 'a', + 'c', 't', 'r', 'e', 's', 's', 'a', 'c', 't', 'u', 'a', 'l', 'a', 'd', 'a', 'p', 't', 'a', 'd', + 'd', 'a', 'd', 'd', 'i', 'c', 't', 'a', 'd', 'd', 'r', 'e', 's', 's', 'a', 'd', 'j', 'u', 's', + 't', 'a', 'd', 'm', 'i', 't', 'a', 'd', 'u', 'l', 't', 'a', 'd', 'v', 'a', 'n', 'c', 'e', 'a', + 'd', 'v', 'i', 'c', 'e', 'a', 'e', 'r', 'o', 'b', 'i', 'c', 'a', 'f', 'f', 'a', 'i', 'r', 'a', + 'f', 'f', 'o', 'r', 'd', 'a', 'f', 'r', 'a', 'i', 'd', 'a', 'g', 'a', 'i', 'n', 'a', 'g', 'e', + 'a', 'g', 'e', 'n', 't', 'a', 'g', 'r', 'e', 'e', 'a', 'h', 'e', 'a', 'd', 'a', 'i', 'm', 'a', + 'i', 'r', 'a', 'i', 'r', 'p', 'o', 'r', 't', 'a', 'i', 's', 'l', 'e', 'a', 'l', 'a', 'r', 'm', + 'a', 'l', 'b', 'u', 'm', 'a', 'l', 'c', 'o', 'h', 'o', 'l', 'a', 'l', 'e', 'r', 't', 'a', 'l', + 'i', 'e', 'n', 'a', 'l', 'l', 'a', 'l', 'l', 'e', 'y', 'a', 'l', 'l', 'o', 'w', 'a', 'l', 'm', + 'o', 's', 't', 'a', 'l', 'o', 'n', 'e', 'a', 'l', 'p', 'h', 'a', 'a', 'l', 'r', 'e', 'a', 'd', + 'y', 'a', 'l', 's', 'o', 'a', 'l', 't', 'e', 'r', 'a', 'l', 'w', 'a', 'y', 's', 'a', 'm', 'a', + 't', 'e', 'u', 'r', 'a', 'm', 'a', 'z', 'i', 'n', 'g', 'a', 'm', 'o', 'n', 'g', 'a', 'm', 'o', + 'u', 'n', 't', 'a', 'm', 'u', 's', 'e', 'd', 'a', 'n', 'a', 'l', 'y', 's', 't', 'a', 'n', 'c', + 'h', 'o', 'r', 'a', 'n', 'c', 'i', 'e', 'n', 't', 'a', 'n', 'g', 'e', 'r', 'a', 'n', 'g', 'l', + 'e', 'a', 'n', 'g', 'r', 'y', 'a', 'n', 'i', 'm', 'a', 'l', 'a', 'n', 'k', 'l', 'e', 'a', 'n', + 'n', 'o', 'u', 'n', 'c', 'e', 'a', 'n', 'n', 'u', 'a', 'l', 'a', 'n', 'o', 't', 'h', 'e', 'r', + 'a', 'n', 's', 'w', 'e', 'r', 'a', 'n', 't', 'e', 'n', 'n', 'a', 'a', 'n', 't', 'i', 'q', 'u', + 'e', 'a', 'n', 'x', 'i', 'e', 't', 'y', 'a', 'n', 'y', 'a', 'p', 'a', 'r', 't', 'a', 'p', 'o', + 'l', 'o', 'g', 'y', 'a', 'p', 'p', 'e', 'a', 'r', 'a', 'p', 'p', 'l', 'e', 'a', 'p', 'p', 'r', + 'o', 'v', 'e', 'a', 'p', 'r', 'i', 'l', 'a', 'r', 'c', 'h', 'a', 'r', 'c', 't', 'i', 'c', 'a', + 'r', 'e', 'a', 'a', 'r', 'e', 'n', 'a', 'a', 'r', 'g', 'u', 'e', 'a', 'r', 'm', 'a', 'r', 'm', + 'e', 'd', 'a', 'r', 'm', 'o', 'r', 'a', 'r', 'm', 'y', 'a', 'r', 'o', 'u', 'n', 'd', 'a', 'r', + 'r', 'a', 'n', 'g', 'e', 'a', 'r', 'r', 'e', 's', 't', 'a', 'r', 'r', 'i', 'v', 'e', 'a', 'r', + 'r', 'o', 'w', 'a', 'r', 't', 'a', 'r', 't', 'e', 'f', 'a', 'c', 't', 'a', 'r', 't', 'i', 's', + 't', 'a', 'r', 't', 'w', 'o', 'r', 'k', 'a', 's', 'k', 'a', 's', 'p', 'e', 'c', 't', 'a', 's', + 's', 'a', 'u', 'l', 't', 'a', 's', 's', 'e', 't', 'a', 's', 's', 'i', 's', 't', 'a', 's', 's', + 'u', 'm', 'e', 'a', 's', 't', 'h', 'm', 'a', 'a', 't', 'h', 'l', 'e', 't', 'e', 'a', 't', 'o', + 'm', 'a', 't', 't', 'a', 'c', 'k', 'a', 't', 't', 'e', 'n', 'd', 'a', 't', 't', 'i', 't', 'u', + 'd', 'e', 'a', 't', 't', 'r', 'a', 'c', 't', 'a', 'u', 'c', 't', 'i', 'o', 'n', 'a', 'u', 'd', + 'i', 't', 'a', 'u', 'g', 'u', 's', 't', 'a', 'u', 'n', 't', 'a', 'u', 't', 'h', 'o', 'r', 'a', + 'u', 't', 'o', 'a', 'u', 't', 'u', 'm', 'n', 'a', 'v', 'e', 'r', 'a', 'g', 'e', 'a', 'v', 'o', + 'c', 'a', 'd', 'o', 'a', 'v', 'o', 'i', 'd', 'a', 'w', 'a', 'k', 'e', 'a', 'w', 'a', 'r', 'e', + 'a', 'w', 'a', 'y', 'a', 'w', 'e', 's', 'o', 'm', 'e', 'a', 'w', 'f', 'u', 'l', 'a', 'w', 'k', + 'w', 'a', 'r', 'd', 'a', 'x', 'i', 's', 'b', 'a', 'b', 'y', 'b', 'a', 'c', 'h', 'e', 'l', 'o', + 'r', 'b', 'a', 'c', 'o', 'n', 'b', 'a', 'd', 'g', 'e', 'b', 'a', 'g', 'b', 'a', 'l', 'a', 'n', + 'c', 'e', 'b', 'a', 'l', 'c', 'o', 'n', 'y', 'b', 'a', 'l', 'l', 'b', 'a', 'm', 'b', 'o', 'o', + 'b', 'a', 'n', 'a', 'n', 'a', 'b', 'a', 'n', 'n', 'e', 'r', 'b', 'a', 'r', 'b', 'a', 'r', 'e', + 'l', 'y', 'b', 'a', 'r', 'g', 'a', 'i', 'n', 'b', 'a', 'r', 'r', 'e', 'l', 'b', 'a', 's', 'e', + 'b', 'a', 's', 'i', 'c', 'b', 'a', 's', 'k', 'e', 't', 'b', 'a', 't', 't', 'l', 'e', 'b', 'e', + 'a', 'c', 'h', 'b', 'e', 'a', 'n', 'b', 'e', 'a', 'u', 't', 'y', 'b', 'e', 'c', 'a', 'u', 's', + 'e', 'b', 'e', 'c', 'o', 'm', 'e', 'b', 'e', 'e', 'f', 'b', 'e', 'f', 'o', 'r', 'e', 'b', 'e', + 'g', 'i', 'n', 'b', 'e', 'h', 'a', 'v', 'e', 'b', 'e', 'h', 'i', 'n', 'd', 'b', 'e', 'l', 'i', + 'e', 'v', 'e', 'b', 'e', 'l', 'o', 'w', 'b', 'e', 'l', 't', 'b', 'e', 'n', 'c', 'h', 'b', 'e', + 'n', 'e', 'f', 'i', 't', 'b', 'e', 's', 't', 'b', 'e', 't', 'r', 'a', 'y', 'b', 'e', 't', 't', + 'e', 'r', 'b', 'e', 't', 'w', 'e', 'e', 'n', 'b', 'e', 'y', 'o', 'n', 'd', 'b', 'i', 'c', 'y', + 'c', 'l', 'e', 'b', 'i', 'd', 'b', 'i', 'k', 'e', 'b', 'i', 'n', 'd', 'b', 'i', 'o', 'l', 'o', + 'g', 'y', 'b', 'i', 'r', 'd', 'b', 'i', 'r', 't', 'h', 'b', 'i', 't', 't', 'e', 'r', 'b', 'l', + 'a', 'c', 'k', 'b', 'l', 'a', 'd', 'e', 'b', 'l', 'a', 'm', 'e', 'b', 'l', 'a', 'n', 'k', 'e', + 't', 'b', 'l', 'a', 's', 't', 'b', 'l', 'e', 'a', 'k', 'b', 'l', 'e', 's', 's', 'b', 'l', 'i', + 'n', 'd', 'b', 'l', 'o', 'o', 'd', 'b', 'l', 'o', 's', 's', 'o', 'm', 'b', 'l', 'o', 'u', 's', + 'e', 'b', 'l', 'u', 'e', 'b', 'l', 'u', 'r', 'b', 'l', 'u', 's', 'h', 'b', 'o', 'a', 'r', 'd', + 'b', 'o', 'a', 't', 'b', 'o', 'd', 'y', 'b', 'o', 'i', 'l', 'b', 'o', 'm', 'b', 'b', 'o', 'n', + 'e', 'b', 'o', 'n', 'u', 's', 'b', 'o', 'o', 'k', 'b', 'o', 'o', 's', 't', 'b', 'o', 'r', 'd', + 'e', 'r', 'b', 'o', 'r', 'i', 'n', 'g', 'b', 'o', 'r', 'r', 'o', 'w', 'b', 'o', 's', 's', 'b', + 'o', 't', 't', 'o', 'm', 'b', 'o', 'u', 'n', 'c', 'e', 'b', 'o', 'x', 'b', 'o', 'y', 'b', 'r', + 'a', 'c', 'k', 'e', 't', 'b', 'r', 'a', 'i', 'n', 'b', 'r', 'a', 'n', 'd', 'b', 'r', 'a', 's', + 's', 'b', 'r', 'a', 'v', 'e', 'b', 'r', 'e', 'a', 'd', 'b', 'r', 'e', 'e', 'z', 'e', 'b', 'r', + 'i', 'c', 'k', 'b', 'r', 'i', 'd', 'g', 'e', 'b', 'r', 'i', 'e', 'f', 'b', 'r', 'i', 'g', 'h', + 't', 'b', 'r', 'i', 'n', 'g', 'b', 'r', 'i', 's', 'k', 'b', 'r', 'o', 'c', 'c', 'o', 'l', 'i', + 'b', 'r', 'o', 'k', 'e', 'n', 'b', 'r', 'o', 'n', 'z', 'e', 'b', 'r', 'o', 'o', 'm', 'b', 'r', + 'o', 't', 'h', 'e', 'r', 'b', 'r', 'o', 'w', 'n', 'b', 'r', 'u', 's', 'h', 'b', 'u', 'b', 'b', + 'l', 'e', 'b', 'u', 'd', 'd', 'y', 'b', 'u', 'd', 'g', 'e', 't', 'b', 'u', 'f', 'f', 'a', 'l', + 'o', 'b', 'u', 'i', 'l', 'd', 'b', 'u', 'l', 'b', 'b', 'u', 'l', 'k', 'b', 'u', 'l', 'l', 'e', + 't', 'b', 'u', 'n', 'd', 'l', 'e', 'b', 'u', 'n', 'k', 'e', 'r', 'b', 'u', 'r', 'd', 'e', 'n', + 'b', 'u', 'r', 'g', 'e', 'r', 'b', 'u', 'r', 's', 't', 'b', 'u', 's', 'b', 'u', 's', 'i', 'n', + 'e', 's', 's', 'b', 'u', 's', 'y', 'b', 'u', 't', 't', 'e', 'r', 'b', 'u', 'y', 'e', 'r', 'b', + 'u', 'z', 'z', 'c', 'a', 'b', 'b', 'a', 'g', 'e', 'c', 'a', 'b', 'i', 'n', 'c', 'a', 'b', 'l', + 'e', 'c', 'a', 'c', 't', 'u', 's', 'c', 'a', 'g', 'e', 'c', 'a', 'k', 'e', 'c', 'a', 'l', 'l', + 'c', 'a', 'l', 'm', 'c', 'a', 'm', 'e', 'r', 'a', 'c', 'a', 'm', 'p', 'c', 'a', 'n', 'c', 'a', + 'n', 'a', 'l', 'c', 'a', 'n', 'c', 'e', 'l', 'c', 'a', 'n', 'd', 'y', 'c', 'a', 'n', 'n', 'o', + 'n', 'c', 'a', 'n', 'o', 'e', 'c', 'a', 'n', 'v', 'a', 's', 'c', 'a', 'n', 'y', 'o', 'n', 'c', + 'a', 'p', 'a', 'b', 'l', 'e', 'c', 'a', 'p', 'i', 't', 'a', 'l', 'c', 'a', 'p', 't', 'a', 'i', + 'n', 'c', 'a', 'r', 'c', 'a', 'r', 'b', 'o', 'n', 'c', 'a', 'r', 'd', 'c', 'a', 'r', 'g', 'o', + 'c', 'a', 'r', 'p', 'e', 't', 'c', 'a', 'r', 'r', 'y', 'c', 'a', 'r', 't', 'c', 'a', 's', 'e', + 'c', 'a', 's', 'h', 'c', 'a', 's', 'i', 'n', 'o', 'c', 'a', 's', 't', 'l', 'e', 'c', 'a', 's', + 'u', 'a', 'l', 'c', 'a', 't', 'c', 'a', 't', 'a', 'l', 'o', 'g', 'c', 'a', 't', 'c', 'h', 'c', + 'a', 't', 'e', 'g', 'o', 'r', 'y', 'c', 'a', 't', 't', 'l', 'e', 'c', 'a', 'u', 'g', 'h', 't', + 'c', 'a', 'u', 's', 'e', 'c', 'a', 'u', 't', 'i', 'o', 'n', 'c', 'a', 'v', 'e', 'c', 'e', 'i', + 'l', 'i', 'n', 'g', 'c', 'e', 'l', 'e', 'r', 'y', 'c', 'e', 'm', 'e', 'n', 't', 'c', 'e', 'n', + 's', 'u', 's', 'c', 'e', 'n', 't', 'u', 'r', 'y', 'c', 'e', 'r', 'e', 'a', 'l', 'c', 'e', 'r', + 't', 'a', 'i', 'n', 'c', 'h', 'a', 'i', 'r', 'c', 'h', 'a', 'l', 'k', 'c', 'h', 'a', 'm', 'p', + 'i', 'o', 'n', 'c', 'h', 'a', 'n', 'g', 'e', 'c', 'h', 'a', 'o', 's', 'c', 'h', 'a', 'p', 't', + 'e', 'r', 'c', 'h', 'a', 'r', 'g', 'e', 'c', 'h', 'a', 's', 'e', 'c', 'h', 'a', 't', 'c', 'h', + 'e', 'a', 'p', 'c', 'h', 'e', 'c', 'k', 'c', 'h', 'e', 'e', 's', 'e', 'c', 'h', 'e', 'f', 'c', + 'h', 'e', 'r', 'r', 'y', 'c', 'h', 'e', 's', 't', 'c', 'h', 'i', 'c', 'k', 'e', 'n', 'c', 'h', + 'i', 'e', 'f', 'c', 'h', 'i', 'l', 'd', 'c', 'h', 'i', 'm', 'n', 'e', 'y', 'c', 'h', 'o', 'i', + 'c', 'e', 'c', 'h', 'o', 'o', 's', 'e', 'c', 'h', 'r', 'o', 'n', 'i', 'c', 'c', 'h', 'u', 'c', + 'k', 'l', 'e', 'c', 'h', 'u', 'n', 'k', 'c', 'h', 'u', 'r', 'n', 'c', 'i', 'g', 'a', 'r', 'c', + 'i', 'n', 'n', 'a', 'm', 'o', 'n', 'c', 'i', 'r', 'c', 'l', 'e', 'c', 'i', 't', 'i', 'z', 'e', + 'n', 'c', 'i', 't', 'y', 'c', 'i', 'v', 'i', 'l', 'c', 'l', 'a', 'i', 'm', 'c', 'l', 'a', 'p', + 'c', 'l', 'a', 'r', 'i', 'f', 'y', 'c', 'l', 'a', 'w', 'c', 'l', 'a', 'y', 'c', 'l', 'e', 'a', + 'n', 'c', 'l', 'e', 'r', 'k', 'c', 'l', 'e', 'v', 'e', 'r', 'c', 'l', 'i', 'c', 'k', 'c', 'l', + 'i', 'e', 'n', 't', 'c', 'l', 'i', 'f', 'f', 'c', 'l', 'i', 'm', 'b', 'c', 'l', 'i', 'n', 'i', + 'c', 'c', 'l', 'i', 'p', 'c', 'l', 'o', 'c', 'k', 'c', 'l', 'o', 'g', 'c', 'l', 'o', 's', 'e', + 'c', 'l', 'o', 't', 'h', 'c', 'l', 'o', 'u', 'd', 'c', 'l', 'o', 'w', 'n', 'c', 'l', 'u', 'b', + 'c', 'l', 'u', 'm', 'p', 'c', 'l', 'u', 's', 't', 'e', 'r', 'c', 'l', 'u', 't', 'c', 'h', 'c', + 'o', 'a', 'c', 'h', 'c', 'o', 'a', 's', 't', 'c', 'o', 'c', 'o', 'n', 'u', 't', 'c', 'o', 'd', + 'e', 'c', 'o', 'f', 'f', 'e', 'e', 'c', 'o', 'i', 'l', 'c', 'o', 'i', 'n', 'c', 'o', 'l', 'l', + 'e', 'c', 't', 'c', 'o', 'l', 'o', 'r', 'c', 'o', 'l', 'u', 'm', 'n', 'c', 'o', 'm', 'b', 'i', + 'n', 'e', 'c', 'o', 'm', 'e', 'c', 'o', 'm', 'f', 'o', 'r', 't', 'c', 'o', 'm', 'i', 'c', 'c', + 'o', 'm', 'm', 'o', 'n', 'c', 'o', 'm', 'p', 'a', 'n', 'y', 'c', 'o', 'n', 'c', 'e', 'r', 't', + 'c', 'o', 'n', 'd', 'u', 'c', 't', 'c', 'o', 'n', 'f', 'i', 'r', 'm', 'c', 'o', 'n', 'g', 'r', + 'e', 's', 's', 'c', 'o', 'n', 'n', 'e', 'c', 't', 'c', 'o', 'n', 's', 'i', 'd', 'e', 'r', 'c', + 'o', 'n', 't', 'r', 'o', 'l', 'c', 'o', 'n', 'v', 'i', 'n', 'c', 'e', 'c', 'o', 'o', 'k', 'c', + 'o', 'o', 'l', 'c', 'o', 'p', 'p', 'e', 'r', 'c', 'o', 'p', 'y', 'c', 'o', 'r', 'a', 'l', 'c', + 'o', 'r', 'e', 'c', 'o', 'r', 'n', 'c', 'o', 'r', 'r', 'e', 'c', 't', 'c', 'o', 's', 't', 'c', + 'o', 't', 't', 'o', 'n', 'c', 'o', 'u', 'c', 'h', 'c', 'o', 'u', 'n', 't', 'r', 'y', 'c', 'o', + 'u', 'p', 'l', 'e', 'c', 'o', 'u', 'r', 's', 'e', 'c', 'o', 'u', 's', 'i', 'n', 'c', 'o', 'v', + 'e', 'r', 'c', 'o', 'y', 'o', 't', 'e', 'c', 'r', 'a', 'c', 'k', 'c', 'r', 'a', 'd', 'l', 'e', + 'c', 'r', 'a', 'f', 't', 'c', 'r', 'a', 'm', 'c', 'r', 'a', 'n', 'e', 'c', 'r', 'a', 's', 'h', + 'c', 'r', 'a', 't', 'e', 'r', 'c', 'r', 'a', 'w', 'l', 'c', 'r', 'a', 'z', 'y', 'c', 'r', 'e', + 'a', 'm', 'c', 'r', 'e', 'd', 'i', 't', 'c', 'r', 'e', 'e', 'k', 'c', 'r', 'e', 'w', 'c', 'r', + 'i', 'c', 'k', 'e', 't', 'c', 'r', 'i', 'm', 'e', 'c', 'r', 'i', 's', 'p', 'c', 'r', 'i', 't', + 'i', 'c', 'c', 'r', 'o', 'p', 'c', 'r', 'o', 's', 's', 'c', 'r', 'o', 'u', 'c', 'h', 'c', 'r', + 'o', 'w', 'd', 'c', 'r', 'u', 'c', 'i', 'a', 'l', 'c', 'r', 'u', 'e', 'l', 'c', 'r', 'u', 'i', + 's', 'e', 'c', 'r', 'u', 'm', 'b', 'l', 'e', 'c', 'r', 'u', 'n', 'c', 'h', 'c', 'r', 'u', 's', + 'h', 'c', 'r', 'y', 'c', 'r', 'y', 's', 't', 'a', 'l', 'c', 'u', 'b', 'e', 'c', 'u', 'l', 't', + 'u', 'r', 'e', 'c', 'u', 'p', 'c', 'u', 'p', 'b', 'o', 'a', 'r', 'd', 'c', 'u', 'r', 'i', 'o', + 'u', 's', 'c', 'u', 'r', 'r', 'e', 'n', 't', 'c', 'u', 'r', 't', 'a', 'i', 'n', 'c', 'u', 'r', + 'v', 'e', 'c', 'u', 's', 'h', 'i', 'o', 'n', 'c', 'u', 's', 't', 'o', 'm', 'c', 'u', 't', 'e', + 'c', 'y', 'c', 'l', 'e', 'd', 'a', 'd', 'd', 'a', 'm', 'a', 'g', 'e', 'd', 'a', 'm', 'p', 'd', + 'a', 'n', 'c', 'e', 'd', 'a', 'n', 'g', 'e', 'r', 'd', 'a', 'r', 'i', 'n', 'g', 'd', 'a', 's', + 'h', 'd', 'a', 'u', 'g', 'h', 't', 'e', 'r', 'd', 'a', 'w', 'n', 'd', 'a', 'y', 'd', 'e', 'a', + 'l', 'd', 'e', 'b', 'a', 't', 'e', 'd', 'e', 'b', 'r', 'i', 's', 'd', 'e', 'c', 'a', 'd', 'e', + 'd', 'e', 'c', 'e', 'm', 'b', 'e', 'r', 'd', 'e', 'c', 'i', 'd', 'e', 'd', 'e', 'c', 'l', 'i', + 'n', 'e', 'd', 'e', 'c', 'o', 'r', 'a', 't', 'e', 'd', 'e', 'c', 'r', 'e', 'a', 's', 'e', 'd', + 'e', 'e', 'r', 'd', 'e', 'f', 'e', 'n', 's', 'e', 'd', 'e', 'f', 'i', 'n', 'e', 'd', 'e', 'f', + 'y', 'd', 'e', 'g', 'r', 'e', 'e', 'd', 'e', 'l', 'a', 'y', 'd', 'e', 'l', 'i', 'v', 'e', 'r', + 'd', 'e', 'm', 'a', 'n', 'd', 'd', 'e', 'm', 'i', 's', 'e', 'd', 'e', 'n', 'i', 'a', 'l', 'd', + 'e', 'n', 't', 'i', 's', 't', 'd', 'e', 'n', 'y', 'd', 'e', 'p', 'a', 'r', 't', 'd', 'e', 'p', + 'e', 'n', 'd', 'd', 'e', 'p', 'o', 's', 'i', 't', 'd', 'e', 'p', 't', 'h', 'd', 'e', 'p', 'u', + 't', 'y', 'd', 'e', 'r', 'i', 'v', 'e', 'd', 'e', 's', 'c', 'r', 'i', 'b', 'e', 'd', 'e', 's', + 'e', 'r', 't', 'd', 'e', 's', 'i', 'g', 'n', 'd', 'e', 's', 'k', 'd', 'e', 's', 'p', 'a', 'i', + 'r', 'd', 'e', 's', 't', 'r', 'o', 'y', 'd', 'e', 't', 'a', 'i', 'l', 'd', 'e', 't', 'e', 'c', + 't', 'd', 'e', 'v', 'e', 'l', 'o', 'p', 'd', 'e', 'v', 'i', 'c', 'e', 'd', 'e', 'v', 'o', 't', + 'e', 'd', 'i', 'a', 'g', 'r', 'a', 'm', 'd', 'i', 'a', 'l', 'd', 'i', 'a', 'm', 'o', 'n', 'd', + 'd', 'i', 'a', 'r', 'y', 'd', 'i', 'c', 'e', 'd', 'i', 'e', 's', 'e', 'l', 'd', 'i', 'e', 't', + 'd', 'i', 'f', 'f', 'e', 'r', 'd', 'i', 'g', 'i', 't', 'a', 'l', 'd', 'i', 'g', 'n', 'i', 't', + 'y', 'd', 'i', 'l', 'e', 'm', 'm', 'a', 'd', 'i', 'n', 'n', 'e', 'r', 'd', 'i', 'n', 'o', 's', + 'a', 'u', 'r', 'd', 'i', 'r', 'e', 'c', 't', 'd', 'i', 'r', 't', 'd', 'i', 's', 'a', 'g', 'r', + 'e', 'e', 'd', 'i', 's', 'c', 'o', 'v', 'e', 'r', 'd', 'i', 's', 'e', 'a', 's', 'e', 'd', 'i', + 's', 'h', 'd', 'i', 's', 'm', 'i', 's', 's', 'd', 'i', 's', 'o', 'r', 'd', 'e', 'r', 'd', 'i', + 's', 'p', 'l', 'a', 'y', 'd', 'i', 's', 't', 'a', 'n', 'c', 'e', 'd', 'i', 'v', 'e', 'r', 't', + 'd', 'i', 'v', 'i', 'd', 'e', 'd', 'i', 'v', 'o', 'r', 'c', 'e', 'd', 'i', 'z', 'z', 'y', 'd', + 'o', 'c', 't', 'o', 'r', 'd', 'o', 'c', 'u', 'm', 'e', 'n', 't', 'd', 'o', 'g', 'd', 'o', 'l', + 'l', 'd', 'o', 'l', 'p', 'h', 'i', 'n', 'd', 'o', 'm', 'a', 'i', 'n', 'd', 'o', 'n', 'a', 't', + 'e', 'd', 'o', 'n', 'k', 'e', 'y', 'd', 'o', 'n', 'o', 'r', 'd', 'o', 'o', 'r', 'd', 'o', 's', + 'e', 'd', 'o', 'u', 'b', 'l', 'e', 'd', 'o', 'v', 'e', 'd', 'r', 'a', 'f', 't', 'd', 'r', 'a', + 'g', 'o', 'n', 'd', 'r', 'a', 'm', 'a', 'd', 'r', 'a', 's', 't', 'i', 'c', 'd', 'r', 'a', 'w', + 'd', 'r', 'e', 'a', 'm', 'd', 'r', 'e', 's', 's', 'd', 'r', 'i', 'f', 't', 'd', 'r', 'i', 'l', + 'l', 'd', 'r', 'i', 'n', 'k', 'd', 'r', 'i', 'p', 'd', 'r', 'i', 'v', 'e', 'd', 'r', 'o', 'p', + 'd', 'r', 'u', 'm', 'd', 'r', 'y', 'd', 'u', 'c', 'k', 'd', 'u', 'm', 'b', 'd', 'u', 'n', 'e', + 'd', 'u', 'r', 'i', 'n', 'g', 'd', 'u', 's', 't', 'd', 'u', 't', 'c', 'h', 'd', 'u', 't', 'y', + 'd', 'w', 'a', 'r', 'f', 'd', 'y', 'n', 'a', 'm', 'i', 'c', 'e', 'a', 'g', 'e', 'r', 'e', 'a', + 'g', 'l', 'e', 'e', 'a', 'r', 'l', 'y', 'e', 'a', 'r', 'n', 'e', 'a', 'r', 't', 'h', 'e', 'a', + 's', 'i', 'l', 'y', 'e', 'a', 's', 't', 'e', 'a', 's', 'y', 'e', 'c', 'h', 'o', 'e', 'c', 'o', + 'l', 'o', 'g', 'y', 'e', 'c', 'o', 'n', 'o', 'm', 'y', 'e', 'd', 'g', 'e', 'e', 'd', 'i', 't', + 'e', 'd', 'u', 'c', 'a', 't', 'e', 'e', 'f', 'f', 'o', 'r', 't', 'e', 'g', 'g', 'e', 'i', 'g', + 'h', 't', 'e', 'i', 't', 'h', 'e', 'r', 'e', 'l', 'b', 'o', 'w', 'e', 'l', 'd', 'e', 'r', 'e', + 'l', 'e', 'c', 't', 'r', 'i', 'c', 'e', 'l', 'e', 'g', 'a', 'n', 't', 'e', 'l', 'e', 'm', 'e', + 'n', 't', 'e', 'l', 'e', 'p', 'h', 'a', 'n', 't', 'e', 'l', 'e', 'v', 'a', 't', 'o', 'r', 'e', + 'l', 'i', 't', 'e', 'e', 'l', 's', 'e', 'e', 'm', 'b', 'a', 'r', 'k', 'e', 'm', 'b', 'o', 'd', + 'y', 'e', 'm', 'b', 'r', 'a', 'c', 'e', 'e', 'm', 'e', 'r', 'g', 'e', 'e', 'm', 'o', 't', 'i', + 'o', 'n', 'e', 'm', 'p', 'l', 'o', 'y', 'e', 'm', 'p', 'o', 'w', 'e', 'r', 'e', 'm', 'p', 't', + 'y', 'e', 'n', 'a', 'b', 'l', 'e', 'e', 'n', 'a', 'c', 't', 'e', 'n', 'd', 'e', 'n', 'd', 'l', + 'e', 's', 's', 'e', 'n', 'd', 'o', 'r', 's', 'e', 'e', 'n', 'e', 'm', 'y', 'e', 'n', 'e', 'r', + 'g', 'y', 'e', 'n', 'f', 'o', 'r', 'c', 'e', 'e', 'n', 'g', 'a', 'g', 'e', 'e', 'n', 'g', 'i', + 'n', 'e', 'e', 'n', 'h', 'a', 'n', 'c', 'e', 'e', 'n', 'j', 'o', 'y', 'e', 'n', 'l', 'i', 's', + 't', 'e', 'n', 'o', 'u', 'g', 'h', 'e', 'n', 'r', 'i', 'c', 'h', 'e', 'n', 'r', 'o', 'l', 'l', + 'e', 'n', 's', 'u', 'r', 'e', 'e', 'n', 't', 'e', 'r', 'e', 'n', 't', 'i', 'r', 'e', 'e', 'n', + 't', 'r', 'y', 'e', 'n', 'v', 'e', 'l', 'o', 'p', 'e', 'e', 'p', 'i', 's', 'o', 'd', 'e', 'e', + 'q', 'u', 'a', 'l', 'e', 'q', 'u', 'i', 'p', 'e', 'r', 'a', 'e', 'r', 'a', 's', 'e', 'e', 'r', + 'o', 'd', 'e', 'e', 'r', 'o', 's', 'i', 'o', 'n', 'e', 'r', 'r', 'o', 'r', 'e', 'r', 'u', 'p', + 't', 'e', 's', 'c', 'a', 'p', 'e', 'e', 's', 's', 'a', 'y', 'e', 's', 's', 'e', 'n', 'c', 'e', + 'e', 's', 't', 'a', 't', 'e', 'e', 't', 'e', 'r', 'n', 'a', 'l', 'e', 't', 'h', 'i', 'c', 's', + 'e', 'v', 'i', 'd', 'e', 'n', 'c', 'e', 'e', 'v', 'i', 'l', 'e', 'v', 'o', 'k', 'e', 'e', 'v', + 'o', 'l', 'v', 'e', 'e', 'x', 'a', 'c', 't', 'e', 'x', 'a', 'm', 'p', 'l', 'e', 'e', 'x', 'c', + 'e', 's', 's', 'e', 'x', 'c', 'h', 'a', 'n', 'g', 'e', 'e', 'x', 'c', 'i', 't', 'e', 'e', 'x', + 'c', 'l', 'u', 'd', 'e', 'e', 'x', 'c', 'u', 's', 'e', 'e', 'x', 'e', 'c', 'u', 't', 'e', 'e', + 'x', 'e', 'r', 'c', 'i', 's', 'e', 'e', 'x', 'h', 'a', 'u', 's', 't', 'e', 'x', 'h', 'i', 'b', + 'i', 't', 'e', 'x', 'i', 'l', 'e', 'e', 'x', 'i', 's', 't', 'e', 'x', 'i', 't', 'e', 'x', 'o', + 't', 'i', 'c', 'e', 'x', 'p', 'a', 'n', 'd', 'e', 'x', 'p', 'e', 'c', 't', 'e', 'x', 'p', 'i', + 'r', 'e', 'e', 'x', 'p', 'l', 'a', 'i', 'n', 'e', 'x', 'p', 'o', 's', 'e', 'e', 'x', 'p', 'r', + 'e', 's', 's', 'e', 'x', 't', 'e', 'n', 'd', 'e', 'x', 't', 'r', 'a', 'e', 'y', 'e', 'e', 'y', + 'e', 'b', 'r', 'o', 'w', 'f', 'a', 'b', 'r', 'i', 'c', 'f', 'a', 'c', 'e', 'f', 'a', 'c', 'u', + 'l', 't', 'y', 'f', 'a', 'd', 'e', 'f', 'a', 'i', 'n', 't', 'f', 'a', 'i', 't', 'h', 'f', 'a', + 'l', 'l', 'f', 'a', 'l', 's', 'e', 'f', 'a', 'm', 'e', 'f', 'a', 'm', 'i', 'l', 'y', 'f', 'a', + 'm', 'o', 'u', 's', 'f', 'a', 'n', 'f', 'a', 'n', 'c', 'y', 'f', 'a', 'n', 't', 'a', 's', 'y', + 'f', 'a', 'r', 'm', 'f', 'a', 's', 'h', 'i', 'o', 'n', 'f', 'a', 't', 'f', 'a', 't', 'a', 'l', + 'f', 'a', 't', 'h', 'e', 'r', 'f', 'a', 't', 'i', 'g', 'u', 'e', 'f', 'a', 'u', 'l', 't', 'f', + 'a', 'v', 'o', 'r', 'i', 't', 'e', 'f', 'e', 'a', 't', 'u', 'r', 'e', 'f', 'e', 'b', 'r', 'u', + 'a', 'r', 'y', 'f', 'e', 'd', 'e', 'r', 'a', 'l', 'f', 'e', 'e', 'f', 'e', 'e', 'd', 'f', 'e', + 'e', 'l', 'f', 'e', 'm', 'a', 'l', 'e', 'f', 'e', 'n', 'c', 'e', 'f', 'e', 's', 't', 'i', 'v', + 'a', 'l', 'f', 'e', 't', 'c', 'h', 'f', 'e', 'v', 'e', 'r', 'f', 'e', 'w', 'f', 'i', 'b', 'e', + 'r', 'f', 'i', 'c', 't', 'i', 'o', 'n', 'f', 'i', 'e', 'l', 'd', 'f', 'i', 'g', 'u', 'r', 'e', + 'f', 'i', 'l', 'e', 'f', 'i', 'l', 'm', 'f', 'i', 'l', 't', 'e', 'r', 'f', 'i', 'n', 'a', 'l', + 'f', 'i', 'n', 'd', 'f', 'i', 'n', 'e', 'f', 'i', 'n', 'g', 'e', 'r', 'f', 'i', 'n', 'i', 's', + 'h', 'f', 'i', 'r', 'e', 'f', 'i', 'r', 'm', 'f', 'i', 'r', 's', 't', 'f', 'i', 's', 'c', 'a', + 'l', 'f', 'i', 's', 'h', 'f', 'i', 't', 'f', 'i', 't', 'n', 'e', 's', 's', 'f', 'i', 'x', 'f', + 'l', 'a', 'g', 'f', 'l', 'a', 'm', 'e', 'f', 'l', 'a', 's', 'h', 'f', 'l', 'a', 't', 'f', 'l', + 'a', 'v', 'o', 'r', 'f', 'l', 'e', 'e', 'f', 'l', 'i', 'g', 'h', 't', 'f', 'l', 'i', 'p', 'f', + 'l', 'o', 'a', 't', 'f', 'l', 'o', 'c', 'k', 'f', 'l', 'o', 'o', 'r', 'f', 'l', 'o', 'w', 'e', + 'r', 'f', 'l', 'u', 'i', 'd', 'f', 'l', 'u', 's', 'h', 'f', 'l', 'y', 'f', 'o', 'a', 'm', 'f', + 'o', 'c', 'u', 's', 'f', 'o', 'g', 'f', 'o', 'i', 'l', 'f', 'o', 'l', 'd', 'f', 'o', 'l', 'l', + 'o', 'w', 'f', 'o', 'o', 'd', 'f', 'o', 'o', 't', 'f', 'o', 'r', 'c', 'e', 'f', 'o', 'r', 'e', + 's', 't', 'f', 'o', 'r', 'g', 'e', 't', 'f', 'o', 'r', 'k', 'f', 'o', 'r', 't', 'u', 'n', 'e', + 'f', 'o', 'r', 'u', 'm', 'f', 'o', 'r', 'w', 'a', 'r', 'd', 'f', 'o', 's', 's', 'i', 'l', 'f', + 'o', 's', 't', 'e', 'r', 'f', 'o', 'u', 'n', 'd', 'f', 'o', 'x', 'f', 'r', 'a', 'g', 'i', 'l', + 'e', 'f', 'r', 'a', 'm', 'e', 'f', 'r', 'e', 'q', 'u', 'e', 'n', 't', 'f', 'r', 'e', 's', 'h', + 'f', 'r', 'i', 'e', 'n', 'd', 'f', 'r', 'i', 'n', 'g', 'e', 'f', 'r', 'o', 'g', 'f', 'r', 'o', + 'n', 't', 'f', 'r', 'o', 's', 't', 'f', 'r', 'o', 'w', 'n', 'f', 'r', 'o', 'z', 'e', 'n', 'f', + 'r', 'u', 'i', 't', 'f', 'u', 'e', 'l', 'f', 'u', 'n', 'f', 'u', 'n', 'n', 'y', 'f', 'u', 'r', + 'n', 'a', 'c', 'e', 'f', 'u', 'r', 'y', 'f', 'u', 't', 'u', 'r', 'e', 'g', 'a', 'd', 'g', 'e', + 't', 'g', 'a', 'i', 'n', 'g', 'a', 'l', 'a', 'x', 'y', 'g', 'a', 'l', 'l', 'e', 'r', 'y', 'g', + 'a', 'm', 'e', 'g', 'a', 'p', 'g', 'a', 'r', 'a', 'g', 'e', 'g', 'a', 'r', 'b', 'a', 'g', 'e', + 'g', 'a', 'r', 'd', 'e', 'n', 'g', 'a', 'r', 'l', 'i', 'c', 'g', 'a', 'r', 'm', 'e', 'n', 't', + 'g', 'a', 's', 'g', 'a', 's', 'p', 'g', 'a', 't', 'e', 'g', 'a', 't', 'h', 'e', 'r', 'g', 'a', + 'u', 'g', 'e', 'g', 'a', 'z', 'e', 'g', 'e', 'n', 'e', 'r', 'a', 'l', 'g', 'e', 'n', 'i', 'u', + 's', 'g', 'e', 'n', 'r', 'e', 'g', 'e', 'n', 't', 'l', 'e', 'g', 'e', 'n', 'u', 'i', 'n', 'e', + 'g', 'e', 's', 't', 'u', 'r', 'e', 'g', 'h', 'o', 's', 't', 'g', 'i', 'a', 'n', 't', 'g', 'i', + 'f', 't', 'g', 'i', 'g', 'g', 'l', 'e', 'g', 'i', 'n', 'g', 'e', 'r', 'g', 'i', 'r', 'a', 'f', + 'f', 'e', 'g', 'i', 'r', 'l', 'g', 'i', 'v', 'e', 'g', 'l', 'a', 'd', 'g', 'l', 'a', 'n', 'c', + 'e', 'g', 'l', 'a', 'r', 'e', 'g', 'l', 'a', 's', 's', 'g', 'l', 'i', 'd', 'e', 'g', 'l', 'i', + 'm', 'p', 's', 'e', 'g', 'l', 'o', 'b', 'e', 'g', 'l', 'o', 'o', 'm', 'g', 'l', 'o', 'r', 'y', + 'g', 'l', 'o', 'v', 'e', 'g', 'l', 'o', 'w', 'g', 'l', 'u', 'e', 'g', 'o', 'a', 't', 'g', 'o', + 'd', 'd', 'e', 's', 's', 'g', 'o', 'l', 'd', 'g', 'o', 'o', 'd', 'g', 'o', 'o', 's', 'e', 'g', + 'o', 'r', 'i', 'l', 'l', 'a', 'g', 'o', 's', 'p', 'e', 'l', 'g', 'o', 's', 's', 'i', 'p', 'g', + 'o', 'v', 'e', 'r', 'n', 'g', 'o', 'w', 'n', 'g', 'r', 'a', 'b', 'g', 'r', 'a', 'c', 'e', 'g', + 'r', 'a', 'i', 'n', 'g', 'r', 'a', 'n', 't', 'g', 'r', 'a', 'p', 'e', 'g', 'r', 'a', 's', 's', + 'g', 'r', 'a', 'v', 'i', 't', 'y', 'g', 'r', 'e', 'a', 't', 'g', 'r', 'e', 'e', 'n', 'g', 'r', + 'i', 'd', 'g', 'r', 'i', 'e', 'f', 'g', 'r', 'i', 't', 'g', 'r', 'o', 'c', 'e', 'r', 'y', 'g', + 'r', 'o', 'u', 'p', 'g', 'r', 'o', 'w', 'g', 'r', 'u', 'n', 't', 'g', 'u', 'a', 'r', 'd', 'g', + 'u', 'e', 's', 's', 'g', 'u', 'i', 'd', 'e', 'g', 'u', 'i', 'l', 't', 'g', 'u', 'i', 't', 'a', + 'r', 'g', 'u', 'n', 'g', 'y', 'm', 'h', 'a', 'b', 'i', 't', 'h', 'a', 'i', 'r', 'h', 'a', 'l', + 'f', 'h', 'a', 'm', 'm', 'e', 'r', 'h', 'a', 'm', 's', 't', 'e', 'r', 'h', 'a', 'n', 'd', 'h', + 'a', 'p', 'p', 'y', 'h', 'a', 'r', 'b', 'o', 'r', 'h', 'a', 'r', 'd', 'h', 'a', 'r', 's', 'h', + 'h', 'a', 'r', 'v', 'e', 's', 't', 'h', 'a', 't', 'h', 'a', 'v', 'e', 'h', 'a', 'w', 'k', 'h', + 'a', 'z', 'a', 'r', 'd', 'h', 'e', 'a', 'd', 'h', 'e', 'a', 'l', 't', 'h', 'h', 'e', 'a', 'r', + 't', 'h', 'e', 'a', 'v', 'y', 'h', 'e', 'd', 'g', 'e', 'h', 'o', 'g', 'h', 'e', 'i', 'g', 'h', + 't', 'h', 'e', 'l', 'l', 'o', 'h', 'e', 'l', 'm', 'e', 't', 'h', 'e', 'l', 'p', 'h', 'e', 'n', + 'h', 'e', 'r', 'o', 'h', 'i', 'd', 'd', 'e', 'n', 'h', 'i', 'g', 'h', 'h', 'i', 'l', 'l', 'h', + 'i', 'n', 't', 'h', 'i', 'p', 'h', 'i', 'r', 'e', 'h', 'i', 's', 't', 'o', 'r', 'y', 'h', 'o', + 'b', 'b', 'y', 'h', 'o', 'c', 'k', 'e', 'y', 'h', 'o', 'l', 'd', 'h', 'o', 'l', 'e', 'h', 'o', + 'l', 'i', 'd', 'a', 'y', 'h', 'o', 'l', 'l', 'o', 'w', 'h', 'o', 'm', 'e', 'h', 'o', 'n', 'e', + 'y', 'h', 'o', 'o', 'd', 'h', 'o', 'p', 'e', 'h', 'o', 'r', 'n', 'h', 'o', 'r', 'r', 'o', 'r', + 'h', 'o', 'r', 's', 'e', 'h', 'o', 's', 'p', 'i', 't', 'a', 'l', 'h', 'o', 's', 't', 'h', 'o', + 't', 'e', 'l', 'h', 'o', 'u', 'r', 'h', 'o', 'v', 'e', 'r', 'h', 'u', 'b', 'h', 'u', 'g', 'e', + 'h', 'u', 'm', 'a', 'n', 'h', 'u', 'm', 'b', 'l', 'e', 'h', 'u', 'm', 'o', 'r', 'h', 'u', 'n', + 'd', 'r', 'e', 'd', 'h', 'u', 'n', 'g', 'r', 'y', 'h', 'u', 'n', 't', 'h', 'u', 'r', 'd', 'l', + 'e', 'h', 'u', 'r', 'r', 'y', 'h', 'u', 'r', 't', 'h', 'u', 's', 'b', 'a', 'n', 'd', 'h', 'y', + 'b', 'r', 'i', 'd', 'i', 'c', 'e', 'i', 'c', 'o', 'n', 'i', 'd', 'e', 'a', 'i', 'd', 'e', 'n', + 't', 'i', 'f', 'y', 'i', 'd', 'l', 'e', 'i', 'g', 'n', 'o', 'r', 'e', 'i', 'l', 'l', 'i', 'l', + 'l', 'e', 'g', 'a', 'l', 'i', 'l', 'l', 'n', 'e', 's', 's', 'i', 'm', 'a', 'g', 'e', 'i', 'm', + 'i', 't', 'a', 't', 'e', 'i', 'm', 'm', 'e', 'n', 's', 'e', 'i', 'm', 'm', 'u', 'n', 'e', 'i', + 'm', 'p', 'a', 'c', 't', 'i', 'm', 'p', 'o', 's', 'e', 'i', 'm', 'p', 'r', 'o', 'v', 'e', 'i', + 'm', 'p', 'u', 'l', 's', 'e', 'i', 'n', 'c', 'h', 'i', 'n', 'c', 'l', 'u', 'd', 'e', 'i', 'n', + 'c', 'o', 'm', 'e', 'i', 'n', 'c', 'r', 'e', 'a', 's', 'e', 'i', 'n', 'd', 'e', 'x', 'i', 'n', + 'd', 'i', 'c', 'a', 't', 'e', 'i', 'n', 'd', 'o', 'o', 'r', 'i', 'n', 'd', 'u', 's', 't', 'r', + 'y', 'i', 'n', 'f', 'a', 'n', 't', 'i', 'n', 'f', 'l', 'i', 'c', 't', 'i', 'n', 'f', 'o', 'r', + 'm', 'i', 'n', 'h', 'a', 'l', 'e', 'i', 'n', 'h', 'e', 'r', 'i', 't', 'i', 'n', 'i', 't', 'i', + 'a', 'l', 'i', 'n', 'j', 'e', 'c', 't', 'i', 'n', 'j', 'u', 'r', 'y', 'i', 'n', 'm', 'a', 't', + 'e', 'i', 'n', 'n', 'e', 'r', 'i', 'n', 'n', 'o', 'c', 'e', 'n', 't', 'i', 'n', 'p', 'u', 't', + 'i', 'n', 'q', 'u', 'i', 'r', 'y', 'i', 'n', 's', 'a', 'n', 'e', 'i', 'n', 's', 'e', 'c', 't', + 'i', 'n', 's', 'i', 'd', 'e', 'i', 'n', 's', 'p', 'i', 'r', 'e', 'i', 'n', 's', 't', 'a', 'l', + 'l', 'i', 'n', 't', 'a', 'c', 't', 'i', 'n', 't', 'e', 'r', 'e', 's', 't', 'i', 'n', 't', 'o', + 'i', 'n', 'v', 'e', 's', 't', 'i', 'n', 'v', 'i', 't', 'e', 'i', 'n', 'v', 'o', 'l', 'v', 'e', + 'i', 'r', 'o', 'n', 'i', 's', 'l', 'a', 'n', 'd', 'i', 's', 'o', 'l', 'a', 't', 'e', 'i', 's', + 's', 'u', 'e', 'i', 't', 'e', 'm', 'i', 'v', 'o', 'r', 'y', 'j', 'a', 'c', 'k', 'e', 't', 'j', + 'a', 'g', 'u', 'a', 'r', 'j', 'a', 'r', 'j', 'a', 'z', 'z', 'j', 'e', 'a', 'l', 'o', 'u', 's', + 'j', 'e', 'a', 'n', 's', 'j', 'e', 'l', 'l', 'y', 'j', 'e', 'w', 'e', 'l', 'j', 'o', 'b', 'j', + 'o', 'i', 'n', 'j', 'o', 'k', 'e', 'j', 'o', 'u', 'r', 'n', 'e', 'y', 'j', 'o', 'y', 'j', 'u', + 'd', 'g', 'e', 'j', 'u', 'i', 'c', 'e', 'j', 'u', 'm', 'p', 'j', 'u', 'n', 'g', 'l', 'e', 'j', + 'u', 'n', 'i', 'o', 'r', 'j', 'u', 'n', 'k', 'j', 'u', 's', 't', 'k', 'a', 'n', 'g', 'a', 'r', + 'o', 'o', 'k', 'e', 'e', 'n', 'k', 'e', 'e', 'p', 'k', 'e', 't', 'c', 'h', 'u', 'p', 'k', 'e', + 'y', 'k', 'i', 'c', 'k', 'k', 'i', 'd', 'k', 'i', 'd', 'n', 'e', 'y', 'k', 'i', 'n', 'd', 'k', + 'i', 'n', 'g', 'd', 'o', 'm', 'k', 'i', 's', 's', 'k', 'i', 't', 'k', 'i', 't', 'c', 'h', 'e', + 'n', 'k', 'i', 't', 'e', 'k', 'i', 't', 't', 'e', 'n', 'k', 'i', 'w', 'i', 'k', 'n', 'e', 'e', + 'k', 'n', 'i', 'f', 'e', 'k', 'n', 'o', 'c', 'k', 'k', 'n', 'o', 'w', 'l', 'a', 'b', 'l', 'a', + 'b', 'e', 'l', 'l', 'a', 'b', 'o', 'r', 'l', 'a', 'd', 'd', 'e', 'r', 'l', 'a', 'd', 'y', 'l', + 'a', 'k', 'e', 'l', 'a', 'm', 'p', 'l', 'a', 'n', 'g', 'u', 'a', 'g', 'e', 'l', 'a', 'p', 't', + 'o', 'p', 'l', 'a', 'r', 'g', 'e', 'l', 'a', 't', 'e', 'r', 'l', 'a', 't', 'i', 'n', 'l', 'a', + 'u', 'g', 'h', 'l', 'a', 'u', 'n', 'd', 'r', 'y', 'l', 'a', 'v', 'a', 'l', 'a', 'w', 'l', 'a', + 'w', 'n', 'l', 'a', 'w', 's', 'u', 'i', 't', 'l', 'a', 'y', 'e', 'r', 'l', 'a', 'z', 'y', 'l', + 'e', 'a', 'd', 'e', 'r', 'l', 'e', 'a', 'f', 'l', 'e', 'a', 'r', 'n', 'l', 'e', 'a', 'v', 'e', + 'l', 'e', 'c', 't', 'u', 'r', 'e', 'l', 'e', 'f', 't', 'l', 'e', 'g', 'l', 'e', 'g', 'a', 'l', + 'l', 'e', 'g', 'e', 'n', 'd', 'l', 'e', 'i', 's', 'u', 'r', 'e', 'l', 'e', 'm', 'o', 'n', 'l', + 'e', 'n', 'd', 'l', 'e', 'n', 'g', 't', 'h', 'l', 'e', 'n', 's', 'l', 'e', 'o', 'p', 'a', 'r', + 'd', 'l', 'e', 's', 's', 'o', 'n', 'l', 'e', 't', 't', 'e', 'r', 'l', 'e', 'v', 'e', 'l', 'l', + 'i', 'a', 'r', 'l', 'i', 'b', 'e', 'r', 't', 'y', 'l', 'i', 'b', 'r', 'a', 'r', 'y', 'l', 'i', + 'c', 'e', 'n', 's', 'e', 'l', 'i', 'f', 'e', 'l', 'i', 'f', 't', 'l', 'i', 'g', 'h', 't', 'l', + 'i', 'k', 'e', 'l', 'i', 'm', 'b', 'l', 'i', 'm', 'i', 't', 'l', 'i', 'n', 'k', 'l', 'i', 'o', + 'n', 'l', 'i', 'q', 'u', 'i', 'd', 'l', 'i', 's', 't', 'l', 'i', 't', 't', 'l', 'e', 'l', 'i', + 'v', 'e', 'l', 'i', 'z', 'a', 'r', 'd', 'l', 'o', 'a', 'd', 'l', 'o', 'a', 'n', 'l', 'o', 'b', + 's', 't', 'e', 'r', 'l', 'o', 'c', 'a', 'l', 'l', 'o', 'c', 'k', 'l', 'o', 'g', 'i', 'c', 'l', + 'o', 'n', 'e', 'l', 'y', 'l', 'o', 'n', 'g', 'l', 'o', 'o', 'p', 'l', 'o', 't', 't', 'e', 'r', + 'y', 'l', 'o', 'u', 'd', 'l', 'o', 'u', 'n', 'g', 'e', 'l', 'o', 'v', 'e', 'l', 'o', 'y', 'a', + 'l', 'l', 'u', 'c', 'k', 'y', 'l', 'u', 'g', 'g', 'a', 'g', 'e', 'l', 'u', 'm', 'b', 'e', 'r', + 'l', 'u', 'n', 'a', 'r', 'l', 'u', 'n', 'c', 'h', 'l', 'u', 'x', 'u', 'r', 'y', 'l', 'y', 'r', + 'i', 'c', 's', 'm', 'a', 'c', 'h', 'i', 'n', 'e', 'm', 'a', 'd', 'm', 'a', 'g', 'i', 'c', 'm', + 'a', 'g', 'n', 'e', 't', 'm', 'a', 'i', 'd', 'm', 'a', 'i', 'l', 'm', 'a', 'i', 'n', 'm', 'a', + 'j', 'o', 'r', 'm', 'a', 'k', 'e', 'm', 'a', 'm', 'm', 'a', 'l', 'm', 'a', 'n', 'm', 'a', 'n', + 'a', 'g', 'e', 'm', 'a', 'n', 'd', 'a', 't', 'e', 'm', 'a', 'n', 'g', 'o', 'm', 'a', 'n', 's', + 'i', 'o', 'n', 'm', 'a', 'n', 'u', 'a', 'l', 'm', 'a', 'p', 'l', 'e', 'm', 'a', 'r', 'b', 'l', + 'e', 'm', 'a', 'r', 'c', 'h', 'm', 'a', 'r', 'g', 'i', 'n', 'm', 'a', 'r', 'i', 'n', 'e', 'm', + 'a', 'r', 'k', 'e', 't', 'm', 'a', 'r', 'r', 'i', 'a', 'g', 'e', 'm', 'a', 's', 'k', 'm', 'a', + 's', 's', 'm', 'a', 's', 't', 'e', 'r', 'm', 'a', 't', 'c', 'h', 'm', 'a', 't', 'e', 'r', 'i', + 'a', 'l', 'm', 'a', 't', 'h', 'm', 'a', 't', 'r', 'i', 'x', 'm', 'a', 't', 't', 'e', 'r', 'm', + 'a', 'x', 'i', 'm', 'u', 'm', 'm', 'a', 'z', 'e', 'm', 'e', 'a', 'd', 'o', 'w', 'm', 'e', 'a', + 'n', 'm', 'e', 'a', 's', 'u', 'r', 'e', 'm', 'e', 'a', 't', 'm', 'e', 'c', 'h', 'a', 'n', 'i', + 'c', 'm', 'e', 'd', 'a', 'l', 'm', 'e', 'd', 'i', 'a', 'm', 'e', 'l', 'o', 'd', 'y', 'm', 'e', + 'l', 't', 'm', 'e', 'm', 'b', 'e', 'r', 'm', 'e', 'm', 'o', 'r', 'y', 'm', 'e', 'n', 't', 'i', + 'o', 'n', 'm', 'e', 'n', 'u', 'm', 'e', 'r', 'c', 'y', 'm', 'e', 'r', 'g', 'e', 'm', 'e', 'r', + 'i', 't', 'm', 'e', 'r', 'r', 'y', 'm', 'e', 's', 'h', 'm', 'e', 's', 's', 'a', 'g', 'e', 'm', + 'e', 't', 'a', 'l', 'm', 'e', 't', 'h', 'o', 'd', 'm', 'i', 'd', 'd', 'l', 'e', 'm', 'i', 'd', + 'n', 'i', 'g', 'h', 't', 'm', 'i', 'l', 'k', 'm', 'i', 'l', 'l', 'i', 'o', 'n', 'm', 'i', 'm', + 'i', 'c', 'm', 'i', 'n', 'd', 'm', 'i', 'n', 'i', 'm', 'u', 'm', 'm', 'i', 'n', 'o', 'r', 'm', + 'i', 'n', 'u', 't', 'e', 'm', 'i', 'r', 'a', 'c', 'l', 'e', 'm', 'i', 'r', 'r', 'o', 'r', 'm', + 'i', 's', 'e', 'r', 'y', 'm', 'i', 's', 's', 'm', 'i', 's', 't', 'a', 'k', 'e', 'm', 'i', 'x', + 'm', 'i', 'x', 'e', 'd', 'm', 'i', 'x', 't', 'u', 'r', 'e', 'm', 'o', 'b', 'i', 'l', 'e', 'm', + 'o', 'd', 'e', 'l', 'm', 'o', 'd', 'i', 'f', 'y', 'm', 'o', 'm', 'm', 'o', 'm', 'e', 'n', 't', + 'm', 'o', 'n', 'i', 't', 'o', 'r', 'm', 'o', 'n', 'k', 'e', 'y', 'm', 'o', 'n', 's', 't', 'e', + 'r', 'm', 'o', 'n', 't', 'h', 'm', 'o', 'o', 'n', 'm', 'o', 'r', 'a', 'l', 'm', 'o', 'r', 'e', + 'm', 'o', 'r', 'n', 'i', 'n', 'g', 'm', 'o', 's', 'q', 'u', 'i', 't', 'o', 'm', 'o', 't', 'h', + 'e', 'r', 'm', 'o', 't', 'i', 'o', 'n', 'm', 'o', 't', 'o', 'r', 'm', 'o', 'u', 'n', 't', 'a', + 'i', 'n', 'm', 'o', 'u', 's', 'e', 'm', 'o', 'v', 'e', 'm', 'o', 'v', 'i', 'e', 'm', 'u', 'c', + 'h', 'm', 'u', 'f', 'f', 'i', 'n', 'm', 'u', 'l', 'e', 'm', 'u', 'l', 't', 'i', 'p', 'l', 'y', + 'm', 'u', 's', 'c', 'l', 'e', 'm', 'u', 's', 'e', 'u', 'm', 'm', 'u', 's', 'h', 'r', 'o', 'o', + 'm', 'm', 'u', 's', 'i', 'c', 'm', 'u', 's', 't', 'm', 'u', 't', 'u', 'a', 'l', 'm', 'y', 's', + 'e', 'l', 'f', 'm', 'y', 's', 't', 'e', 'r', 'y', 'm', 'y', 't', 'h', 'n', 'a', 'i', 'v', 'e', + 'n', 'a', 'm', 'e', 'n', 'a', 'p', 'k', 'i', 'n', 'n', 'a', 'r', 'r', 'o', 'w', 'n', 'a', 's', + 't', 'y', 'n', 'a', 't', 'i', 'o', 'n', 'n', 'a', 't', 'u', 'r', 'e', 'n', 'e', 'a', 'r', 'n', + 'e', 'c', 'k', 'n', 'e', 'e', 'd', 'n', 'e', 'g', 'a', 't', 'i', 'v', 'e', 'n', 'e', 'g', 'l', + 'e', 'c', 't', 'n', 'e', 'i', 't', 'h', 'e', 'r', 'n', 'e', 'p', 'h', 'e', 'w', 'n', 'e', 'r', + 'v', 'e', 'n', 'e', 's', 't', 'n', 'e', 't', 'n', 'e', 't', 'w', 'o', 'r', 'k', 'n', 'e', 'u', + 't', 'r', 'a', 'l', 'n', 'e', 'v', 'e', 'r', 'n', 'e', 'w', 's', 'n', 'e', 'x', 't', 'n', 'i', + 'c', 'e', 'n', 'i', 'g', 'h', 't', 'n', 'o', 'b', 'l', 'e', 'n', 'o', 'i', 's', 'e', 'n', 'o', + 'm', 'i', 'n', 'e', 'e', 'n', 'o', 'o', 'd', 'l', 'e', 'n', 'o', 'r', 'm', 'a', 'l', 'n', 'o', + 'r', 't', 'h', 'n', 'o', 's', 'e', 'n', 'o', 't', 'a', 'b', 'l', 'e', 'n', 'o', 't', 'e', 'n', + 'o', 't', 'h', 'i', 'n', 'g', 'n', 'o', 't', 'i', 'c', 'e', 'n', 'o', 'v', 'e', 'l', 'n', 'o', + 'w', 'n', 'u', 'c', 'l', 'e', 'a', 'r', 'n', 'u', 'm', 'b', 'e', 'r', 'n', 'u', 'r', 's', 'e', + 'n', 'u', 't', 'o', 'a', 'k', 'o', 'b', 'e', 'y', 'o', 'b', 'j', 'e', 'c', 't', 'o', 'b', 'l', + 'i', 'g', 'e', 'o', 'b', 's', 'c', 'u', 'r', 'e', 'o', 'b', 's', 'e', 'r', 'v', 'e', 'o', 'b', + 't', 'a', 'i', 'n', 'o', 'b', 'v', 'i', 'o', 'u', 's', 'o', 'c', 'c', 'u', 'r', 'o', 'c', 'e', + 'a', 'n', 'o', 'c', 't', 'o', 'b', 'e', 'r', 'o', 'd', 'o', 'r', 'o', 'f', 'f', 'o', 'f', 'f', + 'e', 'r', 'o', 'f', 'f', 'i', 'c', 'e', 'o', 'f', 't', 'e', 'n', 'o', 'i', 'l', 'o', 'k', 'a', + 'y', 'o', 'l', 'd', 'o', 'l', 'i', 'v', 'e', 'o', 'l', 'y', 'm', 'p', 'i', 'c', 'o', 'm', 'i', + 't', 'o', 'n', 'c', 'e', 'o', 'n', 'e', 'o', 'n', 'i', 'o', 'n', 'o', 'n', 'l', 'i', 'n', 'e', + 'o', 'n', 'l', 'y', 'o', 'p', 'e', 'n', 'o', 'p', 'e', 'r', 'a', 'o', 'p', 'i', 'n', 'i', 'o', + 'n', 'o', 'p', 'p', 'o', 's', 'e', 'o', 'p', 't', 'i', 'o', 'n', 'o', 'r', 'a', 'n', 'g', 'e', + 'o', 'r', 'b', 'i', 't', 'o', 'r', 'c', 'h', 'a', 'r', 'd', 'o', 'r', 'd', 'e', 'r', 'o', 'r', + 'd', 'i', 'n', 'a', 'r', 'y', 'o', 'r', 'g', 'a', 'n', 'o', 'r', 'i', 'e', 'n', 't', 'o', 'r', + 'i', 'g', 'i', 'n', 'a', 'l', 'o', 'r', 'p', 'h', 'a', 'n', 'o', 's', 't', 'r', 'i', 'c', 'h', + 'o', 't', 'h', 'e', 'r', 'o', 'u', 't', 'd', 'o', 'o', 'r', 'o', 'u', 't', 'e', 'r', 'o', 'u', + 't', 'p', 'u', 't', 'o', 'u', 't', 's', 'i', 'd', 'e', 'o', 'v', 'a', 'l', 'o', 'v', 'e', 'n', + 'o', 'v', 'e', 'r', 'o', 'w', 'n', 'o', 'w', 'n', 'e', 'r', 'o', 'x', 'y', 'g', 'e', 'n', 'o', + 'y', 's', 't', 'e', 'r', 'o', 'z', 'o', 'n', 'e', 'p', 'a', 'c', 't', 'p', 'a', 'd', 'd', 'l', + 'e', 'p', 'a', 'g', 'e', 'p', 'a', 'i', 'r', 'p', 'a', 'l', 'a', 'c', 'e', 'p', 'a', 'l', 'm', + 'p', 'a', 'n', 'd', 'a', 'p', 'a', 'n', 'e', 'l', 'p', 'a', 'n', 'i', 'c', 'p', 'a', 'n', 't', + 'h', 'e', 'r', 'p', 'a', 'p', 'e', 'r', 'p', 'a', 'r', 'a', 'd', 'e', 'p', 'a', 'r', 'e', 'n', + 't', 'p', 'a', 'r', 'k', 'p', 'a', 'r', 'r', 'o', 't', 'p', 'a', 'r', 't', 'y', 'p', 'a', 's', + 's', 'p', 'a', 't', 'c', 'h', 'p', 'a', 't', 'h', 'p', 'a', 't', 'i', 'e', 'n', 't', 'p', 'a', + 't', 'r', 'o', 'l', 'p', 'a', 't', 't', 'e', 'r', 'n', 'p', 'a', 'u', 's', 'e', 'p', 'a', 'v', + 'e', 'p', 'a', 'y', 'm', 'e', 'n', 't', 'p', 'e', 'a', 'c', 'e', 'p', 'e', 'a', 'n', 'u', 't', + 'p', 'e', 'a', 'r', 'p', 'e', 'a', 's', 'a', 'n', 't', 'p', 'e', 'l', 'i', 'c', 'a', 'n', 'p', + 'e', 'n', 'p', 'e', 'n', 'a', 'l', 't', 'y', 'p', 'e', 'n', 'c', 'i', 'l', 'p', 'e', 'o', 'p', + 'l', 'e', 'p', 'e', 'p', 'p', 'e', 'r', 'p', 'e', 'r', 'f', 'e', 'c', 't', 'p', 'e', 'r', 'm', + 'i', 't', 'p', 'e', 'r', 's', 'o', 'n', 'p', 'e', 't', 'p', 'h', 'o', 'n', 'e', 'p', 'h', 'o', + 't', 'o', 'p', 'h', 'r', 'a', 's', 'e', 'p', 'h', 'y', 's', 'i', 'c', 'a', 'l', 'p', 'i', 'a', + 'n', 'o', 'p', 'i', 'c', 'n', 'i', 'c', 'p', 'i', 'c', 't', 'u', 'r', 'e', 'p', 'i', 'e', 'c', + 'e', 'p', 'i', 'g', 'p', 'i', 'g', 'e', 'o', 'n', 'p', 'i', 'l', 'l', 'p', 'i', 'l', 'o', 't', + 'p', 'i', 'n', 'k', 'p', 'i', 'o', 'n', 'e', 'e', 'r', 'p', 'i', 'p', 'e', 'p', 'i', 's', 't', + 'o', 'l', 'p', 'i', 't', 'c', 'h', 'p', 'i', 'z', 'z', 'a', 'p', 'l', 'a', 'c', 'e', 'p', 'l', + 'a', 'n', 'e', 't', 'p', 'l', 'a', 's', 't', 'i', 'c', 'p', 'l', 'a', 't', 'e', 'p', 'l', 'a', + 'y', 'p', 'l', 'e', 'a', 's', 'e', 'p', 'l', 'e', 'd', 'g', 'e', 'p', 'l', 'u', 'c', 'k', 'p', + 'l', 'u', 'g', 'p', 'l', 'u', 'n', 'g', 'e', 'p', 'o', 'e', 'm', 'p', 'o', 'e', 't', 'p', 'o', + 'i', 'n', 't', 'p', 'o', 'l', 'a', 'r', 'p', 'o', 'l', 'e', 'p', 'o', 'l', 'i', 'c', 'e', 'p', + 'o', 'n', 'd', 'p', 'o', 'n', 'y', 'p', 'o', 'o', 'l', 'p', 'o', 'p', 'u', 'l', 'a', 'r', 'p', + 'o', 'r', 't', 'i', 'o', 'n', 'p', 'o', 's', 'i', 't', 'i', 'o', 'n', 'p', 'o', 's', 's', 'i', + 'b', 'l', 'e', 'p', 'o', 's', 't', 'p', 'o', 't', 'a', 't', 'o', 'p', 'o', 't', 't', 'e', 'r', + 'y', 'p', 'o', 'v', 'e', 'r', 't', 'y', 'p', 'o', 'w', 'd', 'e', 'r', 'p', 'o', 'w', 'e', 'r', + 'p', 'r', 'a', 'c', 't', 'i', 'c', 'e', 'p', 'r', 'a', 'i', 's', 'e', 'p', 'r', 'e', 'd', 'i', + 'c', 't', 'p', 'r', 'e', 'f', 'e', 'r', 'p', 'r', 'e', 'p', 'a', 'r', 'e', 'p', 'r', 'e', 's', + 'e', 'n', 't', 'p', 'r', 'e', 't', 't', 'y', 'p', 'r', 'e', 'v', 'e', 'n', 't', 'p', 'r', 'i', + 'c', 'e', 'p', 'r', 'i', 'd', 'e', 'p', 'r', 'i', 'm', 'a', 'r', 'y', 'p', 'r', 'i', 'n', 't', + 'p', 'r', 'i', 'o', 'r', 'i', 't', 'y', 'p', 'r', 'i', 's', 'o', 'n', 'p', 'r', 'i', 'v', 'a', + 't', 'e', 'p', 'r', 'i', 'z', 'e', 'p', 'r', 'o', 'b', 'l', 'e', 'm', 'p', 'r', 'o', 'c', 'e', + 's', 's', 'p', 'r', 'o', 'd', 'u', 'c', 'e', 'p', 'r', 'o', 'f', 'i', 't', 'p', 'r', 'o', 'g', + 'r', 'a', 'm', 'p', 'r', 'o', 'j', 'e', 'c', 't', 'p', 'r', 'o', 'm', 'o', 't', 'e', 'p', 'r', + 'o', 'o', 'f', 'p', 'r', 'o', 'p', 'e', 'r', 't', 'y', 'p', 'r', 'o', 's', 'p', 'e', 'r', 'p', + 'r', 'o', 't', 'e', 'c', 't', 'p', 'r', 'o', 'u', 'd', 'p', 'r', 'o', 'v', 'i', 'd', 'e', 'p', + 'u', 'b', 'l', 'i', 'c', 'p', 'u', 'd', 'd', 'i', 'n', 'g', 'p', 'u', 'l', 'l', 'p', 'u', 'l', + 'p', 'p', 'u', 'l', 's', 'e', 'p', 'u', 'm', 'p', 'k', 'i', 'n', 'p', 'u', 'n', 'c', 'h', 'p', + 'u', 'p', 'i', 'l', 'p', 'u', 'p', 'p', 'y', 'p', 'u', 'r', 'c', 'h', 'a', 's', 'e', 'p', 'u', + 'r', 'i', 't', 'y', 'p', 'u', 'r', 'p', 'o', 's', 'e', 'p', 'u', 'r', 's', 'e', 'p', 'u', 's', + 'h', 'p', 'u', 't', 'p', 'u', 'z', 'z', 'l', 'e', 'p', 'y', 'r', 'a', 'm', 'i', 'd', 'q', 'u', + 'a', 'l', 'i', 't', 'y', 'q', 'u', 'a', 'n', 't', 'u', 'm', 'q', 'u', 'a', 'r', 't', 'e', 'r', + 'q', 'u', 'e', 's', 't', 'i', 'o', 'n', 'q', 'u', 'i', 'c', 'k', 'q', 'u', 'i', 't', 'q', 'u', + 'i', 'z', 'q', 'u', 'o', 't', 'e', 'r', 'a', 'b', 'b', 'i', 't', 'r', 'a', 'c', 'c', 'o', 'o', + 'n', 'r', 'a', 'c', 'e', 'r', 'a', 'c', 'k', 'r', 'a', 'd', 'a', 'r', 'r', 'a', 'd', 'i', 'o', + 'r', 'a', 'i', 'l', 'r', 'a', 'i', 'n', 'r', 'a', 'i', 's', 'e', 'r', 'a', 'l', 'l', 'y', 'r', + 'a', 'm', 'p', 'r', 'a', 'n', 'c', 'h', 'r', 'a', 'n', 'd', 'o', 'm', 'r', 'a', 'n', 'g', 'e', + 'r', 'a', 'p', 'i', 'd', 'r', 'a', 'r', 'e', 'r', 'a', 't', 'e', 'r', 'a', 't', 'h', 'e', 'r', + 'r', 'a', 'v', 'e', 'n', 'r', 'a', 'w', 'r', 'a', 'z', 'o', 'r', 'r', 'e', 'a', 'd', 'y', 'r', + 'e', 'a', 'l', 'r', 'e', 'a', 's', 'o', 'n', 'r', 'e', 'b', 'e', 'l', 'r', 'e', 'b', 'u', 'i', + 'l', 'd', 'r', 'e', 'c', 'a', 'l', 'l', 'r', 'e', 'c', 'e', 'i', 'v', 'e', 'r', 'e', 'c', 'i', + 'p', 'e', 'r', 'e', 'c', 'o', 'r', 'd', 'r', 'e', 'c', 'y', 'c', 'l', 'e', 'r', 'e', 'd', 'u', + 'c', 'e', 'r', 'e', 'f', 'l', 'e', 'c', 't', 'r', 'e', 'f', 'o', 'r', 'm', 'r', 'e', 'f', 'u', + 's', 'e', 'r', 'e', 'g', 'i', 'o', 'n', 'r', 'e', 'g', 'r', 'e', 't', 'r', 'e', 'g', 'u', 'l', + 'a', 'r', 'r', 'e', 'j', 'e', 'c', 't', 'r', 'e', 'l', 'a', 'x', 'r', 'e', 'l', 'e', 'a', 's', + 'e', 'r', 'e', 'l', 'i', 'e', 'f', 'r', 'e', 'l', 'y', 'r', 'e', 'm', 'a', 'i', 'n', 'r', 'e', + 'm', 'e', 'm', 'b', 'e', 'r', 'r', 'e', 'm', 'i', 'n', 'd', 'r', 'e', 'm', 'o', 'v', 'e', 'r', + 'e', 'n', 'd', 'e', 'r', 'r', 'e', 'n', 'e', 'w', 'r', 'e', 'n', 't', 'r', 'e', 'o', 'p', 'e', + 'n', 'r', 'e', 'p', 'a', 'i', 'r', 'r', 'e', 'p', 'e', 'a', 't', 'r', 'e', 'p', 'l', 'a', 'c', + 'e', 'r', 'e', 'p', 'o', 'r', 't', 'r', 'e', 'q', 'u', 'i', 'r', 'e', 'r', 'e', 's', 'c', 'u', + 'e', 'r', 'e', 's', 'e', 'm', 'b', 'l', 'e', 'r', 'e', 's', 'i', 's', 't', 'r', 'e', 's', 'o', + 'u', 'r', 'c', 'e', 'r', 'e', 's', 'p', 'o', 'n', 's', 'e', 'r', 'e', 's', 'u', 'l', 't', 'r', + 'e', 't', 'i', 'r', 'e', 'r', 'e', 't', 'r', 'e', 'a', 't', 'r', 'e', 't', 'u', 'r', 'n', 'r', + 'e', 'u', 'n', 'i', 'o', 'n', 'r', 'e', 'v', 'e', 'a', 'l', 'r', 'e', 'v', 'i', 'e', 'w', 'r', + 'e', 'w', 'a', 'r', 'd', 'r', 'h', 'y', 't', 'h', 'm', 'r', 'i', 'b', 'r', 'i', 'b', 'b', 'o', + 'n', 'r', 'i', 'c', 'e', 'r', 'i', 'c', 'h', 'r', 'i', 'd', 'e', 'r', 'i', 'd', 'g', 'e', 'r', + 'i', 'f', 'l', 'e', 'r', 'i', 'g', 'h', 't', 'r', 'i', 'g', 'i', 'd', 'r', 'i', 'n', 'g', 'r', + 'i', 'o', 't', 'r', 'i', 'p', 'p', 'l', 'e', 'r', 'i', 's', 'k', 'r', 'i', 't', 'u', 'a', 'l', + 'r', 'i', 'v', 'a', 'l', 'r', 'i', 'v', 'e', 'r', 'r', 'o', 'a', 'd', 'r', 'o', 'a', 's', 't', + 'r', 'o', 'b', 'o', 't', 'r', 'o', 'b', 'u', 's', 't', 'r', 'o', 'c', 'k', 'e', 't', 'r', 'o', + 'm', 'a', 'n', 'c', 'e', 'r', 'o', 'o', 'f', 'r', 'o', 'o', 'k', 'i', 'e', 'r', 'o', 'o', 'm', + 'r', 'o', 's', 'e', 'r', 'o', 't', 'a', 't', 'e', 'r', 'o', 'u', 'g', 'h', 'r', 'o', 'u', 'n', + 'd', 'r', 'o', 'u', 't', 'e', 'r', 'o', 'y', 'a', 'l', 'r', 'u', 'b', 'b', 'e', 'r', 'r', 'u', + 'd', 'e', 'r', 'u', 'g', 'r', 'u', 'l', 'e', 'r', 'u', 'n', 'r', 'u', 'n', 'w', 'a', 'y', 'r', + 'u', 'r', 'a', 'l', 's', 'a', 'd', 's', 'a', 'd', 'd', 'l', 'e', 's', 'a', 'd', 'n', 'e', 's', + 's', 's', 'a', 'f', 'e', 's', 'a', 'i', 'l', 's', 'a', 'l', 'a', 'd', 's', 'a', 'l', 'm', 'o', + 'n', 's', 'a', 'l', 'o', 'n', 's', 'a', 'l', 't', 's', 'a', 'l', 'u', 't', 'e', 's', 'a', 'm', + 'e', 's', 'a', 'm', 'p', 'l', 'e', 's', 'a', 'n', 'd', 's', 'a', 't', 'i', 's', 'f', 'y', 's', + 'a', 't', 'o', 's', 'h', 'i', 's', 'a', 'u', 'c', 'e', 's', 'a', 'u', 's', 'a', 'g', 'e', 's', + 'a', 'v', 'e', 's', 'a', 'y', 's', 'c', 'a', 'l', 'e', 's', 'c', 'a', 'n', 's', 'c', 'a', 'r', + 'e', 's', 'c', 'a', 't', 't', 'e', 'r', 's', 'c', 'e', 'n', 'e', 's', 'c', 'h', 'e', 'm', 'e', + 's', 'c', 'h', 'o', 'o', 'l', 's', 'c', 'i', 'e', 'n', 'c', 'e', 's', 'c', 'i', 's', 's', 'o', + 'r', 's', 's', 'c', 'o', 'r', 'p', 'i', 'o', 'n', 's', 'c', 'o', 'u', 't', 's', 'c', 'r', 'a', + 'p', 's', 'c', 'r', 'e', 'e', 'n', 's', 'c', 'r', 'i', 'p', 't', 's', 'c', 'r', 'u', 'b', 's', + 'e', 'a', 's', 'e', 'a', 'r', 'c', 'h', 's', 'e', 'a', 's', 'o', 'n', 's', 'e', 'a', 't', 's', + 'e', 'c', 'o', 'n', 'd', 's', 'e', 'c', 'r', 'e', 't', 's', 'e', 'c', 't', 'i', 'o', 'n', 's', + 'e', 'c', 'u', 'r', 'i', 't', 'y', 's', 'e', 'e', 'd', 's', 'e', 'e', 'k', 's', 'e', 'g', 'm', + 'e', 'n', 't', 's', 'e', 'l', 'e', 'c', 't', 's', 'e', 'l', 'l', 's', 'e', 'm', 'i', 'n', 'a', + 'r', 's', 'e', 'n', 'i', 'o', 'r', 's', 'e', 'n', 's', 'e', 's', 'e', 'n', 't', 'e', 'n', 'c', + 'e', 's', 'e', 'r', 'i', 'e', 's', 's', 'e', 'r', 'v', 'i', 'c', 'e', 's', 'e', 's', 's', 'i', + 'o', 'n', 's', 'e', 't', 't', 'l', 'e', 's', 'e', 't', 'u', 'p', 's', 'e', 'v', 'e', 'n', 's', + 'h', 'a', 'd', 'o', 'w', 's', 'h', 'a', 'f', 't', 's', 'h', 'a', 'l', 'l', 'o', 'w', 's', 'h', + 'a', 'r', 'e', 's', 'h', 'e', 'd', 's', 'h', 'e', 'l', 'l', 's', 'h', 'e', 'r', 'i', 'f', 'f', + 's', 'h', 'i', 'e', 'l', 'd', 's', 'h', 'i', 'f', 't', 's', 'h', 'i', 'n', 'e', 's', 'h', 'i', + 'p', 's', 'h', 'i', 'v', 'e', 'r', 's', 'h', 'o', 'c', 'k', 's', 'h', 'o', 'e', 's', 'h', 'o', + 'o', 't', 's', 'h', 'o', 'p', 's', 'h', 'o', 'r', 't', 's', 'h', 'o', 'u', 'l', 'd', 'e', 'r', + 's', 'h', 'o', 'v', 'e', 's', 'h', 'r', 'i', 'm', 'p', 's', 'h', 'r', 'u', 'g', 's', 'h', 'u', + 'f', 'f', 'l', 'e', 's', 'h', 'y', 's', 'i', 'b', 'l', 'i', 'n', 'g', 's', 'i', 'c', 'k', 's', + 'i', 'd', 'e', 's', 'i', 'e', 'g', 'e', 's', 'i', 'g', 'h', 't', 's', 'i', 'g', 'n', 's', 'i', + 'l', 'e', 'n', 't', 's', 'i', 'l', 'k', 's', 'i', 'l', 'l', 'y', 's', 'i', 'l', 'v', 'e', 'r', + 's', 'i', 'm', 'i', 'l', 'a', 'r', 's', 'i', 'm', 'p', 'l', 'e', 's', 'i', 'n', 'c', 'e', 's', + 'i', 'n', 'g', 's', 'i', 'r', 'e', 'n', 's', 'i', 's', 't', 'e', 'r', 's', 'i', 't', 'u', 'a', + 't', 'e', 's', 'i', 'x', 's', 'i', 'z', 'e', 's', 'k', 'a', 't', 'e', 's', 'k', 'e', 't', 'c', + 'h', 's', 'k', 'i', 's', 'k', 'i', 'l', 'l', 's', 'k', 'i', 'n', 's', 'k', 'i', 'r', 't', 's', + 'k', 'u', 'l', 'l', 's', 'l', 'a', 'b', 's', 'l', 'a', 'm', 's', 'l', 'e', 'e', 'p', 's', 'l', + 'e', 'n', 'd', 'e', 'r', 's', 'l', 'i', 'c', 'e', 's', 'l', 'i', 'd', 'e', 's', 'l', 'i', 'g', + 'h', 't', 's', 'l', 'i', 'm', 's', 'l', 'o', 'g', 'a', 'n', 's', 'l', 'o', 't', 's', 'l', 'o', + 'w', 's', 'l', 'u', 's', 'h', 's', 'm', 'a', 'l', 'l', 's', 'm', 'a', 'r', 't', 's', 'm', 'i', + 'l', 'e', 's', 'm', 'o', 'k', 'e', 's', 'm', 'o', 'o', 't', 'h', 's', 'n', 'a', 'c', 'k', 's', + 'n', 'a', 'k', 'e', 's', 'n', 'a', 'p', 's', 'n', 'i', 'f', 'f', 's', 'n', 'o', 'w', 's', 'o', + 'a', 'p', 's', 'o', 'c', 'c', 'e', 'r', 's', 'o', 'c', 'i', 'a', 'l', 's', 'o', 'c', 'k', 's', + 'o', 'd', 'a', 's', 'o', 'f', 't', 's', 'o', 'l', 'a', 'r', 's', 'o', 'l', 'd', 'i', 'e', 'r', + 's', 'o', 'l', 'i', 'd', 's', 'o', 'l', 'u', 't', 'i', 'o', 'n', 's', 'o', 'l', 'v', 'e', 's', + 'o', 'm', 'e', 'o', 'n', 'e', 's', 'o', 'n', 'g', 's', 'o', 'o', 'n', 's', 'o', 'r', 'r', 'y', + 's', 'o', 'r', 't', 's', 'o', 'u', 'l', 's', 'o', 'u', 'n', 'd', 's', 'o', 'u', 'p', 's', 'o', + 'u', 'r', 'c', 'e', 's', 'o', 'u', 't', 'h', 's', 'p', 'a', 'c', 'e', 's', 'p', 'a', 'r', 'e', + 's', 'p', 'a', 't', 'i', 'a', 'l', 's', 'p', 'a', 'w', 'n', 's', 'p', 'e', 'a', 'k', 's', 'p', + 'e', 'c', 'i', 'a', 'l', 's', 'p', 'e', 'e', 'd', 's', 'p', 'e', 'l', 'l', 's', 'p', 'e', 'n', + 'd', 's', 'p', 'h', 'e', 'r', 'e', 's', 'p', 'i', 'c', 'e', 's', 'p', 'i', 'd', 'e', 'r', 's', + 'p', 'i', 'k', 'e', 's', 'p', 'i', 'n', 's', 'p', 'i', 'r', 'i', 't', 's', 'p', 'l', 'i', 't', + 's', 'p', 'o', 'i', 'l', 's', 'p', 'o', 'n', 's', 'o', 'r', 's', 'p', 'o', 'o', 'n', 's', 'p', + 'o', 'r', 't', 's', 'p', 'o', 't', 's', 'p', 'r', 'a', 'y', 's', 'p', 'r', 'e', 'a', 'd', 's', + 'p', 'r', 'i', 'n', 'g', 's', 'p', 'y', 's', 'q', 'u', 'a', 'r', 'e', 's', 'q', 'u', 'e', 'e', + 'z', 'e', 's', 'q', 'u', 'i', 'r', 'r', 'e', 'l', 's', 't', 'a', 'b', 'l', 'e', 's', 't', 'a', + 'd', 'i', 'u', 'm', 's', 't', 'a', 'f', 'f', 's', 't', 'a', 'g', 'e', 's', 't', 'a', 'i', 'r', + 's', 's', 't', 'a', 'm', 'p', 's', 't', 'a', 'n', 'd', 's', 't', 'a', 'r', 't', 's', 't', 'a', + 't', 'e', 's', 't', 'a', 'y', 's', 't', 'e', 'a', 'k', 's', 't', 'e', 'e', 'l', 's', 't', 'e', + 'm', 's', 't', 'e', 'p', 's', 't', 'e', 'r', 'e', 'o', 's', 't', 'i', 'c', 'k', 's', 't', 'i', + 'l', 'l', 's', 't', 'i', 'n', 'g', 's', 't', 'o', 'c', 'k', 's', 't', 'o', 'm', 'a', 'c', 'h', + 's', 't', 'o', 'n', 'e', 's', 't', 'o', 'o', 'l', 's', 't', 'o', 'r', 'y', 's', 't', 'o', 'v', + 'e', 's', 't', 'r', 'a', 't', 'e', 'g', 'y', 's', 't', 'r', 'e', 'e', 't', 's', 't', 'r', 'i', + 'k', 'e', 's', 't', 'r', 'o', 'n', 'g', 's', 't', 'r', 'u', 'g', 'g', 'l', 'e', 's', 't', 'u', + 'd', 'e', 'n', 't', 's', 't', 'u', 'f', 'f', 's', 't', 'u', 'm', 'b', 'l', 'e', 's', 't', 'y', + 'l', 'e', 's', 'u', 'b', 'j', 'e', 'c', 't', 's', 'u', 'b', 'm', 'i', 't', 's', 'u', 'b', 'w', + 'a', 'y', 's', 'u', 'c', 'c', 'e', 's', 's', 's', 'u', 'c', 'h', 's', 'u', 'd', 'd', 'e', 'n', + 's', 'u', 'f', 'f', 'e', 'r', 's', 'u', 'g', 'a', 'r', 's', 'u', 'g', 'g', 'e', 's', 't', 's', + 'u', 'i', 't', 's', 'u', 'm', 'm', 'e', 'r', 's', 'u', 'n', 's', 'u', 'n', 'n', 'y', 's', 'u', + 'n', 's', 'e', 't', 's', 'u', 'p', 'e', 'r', 's', 'u', 'p', 'p', 'l', 'y', 's', 'u', 'p', 'r', + 'e', 'm', 'e', 's', 'u', 'r', 'e', 's', 'u', 'r', 'f', 'a', 'c', 'e', 's', 'u', 'r', 'g', 'e', + 's', 'u', 'r', 'p', 'r', 'i', 's', 'e', 's', 'u', 'r', 'r', 'o', 'u', 'n', 'd', 's', 'u', 'r', + 'v', 'e', 'y', 's', 'u', 's', 'p', 'e', 'c', 't', 's', 'u', 's', 't', 'a', 'i', 'n', 's', 'w', + 'a', 'l', 'l', 'o', 'w', 's', 'w', 'a', 'm', 'p', 's', 'w', 'a', 'p', 's', 'w', 'a', 'r', 'm', + 's', 'w', 'e', 'a', 'r', 's', 'w', 'e', 'e', 't', 's', 'w', 'i', 'f', 't', 's', 'w', 'i', 'm', + 's', 'w', 'i', 'n', 'g', 's', 'w', 'i', 't', 'c', 'h', 's', 'w', 'o', 'r', 'd', 's', 'y', 'm', + 'b', 'o', 'l', 's', 'y', 'm', 'p', 't', 'o', 'm', 's', 'y', 'r', 'u', 'p', 's', 'y', 's', 't', + 'e', 'm', 't', 'a', 'b', 'l', 'e', 't', 'a', 'c', 'k', 'l', 'e', 't', 'a', 'g', 't', 'a', 'i', + 'l', 't', 'a', 'l', 'e', 'n', 't', 't', 'a', 'l', 'k', 't', 'a', 'n', 'k', 't', 'a', 'p', 'e', + 't', 'a', 'r', 'g', 'e', 't', 't', 'a', 's', 'k', 't', 'a', 's', 't', 'e', 't', 'a', 't', 't', + 'o', 'o', 't', 'a', 'x', 'i', 't', 'e', 'a', 'c', 'h', 't', 'e', 'a', 'm', 't', 'e', 'l', 'l', + 't', 'e', 'n', 't', 'e', 'n', 'a', 'n', 't', 't', 'e', 'n', 'n', 'i', 's', 't', 'e', 'n', 't', + 't', 'e', 'r', 'm', 't', 'e', 's', 't', 't', 'e', 'x', 't', 't', 'h', 'a', 'n', 'k', 't', 'h', + 'a', 't', 't', 'h', 'e', 'm', 'e', 't', 'h', 'e', 'n', 't', 'h', 'e', 'o', 'r', 'y', 't', 'h', + 'e', 'r', 'e', 't', 'h', 'e', 'y', 't', 'h', 'i', 'n', 'g', 't', 'h', 'i', 's', 't', 'h', 'o', + 'u', 'g', 'h', 't', 't', 'h', 'r', 'e', 'e', 't', 'h', 'r', 'i', 'v', 'e', 't', 'h', 'r', 'o', + 'w', 't', 'h', 'u', 'm', 'b', 't', 'h', 'u', 'n', 'd', 'e', 'r', 't', 'i', 'c', 'k', 'e', 't', + 't', 'i', 'd', 'e', 't', 'i', 'g', 'e', 'r', 't', 'i', 'l', 't', 't', 'i', 'm', 'b', 'e', 'r', + 't', 'i', 'm', 'e', 't', 'i', 'n', 'y', 't', 'i', 'p', 't', 'i', 'r', 'e', 'd', 't', 'i', 's', + 's', 'u', 'e', 't', 'i', 't', 'l', 'e', 't', 'o', 'a', 's', 't', 't', 'o', 'b', 'a', 'c', 'c', + 'o', 't', 'o', 'd', 'a', 'y', 't', 'o', 'd', 'd', 'l', 'e', 'r', 't', 'o', 'e', 't', 'o', 'g', + 'e', 't', 'h', 'e', 'r', 't', 'o', 'i', 'l', 'e', 't', 't', 'o', 'k', 'e', 'n', 't', 'o', 'm', + 'a', 't', 'o', 't', 'o', 'm', 'o', 'r', 'r', 'o', 'w', 't', 'o', 'n', 'e', 't', 'o', 'n', 'g', + 'u', 'e', 't', 'o', 'n', 'i', 'g', 'h', 't', 't', 'o', 'o', 'l', 't', 'o', 'o', 't', 'h', 't', + 'o', 'p', 't', 'o', 'p', 'i', 'c', 't', 'o', 'p', 'p', 'l', 'e', 't', 'o', 'r', 'c', 'h', 't', + 'o', 'r', 'n', 'a', 'd', 'o', 't', 'o', 'r', 't', 'o', 'i', 's', 'e', 't', 'o', 's', 's', 't', + 'o', 't', 'a', 'l', 't', 'o', 'u', 'r', 'i', 's', 't', 't', 'o', 'w', 'a', 'r', 'd', 't', 'o', + 'w', 'e', 'r', 't', 'o', 'w', 'n', 't', 'o', 'y', 't', 'r', 'a', 'c', 'k', 't', 'r', 'a', 'd', + 'e', 't', 'r', 'a', 'f', 'f', 'i', 'c', 't', 'r', 'a', 'g', 'i', 'c', 't', 'r', 'a', 'i', 'n', + 't', 'r', 'a', 'n', 's', 'f', 'e', 'r', 't', 'r', 'a', 'p', 't', 'r', 'a', 's', 'h', 't', 'r', + 'a', 'v', 'e', 'l', 't', 'r', 'a', 'y', 't', 'r', 'e', 'a', 't', 't', 'r', 'e', 'e', 't', 'r', + 'e', 'n', 'd', 't', 'r', 'i', 'a', 'l', 't', 'r', 'i', 'b', 'e', 't', 'r', 'i', 'c', 'k', 't', + 'r', 'i', 'g', 'g', 'e', 'r', 't', 'r', 'i', 'm', 't', 'r', 'i', 'p', 't', 'r', 'o', 'p', 'h', + 'y', 't', 'r', 'o', 'u', 'b', 'l', 'e', 't', 'r', 'u', 'c', 'k', 't', 'r', 'u', 'e', 't', 'r', + 'u', 'l', 'y', 't', 'r', 'u', 'm', 'p', 'e', 't', 't', 'r', 'u', 's', 't', 't', 'r', 'u', 't', + 'h', 't', 'r', 'y', 't', 'u', 'b', 'e', 't', 'u', 'i', 't', 'i', 'o', 'n', 't', 'u', 'm', 'b', + 'l', 'e', 't', 'u', 'n', 'a', 't', 'u', 'n', 'n', 'e', 'l', 't', 'u', 'r', 'k', 'e', 'y', 't', + 'u', 'r', 'n', 't', 'u', 'r', 't', 'l', 'e', 't', 'w', 'e', 'l', 'v', 'e', 't', 'w', 'e', 'n', + 't', 'y', 't', 'w', 'i', 'c', 'e', 't', 'w', 'i', 'n', 't', 'w', 'i', 's', 't', 't', 'w', 'o', + 't', 'y', 'p', 'e', 't', 'y', 'p', 'i', 'c', 'a', 'l', 'u', 'g', 'l', 'y', 'u', 'm', 'b', 'r', + 'e', 'l', 'l', 'a', 'u', 'n', 'a', 'b', 'l', 'e', 'u', 'n', 'a', 'w', 'a', 'r', 'e', 'u', 'n', + 'c', 'l', 'e', 'u', 'n', 'c', 'o', 'v', 'e', 'r', 'u', 'n', 'd', 'e', 'r', 'u', 'n', 'd', 'o', + 'u', 'n', 'f', 'a', 'i', 'r', 'u', 'n', 'f', 'o', 'l', 'd', 'u', 'n', 'h', 'a', 'p', 'p', 'y', + 'u', 'n', 'i', 'f', 'o', 'r', 'm', 'u', 'n', 'i', 'q', 'u', 'e', 'u', 'n', 'i', 't', 'u', 'n', + 'i', 'v', 'e', 'r', 's', 'e', 'u', 'n', 'k', 'n', 'o', 'w', 'n', 'u', 'n', 'l', 'o', 'c', 'k', + 'u', 'n', 't', 'i', 'l', 'u', 'n', 'u', 's', 'u', 'a', 'l', 'u', 'n', 'v', 'e', 'i', 'l', 'u', + 'p', 'd', 'a', 't', 'e', 'u', 'p', 'g', 'r', 'a', 'd', 'e', 'u', 'p', 'h', 'o', 'l', 'd', 'u', + 'p', 'o', 'n', 'u', 'p', 'p', 'e', 'r', 'u', 'p', 's', 'e', 't', 'u', 'r', 'b', 'a', 'n', 'u', + 'r', 'g', 'e', 'u', 's', 'a', 'g', 'e', 'u', 's', 'e', 'u', 's', 'e', 'd', 'u', 's', 'e', 'f', + 'u', 'l', 'u', 's', 'e', 'l', 'e', 's', 's', 'u', 's', 'u', 'a', 'l', 'u', 't', 'i', 'l', 'i', + 't', 'y', 'v', 'a', 'c', 'a', 'n', 't', 'v', 'a', 'c', 'u', 'u', 'm', 'v', 'a', 'g', 'u', 'e', + 'v', 'a', 'l', 'i', 'd', 'v', 'a', 'l', 'l', 'e', 'y', 'v', 'a', 'l', 'v', 'e', 'v', 'a', 'n', + 'v', 'a', 'n', 'i', 's', 'h', 'v', 'a', 'p', 'o', 'r', 'v', 'a', 'r', 'i', 'o', 'u', 's', 'v', + 'a', 's', 't', 'v', 'a', 'u', 'l', 't', 'v', 'e', 'h', 'i', 'c', 'l', 'e', 'v', 'e', 'l', 'v', + 'e', 't', 'v', 'e', 'n', 'd', 'o', 'r', 'v', 'e', 'n', 't', 'u', 'r', 'e', 'v', 'e', 'n', 'u', + 'e', 'v', 'e', 'r', 'b', 'v', 'e', 'r', 'i', 'f', 'y', 'v', 'e', 'r', 's', 'i', 'o', 'n', 'v', + 'e', 'r', 'y', 'v', 'e', 's', 's', 'e', 'l', 'v', 'e', 't', 'e', 'r', 'a', 'n', 'v', 'i', 'a', + 'b', 'l', 'e', 'v', 'i', 'b', 'r', 'a', 'n', 't', 'v', 'i', 'c', 'i', 'o', 'u', 's', 'v', 'i', + 'c', 't', 'o', 'r', 'y', 'v', 'i', 'd', 'e', 'o', 'v', 'i', 'e', 'w', 'v', 'i', 'l', 'l', 'a', + 'g', 'e', 'v', 'i', 'n', 't', 'a', 'g', 'e', 'v', 'i', 'o', 'l', 'i', 'n', 'v', 'i', 'r', 't', + 'u', 'a', 'l', 'v', 'i', 'r', 'u', 's', 'v', 'i', 's', 'a', 'v', 'i', 's', 'i', 't', 'v', 'i', + 's', 'u', 'a', 'l', 'v', 'i', 't', 'a', 'l', 'v', 'i', 'v', 'i', 'd', 'v', 'o', 'c', 'a', 'l', + 'v', 'o', 'i', 'c', 'e', 'v', 'o', 'i', 'd', 'v', 'o', 'l', 'c', 'a', 'n', 'o', 'v', 'o', 'l', + 'u', 'm', 'e', 'v', 'o', 't', 'e', 'v', 'o', 'y', 'a', 'g', 'e', 'w', 'a', 'g', 'e', 'w', 'a', + 'g', 'o', 'n', 'w', 'a', 'i', 't', 'w', 'a', 'l', 'k', 'w', 'a', 'l', 'l', 'w', 'a', 'l', 'n', + 'u', 't', 'w', 'a', 'n', 't', 'w', 'a', 'r', 'f', 'a', 'r', 'e', 'w', 'a', 'r', 'm', 'w', 'a', + 'r', 'r', 'i', 'o', 'r', 'w', 'a', 's', 'h', 'w', 'a', 's', 'p', 'w', 'a', 's', 't', 'e', 'w', + 'a', 't', 'e', 'r', 'w', 'a', 'v', 'e', 'w', 'a', 'y', 'w', 'e', 'a', 'l', 't', 'h', 'w', 'e', + 'a', 'p', 'o', 'n', 'w', 'e', 'a', 'r', 'w', 'e', 'a', 's', 'e', 'l', 'w', 'e', 'a', 't', 'h', + 'e', 'r', 'w', 'e', 'b', 'w', 'e', 'd', 'd', 'i', 'n', 'g', 'w', 'e', 'e', 'k', 'e', 'n', 'd', + 'w', 'e', 'i', 'r', 'd', 'w', 'e', 'l', 'c', 'o', 'm', 'e', 'w', 'e', 's', 't', 'w', 'e', 't', + 'w', 'h', 'a', 'l', 'e', 'w', 'h', 'a', 't', 'w', 'h', 'e', 'a', 't', 'w', 'h', 'e', 'e', 'l', + 'w', 'h', 'e', 'n', 'w', 'h', 'e', 'r', 'e', 'w', 'h', 'i', 'p', 'w', 'h', 'i', 's', 'p', 'e', + 'r', 'w', 'i', 'd', 'e', 'w', 'i', 'd', 't', 'h', 'w', 'i', 'f', 'e', 'w', 'i', 'l', 'd', 'w', + 'i', 'l', 'l', 'w', 'i', 'n', 'w', 'i', 'n', 'd', 'o', 'w', 'w', 'i', 'n', 'e', 'w', 'i', 'n', + 'g', 'w', 'i', 'n', 'k', 'w', 'i', 'n', 'n', 'e', 'r', 'w', 'i', 'n', 't', 'e', 'r', 'w', 'i', + 'r', 'e', 'w', 'i', 's', 'd', 'o', 'm', 'w', 'i', 's', 'e', 'w', 'i', 's', 'h', 'w', 'i', 't', + 'n', 'e', 's', 's', 'w', 'o', 'l', 'f', 'w', 'o', 'm', 'a', 'n', 'w', 'o', 'n', 'd', 'e', 'r', + 'w', 'o', 'o', 'd', 'w', 'o', 'o', 'l', 'w', 'o', 'r', 'd', 'w', 'o', 'r', 'k', 'w', 'o', 'r', + 'l', 'd', 'w', 'o', 'r', 'r', 'y', 'w', 'o', 'r', 't', 'h', 'w', 'r', 'a', 'p', 'w', 'r', 'e', + 'c', 'k', 'w', 'r', 'e', 's', 't', 'l', 'e', 'w', 'r', 'i', 's', 't', 'w', 'r', 'i', 't', 'e', + 'w', 'r', 'o', 'n', 'g', 'y', 'a', 'r', 'd', 'y', 'e', 'a', 'r', 'y', 'e', 'l', 'l', 'o', 'w', + 'y', 'o', 'u', 'y', 'o', 'u', 'n', 'g', 'y', 'o', 'u', 't', 'h', 'z', 'e', 'b', 'r', 'a', 'z', + 'e', 'r', 'o', 'z', 'o', 'n', 'e', 'z', 'o', 'o'}; -unsigned short const BIP39_WORDLIST_OFFSETS[] = {0, -7, -14, -18, -23, -28, -34, -40, -48, -54, -59, -65, -73, -80, -86, -93, -97, -105, -112, -118, -121, -127, -132, -139, -145, -150, -153, -159, -166, -172, -177, -182, -189, -195, -202, -208, -214, -220, -225, -228, -233, -238, -243, -246, -249, -256, -261, -266, -271, -278, -283, -288, -291, -296, -301, -307, -312, -317, -324, -328, -333, -339, -346, -353, -358, -364, -370, -377, -383, -390, -395, -400, -405, -411, -416, -424, -430, -437, -443, -450, -457, -464, -467, -472, -479, -485, -490, -497, -502, -506, -512, -516, -521, -526, -529, -534, -539, -543, -549, -556, -562, -568, -573, -576, -584, -590, -597, -600, -606, -613, -618, -624, -630, -636, -643, -647, -653, -659, -667, -674, -681, -686, -692, -696, -702, -706, -712, -719, -726, -731, -736, -741, -745, -752, -757, -764, -768, -772, -780, -785, -790, -793, -800, -807, -811, -817, -823, -829, -832, -838, -845, -851, -855, -860, -866, -872, -877, -881, -887, -894, -900, -904, -910, -915, -921, -927, -934, -939, -943, -948, -955, -959, -965, -971, -978, -984, -991, -994, -998, -1002, -1009, -1013, -1018, -1024, -1029, -1034, -1039, -1046, -1051, -1056, -1061, -1066, -1071, -1078, -1084, -1088, -1092, -1097, -1102, -1106, -1110, -1114, -1118, -1122, -1127, -1131, -1136, -1142, -1148, -1154, -1158, -1164, -1170, -1173, -1176, -1183, -1188, -1193, -1198, -1203, -1208, -1214, -1219, -1225, -1230, -1236, -1241, -1246, -1254, -1260, -1266, -1271, -1278, -1283, -1288, -1294, -1299, -1305, -1312, -1317, -1321, -1325, -1331, -1337, -1343, -1349, -1355, -1360, -1363, -1371, -1375, -1381, -1386, -1390, -1397, -1402, -1407, -1413, -1417, -1421, -1425, -1429, -1435, -1439, -1442, -1447, -1453, -1458, -1464, -1469, -1475, -1481, -1488, -1495, -1502, -1505, -1511, -1515, -1520, -1526, -1531, -1535, -1539, -1543, -1549, -1555, -1561, -1564, -1571, -1576, -1584, -1590, -1596, -1601, -1608, -1612, -1619, -1625, -1631, -1637, -1644, -1650, -1657, -1662, -1667, -1675, -1681, -1686, -1693, -1699, -1704, -1708, -1713, -1718, -1724, -1728, -1734, -1739, -1746, -1751, -1756, -1763, -1769, -1775, -1782, -1789, -1794, -1799, -1804, -1812, -1818, -1825, -1829, -1834, -1839, -1843, -1850, -1854, -1858, -1863, -1868, -1874, -1879, -1885, -1890, -1895, -1901, -1905, -1910, -1914, -1919, -1924, -1929, -1934, -1938, -1943, -1950, -1956, -1961, -1966, -1973, -1977, -1983, -1987, -1991, -1998, -2003, -2009, -2016, -2020, -2027, -2032, -2038, -2045, -2052, -2059, -2066, -2074, -2081, -2089, -2096, -2104, -2108, -2112, -2118, -2122, -2127, -2131, -2135, -2142, -2146, -2152, -2157, -2164, -2170, -2176, -2182, -2187, -2193, -2198, -2204, -2209, -2213, -2218, -2223, -2229, -2234, -2239, -2244, -2250, -2255, -2259, -2266, -2271, -2276, -2282, -2286, -2291, -2297, -2302, -2309, -2314, -2320, -2327, -2333, -2338, -2341, -2348, -2352, -2359, -2362, -2370, -2377, -2384, -2391, -2396, -2403, -2409, -2413, -2418, -2421, -2427, -2431, -2436, -2442, -2448, -2452, -2460, -2464, -2467, -2471, -2477, -2483, -2489, -2497, -2503, -2510, -2518, -2526, -2530, -2537, -2543, -2547, -2553, -2558, -2565, -2571, -2577, -2583, -2590, -2594, -2600, -2606, -2613, -2618, -2624, -2630, -2638, -2644, -2650, -2654, -2661, -2668, -2674, -2680, -2687, -2693, -2699, -2706, -2710, -2717, -2722, -2726, -2732, -2736, -2742, -2749, -2756, -2763, -2769, -2777, -2783, -2787, -2795, -2803, -2810, -2814, -2821, -2829, -2836, -2844, -2850, -2856, -2863, -2868, -2874, -2882, -2885, -2889, -2896, -2902, -2908, -2914, -2919, -2923, -2927, -2933, -2937, -2942, -2948, -2953, -2960, -2964, -2969, -2974, -2979, -2984, -2989, -2993, -2998, -3002, -3006, -3009, -3013, -3017, -3021, -3027, -3031, -3036, -3040, -3045, -3052, -3057, -3062, -3067, -3071, -3076, -3082, -3086, -3090, -3094, -3101, -3108, -3112, -3116, -3123, -3129, -3132, -3137, -3143, -3148, -3153, -3161, -3168, -3175, -3183, -3191, -3196, -3200, -3206, -3212, -3219, -3225, -3232, -3238, -3245, -3250, -3256, -3261, -3264, -3271, -3278, -3283, -3289, -3296, -3302, -3308, -3315, -3320, -3326, -3332, -3338, -3344, -3350, -3355, -3361, -3366, -3374, -3381, -3386, -3391, -3394, -3399, -3404, -3411, -3416, -3421, -3427, -3432, -3439, -3445, -3452, -3458, -3466, -3470, -3475, -3481, -3486, -3493, -3499, -3507, -3513, -3520, -3526, -3533, -3541, -3548, -3555, -3560, -3565, -3569, -3575, -3581, -3587, -3593, -3600, -3606, -3613, -3619, -3624, -3627, -3634, -3640, -3644, -3651, -3655, -3660, -3665, -3669, -3674, -3678, -3684, -3690, -3693, -3698, -3705, -3709, -3716, -3719, -3724, -3730, -3737, -3742, -3750, -3757, -3765, -3772, -3775, -3779, -3783, -3789, -3794, -3802, -3807, -3812, -3815, -3820, -3827, -3832, -3838, -3842, -3846, -3852, -3857, -3861, -3865, -3871, -3877, -3881, -3885, -3890, -3896, -3900, -3903, -3910, -3913, -3917, -3922, -3927, -3931, -3937, -3941, -3947, -3951, -3956, -3961, -3966, -3972, -3977, -3982, -3985, -3989, -3994, -3997, -4001, -4005, -4011, -4015, -4019, -4024, -4030, -4036, -4040, -4047, -4052, -4059, -4065, -4071, -4076, -4079, -4086, -4091, -4099, -4104, -4110, -4116, -4120, -4125, -4130, -4135, -4141, -4146, -4150, -4153, -4158, -4165, -4169, -4175, -4181, -4185, -4191, -4198, -4202, -4205, -4211, -4218, -4224, -4230, -4237, -4240, -4244, -4248, -4254, -4259, -4263, -4270, -4276, -4281, -4287, -4294, -4301, -4306, -4311, -4315, -4321, -4327, -4334, -4338, -4342, -4346, -4352, -4357, -4362, -4367, -4374, -4379, -4384, -4389, -4394, -4398, -4402, -4406, -4413, -4417, -4421, -4426, -4433, -4439, -4445, -4451, -4455, -4459, -4464, -4469, -4474, -4479, -4484, -4491, -4496, -4501, -4505, -4510, -4514, -4521, -4526, -4530, -4535, -4540, -4545, -4550, -4555, -4561, -4564, -4567, -4572, -4576, -4580, -4586, -4593, -4597, -4602, -4608, -4612, -4617, -4624, -4627, -4631, -4635, -4641, -4645, -4651, -4656, -4661, -4669, -4675, -4680, -4686, -4690, -4693, -4697, -4703, -4707, -4711, -4715, -4718, -4722, -4729, -4734, -4740, -4744, -4748, -4755, -4761, -4765, -4770, -4774, -4778, -4782, -4788, -4793, -4801, -4805, -4810, -4814, -4819, -4822, -4826, -4831, -4837, -4842, -4849, -4855, -4859, -4865, -4870, -4874, -4881, -4887, -4890, -4894, -4898, -4906, -4910, -4916, -4919, -4926, -4933, -4938, -4945, -4952, -4958, -4964, -4970, -4977, -4984, -4988, -4995, -5001, -5009, -5014, -5022, -5028, -5036, -5042, -5049, -5055, -5061, -5068, -5075, -5081, -5087, -5093, -5098, -5106, -5111, -5118, -5124, -5130, -5136, -5143, -5150, -5156, -5164, -5168, -5174, -5180, -5187, -5191, -5197, -5204, -5209, -5213, -5218, -5224, -5230, -5233, -5237, -5244, -5249, -5254, -5259, -5262, -5266, -5270, -5277, -5280, -5285, -5290, -5294, -5300, -5306, -5310, -5314, -5322, -5326, -5330, -5337, -5340, -5344, -5347, -5353, -5357, -5364, -5368, -5371, -5378, -5382, -5388, -5392, -5396, -5401, -5406, -5410, -5413, -5418, -5423, -5429, -5433, -5437, -5441, -5449, -5455, -5460, -5465, -5470, -5475, -5482, -5486, -5489, -5493, -5500, -5505, -5509, -5515, -5519, -5524, -5529, -5536, -5540, -5543, -5548, -5554, -5561, -5566, -5570, -5576, -5580, -5587, -5593, -5599, -5604, -5608, -5615, -5622, -5629, -5633, -5637, -5642, -5646, -5650, -5655, -5659, -5663, -5669, -5673, -5679, -5683, -5689, -5693, -5697, -5704, -5709, -5713, -5718, -5724, -5728, -5732, -5739, -5743, -5749, -5753, -5758, -5763, -5770, -5776, -5781, -5786, -5792, -5798, -5805, -5808, -5813, -5819, -5823, -5827, -5831, -5836, -5840, -5846, -5849, -5855, -5862, -5867, -5874, -5880, -5885, -5891, -5896, -5902, -5908, -5914, -5922, -5926, -5930, -5936, -5941, -5949, -5953, -5959, -5965, -5972, -5976, -5982, -5986, -5993, -5997, -6005, -6010, -6015, -6021, -6025, -6031, -6037, -6044, -6048, -6053, -6058, -6063, -6068, -6072, -6079, -6084, -6090, -6096, -6104, -6108, -6115, -6120, -6124, -6131, -6136, -6142, -6149, -6155, -6161, -6165, -6172, -6175, -6180, -6187, -6193, -6198, -6204, -6207, -6213, -6220, -6226, -6233, -6238, -6242, -6247, -6251, -6258, -6266, -6272, -6278, -6283, -6291, -6296, -6300, -6305, -6309, -6315, -6319, -6327, -6333, -6339, -6347, -6352, -6356, -6362, -6368, -6375, -6379, -6384, -6388, -6394, -6400, -6405, -6411, -6417, -6421, -6425, -6429, -6437, -6444, -6451, -6457, -6462, -6466, -6469, -6476, -6483, -6488, -6492, -6496, -6500, -6505, -6510, -6515, -6522, -6528, -6534, -6539, -6543, -6550, -6554, -6561, -6567, -6572, -6575, -6582, -6588, -6593, -6596, -6599, -6603, -6609, -6615, -6622, -6629, -6635, -6642, -6647, -6652, -6659, -6663, -6666, -6671, -6677, -6682, -6685, -6689, -6692, -6697, -6704, -6708, -6712, -6715, -6720, -6726, -6730, -6734, -6739, -6746, -6752, -6758, -6764, -6769, -6776, -6781, -6789, -6794, -6800, -6808, -6814, -6821, -6826, -6833, -6838, -6844, -6851, -6855, -6859, -6863, -6866, -6871, -6877, -6883, -6888, -6892, -6898, -6902, -6906, -6912, -6916, -6921, -6926, -6931, -6938, -6943, -6949, -6955, -6959, -6965, -6970, -6974, -6979, -6983, -6990, -6996, -7003, -7008, -7012, -7019, -7024, -7030, -7034, -7041, -7048, -7051, -7058, -7064, -7070, -7076, -7083, -7089, -7095, -7098, -7103, -7108, -7114, -7122, -7127, -7133, -7140, -7145, -7148, -7154, -7158, -7163, -7167, -7174, -7178, -7184, -7189, -7194, -7199, -7205, -7212, -7217, -7221, -7227, -7233, -7238, -7242, -7248, -7252, -7256, -7261, -7266, -7270, -7276, -7280, -7284, -7288, -7295, -7302, -7310, -7318, -7322, -7328, -7335, -7342, -7348, -7353, -7361, -7367, -7374, -7380, -7387, -7394, -7400, -7407, -7412, -7417, -7424, -7429, -7437, -7443, -7450, -7455, -7462, -7469, -7476, -7482, -7489, -7496, -7503, -7508, -7516, -7523, -7530, -7535, -7542, -7548, -7555, -7559, -7563, -7568, -7575, -7580, -7585, -7590, -7598, -7604, -7611, -7616, -7620, -7623, -7629, -7636, -7643, -7650, -7657, -7665, -7670, -7674, -7678, -7683, -7689, -7696, -7700, -7704, -7709, -7714, -7718, -7722, -7727, -7732, -7736, -7741, -7747, -7752, -7757, -7761, -7765, -7771, -7776, -7779, -7784, -7789, -7793, -7799, -7804, -7811, -7817, -7824, -7830, -7836, -7843, -7849, -7856, -7862, -7868, -7874, -7880, -7887, -7893, -7898, -7905, -7911, -7915, -7921, -7929, -7935, -7941, -7947, -7952, -7956, -7962, -7968, -7974, -7981, -7987, -7994, -8000, -8008, -8014, -8022, -8030, -8036, -8042, -8049, -8055, -8062, -8068, -8074, -8080, -8086, -8089, -8095, -8099, -8103, -8107, -8112, -8117, -8122, -8127, -8131, -8135, -8141, -8145, -8151, -8156, -8161, -8165, -8170, -8175, -8181, -8187, -8194, -8198, -8204, -8208, -8212, -8218, -8223, -8228, -8233, -8238, -8244, -8248, -8251, -8255, -8258, -8264, -8269, -8272, -8278, -8285, -8289, -8293, -8298, -8304, -8309, -8313, -8319, -8323, -8329, -8333, -8340, -8347, -8352, -8359, -8363, -8366, -8371, -8375, -8380, -8387, -8392, -8398, -8404, -8411, -8419, -8427, -8432, -8437, -8443, -8449, -8454, -8457, -8463, -8469, -8473, -8479, -8485, -8492, -8500, -8504, -8508, -8515, -8521, -8525, -8532, -8538, -8543, -8551, -8557, -8564, -8571, -8577, -8582, -8587, -8593, -8598, -8605, -8610, -8614, -8619, -8626, -8632, -8637, -8642, -8646, -8652, -8657, -8661, -8666, -8670, -8675, -8683, -8688, -8694, -8699, -8706, -8709, -8716, -8720, -8724, -8729, -8734, -8738, -8744, -8748, -8753, -8759, -8766, -8772, -8777, -8781, -8786, -8792, -8799, -8802, -8806, -8811, -8817, -8820, -8825, -8829, -8834, -8839, -8843, -8847, -8852, -8859, -8864, -8869, -8875, -8879, -8885, -8889, -8893, -8898, -8903, -8908, -8913, -8918, -8924, -8929, -8934, -8938, -8943, -8947, -8951, -8957, -8963, -8967, -8971, -8975, -8980, -8987, -8992, -9000, -9005, -9012, -9016, -9020, -9025, -9029, -9033, -9038, -9042, -9048, -9053, -9058, -9063, -9070, -9075, -9080, -9087, -9092, -9097, -9102, -9108, -9113, -9119, -9124, -9128, -9134, -9139, -9144, -9151, -9156, -9161, -9165, -9170, -9176, -9182, -9185, -9191, -9198, -9206, -9212, -9219, -9224, -9229, -9235, -9240, -9245, -9250, -9255, -9259, -9264, -9269, -9273, -9277, -9283, -9288, -9293, -9298, -9303, -9310, -9315, -9320, -9325, -9330, -9338, -9344, -9350, -9356, -9364, -9371, -9376, -9383, -9388, -9395, -9401, -9407, -9414, -9418, -9424, -9430, -9435, -9442, -9446, -9452, -9455, -9460, -9466, -9471, -9477, -9484, -9488, -9495, -9500, -9508, -9516, -9522, -9529, -9536, -9543, -9548, -9552, -9557, -9562, -9567, -9572, -9576, -9581, -9587, -9592, -9598, -9605, -9610, -9616, -9621, -9627, -9630, -9634, -9640, -9644, -9648, -9652, -9658, -9662, -9667, -9673, -9677, -9682, -9686, -9690, -9693, -9699, -9705, -9709, -9713, -9717, -9721, -9726, -9730, -9735, -9739, -9745, -9750, -9754, -9759, -9763, -9770, -9775, -9781, -9786, -9791, -9798, -9804, -9808, -9813, -9817, -9823, -9827, -9831, -9834, -9839, -9845, -9850, -9855, -9862, -9867, -9874, -9877, -9885, -9891, -9896, -9902, -9910, -9914, -9920, -9927, -9931, -9936, -9939, -9944, -9950, -9955, -9962, -9970, -9974, -9979, -9986, -9992, -9997, -10001, -10004, -10009, -10014, -10021, -10027, -10032, -10040, -10044, -10049, -10055, -10059, -10064, -10068, -10073, -10078, -10083, -10088, -10095, -10099, -10103, -10109, -10116, -10121, -10125, -10130, -10137, -10142, -10147, -10150, -10154, -10161, -10167, -10171, -10177, -10183, -10187, -10193, -10199, -10205, -10210, -10214, -10219, -10222, -10226, -10233, -10237, -10245, -10251, -10258, -10263, -10270, -10275, -10279, -10285, -10291, -10298, -10305, -10311, -10315, -10323, -10330, -10336, -10341, -10348, -10354, -10360, -10367, -10373, -10377, -10382, -10387, -10392, -10396, -10401, -10404, -10408, -10414, -10421, -10426, -10433, -10439, -10445, -10450, -10455, -10461, -10466, -10469, -10475, -10480, -10487, -10491, -10496, -10503, -10509, -10515, -10522, -10527, -10531, -10537, -10544, -10548, -10554, -10561, -10567, -10574, -10581, -10588, -10593, -10597, -10604, -10611, -10617, -10624, -10629, -10633, -10638, -10644, -10649, -10654, -10659, -10664, -10668, -10675, -10681, -10685, -10691, -10695, -10700, -10704, -10708, -10712, -10718, -10722, -10729, -10733, -10740, -10744, -10748, -10753, -10758, -10762, -10765, -10771, -10777, -10781, -10787, -10794, -10797, -10804, -10811, -10816, -10823, -10827, -10830, -10835, -10839, -10844, -10849, -10853, -10858, -10862, -10869, -10873, -10878, -10882, -10886, -10890, -10893, -10899, -10903, -10907, -10911, -10917, -10923, -10927, -10933, -10937, -10941, -10948, -10952, -10957, -10963, -10967, -10971, -10975, -10979, -10984, -10989, -10994, -10998, -11003, -11010, -11015, -11020, -11025, -11029, -11033, -11039, -11042, -11047, -11052, -11057, -11061, -11065, -11068 -}; +unsigned short const BIP39_WORDLIST_OFFSETS[] = { + 0, 7, 14, 18, 23, 28, 34, 40, 48, 54, 59, 65, 73, + 80, 86, 93, 97, 105, 112, 118, 121, 127, 132, 139, 145, 150, + 153, 159, 166, 172, 177, 182, 189, 195, 202, 208, 214, 220, 225, + 228, 233, 238, 243, 246, 249, 256, 261, 266, 271, 278, 283, 288, + 291, 296, 301, 307, 312, 317, 324, 328, 333, 339, 346, 353, 358, + 364, 370, 377, 383, 390, 395, 400, 405, 411, 416, 424, 430, 437, + 443, 450, 457, 464, 467, 472, 479, 485, 490, 497, 502, 506, 512, + 516, 521, 526, 529, 534, 539, 543, 549, 556, 562, 568, 573, 576, + 584, 590, 597, 600, 606, 613, 618, 624, 630, 636, 643, 647, 653, + 659, 667, 674, 681, 686, 692, 696, 702, 706, 712, 719, 726, 731, + 736, 741, 745, 752, 757, 764, 768, 772, 780, 785, 790, 793, 800, + 807, 811, 817, 823, 829, 832, 838, 845, 851, 855, 860, 866, 872, + 877, 881, 887, 894, 900, 904, 910, 915, 921, 927, 934, 939, 943, + 948, 955, 959, 965, 971, 978, 984, 991, 994, 998, 1002, 1009, 1013, + 1018, 1024, 1029, 1034, 1039, 1046, 1051, 1056, 1061, 1066, 1071, 1078, 1084, + 1088, 1092, 1097, 1102, 1106, 1110, 1114, 1118, 1122, 1127, 1131, 1136, 1142, + 1148, 1154, 1158, 1164, 1170, 1173, 1176, 1183, 1188, 1193, 1198, 1203, 1208, + 1214, 1219, 1225, 1230, 1236, 1241, 1246, 1254, 1260, 1266, 1271, 1278, 1283, + 1288, 1294, 1299, 1305, 1312, 1317, 1321, 1325, 1331, 1337, 1343, 1349, 1355, + 1360, 1363, 1371, 1375, 1381, 1386, 1390, 1397, 1402, 1407, 1413, 1417, 1421, + 1425, 1429, 1435, 1439, 1442, 1447, 1453, 1458, 1464, 1469, 1475, 1481, 1488, + 1495, 1502, 1505, 1511, 1515, 1520, 1526, 1531, 1535, 1539, 1543, 1549, 1555, + 1561, 1564, 1571, 1576, 1584, 1590, 1596, 1601, 1608, 1612, 1619, 1625, 1631, + 1637, 1644, 1650, 1657, 1662, 1667, 1675, 1681, 1686, 1693, 1699, 1704, 1708, + 1713, 1718, 1724, 1728, 1734, 1739, 1746, 1751, 1756, 1763, 1769, 1775, 1782, + 1789, 1794, 1799, 1804, 1812, 1818, 1825, 1829, 1834, 1839, 1843, 1850, 1854, + 1858, 1863, 1868, 1874, 1879, 1885, 1890, 1895, 1901, 1905, 1910, 1914, 1919, + 1924, 1929, 1934, 1938, 1943, 1950, 1956, 1961, 1966, 1973, 1977, 1983, 1987, + 1991, 1998, 2003, 2009, 2016, 2020, 2027, 2032, 2038, 2045, 2052, 2059, 2066, + 2074, 2081, 2089, 2096, 2104, 2108, 2112, 2118, 2122, 2127, 2131, 2135, 2142, + 2146, 2152, 2157, 2164, 2170, 2176, 2182, 2187, 2193, 2198, 2204, 2209, 2213, + 2218, 2223, 2229, 2234, 2239, 2244, 2250, 2255, 2259, 2266, 2271, 2276, 2282, + 2286, 2291, 2297, 2302, 2309, 2314, 2320, 2327, 2333, 2338, 2341, 2348, 2352, + 2359, 2362, 2370, 2377, 2384, 2391, 2396, 2403, 2409, 2413, 2418, 2421, 2427, + 2431, 2436, 2442, 2448, 2452, 2460, 2464, 2467, 2471, 2477, 2483, 2489, 2497, + 2503, 2510, 2518, 2526, 2530, 2537, 2543, 2547, 2553, 2558, 2565, 2571, 2577, + 2583, 2590, 2594, 2600, 2606, 2613, 2618, 2624, 2630, 2638, 2644, 2650, 2654, + 2661, 2668, 2674, 2680, 2687, 2693, 2699, 2706, 2710, 2717, 2722, 2726, 2732, + 2736, 2742, 2749, 2756, 2763, 2769, 2777, 2783, 2787, 2795, 2803, 2810, 2814, + 2821, 2829, 2836, 2844, 2850, 2856, 2863, 2868, 2874, 2882, 2885, 2889, 2896, + 2902, 2908, 2914, 2919, 2923, 2927, 2933, 2937, 2942, 2948, 2953, 2960, 2964, + 2969, 2974, 2979, 2984, 2989, 2993, 2998, 3002, 3006, 3009, 3013, 3017, 3021, + 3027, 3031, 3036, 3040, 3045, 3052, 3057, 3062, 3067, 3071, 3076, 3082, 3086, + 3090, 3094, 3101, 3108, 3112, 3116, 3123, 3129, 3132, 3137, 3143, 3148, 3153, + 3161, 3168, 3175, 3183, 3191, 3196, 3200, 3206, 3212, 3219, 3225, 3232, 3238, + 3245, 3250, 3256, 3261, 3264, 3271, 3278, 3283, 3289, 3296, 3302, 3308, 3315, + 3320, 3326, 3332, 3338, 3344, 3350, 3355, 3361, 3366, 3374, 3381, 3386, 3391, + 3394, 3399, 3404, 3411, 3416, 3421, 3427, 3432, 3439, 3445, 3452, 3458, 3466, + 3470, 3475, 3481, 3486, 3493, 3499, 3507, 3513, 3520, 3526, 3533, 3541, 3548, + 3555, 3560, 3565, 3569, 3575, 3581, 3587, 3593, 3600, 3606, 3613, 3619, 3624, + 3627, 3634, 3640, 3644, 3651, 3655, 3660, 3665, 3669, 3674, 3678, 3684, 3690, + 3693, 3698, 3705, 3709, 3716, 3719, 3724, 3730, 3737, 3742, 3750, 3757, 3765, + 3772, 3775, 3779, 3783, 3789, 3794, 3802, 3807, 3812, 3815, 3820, 3827, 3832, + 3838, 3842, 3846, 3852, 3857, 3861, 3865, 3871, 3877, 3881, 3885, 3890, 3896, + 3900, 3903, 3910, 3913, 3917, 3922, 3927, 3931, 3937, 3941, 3947, 3951, 3956, + 3961, 3966, 3972, 3977, 3982, 3985, 3989, 3994, 3997, 4001, 4005, 4011, 4015, + 4019, 4024, 4030, 4036, 4040, 4047, 4052, 4059, 4065, 4071, 4076, 4079, 4086, + 4091, 4099, 4104, 4110, 4116, 4120, 4125, 4130, 4135, 4141, 4146, 4150, 4153, + 4158, 4165, 4169, 4175, 4181, 4185, 4191, 4198, 4202, 4205, 4211, 4218, 4224, + 4230, 4237, 4240, 4244, 4248, 4254, 4259, 4263, 4270, 4276, 4281, 4287, 4294, + 4301, 4306, 4311, 4315, 4321, 4327, 4334, 4338, 4342, 4346, 4352, 4357, 4362, + 4367, 4374, 4379, 4384, 4389, 4394, 4398, 4402, 4406, 4413, 4417, 4421, 4426, + 4433, 4439, 4445, 4451, 4455, 4459, 4464, 4469, 4474, 4479, 4484, 4491, 4496, + 4501, 4505, 4510, 4514, 4521, 4526, 4530, 4535, 4540, 4545, 4550, 4555, 4561, + 4564, 4567, 4572, 4576, 4580, 4586, 4593, 4597, 4602, 4608, 4612, 4617, 4624, + 4627, 4631, 4635, 4641, 4645, 4651, 4656, 4661, 4669, 4675, 4680, 4686, 4690, + 4693, 4697, 4703, 4707, 4711, 4715, 4718, 4722, 4729, 4734, 4740, 4744, 4748, + 4755, 4761, 4765, 4770, 4774, 4778, 4782, 4788, 4793, 4801, 4805, 4810, 4814, + 4819, 4822, 4826, 4831, 4837, 4842, 4849, 4855, 4859, 4865, 4870, 4874, 4881, + 4887, 4890, 4894, 4898, 4906, 4910, 4916, 4919, 4926, 4933, 4938, 4945, 4952, + 4958, 4964, 4970, 4977, 4984, 4988, 4995, 5001, 5009, 5014, 5022, 5028, 5036, + 5042, 5049, 5055, 5061, 5068, 5075, 5081, 5087, 5093, 5098, 5106, 5111, 5118, + 5124, 5130, 5136, 5143, 5150, 5156, 5164, 5168, 5174, 5180, 5187, 5191, 5197, + 5204, 5209, 5213, 5218, 5224, 5230, 5233, 5237, 5244, 5249, 5254, 5259, 5262, + 5266, 5270, 5277, 5280, 5285, 5290, 5294, 5300, 5306, 5310, 5314, 5322, 5326, + 5330, 5337, 5340, 5344, 5347, 5353, 5357, 5364, 5368, 5371, 5378, 5382, 5388, + 5392, 5396, 5401, 5406, 5410, 5413, 5418, 5423, 5429, 5433, 5437, 5441, 5449, + 5455, 5460, 5465, 5470, 5475, 5482, 5486, 5489, 5493, 5500, 5505, 5509, 5515, + 5519, 5524, 5529, 5536, 5540, 5543, 5548, 5554, 5561, 5566, 5570, 5576, 5580, + 5587, 5593, 5599, 5604, 5608, 5615, 5622, 5629, 5633, 5637, 5642, 5646, 5650, + 5655, 5659, 5663, 5669, 5673, 5679, 5683, 5689, 5693, 5697, 5704, 5709, 5713, + 5718, 5724, 5728, 5732, 5739, 5743, 5749, 5753, 5758, 5763, 5770, 5776, 5781, + 5786, 5792, 5798, 5805, 5808, 5813, 5819, 5823, 5827, 5831, 5836, 5840, 5846, + 5849, 5855, 5862, 5867, 5874, 5880, 5885, 5891, 5896, 5902, 5908, 5914, 5922, + 5926, 5930, 5936, 5941, 5949, 5953, 5959, 5965, 5972, 5976, 5982, 5986, 5993, + 5997, 6005, 6010, 6015, 6021, 6025, 6031, 6037, 6044, 6048, 6053, 6058, 6063, + 6068, 6072, 6079, 6084, 6090, 6096, 6104, 6108, 6115, 6120, 6124, 6131, 6136, + 6142, 6149, 6155, 6161, 6165, 6172, 6175, 6180, 6187, 6193, 6198, 6204, 6207, + 6213, 6220, 6226, 6233, 6238, 6242, 6247, 6251, 6258, 6266, 6272, 6278, 6283, + 6291, 6296, 6300, 6305, 6309, 6315, 6319, 6327, 6333, 6339, 6347, 6352, 6356, + 6362, 6368, 6375, 6379, 6384, 6388, 6394, 6400, 6405, 6411, 6417, 6421, 6425, + 6429, 6437, 6444, 6451, 6457, 6462, 6466, 6469, 6476, 6483, 6488, 6492, 6496, + 6500, 6505, 6510, 6515, 6522, 6528, 6534, 6539, 6543, 6550, 6554, 6561, 6567, + 6572, 6575, 6582, 6588, 6593, 6596, 6599, 6603, 6609, 6615, 6622, 6629, 6635, + 6642, 6647, 6652, 6659, 6663, 6666, 6671, 6677, 6682, 6685, 6689, 6692, 6697, + 6704, 6708, 6712, 6715, 6720, 6726, 6730, 6734, 6739, 6746, 6752, 6758, 6764, + 6769, 6776, 6781, 6789, 6794, 6800, 6808, 6814, 6821, 6826, 6833, 6838, 6844, + 6851, 6855, 6859, 6863, 6866, 6871, 6877, 6883, 6888, 6892, 6898, 6902, 6906, + 6912, 6916, 6921, 6926, 6931, 6938, 6943, 6949, 6955, 6959, 6965, 6970, 6974, + 6979, 6983, 6990, 6996, 7003, 7008, 7012, 7019, 7024, 7030, 7034, 7041, 7048, + 7051, 7058, 7064, 7070, 7076, 7083, 7089, 7095, 7098, 7103, 7108, 7114, 7122, + 7127, 7133, 7140, 7145, 7148, 7154, 7158, 7163, 7167, 7174, 7178, 7184, 7189, + 7194, 7199, 7205, 7212, 7217, 7221, 7227, 7233, 7238, 7242, 7248, 7252, 7256, + 7261, 7266, 7270, 7276, 7280, 7284, 7288, 7295, 7302, 7310, 7318, 7322, 7328, + 7335, 7342, 7348, 7353, 7361, 7367, 7374, 7380, 7387, 7394, 7400, 7407, 7412, + 7417, 7424, 7429, 7437, 7443, 7450, 7455, 7462, 7469, 7476, 7482, 7489, 7496, + 7503, 7508, 7516, 7523, 7530, 7535, 7542, 7548, 7555, 7559, 7563, 7568, 7575, + 7580, 7585, 7590, 7598, 7604, 7611, 7616, 7620, 7623, 7629, 7636, 7643, 7650, + 7657, 7665, 7670, 7674, 7678, 7683, 7689, 7696, 7700, 7704, 7709, 7714, 7718, + 7722, 7727, 7732, 7736, 7741, 7747, 7752, 7757, 7761, 7765, 7771, 7776, 7779, + 7784, 7789, 7793, 7799, 7804, 7811, 7817, 7824, 7830, 7836, 7843, 7849, 7856, + 7862, 7868, 7874, 7880, 7887, 7893, 7898, 7905, 7911, 7915, 7921, 7929, 7935, + 7941, 7947, 7952, 7956, 7962, 7968, 7974, 7981, 7987, 7994, 8000, 8008, 8014, + 8022, 8030, 8036, 8042, 8049, 8055, 8062, 8068, 8074, 8080, 8086, 8089, 8095, + 8099, 8103, 8107, 8112, 8117, 8122, 8127, 8131, 8135, 8141, 8145, 8151, 8156, + 8161, 8165, 8170, 8175, 8181, 8187, 8194, 8198, 8204, 8208, 8212, 8218, 8223, + 8228, 8233, 8238, 8244, 8248, 8251, 8255, 8258, 8264, 8269, 8272, 8278, 8285, + 8289, 8293, 8298, 8304, 8309, 8313, 8319, 8323, 8329, 8333, 8340, 8347, 8352, + 8359, 8363, 8366, 8371, 8375, 8380, 8387, 8392, 8398, 8404, 8411, 8419, 8427, + 8432, 8437, 8443, 8449, 8454, 8457, 8463, 8469, 8473, 8479, 8485, 8492, 8500, + 8504, 8508, 8515, 8521, 8525, 8532, 8538, 8543, 8551, 8557, 8564, 8571, 8577, + 8582, 8587, 8593, 8598, 8605, 8610, 8614, 8619, 8626, 8632, 8637, 8642, 8646, + 8652, 8657, 8661, 8666, 8670, 8675, 8683, 8688, 8694, 8699, 8706, 8709, 8716, + 8720, 8724, 8729, 8734, 8738, 8744, 8748, 8753, 8759, 8766, 8772, 8777, 8781, + 8786, 8792, 8799, 8802, 8806, 8811, 8817, 8820, 8825, 8829, 8834, 8839, 8843, + 8847, 8852, 8859, 8864, 8869, 8875, 8879, 8885, 8889, 8893, 8898, 8903, 8908, + 8913, 8918, 8924, 8929, 8934, 8938, 8943, 8947, 8951, 8957, 8963, 8967, 8971, + 8975, 8980, 8987, 8992, 9000, 9005, 9012, 9016, 9020, 9025, 9029, 9033, 9038, + 9042, 9048, 9053, 9058, 9063, 9070, 9075, 9080, 9087, 9092, 9097, 9102, 9108, + 9113, 9119, 9124, 9128, 9134, 9139, 9144, 9151, 9156, 9161, 9165, 9170, 9176, + 9182, 9185, 9191, 9198, 9206, 9212, 9219, 9224, 9229, 9235, 9240, 9245, 9250, + 9255, 9259, 9264, 9269, 9273, 9277, 9283, 9288, 9293, 9298, 9303, 9310, 9315, + 9320, 9325, 9330, 9338, 9344, 9350, 9356, 9364, 9371, 9376, 9383, 9388, 9395, + 9401, 9407, 9414, 9418, 9424, 9430, 9435, 9442, 9446, 9452, 9455, 9460, 9466, + 9471, 9477, 9484, 9488, 9495, 9500, 9508, 9516, 9522, 9529, 9536, 9543, 9548, + 9552, 9557, 9562, 9567, 9572, 9576, 9581, 9587, 9592, 9598, 9605, 9610, 9616, + 9621, 9627, 9630, 9634, 9640, 9644, 9648, 9652, 9658, 9662, 9667, 9673, 9677, + 9682, 9686, 9690, 9693, 9699, 9705, 9709, 9713, 9717, 9721, 9726, 9730, 9735, + 9739, 9745, 9750, 9754, 9759, 9763, 9770, 9775, 9781, 9786, 9791, 9798, 9804, + 9808, 9813, 9817, 9823, 9827, 9831, 9834, 9839, 9845, 9850, 9855, 9862, 9867, + 9874, 9877, 9885, 9891, 9896, 9902, 9910, 9914, 9920, 9927, 9931, 9936, 9939, + 9944, 9950, 9955, 9962, 9970, 9974, 9979, 9986, 9992, 9997, 10001, 10004, 10009, + 10014, 10021, 10027, 10032, 10040, 10044, 10049, 10055, 10059, 10064, 10068, 10073, 10078, + 10083, 10088, 10095, 10099, 10103, 10109, 10116, 10121, 10125, 10130, 10137, 10142, 10147, + 10150, 10154, 10161, 10167, 10171, 10177, 10183, 10187, 10193, 10199, 10205, 10210, 10214, + 10219, 10222, 10226, 10233, 10237, 10245, 10251, 10258, 10263, 10270, 10275, 10279, 10285, + 10291, 10298, 10305, 10311, 10315, 10323, 10330, 10336, 10341, 10348, 10354, 10360, 10367, + 10373, 10377, 10382, 10387, 10392, 10396, 10401, 10404, 10408, 10414, 10421, 10426, 10433, + 10439, 10445, 10450, 10455, 10461, 10466, 10469, 10475, 10480, 10487, 10491, 10496, 10503, + 10509, 10515, 10522, 10527, 10531, 10537, 10544, 10548, 10554, 10561, 10567, 10574, 10581, + 10588, 10593, 10597, 10604, 10611, 10617, 10624, 10629, 10633, 10638, 10644, 10649, 10654, + 10659, 10664, 10668, 10675, 10681, 10685, 10691, 10695, 10700, 10704, 10708, 10712, 10718, + 10722, 10729, 10733, 10740, 10744, 10748, 10753, 10758, 10762, 10765, 10771, 10777, 10781, + 10787, 10794, 10797, 10804, 10811, 10816, 10823, 10827, 10830, 10835, 10839, 10844, 10849, + 10853, 10858, 10862, 10869, 10873, 10878, 10882, 10886, 10890, 10893, 10899, 10903, 10907, + 10911, 10917, 10923, 10927, 10933, 10937, 10941, 10948, 10952, 10957, 10963, 10967, 10971, + 10975, 10979, 10984, 10989, 10994, 10998, 11003, 11010, 11015, 11020, 11025, 11029, 11033, + 11039, 11042, 11047, 11052, 11057, 11061, 11065, 11068}; - -unsigned char const BIP39_MNEMONIC[] = { - 'm', 'n', 'e', 'm', 'o', 'n', 'i', 'c' -}; +unsigned char const BIP39_MNEMONIC[] = {'m', 'n', 'e', 'm', 'o', 'n', 'i', 'c'}; #ifdef HAVE_ELECTRUM -unsigned char const ELECTRUM_SEED_VERSION[] = { - 'S', 'e', 'e', 'd', ' ', 'v', 'e', 'r', 's', 'i', 'o', 'n' -}; +unsigned char const ELECTRUM_SEED_VERSION[] = + {'S', 'e', 'e', 'd', ' ', 'v', 'e', 'r', 's', 'i', 'o', 'n'}; -unsigned char const ELECTRUM_MNEMONIC[] = { - 'e', 'l', 'e', 'c', 't', 'r', 'u', 'm' -}; +unsigned char const ELECTRUM_MNEMONIC[] = {'e', 'l', 'e', 'c', 't', 'r', 'u', 'm'}; #endif diff --git a/src_ux_common/bolos_ux_onboarding_seed_rom_variables.h b/src_ux_common/bolos_ux_onboarding_seed_rom_variables.h old mode 100755 new mode 100644 index bba65e20..a33a2975 --- a/src_ux_common/bolos_ux_onboarding_seed_rom_variables.h +++ b/src_ux_common/bolos_ux_onboarding_seed_rom_variables.h @@ -1,32 +1,32 @@ -/* @BANNER@ */ +/* @BANNER@ */ #ifndef BUSRV_H #define BUSRV_H -#define BIP39_WORDLIST_LENGTH 11068 +#define BIP39_WORDLIST_LENGTH 11068 #define BIP39_WORDLIST_OFFSETS_LENGTH 2049 -#define BIP39_MNEMONIC_LENGTH 8 +#define BIP39_MNEMONIC_LENGTH 8 #ifdef HAVE_ELECTRUM -#define ELECTRUM_SEED_VERSION_LENGTH 12 -#define ELECTRUM_MNEMONIC_LENGTH 8 +#define ELECTRUM_SEED_VERSION_LENGTH 12 +#define ELECTRUM_MNEMONIC_LENGTH 8 #define ELECTRUM_SEED_PREFIX_STANDARD 0x01 -#define ELECTRUM_PBKDF2_ROUNDS 2048 +#define ELECTRUM_PBKDF2_ROUNDS 2048 #endif #define BIP39_PBKDF2_ROUNDS 2048 -extern unsigned char const WIDE BIP39_WORDLIST [ BIP39_WORDLIST_LENGTH ]; -extern unsigned short const WIDE BIP39_WORDLIST_OFFSETS [ BIP39_WORDLIST_OFFSETS_LENGTH ]; -extern unsigned char const WIDE BIP39_MNEMONIC [ BIP39_MNEMONIC_LENGTH ]; +extern unsigned char const WIDE BIP39_WORDLIST[BIP39_WORDLIST_LENGTH]; +extern unsigned short const WIDE BIP39_WORDLIST_OFFSETS[BIP39_WORDLIST_OFFSETS_LENGTH]; +extern unsigned char const WIDE BIP39_MNEMONIC[BIP39_MNEMONIC_LENGTH]; #ifdef HAVE_ELECTRUM -extern unsigned char const WIDE ELECTRUM_SEED_VERSION [ ELECTRUM_SEED_VERSION_LENGTH ]; -extern unsigned char const WIDE ELECTRUM_MNEMONIC [ ELECTRUM_MNEMONIC_LENGTH ]; +extern unsigned char const WIDE ELECTRUM_SEED_VERSION[ELECTRUM_SEED_VERSION_LENGTH]; +extern unsigned char const WIDE ELECTRUM_MNEMONIC[ELECTRUM_MNEMONIC_LENGTH]; #endif -#endif // BUSRV_H +#endif // BUSRV_H From 78317645e0f0a22e138e4cdbdc61d8d2f0f5cff0 Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Tue, 4 Oct 2022 16:10:16 +0200 Subject: [PATCH 08/65] [clean] #include spaghetti --- .github/workflows/lint-workflow.yml | 2 +- Makefile | 39 ++++--- .../nanos_app_recovery_check.gif | Bin .../nanox_app_recovery_check.gif | Bin .../recovery_check.png | Bin src/main.c | 22 ++-- src/nanos_enter_phrase.c | 4 +- src/nanos_pick_phrase_length.c | 1 + src/nanox_enter_phrase.c | 26 +++-- src/ui.h | 34 ++++-- src/{bolos_ux_common.h => ux_common/common.h} | 108 +++++------------- .../ux_common/common_bip39.h | 2 +- .../ux_common/onboarding_electrum.c | 21 +++- .../ux_common/onboarding_seed_bip39.c | 7 +- .../ux_common/onboarding_seed_rom_variables.c | 16 ++- .../ux_common/onboarding_seed_rom_variables.h | 16 ++- src/{bolos_ux_nanos.c => ux_nanos.c} | 10 +- src/{bolos_ux_nanos.h => ux_nanos.h} | 87 +++----------- ...x_nanos_keyboard.c => ux_nanos_keyboard.c} | 16 +-- src/{bolos_ux_nanox.c => ux_nanox.c} | 21 +++- src/{bolos_ux_nanox.h => ux_nanox.h} | 84 +++++++++++++- ...x_nanox_keyboard.c => ux_nanox_keyboard.c} | 33 +++--- 22 files changed, 294 insertions(+), 255 deletions(-) rename nanos_app_recovery_check.gif => icons/nanos_app_recovery_check.gif (100%) rename nanox_app_recovery_check.gif => icons/nanox_app_recovery_check.gif (100%) rename recovery_check.png => icons/recovery_check.png (100%) rename src/{bolos_ux_common.h => ux_common/common.h} (54%) rename src_ux_common/bolos_ux_common_bip39.h => src/ux_common/common_bip39.h (98%) rename src_ux_common/bolos_ux_onboarding_electrum.c => src/ux_common/onboarding_electrum.c (78%) rename src_ux_common/bolos_ux_onboarding_seed_bip39.c => src/ux_common/onboarding_seed_bip39.c (99%) rename src_ux_common/bolos_ux_onboarding_seed_rom_variables.c => src/ux_common/onboarding_seed_rom_variables.c (98%) rename src_ux_common/bolos_ux_onboarding_seed_rom_variables.h => src/ux_common/onboarding_seed_rom_variables.h (52%) rename src/{bolos_ux_nanos.c => ux_nanos.c} (98%) rename src/{bolos_ux_nanos.h => ux_nanos.h} (64%) rename src/{bolos_ux_nanos_keyboard.c => ux_nanos_keyboard.c} (97%) rename src/{bolos_ux_nanox.c => ux_nanox.c} (89%) rename src/{bolos_ux_nanox.h => ux_nanox.h} (70%) rename src/{bolos_ux_nanox_keyboard.c => ux_nanox_keyboard.c} (90%) diff --git a/.github/workflows/lint-workflow.yml b/.github/workflows/lint-workflow.yml index 628e60b6..b5a2a56c 100644 --- a/.github/workflows/lint-workflow.yml +++ b/.github/workflows/lint-workflow.yml @@ -22,7 +22,7 @@ jobs: - name: Lint uses: DoozyX/clang-format-lint-action@v0.11 with: - source: 'src src_ux_common' + source: 'src' extensions: 'h,c' clangFormatVersion: 11 diff --git a/Makefile b/Makefile index 5bf5d84b..d38206eb 100755 --- a/Makefile +++ b/Makefile @@ -33,9 +33,9 @@ APPVERSION = "$(APPVERSION_M).$(APPVERSION_N).$(APPVERSION_P)" APP_LOAD_PARAMS = --appFlags 0x210 $(COMMON_LOAD_PARAMS) --apdu --curve secp256k1 --path "" ifeq ($(TARGET_NAME),TARGET_NANOS) - ICONNAME=nanos_app_recovery_check.gif + ICONNAME=icons/nanos_app_recovery_check.gif else - ICONNAME=nanox_app_recovery_check.gif + ICONNAME=icons/nanox_app_recovery_check.gif endif # Build configuration @@ -45,27 +45,30 @@ DEFINES += APPVERSION=\"$(APPVERSION)\" DEFINES += LEDGER_MAJOR_VERSION=$(APPVERSION_M) DEFINES += LEDGER_MINOR_VERSION=$(APPVERSION_N) DEFINES += LEDGER_PATCH_VERSION=$(APPVERSION_P) -DEFINES += OS_IO_SEPROXYHAL IO_SEPROXYHAL_BUFFER_SIZE_B=128 +DEFINES += OS_IO_SEPROXYHAL DEFINES += UNUSED\(x\)=\(void\)x -DEFINES += HAVE_BAGL HAVE_SPRINTF + +DEFINES += HAVE_BAGL HAVE_UX_FLOW + DEFINES += BOLOS_APP_ICON_SIZE_B=\(9+32\) #DEFINES += HAVE_ELECTRUM DEFINES += IO_USB_MAX_ENDPOINTS=4 IO_HID_EP_LENGTH=64 +DEFINES += HAVE_SPRINTF +DEFINES += HAVE_BOLOS_UX -DEFINES += HAVE_UX_FLOW - -ifneq ($(TARGET_NAME),TARGET_NANOS) - DEFINES += HAVE_GLO096 HAVE_BOLOS_UX - DEFINES += HAVE_BAGL BAGL_WIDTH=128 BAGL_HEIGHT=64 - DEFINES += HAVE_BAGL_ELLIPSIS # long label truncation feature - DEFINES += HAVE_BAGL_FONT_OPEN_SANS_REGULAR_11PX - DEFINES += HAVE_BAGL_FONT_OPEN_SANS_EXTRABOLD_11PX - DEFINES += HAVE_BAGL_FONT_OPEN_SANS_LIGHT_16PX - DEFINES += HAVE_KEYBOARD_UX +ifeq ($(TARGET_NAME), TARGET_NANOS) + DEFINES += IO_SEPROXYHAL_BUFFER_SIZE_B=128 +else + DEFINES += IO_SEPROXYHAL_BUFFER_SIZE_B=300 + DEFINES += HAVE_GLO096 + DEFINES += BAGL_WIDTH=128 BAGL_HEIGHT=64 + DEFINES += HAVE_BAGL_ELLIPSIS # long label truncation feature + DEFINES += HAVE_BAGL_FONT_OPEN_SANS_REGULAR_11PX + DEFINES += HAVE_BAGL_FONT_OPEN_SANS_EXTRABOLD_11PX + DEFINES += HAVE_BAGL_FONT_OPEN_SANS_LIGHT_16PX + DEFINES += HAVE_KEYBOARD_UX endif - - DEBUG = 0 ifneq ($(DEBUG),0) DEFINES += HAVE_PRINTF @@ -75,7 +78,7 @@ ifneq ($(DEBUG),0) DEFINES += PRINTF=mcu_usb_printf endif else - DEFINES += PRINTF\(...\)= + DEFINES += PRINTF\(...\)= endif ############## @@ -106,7 +109,7 @@ LDLIBS += -lm -lgcc -lc # import rules to compile glyphs(/pone) include $(BOLOS_SDK)/Makefile.glyphs -APP_SOURCE_PATH += src src_ux_common +APP_SOURCE_PATH += src ifneq ($(TARGET_NAME),TARGET_NANOS) SDK_SOURCE_PATH += lib_ux diff --git a/nanos_app_recovery_check.gif b/icons/nanos_app_recovery_check.gif similarity index 100% rename from nanos_app_recovery_check.gif rename to icons/nanos_app_recovery_check.gif diff --git a/nanox_app_recovery_check.gif b/icons/nanox_app_recovery_check.gif similarity index 100% rename from nanox_app_recovery_check.gif rename to icons/nanox_app_recovery_check.gif diff --git a/recovery_check.png b/icons/recovery_check.png similarity index 100% rename from recovery_check.png rename to icons/recovery_check.png diff --git a/src/main.c b/src/main.c index e61853ff..ea83d18b 100644 --- a/src/main.c +++ b/src/main.c @@ -1,6 +1,5 @@ /******************************************************************************* - * Ledger Blue - * (c) 2016 Ledger + * (c) 2016-2022 Ledger SAS * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,10 +14,10 @@ * limitations under the License. ********************************************************************************/ -#include "os.h" -#include "cx.h" +#include +#include +#include -#include "os_io_seproxyhal.h" #include "ui.h" unsigned char G_io_seproxyhal_spi_buffer[IO_SEPROXYHAL_BUFFER_SIZE_B]; @@ -32,17 +31,10 @@ enum UI_STATE { UI_IDLE, UI_TEXT, UI_APPROVAL }; extern enum UI_STATE uiState; #if defined(TARGET_NANOX) || defined(TARGET_NANOS2) -#include "ux.h" -#include "bolos_ux_nanox.h" -#else -#include "bolos_ux_nanos.h" -#endif - -#if defined(TARGET_NANOX) || defined(TARGET_NANOS2) +#include "ux_nanox.h" uint8_t compare_recovery_phrase(void); -#endif - -#ifdef TARGET_NANOS +#else +#include "ux_nanos.h" void compare_recovery_phrase(void); #endif diff --git a/src/nanos_enter_phrase.c b/src/nanos_enter_phrase.c index 2e1b1fc5..49eb6489 100644 --- a/src/nanos_enter_phrase.c +++ b/src/nanos_enter_phrase.c @@ -1,6 +1,5 @@ /******************************************************************************* - * Ledger Blue - Secure firmware - * (c) 2016, 2017 Ledger + * (c) 2016-2022 Ledger SAS * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +15,6 @@ ********************************************************************************/ #include "ui.h" -#include "bolos_ux_common.h" #ifdef TARGET_NANOS diff --git a/src/nanos_pick_phrase_length.c b/src/nanos_pick_phrase_length.c index 0495878f..03408f7f 100644 --- a/src/nanos_pick_phrase_length.c +++ b/src/nanos_pick_phrase_length.c @@ -14,6 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. ********************************************************************************/ + #include "ui.h" #ifdef TARGET_NANOS diff --git a/src/nanox_enter_phrase.c b/src/nanox_enter_phrase.c index 23c77495..b488f50a 100644 --- a/src/nanox_enter_phrase.c +++ b/src/nanox_enter_phrase.c @@ -1,16 +1,22 @@ - -#include "os.h" - -#if defined(TARGET_NANOX) || defined(TARGET_NANOS2) - -#include "cx.h" - -#include "os_io_seproxyhal.h" -#include "string.h" +/******************************************************************************* + * (c) 2016-2022 Ledger SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ********************************************************************************/ #include "ui.h" -#include "glyphs.h" +#if defined(TARGET_NANOX) || defined(TARGET_NANOS2) const bagl_element_t* screen_onboarding_4_restore_word_before_element_display_callback( const bagl_element_t* element); diff --git a/src/ui.h b/src/ui.h index e72f8565..0b4d9cb1 100644 --- a/src/ui.h +++ b/src/ui.h @@ -1,13 +1,33 @@ +/******************************************************************************* + * (c) 2016-2022 Ledger SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ********************************************************************************/ + #ifndef _UI_ #define _UI_ -#include "os.h" -#include "os_io_seproxyhal.h" -#include "bolos_ux_common.h" -#include "glyphs.h" -#include "string.h" -#include "cx.h" +#include +#include +#include +#include + +#if defined(TARGET_NANOS) +#include "ux_nanos.h" +#elif defined(TARGET_NANOX) || defined(TARGET_NANOS2) +#include "ux_nanox.h" +#endif void ui_idle_init(void); -#endif \ No newline at end of file +#endif diff --git a/src/bolos_ux_common.h b/src/ux_common/common.h similarity index 54% rename from src/bolos_ux_common.h rename to src/ux_common/common.h index e6d62dfd..ef1d5e07 100644 --- a/src/bolos_ux_common.h +++ b/src/ux_common/common.h @@ -1,17 +1,30 @@ - -#include "os.h" -#include "cx.h" - -#include "os_io_seproxyhal.h" -#include "string.h" - -#if defined(TARGET_NANOS) -#include "bolos_ux_nanos.h" -#elif defined(TARGET_NANOX) || defined(TARGET_NANOS2) -#include "bolos_ux_nanox.h" -#endif - -#include "glyphs.h" +/******************************************************************************* + * (c) 2016-2022 Ledger SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ********************************************************************************/ + +#ifndef COMMON_UX_H +#define COMMON_UX_H + +#include +#include +#include + +#include +#include + +#include "../glyphs.h" #ifdef OS_IO_SEPROXYHAL @@ -33,8 +46,6 @@ #define CONSENT_INTERVAL_MS 3000 -extern bolos_ux_context_t G_bolos_ux_context; - extern const unsigned char hex_digits[]; unsigned char rng_u8_modulo(unsigned char modulo); @@ -112,69 +123,8 @@ void screen_settings_passphrase_temporary_1_init(void); void screen_settings_passphrase_type_and_review_init(unsigned int kind); void screen_settings_erase_all_init(void); -void screen_common_keyboard_init(unsigned int stack_slot, - unsigned int current_element, - unsigned int nb_elements, - keyboard_callback_t callback); - #define COMMON_KEYBOARD_INDEX_UNCHANGED (-1UL) -#if defined(TARGET_NANOX) || defined(TARGET_NANOS2) -// to be included into all flow that needs to go back to the dashboard -extern const ux_flow_step_t ux_ob_goto_dashboard_step; - -void ux_pairing_init(void); -void ux_pairing_deinit(void); - -void ux_boot_menu_init(void); -void ux_ob_onboarding_init(void); -void screen_control_center_init(unsigned int); - -void screen_help_init(appmain_t help_ended_callback); - -void screen_processing_init(void); - -void screen_prepare_app_name(unsigned int appidx); - -bagl_icon_details_t* screen_prepare_bitmap_14x14(ux_icon_buffer_t* icon_buffer, - const unsigned char* icon_bitmap, - unsigned int masked_inverted); -void screen_prepare_app_icon_14x14(ux_icon_buffer_t* icon_buffer, - const unsigned char* icon_bitmap, - unsigned int icon_bitmap_length, - unsigned int masked_inverted); -void screen_prepare_app_icon(ux_icon_buffer_t* icon_buffer, - unsigned char* icon_bitmap_buffer, - unsigned int icon_bitmap_buffer_length, - unsigned int appidx, - unsigned int inverted); - -void screen_lock(void); - -#ifdef BOLOS_OS_UPGRADER -void screen_os_upgrader(void); -#endif // BOLOS_OS_UPGRADER - -unsigned int screen_consent_button(unsigned int button_mask, unsigned int button_mask_counter); -unsigned int screen_consent_ticker(unsigned int ignored); -void screen_consent_ticker_init(unsigned int number_of_steps, - unsigned int interval_ms, - unsigned int check_pin_to_confirm); -void screen_consent_set_interval(unsigned int interval_ms); - -void screen_common_pin_init(unsigned int stack_slot, pin_callback_t end_callback); - -void screen_keyboard_init(char* buffer, unsigned int maxsize, appmain_t validation_callback); - -void debug(unsigned int id, unsigned char* msg); - -void screen_mcu_upgrade_required_init(void); -void screen_consent_run_app_init(void); -void screen_boot_delay_init(void); - -void settings_general_version(appmain_t end_callback); -void settings_general_regulatory(appmain_t end_callback); -void settings_general_serial(appmain_t end_callback); -#endif // TARGET_NANOX || TARGET_NANOS2 - #endif // OS_IO_SEPROXYHAL + +#endif // COMMON_UX_H diff --git a/src_ux_common/bolos_ux_common_bip39.h b/src/ux_common/common_bip39.h similarity index 98% rename from src_ux_common/bolos_ux_common_bip39.h rename to src/ux_common/common_bip39.h index 5ffbd0d7..aa93bea2 100644 --- a/src_ux_common/bolos_ux_common_bip39.h +++ b/src/ux_common/common_bip39.h @@ -3,7 +3,7 @@ #define COMMON_BIP39_H // BIP39 helpers -#include "bolos_ux_onboarding_seed_rom_variables.h" +#include "onboarding_seed_rom_variables.h" void bolos_ux_pbkdf2(unsigned char *password, unsigned int passwordlen, diff --git a/src_ux_common/bolos_ux_onboarding_electrum.c b/src/ux_common/onboarding_electrum.c similarity index 78% rename from src_ux_common/bolos_ux_onboarding_electrum.c rename to src/ux_common/onboarding_electrum.c index 55d5ac5e..a633f047 100644 --- a/src_ux_common/bolos_ux_onboarding_electrum.c +++ b/src/ux_common/onboarding_electrum.c @@ -1,11 +1,22 @@ -/* @BANNER@ */ +/******************************************************************************* + * (c) 2016-2022 Ledger SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ********************************************************************************/ #ifdef HAVE_ELECTRUM -#include "os.h" -#include "cx.h" - -#include "bolos_ux_common.h" +#include "common.h" int cx_math_shiftr_11(unsigned char *r, unsigned int len) { unsigned int j, b11; diff --git a/src_ux_common/bolos_ux_onboarding_seed_bip39.c b/src/ux_common/onboarding_seed_bip39.c similarity index 99% rename from src_ux_common/bolos_ux_onboarding_seed_bip39.c rename to src/ux_common/onboarding_seed_bip39.c index 4c5a4b86..00d06a99 100644 --- a/src_ux_common/bolos_ux_onboarding_seed_bip39.c +++ b/src/ux_common/onboarding_seed_bip39.c @@ -1,9 +1,10 @@ /* @BANNER@ */ -#include "os.h" -#include "cx.h" +#include +#include -#include "bolos_ux_common.h" +#include "onboarding_seed_rom_variables.h" +#include "common.h" unsigned int bolos_ux_mnemonic_from_data(unsigned char* in, unsigned int inLength, diff --git a/src_ux_common/bolos_ux_onboarding_seed_rom_variables.c b/src/ux_common/onboarding_seed_rom_variables.c similarity index 98% rename from src_ux_common/bolos_ux_onboarding_seed_rom_variables.c rename to src/ux_common/onboarding_seed_rom_variables.c index 6a981f60..849d4a87 100644 --- a/src_ux_common/bolos_ux_onboarding_seed_rom_variables.c +++ b/src/ux_common/onboarding_seed_rom_variables.c @@ -1,4 +1,18 @@ -/* @BANNER@ */ +/******************************************************************************* + * (c) 2016-2022 Ledger SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ********************************************************************************/ unsigned char const BIP39_WORDLIST[] = { 'a', 'b', 'a', 'n', 'd', 'o', 'n', 'a', 'b', 'i', 'l', 'i', 't', 'y', 'a', 'b', 'l', 'e', 'a', diff --git a/src_ux_common/bolos_ux_onboarding_seed_rom_variables.h b/src/ux_common/onboarding_seed_rom_variables.h similarity index 52% rename from src_ux_common/bolos_ux_onboarding_seed_rom_variables.h rename to src/ux_common/onboarding_seed_rom_variables.h index a33a2975..80b29608 100644 --- a/src_ux_common/bolos_ux_onboarding_seed_rom_variables.h +++ b/src/ux_common/onboarding_seed_rom_variables.h @@ -1,4 +1,18 @@ -/* @BANNER@ */ +/******************************************************************************* + * (c) 2016-2022 Ledger SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ********************************************************************************/ #ifndef BUSRV_H #define BUSRV_H diff --git a/src/bolos_ux_nanos.c b/src/ux_nanos.c similarity index 98% rename from src/bolos_ux_nanos.c rename to src/ux_nanos.c index 78cc0ddf..87a5972d 100644 --- a/src/bolos_ux_nanos.c +++ b/src/ux_nanos.c @@ -15,16 +15,16 @@ * limitations under the License. ********************************************************************************/ -#include "os.h" +#include #ifdef TARGET_NANOS -#include "cx.h" +#include -#include "os_io_seproxyhal.h" -#include "string.h" +#include +#include -#include "bolos_ux_common.h" +#include "ux_nanos.h" //#ifdef OS_IO_SEPROXYHAL diff --git a/src/bolos_ux_nanos.h b/src/ux_nanos.h similarity index 64% rename from src/bolos_ux_nanos.h rename to src/ux_nanos.h index 71053e45..56285e42 100644 --- a/src/bolos_ux_nanos.h +++ b/src/ux_nanos.h @@ -1,6 +1,5 @@ /******************************************************************************* - * Ledger Blue - Secure firmware - * (c) 2016, 2017 Ledger + * (c) 2016-2022 Ledger SAS * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,25 +17,21 @@ #ifndef BOLOS_UX_H #define BOLOS_UX_H -#include "os_io_seproxyhal.h" -#include "ux.h" - -#define HAVE_BOLOS_UX #ifdef HAVE_BOLOS_UX +#include "ux_common/common.h" + typedef unsigned int (*callback_t)(unsigned int); #define KEYBOARD_ITEM_VALIDATED \ - 1 // callback is called with the entered item index, tmp_element is - // precharged with element to be displayed and using the common string - // buffer as string parameter + 1 // callback is called with the entered item index, tmp_element is precharged with element to + // be displayed and using the common string buffer as string parameter #define KEYBOARD_RENDER_ITEM \ - 2 // callback is called the element index, tmp_element is precharged with - // element to be displayed and using the common string buffer as string - // parameter + 2 // callback is called with the element index, tmp_element is precharged with element to be + // displayed and using the common string buffer as string parameter #define KEYBOARD_RENDER_WORD \ - 3 // callback is called with a -1 when requesting complete word, or the char - // index else, returnin 0 implies no char is to be displayed + 3 // callback is called with a -1 when requesting complete word, or the char index else, + // returnin 0 implies no char is to be displayed typedef const bagl_element_t *(*keyboard_callback_t)(unsigned int event, unsigned int value); // bolos ux context (not mandatory if redesigning a bolos ux) @@ -188,69 +183,19 @@ void bolos_ux_hslider3_previous(void); #define FAST_LIST_THRESHOLD_CS 8 #define FAST_LIST_ACTION_CS 2 -unsigned int screen_stack_is_element_array_present(const bagl_element_t *element_array); -unsigned int screen_stack_push(void); -unsigned int screen_stack_pop(void); -void screen_stack_remove(unsigned int stack_slot); - -// BIP39 helpers -#include "bolos_ux_onboarding_seed_rom_variables.h" - -void bolos_ux_pbkdf2(unsigned char *password, - unsigned int passwordlen, - unsigned char *salt, - unsigned int saltlen, - unsigned int iterations, - unsigned char *out, - unsigned int outLength); -unsigned char bolos_ux_get_random_bip39_word(unsigned char *word); -// return 0 if mnemonic is invalid -unsigned int bolos_ux_mnemonic_check(unsigned char *mnemonic, unsigned int mnemonicLength); -unsigned char bolos_ux_word_check(unsigned char *word, unsigned int wordLength); - -unsigned int bolos_ux_get_word_ptr(unsigned char **word, - unsigned int max_length, - unsigned int word_index); - -// passphrase will be prefixed with "MNEMONIC" from BIP39, the passphrase -// content shall start @ 8 -void bolos_ux_mnemonic_to_seed(unsigned char *mnemonic, - unsigned int mnemonicLength, - unsigned char *seed /*, unsigned char *workBuffer*/); -unsigned int bolos_ux_mnemonic_indexes_to_words(unsigned char *indexes, unsigned char *words); -unsigned int bolos_ux_mnemonic_from_data(unsigned char *in, - unsigned int inLength, - unsigned char *out, - unsigned int outLength); - -unsigned int bolos_ux_bip39_get_word_idx_starting_with(unsigned char *prefix, - unsigned int prefixlength); -unsigned int bolos_ux_bip39_idx_strcpy(unsigned int index, unsigned char *buffer); -unsigned int bolos_ux_bip39_idx_startswith(unsigned int idx, - unsigned char *prefix, - unsigned int prefixlength); -unsigned int bolos_ux_bip39_get_word_count_starting_with(unsigned char *prefix, - unsigned int prefixlength); -unsigned int bolos_ux_bip39_get_word_next_letters_starting_with(unsigned char *prefix, - unsigned int prefixlength, - unsigned char *next_letters_buffer); - -#ifdef HAVE_ELECTRUM - -unsigned int bolos_ux_electrum_new_mnemonic(unsigned int version, - unsigned char *out, - unsigned int outLength); -unsigned int bolos_ux_electrum_mnemonic_check(unsigned int version, - unsigned char *mnemonic, - unsigned int mnemonicLength); -#endif - /** * Bolos system app internal UX entry point (could be overridden by a further * loaded BOLOS_UX application) */ void bolos_ux_main(void); +void screen_common_keyboard_init(unsigned int stack_slot, + unsigned int current_element, + unsigned int nb_elements, + keyboard_callback_t callback); + +#include "ux_common/common_bip39.h" + extern const bagl_element_t screen_onboarding_word_list_elements[9]; #endif // HAVE_BOLOS_UX diff --git a/src/bolos_ux_nanos_keyboard.c b/src/ux_nanos_keyboard.c similarity index 97% rename from src/bolos_ux_nanos_keyboard.c rename to src/ux_nanos_keyboard.c index 50e0b927..e630c853 100644 --- a/src/bolos_ux_nanos_keyboard.c +++ b/src/ux_nanos_keyboard.c @@ -1,6 +1,5 @@ /******************************************************************************* - * Ledger Blue - Secure firmware - * (c) 2016, 2017 Ledger + * (c) 2016-2022 Ledger SAS * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,19 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. ********************************************************************************/ -#include "os.h" -#ifdef TARGET_NANOS - -#include "cx.h" - -#include "os_io_seproxyhal.h" -#include "string.h" +#include "ui.h" -#include "bolos_ux_nanos.h" -#include "bolos_ux_common.h" - -#include "glyphs.h" +#ifdef TARGET_NANOS //#ifdef OS_IO_SEPROXYHAL diff --git a/src/bolos_ux_nanox.c b/src/ux_nanox.c similarity index 89% rename from src/bolos_ux_nanox.c rename to src/ux_nanox.c index 7a0e499b..c2902cca 100644 --- a/src/bolos_ux_nanox.c +++ b/src/ux_nanox.c @@ -1,4 +1,20 @@ -#include "os.h" +/******************************************************************************* + * (c) 2016-2022 Ledger SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ********************************************************************************/ + +#include #if defined(TARGET_NANOX) || defined(TARGET_NANOS2) @@ -8,8 +24,7 @@ #include #include -#include -#include "glyphs.h" +#include "ux_nanox.h" #ifdef OS_IO_SEPROXYHAL diff --git a/src/bolos_ux_nanox.h b/src/ux_nanox.h similarity index 70% rename from src/bolos_ux_nanox.h rename to src/ux_nanox.h index 12209033..1594cc38 100644 --- a/src/bolos_ux_nanox.h +++ b/src/ux_nanox.h @@ -1,12 +1,25 @@ +/******************************************************************************* + * (c) 2016-2022 Ledger SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ********************************************************************************/ #ifndef BOLOS_UX_H #define BOLOS_UX_H -#include "os_io_seproxyhal.h" - #ifdef HAVE_BOLOS_UX -#include "ux.h" +#include "ux_common/common.h" typedef unsigned int (*pin_callback_t)(unsigned char* pin_buffer, unsigned int pin_length); @@ -14,7 +27,7 @@ typedef unsigned int (*pin_callback_t)(unsigned char* pin_buffer, unsigned int p 1 // callback is called with the entered item index, tmp_element is precharged with element to // be displayed and using the common string buffer as string parameter #define KEYBOARD_RENDER_ITEM \ - 2 // callback is called the element index, tmp_element is precharged with element to be + 2 // callback is called with the element index, tmp_element is precharged with element to be // displayed and using the common string buffer as string parameter #define KEYBOARD_RENDER_WORD \ 3 // callback is called with a -1 when requesting complete word, or the char index else, @@ -220,7 +233,12 @@ void bolos_ux_hslider3_previous(void); */ void bolos_ux_main(void); -#include "bolos_ux_common_bip39.h" +void screen_common_keyboard_init(unsigned int stack_slot, + unsigned int current_element, + unsigned int nb_elements, + keyboard_callback_t callback); + +#include "ux_common/common_bip39.h" #define UX_FLOW_AFTER_PIN(stepname, stackslot, callback) \ UX_STEP_INIT(stepname, NULL, NULL, { \ @@ -232,6 +250,62 @@ void bolos_ux_main(void); G_ux.stack[stackslot].displayed_callback = callback; \ }); +// to be included into all flow that needs to go back to the dashboard +extern const ux_flow_step_t ux_ob_goto_dashboard_step; + +void ux_pairing_init(void); +void ux_pairing_deinit(void); + +void ux_boot_menu_init(void); +void ux_ob_onboarding_init(void); +void screen_control_center_init(unsigned int); + +void screen_help_init(appmain_t help_ended_callback); + +void screen_processing_init(void); + +void screen_prepare_app_name(unsigned int appidx); + +bagl_icon_details_t* screen_prepare_bitmap_14x14(ux_icon_buffer_t* icon_buffer, + const unsigned char* icon_bitmap, + unsigned int masked_inverted); +void screen_prepare_app_icon_14x14(ux_icon_buffer_t* icon_buffer, + const unsigned char* icon_bitmap, + unsigned int icon_bitmap_length, + unsigned int masked_inverted); +void screen_prepare_app_icon(ux_icon_buffer_t* icon_buffer, + unsigned char* icon_bitmap_buffer, + unsigned int icon_bitmap_buffer_length, + unsigned int appidx, + unsigned int inverted); + +void screen_lock(void); + +#ifdef BOLOS_OS_UPGRADER +void screen_os_upgrader(void); +#endif // BOLOS_OS_UPGRADER + +unsigned int screen_consent_button(unsigned int button_mask, unsigned int button_mask_counter); +unsigned int screen_consent_ticker(unsigned int ignored); +void screen_consent_ticker_init(unsigned int number_of_steps, + unsigned int interval_ms, + unsigned int check_pin_to_confirm); +void screen_consent_set_interval(unsigned int interval_ms); + +void screen_common_pin_init(unsigned int stack_slot, pin_callback_t end_callback); + +void screen_keyboard_init(char* buffer, unsigned int maxsize, appmain_t validation_callback); + +void debug(unsigned int id, unsigned char* msg); + +void screen_mcu_upgrade_required_init(void); +void screen_consent_run_app_init(void); +void screen_boot_delay_init(void); + +void settings_general_version(appmain_t end_callback); +void settings_general_regulatory(appmain_t end_callback); +void settings_general_serial(appmain_t end_callback); + #endif // HAVE_BOLOS_UX #endif // BOLOS_UX_H diff --git a/src/bolos_ux_nanox_keyboard.c b/src/ux_nanox_keyboard.c similarity index 90% rename from src/bolos_ux_nanox_keyboard.c rename to src/ux_nanox_keyboard.c index 2b931b73..f8bfcfde 100644 --- a/src/bolos_ux_nanox_keyboard.c +++ b/src/ux_nanox_keyboard.c @@ -1,18 +1,23 @@ - -#include "os.h" +/******************************************************************************* + * (c) 2016-2022 Ledger SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ********************************************************************************/ + +#include "ui.h" #if defined(TARGET_NANOX) || defined(TARGET_NANOS2) -#include "cx.h" - -#include "os_io_seproxyhal.h" -#include "string.h" - -#include "bolos_ux_nanox.h" -#include "bolos_ux_common.h" - -#include "glyphs.h" - #ifdef OS_IO_SEPROXYHAL const bagl_element_t screen_common_keyboard_elements[] = { @@ -329,6 +334,6 @@ void screen_common_keyboard_init(unsigned int stack_slot, ux_stack_display(stack_slot); } -#endif +#endif // OS_IO_SEPROXYHAL -#endif +#endif // TARGETS From bcd0953b30143d1388e26fce433a5efddb08f164 Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Wed, 5 Oct 2022 10:44:19 +0200 Subject: [PATCH 09/65] [add] Enabling USB on debug mode --- Makefile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index d38206eb..689efadf 100755 --- a/Makefile +++ b/Makefile @@ -70,9 +70,11 @@ else endif DEBUG = 0 -ifneq ($(DEBUG),0) +ifneq ($(DEBUG), 0) + DEFINES += HAVE_IO_USB HAVE_USB_APDU + SDK_SOURCE_PATH += lib_stusb lib_stusb_impl DEFINES += HAVE_PRINTF - ifeq ($(TARGET_NAME),TARGET_NANOS) + ifeq ($(TARGET_NAME), TARGET_NANOS) DEFINES += PRINTF=screen_printf else DEFINES += PRINTF=mcu_usb_printf From c41a5758352bc82bd282b3e33c323be3563ede97 Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Wed, 5 Oct 2022 10:50:08 +0200 Subject: [PATCH 10/65] [ci][add] Trigger CI on any PR --- .github/workflows/ci-workflow.yml | 2 ++ .github/workflows/lint-workflow.yml | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index fbcc72e2..65092633 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -7,6 +7,8 @@ on: - master - develop pull_request: + paths-ignore: + - '.github/workflows/*.yml' jobs: nano_build: diff --git a/.github/workflows/lint-workflow.yml b/.github/workflows/lint-workflow.yml index b5a2a56c..d708d468 100644 --- a/.github/workflows/lint-workflow.yml +++ b/.github/workflows/lint-workflow.yml @@ -6,9 +6,9 @@ on: branches: - master pull_request: - branches: - - master - - develop + paths-ignore: + - '.github/workflows/*.yml' + - 'tests/*' jobs: job_lint: From 7732f6b1ffeb9f6c8f6973555b52be27d1646286 Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Wed, 5 Oct 2022 15:25:31 +0200 Subject: [PATCH 11/65] [clean] Dead code --- src/nanos_enter_phrase.c | 6 +- src/nanos_pick_phrase_length.c | 9 +- src/nanox_enter_phrase.c | 6 +- src/ui.c | 6 +- src/ui.h | 2 + src/ux_common/common.h | 95 ++---------- src/ux_common/common_bip39.h | 21 --- src/ux_common/onboarding_seed_bip39.c | 83 ----------- src/ux_nanos.c | 105 +------------ src/ux_nanos.h | 113 +------------- src/ux_nanox.c | 85 +---------- src/ux_nanox.h | 205 +------------------------- 12 files changed, 37 insertions(+), 699 deletions(-) diff --git a/src/nanos_enter_phrase.c b/src/nanos_enter_phrase.c index 49eb6489..87e06c36 100644 --- a/src/nanos_enter_phrase.c +++ b/src/nanos_enter_phrase.c @@ -231,7 +231,7 @@ void compare_recovery_phrase(void) { bolos_ux_mnemonic_to_seed((unsigned char*) G_bolos_ux_context.words_buffer, G_bolos_ux_context.words_buffer_length, buffer); - // PRINTF("Input seed:\n %.*H\n", 64, buffer); + PRINTF("Input seed:\n %.*H\n", 64, buffer); // get rootkey from hex-seed cx_hmac_sha512_t ctx; @@ -239,13 +239,13 @@ void compare_recovery_phrase(void) { cx_hmac_sha512_init(&ctx, (const uint8_t*) key, sizeof(key)); cx_hmac((cx_hmac_t*) &ctx, CX_LAST, buffer, 64, buffer, 64); - // PRINTF("Root key from input:\n%.*H\n", 64, buffer); + PRINTF("Root key from input:\n%.*H\n", 64, buffer); // get rootkey from device's seed uint8_t buffer_device[64]; os_perso_derive_node_bip32(CX_CURVE_256K1, NULL, 0, buffer_device, buffer_device + 32); - // PRINTF("Root key from device: \n%.*H\n", 64, buffer_device); + PRINTF("Root key from device: \n%.*H\n", 64, buffer_device); // compare both rootkey if (os_secure_memcmp(buffer, buffer_device, 64)) { diff --git a/src/nanos_pick_phrase_length.c b/src/nanos_pick_phrase_length.c index 03408f7f..81d643a3 100644 --- a/src/nanos_pick_phrase_length.c +++ b/src/nanos_pick_phrase_length.c @@ -1,6 +1,5 @@ /******************************************************************************* - * Ledger Blue - Secure firmware - * (c) 2016, 2017 Ledger + * (c) 2016-2022 Ledger SAS * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +18,7 @@ #ifdef TARGET_NANOS -UX_STEP_CB(restore_3_1_1, bb, G_bolos_ux_context.onboarding_kind = 24; +UX_STEP_CB(restore_3_1_1, bb, G_bolos_ux_context.onboarding_kind = BOLOS_UX_ONBOARDING_NEW_24; screen_onboarding_4_restore_word_init(RESTORE_WORD_ACTION_FIRST_WORD); , { @@ -27,7 +26,7 @@ UX_STEP_CB(restore_3_1_1, bb, G_bolos_ux_context.onboarding_kind = 24; "with 24 words", }); -UX_STEP_CB(restore_3_1_2, bb, G_bolos_ux_context.onboarding_kind = 18; +UX_STEP_CB(restore_3_1_2, bb, G_bolos_ux_context.onboarding_kind = BOLOS_UX_ONBOARDING_NEW_18; screen_onboarding_4_restore_word_init(RESTORE_WORD_ACTION_FIRST_WORD); , { @@ -35,7 +34,7 @@ UX_STEP_CB(restore_3_1_2, bb, G_bolos_ux_context.onboarding_kind = 18; "with 18 words", }); -UX_STEP_CB(restore_3_1_3, bb, G_bolos_ux_context.onboarding_kind = 12; +UX_STEP_CB(restore_3_1_3, bb, G_bolos_ux_context.onboarding_kind = BOLOS_UX_ONBOARDING_NEW_12; screen_onboarding_4_restore_word_init(RESTORE_WORD_ACTION_FIRST_WORD); , { diff --git a/src/nanox_enter_phrase.c b/src/nanox_enter_phrase.c index b488f50a..263493f5 100644 --- a/src/nanox_enter_phrase.c +++ b/src/nanox_enter_phrase.c @@ -405,7 +405,7 @@ uint8_t compare_recovery_phrase(void) { bolos_ux_mnemonic_to_seed((unsigned char*) G_bolos_ux_context.words_buffer, G_bolos_ux_context.words_buffer_length, buffer); - // PRINTF("Input seed:\n %.*H\n", 64, buffer); + PRINTF("Input seed:\n %.*H\n", 64, buffer); // get rootkey from hex-seed cx_hmac_sha512_t ctx; @@ -413,13 +413,13 @@ uint8_t compare_recovery_phrase(void) { cx_hmac_sha512_init(&ctx, (const uint8_t*) key, sizeof(key)); cx_hmac((cx_hmac_t*) &ctx, CX_LAST, buffer, 64, buffer, 64); - // PRINTF("Root key from input:\n%.*H\n", 64, buffer); + PRINTF("Root key from input:\n%.*H\n", 64, buffer); // get rootkey from device's seed uint8_t buffer_device[64]; os_perso_derive_node_bip32(CX_CURVE_256K1, NULL, 0, buffer_device, buffer_device + 32); - // PRINTF("Root key from device: \n%.*H\n", 64, buffer_device); + PRINTF("Root key from device: \n%.*H\n", 64, buffer_device); // compare both rootkey return os_secure_memcmp(buffer, buffer_device, 64) ? 0 : 1; diff --git a/src/ui.c b/src/ui.c index 68a9262b..a3c9253b 100644 --- a/src/ui.c +++ b/src/ui.c @@ -48,15 +48,15 @@ const char* number_of_words_getter(unsigned int idx) { void number_of_words_selector(unsigned int idx) { switch (idx) { case 0: - G_bolos_ux_context.onboarding_kind = 12; + G_bolos_ux_context.onboarding_kind = BOLOS_UX_ONBOARDING_NEW_12; screen_onboarding_4_restore_word_init(1 /*entering the first word*/); break; case 1: - G_bolos_ux_context.onboarding_kind = 18; + G_bolos_ux_context.onboarding_kind = BOLOS_UX_ONBOARDING_NEW_18; screen_onboarding_4_restore_word_init(1 /*entering the first word*/); break; case 2: - G_bolos_ux_context.onboarding_kind = 24; + G_bolos_ux_context.onboarding_kind = BOLOS_UX_ONBOARDING_NEW_24; screen_onboarding_4_restore_word_init(1 /*entering the first word*/); break; default: diff --git a/src/ui.h b/src/ui.h index 0b4d9cb1..c46cf291 100644 --- a/src/ui.h +++ b/src/ui.h @@ -28,6 +28,8 @@ #include "ux_nanox.h" #endif +#define ARRAYLEN(array) (sizeof(array) / sizeof(array[0])) + void ui_idle_init(void); #endif diff --git a/src/ux_common/common.h b/src/ux_common/common.h index ef1d5e07..8bf29935 100644 --- a/src/ux_common/common.h +++ b/src/ux_common/common.h @@ -28,101 +28,36 @@ #ifdef OS_IO_SEPROXYHAL -#define COLOR_BG_1 0xF9F9F9 -//#define COLOR_BG_1 0xF80000 -//#define ALWAYS_INVERT - -#define KEYCODE_SWITCH '\1' -#define KEYCODE_BACKSPACE '\r' - #ifndef SPRINTF // avoid typing the size each time #define SPRINTF(strbuf, ...) snprintf((char*) (strbuf), sizeof(strbuf), __VA_ARGS__) #endif -#define ONBOARDING_CONFIRM_WORD_COUNT 24 #define ONBOARDING_WORD_COMPLETION_MAX_ITEMS 8 #define BOLOS_UX_HASH_LENGTH 4 // as on the blue -#define CONSENT_INTERVAL_MS 3000 - -extern const unsigned char hex_digits[]; - -unsigned char rng_u8_modulo(unsigned char modulo); - -void screen_hex_identifier_string_buffer(const unsigned char* buffer, unsigned int total); - -// common code for all screens -// reset the screen asynch display machine -void screen_state_init(unsigned int stack_slot); - -// common code for all screens -// start display of first declared element -void screen_display_init(unsigned int stack_slot); - -// request display of the element (taking care of calling screen displayed preprocessors) -void screen_display_element(const bagl_element_t* element); - -// prepare to return the exit code after the next display asynch ack is received (0d 00 00) -void screen_return_after_displayed_touched_element(unsigned int exit_code); - -void screen_pin_keyboard_init(unsigned int screen_current_element_arrays_index); +#define KEYBOARD_ITEM_VALIDATED \ + 1 // callback is called with the entered item index, tmp_element is precharged with element to + // be displayed and using the common string buffer as string parameter +#define KEYBOARD_RENDER_ITEM \ + 2 // callback is called with the element index, tmp_element is precharged with element to be + // displayed and using the common string buffer as string parameter +#define KEYBOARD_RENDER_WORD \ + 3 // callback is called with a -1 when requesting complete word, or the char index else, + // returnin 0 implies no char is to be displayed +typedef const bagl_element_t* (*keyboard_callback_t)(unsigned int event, unsigned int value); + +void bolos_ux_hslider3_init(unsigned int total_count); +void bolos_ux_hslider3_set_current(unsigned int current); +void bolos_ux_hslider3_next(void); +void bolos_ux_hslider3_previous(void); // all screens -void screen_saver_init(void); -void screen_saver_deinit(void); - -void screen_not_personalized_init(void); -void screen_boot_recovery_init(void); -void screen_dashboard_init(void); -unsigned int screen_is_dashboard(void); -void screen_dashboard_disable_bolos_before_app(void); -void screen_dashboard_prepare(void); -void screen_modal_validate_pin_init(void); -void screen_consent_upgrade_init(void); -void screen_consent_add_init(void); -void screen_consent_del_init(void); -void screen_consent_issuer_key_init(void); -void screen_consent_foreign_key_init(void); -void screen_consent_get_device_name_init(void); -void screen_consent_set_device_name_init(void); -void screen_boot_unsafe_wipe_init(void); -void screen_loader_init(void); -void screen_consent_ux_not_signed_init(void); -void screen_consent_customca_key_init(void); -void screen_consent_reset_customca_init(void); -void screen_consent_setup_customca_init(void); - -void screen_random_boarding_init(void); - -void screen_onboarding_0_welcome_init(void); -void screen_onboarding_1_2_pin_init(unsigned int step); -void screen_onboarding_3_new_init(void); -void screen_onboarding_4_confirm_init(unsigned int feilword); - void screen_onboarding_3_restore_init(void); #define RESTORE_WORD_ACTION_REENTER_WORD 0 #define RESTORE_WORD_ACTION_FIRST_WORD 1 void screen_onboarding_4_restore_word_init(unsigned int action); -void screen_onboarding_5_passphrase_init(void); - -void screen_onboarding_7_processing_init(void); - -void screen_settings_init(unsigned int initial); -// apply settings @ boot time -void screen_settings_apply(void); -void screen_settings_change_pin(void); -void screen_settings_erase_all(void); -void screen_settings_set_temporary(void); -void screen_settings_attach_to_pin(void); -void screen_settings_change_pin_1_2_pin_init(unsigned int initial); -void screen_settings_passphrase_attach_1_init(void); -void screen_settings_passphrase_attach_2_init(unsigned int initial); -void screen_settings_passphrase_temporary_1_init(void); -void screen_settings_passphrase_type_and_review_init(unsigned int kind); -void screen_settings_erase_all_init(void); - #define COMMON_KEYBOARD_INDEX_UNCHANGED (-1UL) #endif // OS_IO_SEPROXYHAL diff --git a/src/ux_common/common_bip39.h b/src/ux_common/common_bip39.h index aa93bea2..f629100f 100644 --- a/src/ux_common/common_bip39.h +++ b/src/ux_common/common_bip39.h @@ -5,38 +5,17 @@ // BIP39 helpers #include "onboarding_seed_rom_variables.h" -void bolos_ux_pbkdf2(unsigned char *password, - unsigned int passwordlen, - unsigned char *salt, - unsigned int saltlen, - unsigned int iterations, - unsigned char *out, - unsigned int outLength); -unsigned char bolos_ux_get_random_bip39_word(unsigned char *word); // return 0 if mnemonic is invalid unsigned int bolos_ux_mnemonic_check(unsigned char *mnemonic, unsigned int mnemonicLength); -unsigned char bolos_ux_word_check(unsigned char *word, unsigned int wordLength); - -unsigned int bolos_ux_get_word_ptr(unsigned char **word, - unsigned int max_length, - unsigned int word_index); // passphrase will be prefixed with "MNEMONIC" from BIP39, the passphrase content shall start @ 8 void bolos_ux_mnemonic_to_seed(unsigned char *mnemonic, unsigned int mnemonicLength, unsigned char *seed /*, unsigned char *workBuffer*/); -unsigned int bolos_ux_mnemonic_indexes_to_words(unsigned char *indexes, unsigned char *words); -unsigned int bolos_ux_mnemonic_from_data(unsigned char *in, - unsigned int inLength, - unsigned char *out, - unsigned int outLength); unsigned int bolos_ux_bip39_get_word_idx_starting_with(unsigned char *prefix, unsigned int prefixlength); unsigned int bolos_ux_bip39_idx_strcpy(unsigned int index, unsigned char *buffer); -unsigned int bolos_ux_bip39_idx_startswith(unsigned int idx, - unsigned char *prefix, - unsigned int prefixlength); unsigned int bolos_ux_bip39_get_word_count_starting_with(unsigned char *prefix, unsigned int prefixlength); unsigned int bolos_ux_bip39_get_word_next_letters_starting_with(unsigned char *prefix, diff --git a/src/ux_common/onboarding_seed_bip39.c b/src/ux_common/onboarding_seed_bip39.c index 00d06a99..0c7d7ac0 100644 --- a/src/ux_common/onboarding_seed_bip39.c +++ b/src/ux_common/onboarding_seed_bip39.c @@ -6,45 +6,6 @@ #include "onboarding_seed_rom_variables.h" #include "common.h" -unsigned int bolos_ux_mnemonic_from_data(unsigned char* in, - unsigned int inLength, - unsigned char* out, - unsigned int outLength) { - unsigned char bits[32 + 1]; - unsigned int mlen = inLength * 3 / 4; - unsigned int i, j, idx, offset; - - if ((inLength % 4) || (inLength < 16) || (inLength > 32)) { - THROW(INVALID_PARAMETER); - } - cx_hash_sha256(in, inLength, bits, 32); - - bits[inLength] = bits[0]; - memcpy(bits, in, inLength); - offset = 0; - for (i = 0; i < mlen; i++) { - size_t wordLength; - idx = 0; - for (j = 0; j < 11; j++) { - idx <<= 1; - idx += (bits[(i * 11 + j) / 8] & (1 << (7 - ((i * 11 + j) % 8)))) > 0; - } - wordLength = BIP39_WORDLIST_OFFSETS[idx + 1] - BIP39_WORDLIST_OFFSETS[idx]; - if ((offset + wordLength) > outLength) { - THROW(INVALID_PARAMETER); - } - memcpy(out + offset, BIP39_WORDLIST + BIP39_WORDLIST_OFFSETS[idx], wordLength); - offset += wordLength; - if (i < mlen - 1) { - if (offset > outLength) { - THROW(INVALID_PARAMETER); - } - out[offset++] = ' '; - } - } - return offset; -} - // separated function to lower the stack usage when jumping into pbkdf algorithm unsigned int bolos_ux_mnemonic_to_seed_hash_length128(unsigned char* mnemonic, unsigned int mnemonicLength) { @@ -74,33 +35,6 @@ void bolos_ux_mnemonic_to_seed(unsigned char* mnemonic, // what happen to the second block for a very short seed ? } -unsigned int bolos_ux_get_word_ptr(unsigned char** word, - unsigned int max_length, - unsigned int word_index) { - unsigned int word_length; - - // seek next word - while (word_index--) { - while (*(*word) != ' ' && max_length) { - *word = (*word) + 1; - max_length--; - } - // also skip the space - *word = (*word) + 1; - max_length--; - } - - // seek next word's length - word_length = - 0; // could optim by using the smaller word length here (EOS or space as delim here) - while (word_length < max_length && (*word)[word_length] != ' ' && (*word)[word_length] != 0) { - word_length++; - } - - // word ptr is returned in the parameter - return word_length; -} - unsigned int bolos_ux_mnemonic_check(unsigned char* mnemonic, unsigned int mnemonicLength) { unsigned int i, n = 0; unsigned int bi; @@ -192,23 +126,6 @@ unsigned int bolos_ux_bip39_idx_strcpy(unsigned int index, unsigned char* buffer return 0; } -unsigned int bolos_ux_bip39_idx_startswith(unsigned int index, - unsigned char* prefix, - unsigned int prefixlength) { - unsigned int j = 0; - if (index < BIP39_WORDLIST_OFFSETS_LENGTH - 1) { - while (j < (unsigned int) (BIP39_WORDLIST_OFFSETS[index + 1] - - BIP39_WORDLIST_OFFSETS[index]) && - BIP39_WORDLIST[BIP39_WORDLIST_OFFSETS[index] + j] == prefix[j]) { - j++; - } - if (j == prefixlength) { - return 1; - } - } - return 0; -} - unsigned int bolos_ux_bip39_get_word_idx_starting_with(unsigned char* prefix, unsigned int prefixlength) { unsigned int i; diff --git a/src/ux_nanos.c b/src/ux_nanos.c index 87a5972d..8bccad6e 100644 --- a/src/ux_nanos.c +++ b/src/ux_nanos.c @@ -15,21 +15,12 @@ * limitations under the License. ********************************************************************************/ -#include +#include "ux_nanos.h" #ifdef TARGET_NANOS -#include - -#include -#include - -#include "ux_nanos.h" - //#ifdef OS_IO_SEPROXYHAL -#define ARRAYLEN(array) (sizeof(array) / sizeof(array[0])) - bolos_ux_context_t G_bolos_ux_context; unsigned short io_timeout(unsigned short last_timeout) { @@ -38,100 +29,6 @@ unsigned short io_timeout(unsigned short last_timeout) { return 1; } -const unsigned char C_app_empty_colors[] = { - 0x00, - 0x00, - 0x00, - 0x00, - 0xFF, - 0xFF, - 0xFF, - 0xFF, -}; - -const unsigned char C_app_empty_bitmap[] = { - // color index table - 0x01, - 0x00, - 0x00, - 0x00, - 0x00, - 0xFF, - 0xFF, - 0xFF, - 0xFF, - - // icon mask - 0x00, - 0x00, - 0xF0, - 0x0F, - 0xFC, - 0x3F, - 0xFC, - 0x3F, - 0xFE, - 0x7F, - 0xFE, - 0x7F, - 0xFE, - 0x7F, - 0xFE, - 0x7F, - 0xFE, - 0x7F, - 0xFE, - 0x7F, - 0xFE, - 0x7F, - 0xFE, - 0x7F, - 0xFC, - 0x3F, - 0xFC, - 0x3F, - 0xF0, - 0x0F, - 0x00, - 0x00, -}; - -// prepare the app icon as if it was a icon_detail_t encoded structure in the -// string_buffer -void screen_prepare_masked_icon(unsigned char *icon_bitmap, unsigned int icon_bitmap_length) { - unsigned int i, inversemode; - bagl_icon_details_t *icon_details = (bagl_icon_details_t *) G_bolos_ux_context.string_buffer; - unsigned char *bitmap = - (unsigned char *) G_bolos_ux_context.string_buffer + sizeof(bagl_icon_details_t); - - icon_details->width = 16; - icon_details->height = 16; - // prepare the icon_details content - icon_details->bpp = C_app_empty_bitmap[0]; - // use color table from the const - icon_details->colors = (const unsigned int *) C_app_empty_colors; - icon_details->bitmap = bitmap; - - // when first color of the bitmap is not 0, then, must inverse the icon's - // bit to - // match the C_app_empty_bitmap bit value - inversemode = 0; - if (icon_bitmap[1] != 0 || icon_bitmap[2] != 0 || icon_bitmap[3] != 0 || icon_bitmap[4] != 0) { - inversemode = 1; - } - - for (i = 1 + 8; i < sizeof(C_app_empty_bitmap) && i < icon_bitmap_length; i++) { - if (inversemode) { - bitmap[i - 1 - 8] = C_app_empty_bitmap[i] & (~icon_bitmap[i]); - } else { - bitmap[i - 1 - 8] = C_app_empty_bitmap[i] & icon_bitmap[i]; - } - } - - // the string buffer is now ready to be displayed as an icon details - // structure -} - void io_seproxyhal_display(const bagl_element_t *element) { io_seproxyhal_display_default(element); } diff --git a/src/ux_nanos.h b/src/ux_nanos.h index 56285e42..15c7261d 100644 --- a/src/ux_nanos.h +++ b/src/ux_nanos.h @@ -14,8 +14,8 @@ * limitations under the License. ********************************************************************************/ -#ifndef BOLOS_UX_H -#define BOLOS_UX_H +#ifndef BOLOS_UX_NANOS_H +#define BOLOS_UX_NANOS_H #ifdef HAVE_BOLOS_UX @@ -23,77 +23,8 @@ typedef unsigned int (*callback_t)(unsigned int); -#define KEYBOARD_ITEM_VALIDATED \ - 1 // callback is called with the entered item index, tmp_element is precharged with element to - // be displayed and using the common string buffer as string parameter -#define KEYBOARD_RENDER_ITEM \ - 2 // callback is called with the element index, tmp_element is precharged with element to be - // displayed and using the common string buffer as string parameter -#define KEYBOARD_RENDER_WORD \ - 3 // callback is called with a -1 when requesting complete word, or the char index else, - // returnin 0 implies no char is to be displayed -typedef const bagl_element_t *(*keyboard_callback_t)(unsigned int event, unsigned int value); - // bolos ux context (not mandatory if redesigning a bolos ux) typedef struct bolos_ux_context { -#define STATE_UNINITIALIZED 0 -#ifndef STATE_INITIALIZED -#define STATE_INITIALIZED 0xB01055E5UL -#endif // STATE_INITIALIZED - unsigned int state; - - // unified arrays - struct { - // arrays of element to be displayed (to automate when dealing with - // static and dynamic elements) - struct { - const bagl_element_t *element_array; - unsigned int element_array_count; - } element_arrays[2]; // not more than 2 arrays of elements are in use - // for any screen - unsigned int element_arrays_count; - unsigned int element_index; - - unsigned int exit_code_after_elements_displayed; - unsigned int displayed; - callback_t displayed_callback; - // callback called before the screen callback to change the keyboard - // face - bagl_element_callback_t screen_before_element_display_callback; - button_push_callback_t button_push_callback; - - callback_t ticker_callback; - unsigned int ticker_value; - unsigned int ticker_interval; - - // [onboarding/dashboard/settings] | [pin] | [help] | [screensaver] - } screen_stack[4]; - - unsigned int screen_stack_count; // initialized @0 by the bolos ux initialize - // a screen pop occurred, the underlying screen must optimize its drawing as - // we've probably trashed the whole screen - unsigned int screen_redraw; - - unsigned int ms; - unsigned int setting_auto_lock_delay_ms; - -#define IS_SETTING_PRE_POWER_OFF() \ - (G_bolos_ux_context.setting_auto_lock_delay_ms != -1UL && \ - G_bolos_ux_context.setting_auto_lock_delay_ms != 0) -#define INACTIVITY_MS_AUTO_LOCK (G_bolos_ux_context.setting_auto_lock_delay_ms) - unsigned int ms_last_activity; - - enum { - INACTIVITY_NONE, - INACTIVITY_LOCK, - } inactivity_state; - - bagl_element_t tmp_element; - - unsigned int exit_code; - - unsigned int last_ux_id; - #define BOLOS_UX_ONBOARDING_NEW 1 #define BOLOS_UX_ONBOARDING_NEW_12 12 #define BOLOS_UX_ONBOARDING_NEW_18 18 @@ -111,9 +42,6 @@ typedef struct bolos_ux_context { unsigned int onboarding_step; unsigned int onboarding_index; unsigned int onboarding_words_checked; - unsigned int onboarding_words_are_valid; - unsigned int onboarding_step_checked_inc; - unsigned int onboarding_step_checked; unsigned int words_buffer_length; // after an int to make sure it's aligned @@ -126,18 +54,6 @@ typedef struct bolos_ux_context { #define MAX_PIN_LENGTH 8 #define MIN_PIN_LENGTH 4 - char pin_buffer[MAX_PIN_LENGTH + 1]; // length prepended for custom pin length - - // filled up during os_ux syscall when called by user or bolos. - bolos_ux_params_t parameters; - - unsigned int settings_index; - unsigned int settings_value; - - // unsigned int saver_step; - // unsigned int saver_konami; - int saver_step_x; - int saver_step_y; // slider management unsigned int hslider3_before; @@ -147,17 +63,6 @@ typedef struct bolos_ux_context { keyboard_callback_t keyboard_callback; - // dashboard last selected item - unsigned int dashboard_last_selected; - unsigned int dashboard_redisplayed; // to trigger animation when all - // elements are displayed - // in case autostart is engaged, to avoid starting the app multiple times - unsigned int app_auto_started; - - // to return to the current context after help screens have been validated - appmain_t help_ended_callback; - unsigned int help_screen_idx; - // detect stack/global variable overlap // have a zero byte to avoid buffer overflow from strings in the ux (we never // know) @@ -165,7 +70,6 @@ typedef struct bolos_ux_context { unsigned int canary; // for CheckSeed app only - uint8_t input_seed_is_identical; uint8_t processing; } bolos_ux_context_t; @@ -175,19 +79,6 @@ extern bolos_ux_context_t G_bolos_ux_context; // update before, current, after index for horizontal slider with 3 positions // slider distinguish handling from the data, to be more generic :) #define BOLOS_UX_HSLIDER3_NONE (-1UL) -void bolos_ux_hslider3_init(unsigned int total_count); -void bolos_ux_hslider3_set_current(unsigned int current); -void bolos_ux_hslider3_next(void); -void bolos_ux_hslider3_previous(void); - -#define FAST_LIST_THRESHOLD_CS 8 -#define FAST_LIST_ACTION_CS 2 - -/** - * Bolos system app internal UX entry point (could be overridden by a further - * loaded BOLOS_UX application) - */ -void bolos_ux_main(void); void screen_common_keyboard_init(unsigned int stack_slot, unsigned int current_element, diff --git a/src/ux_nanox.c b/src/ux_nanox.c index c2902cca..5b94dc59 100644 --- a/src/ux_nanox.c +++ b/src/ux_nanox.c @@ -14,18 +14,10 @@ * limitations under the License. ********************************************************************************/ -#include +#include "ux_nanox.h" #if defined(TARGET_NANOX) || defined(TARGET_NANOS2) -#include -#include - -#include -#include - -#include "ux_nanox.h" - #ifdef OS_IO_SEPROXYHAL bolos_ux_context_t G_bolos_ux_context; @@ -36,33 +28,10 @@ unsigned short io_timeout(unsigned short last_timeout) { return 1; } -void screen_hex_identifier_string_buffer(const unsigned char* buffer, unsigned int total) { - SPRINTF(G_ux.string_buffer, - "%.*H...%.*H", - BOLOS_UX_HASH_LENGTH / 2, - buffer, - BOLOS_UX_HASH_LENGTH / 2, - buffer + total - BOLOS_UX_HASH_LENGTH / 2); -} - void io_seproxyhal_display(const bagl_element_t* element) { io_seproxyhal_display_default((bagl_element_t*) element); } -void screen_keyboard_validate_entered_text(void) { - // we're returning the typed text - G_ux.exit_code = BOLOS_UX_OK; - // last keycode is \n - G_ux_params.u.keyboard.keycode = '\n'; - // copy output text - memcpy(G_ux_params.u.keyboard.entered_text, - G_bolos_ux_context.keyboard_user_buffer, - sizeof(G_ux_params.u.keyboard.entered_text)); - // pop the screen - ux_stack_pop(); - ux_stack_redisplay(); -} - void bolos_ux_hslider3_init(unsigned int total_count) { G_bolos_ux_context.hslider3_total = total_count; switch (total_count) { @@ -157,58 +126,6 @@ void bolos_ux_hslider3_previous(void) { } } -unsigned int screen_consent_button(unsigned int button_mask, unsigned int button_mask_counter) { - UNUSED(button_mask_counter); - switch (button_mask) { - case BUTTON_EVT_RELEASED | BUTTON_LEFT: - G_ux.exit_code = BOLOS_UX_CANCEL; - break; - case BUTTON_EVT_RELEASED | BUTTON_RIGHT: - G_ux.exit_code = BOLOS_UX_OK; - break; - } - return 0; -} - -unsigned int screen_consent_ticker(unsigned int ignored) { - UNUSED(ignored); - - // prepare displaying next screen - G_bolos_ux_context.onboarding_index = - (G_bolos_ux_context.onboarding_index + 1) % G_bolos_ux_context.onboarding_step; - - // redisplay taking into account the new counter - ux_stack_display(0); - return 0; -} - -void screen_consent_set_interval(unsigned int interval_ms) { - G_ux.stack[0].ticker_value = interval_ms; - G_ux.stack[0].ticker_interval = interval_ms; -} - -void screen_consent_ticker_init(unsigned int number_of_steps, - unsigned int interval_ms, - unsigned int check_pin_to_confirm) { - UNUSED(check_pin_to_confirm); - // register action callbacks - G_ux.stack[0].ticker_value = interval_ms; - G_ux.stack[0].ticker_interval = interval_ms; - G_ux.stack[0].ticker_callback = screen_consent_ticker; - /*if (!check_pin_to_confirm || ! os_perso_isonboarded() == BOLOS_UX_OK) { managed at bolos task - * level */ - G_ux.stack[0].button_push_callback = screen_consent_button; - /*} - else { - G_ux.stack[0].button_push_callback = screen_consent_button_with_final_pin; - }*/ - - // start displaying - G_bolos_ux_context.onboarding_index = number_of_steps - 1; - G_bolos_ux_context.onboarding_step = number_of_steps; - screen_consent_ticker(0); -} - #endif // OS_IO_SEPROXYHAL #endif diff --git a/src/ux_nanox.h b/src/ux_nanox.h index 1594cc38..498d92a5 100644 --- a/src/ux_nanox.h +++ b/src/ux_nanox.h @@ -14,88 +14,15 @@ * limitations under the License. ********************************************************************************/ -#ifndef BOLOS_UX_H -#define BOLOS_UX_H +#ifndef BOLOS_UX_NANOX_H +#define BOLOS_UX_NANOX_H #ifdef HAVE_BOLOS_UX #include "ux_common/common.h" -typedef unsigned int (*pin_callback_t)(unsigned char* pin_buffer, unsigned int pin_length); - -#define KEYBOARD_ITEM_VALIDATED \ - 1 // callback is called with the entered item index, tmp_element is precharged with element to - // be displayed and using the common string buffer as string parameter -#define KEYBOARD_RENDER_ITEM \ - 2 // callback is called with the element index, tmp_element is precharged with element to be - // displayed and using the common string buffer as string parameter -#define KEYBOARD_RENDER_WORD \ - 3 // callback is called with a -1 when requesting complete word, or the char index else, - // returnin 0 implies no char is to be displayed -typedef const bagl_element_t* (*keyboard_callback_t)(unsigned int event, unsigned int value); - -#define ICON_14x14_SIZE_B \ - (UPPER_ALIGN(GLYPH_dashboard_mask_WIDTH * GLYPH_dashboard_mask_HEIGHT, 8, unsigned int) / 8) - -typedef struct { - bagl_icon_details_t details; - unsigned char bitmap[ICON_14x14_SIZE_B]; -} ux_icon_buffer_t; - -#define APPLICATION_MAXCOUNT 100 - // bolos ux context (not mandatory if redesigning a bolos ux) typedef struct bolos_ux_context { -#define STATE_UNINITIALIZED 0 -#ifndef STATE_INITIALIZED -#define STATE_INITIALIZED 0xB01055E5UL -#endif // STATE_INITIALIZED - unsigned int state; - - // a screen pop occurred, the underlying screen must optimize its drawing as we've probably - // trashed the whole screen - unsigned int screen_redraw; - -#define BATTERY_CHECK_INTERVAL_MS 5000 - unsigned int ms_last_batt_check; - - unsigned int ms; - unsigned int setting_auto_lock_delay_ms; - unsigned int setting_power_off_delay_ms; - -#define AUTO_LOCK_DEFAULT 300000 -#define POWER_OFF_DEFAULT 600000 - -#define IS_SETTING_PRE_POWER_OFF() \ - (G_bolos_ux_context.setting_auto_lock_delay_ms != -1UL && \ - G_bolos_ux_context.setting_auto_lock_delay_ms != 0) -#define IS_SETTING_POWER_OFF() \ - (G_bolos_ux_context.setting_power_off_delay_ms != -1UL && \ - G_bolos_ux_context.setting_power_off_delay_ms != 0) -#define INACTIVITY_MS_AUTO_LOCK (G_bolos_ux_context.setting_auto_lock_delay_ms) -#define INACTIVITY_MS_POWER_OFF \ - (/*G_bolos_ux_context.setting_auto_lock_delay_ms+*/ G_bolos_ux_context \ - .setting_power_off_delay_ms) - unsigned int ms_last_activity; - unsigned int ms_last_activity_saver; - unsigned int ms_last_activity_pwroff; - - unsigned int last_button_state; - - // control center display push both buttons delay -#define BOLOS_CC_PERIOD_MS 1500UL - unsigned int ms_cc_start; - - enum { - INACTIVITY_NONE, - INACTIVITY_LOCK, - INACTIVITY_SAVER, - } inactivity_state; - - unsigned int control_center_entering; - - unsigned int last_ux_id; - #define BOLOS_UX_ONBOARDING_NEW 1 #define BOLOS_UX_ONBOARDING_NEW_12 12 #define BOLOS_UX_ONBOARDING_NEW_18 18 @@ -113,9 +40,6 @@ typedef struct bolos_ux_context { unsigned int onboarding_step; unsigned int onboarding_index; unsigned int onboarding_words_checked; - unsigned int onboarding_words_are_valid; - unsigned int onboarding_step_checked_inc; - unsigned int onboarding_step_checked; unsigned int words_buffer_length; // 128 of words (215 => hashed to 64, or 128) + HMAC_LENGTH*2 = 256 @@ -123,31 +47,13 @@ typedef struct bolos_ux_context { char words_buffer[WORDS_BUFFER_MAX_SIZE_B]; #define MAX_PIN_LENGTH 8 #define MIN_PIN_LENGTH 4 - pin_callback_t pin_end_callback; - char pin_buffer[MAX_PIN_LENGTH + 1]; // length prepended for custom pin length - char pin_digit_buffer; // digit to be displayed + char pin_digit_buffer; // digit to be displayed appmain_t flow_end_callback; - // unsigned int settings_index; - // unsigned int settings_value; - - bagl_element_t saver_element; - // int saver_step_x; - // int saver_step_y; -#define SAVER_ELEMENTS 8 - struct { - int step_x; - int step_y; - int x; - int y; - } savers[SAVER_ELEMENTS]; - // label line for common PIN and common keyboard screen (displayed over the entry) const char* common_label; - unsigned int pairing_stack_slot_plus_1; - // slider management / menu list management unsigned int hslider3_before; unsigned int hslider3_current; @@ -155,28 +61,6 @@ typedef struct bolos_ux_context { unsigned int hslider3_total; keyboard_callback_t keyboard_callback; - unsigned char keyboard_user_buffer[BOLOS_UX_KEYBOARD_TEXT_BUFFER_SIZE + 1]; - - unsigned int last_installed_os_index; - - unsigned int control_center_enabled; - unsigned int control_center_current; - unsigned int control_center_total; - unsigned char* control_center_mapping; - unsigned char control_center_string[MAX(128, sizeof(bagl_icon_details_t) - 1)]; - - unsigned int dashboard_current; // index of the currently selected icon on the dashboard - unsigned int dashboard_previous; // index of the previously selected icon on the dashboard - unsigned int - dashboard_first; // index of the first item on the top line displayed on the dashboard - unsigned int dashboard_total; - unsigned char dashboard_sorted_app_os_idx[APPLICATION_MAXCOUNT]; - unsigned int dashboard_last_selected; - // unsigned int dashboard_redisplayed; // to trigger animation when all elements are displayed - // unsigned int dashboard_no_apps_displayed; // when the message saying no apps installed has - // been displayed or not - // in case autostart is engaged, to avoid starting the app multiple times - unsigned int app_auto_started; unsigned int overlay_refresh; unsigned int battery_percentage; @@ -197,41 +81,21 @@ typedef struct bolos_ux_context { #define BATTERY_CRITICAL_LEVEL_PERCENT 10 #define BATTERY_AUTO_POWER_OFF_LEVEL_PERCENT 2 - // to return to the current context after help screens have been validated - appmain_t help_ended_callback; - unsigned int help_screen_idx; - // detect stack/global variable overlap // have a zero byte to avoid buffer overflow from strings in the ux (we never know) #define CANARY_MAGIC 0x7600E9AB unsigned int canary; // for CheckSeed app only - uint8_t input_seed_is_identical; uint8_t processing; } bolos_ux_context_t; extern bolos_ux_context_t G_bolos_ux_context; -void ux_stack_display_elements(ux_stack_slot_t* slot); // not to be displayed in the SDK - // update before, current, after index for horizontal slider with 3 positions // slider distinguish handling from the data, to be more generic :) #define BOLOS_UX_HSLIDER3_NONE (-1UL) -void bolos_ux_hslider3_init(unsigned int total_count); -void bolos_ux_hslider3_set_current(unsigned int current); -void bolos_ux_hslider3_next(void); -void bolos_ux_hslider3_previous(void); - -#define FAST_LIST_THRESHOLD_CS 8 -#define FAST_LIST_ACTION_CS 2 - -/** - * Bolos system app internal UX entry point (could be overridden by a further loaded BOLOS_UX - * application) - */ -void bolos_ux_main(void); void screen_common_keyboard_init(unsigned int stack_slot, unsigned int current_element, @@ -240,72 +104,9 @@ void screen_common_keyboard_init(unsigned int stack_slot, #include "ux_common/common_bip39.h" -#define UX_FLOW_AFTER_PIN(stepname, stackslot, callback) \ - UX_STEP_INIT(stepname, NULL, NULL, { \ - /* invalidate pin and display pin lock */ \ - screen_modal_validate_pin_init(); \ - /* display processing screen on slot 0 (pin modal has been pushed over) */ \ - /* after display then perform */ \ - ux_stack_init(stackslot); \ - G_ux.stack[stackslot].displayed_callback = callback; \ - }); - // to be included into all flow that needs to go back to the dashboard extern const ux_flow_step_t ux_ob_goto_dashboard_step; -void ux_pairing_init(void); -void ux_pairing_deinit(void); - -void ux_boot_menu_init(void); -void ux_ob_onboarding_init(void); -void screen_control_center_init(unsigned int); - -void screen_help_init(appmain_t help_ended_callback); - -void screen_processing_init(void); - -void screen_prepare_app_name(unsigned int appidx); - -bagl_icon_details_t* screen_prepare_bitmap_14x14(ux_icon_buffer_t* icon_buffer, - const unsigned char* icon_bitmap, - unsigned int masked_inverted); -void screen_prepare_app_icon_14x14(ux_icon_buffer_t* icon_buffer, - const unsigned char* icon_bitmap, - unsigned int icon_bitmap_length, - unsigned int masked_inverted); -void screen_prepare_app_icon(ux_icon_buffer_t* icon_buffer, - unsigned char* icon_bitmap_buffer, - unsigned int icon_bitmap_buffer_length, - unsigned int appidx, - unsigned int inverted); - -void screen_lock(void); - -#ifdef BOLOS_OS_UPGRADER -void screen_os_upgrader(void); -#endif // BOLOS_OS_UPGRADER - -unsigned int screen_consent_button(unsigned int button_mask, unsigned int button_mask_counter); -unsigned int screen_consent_ticker(unsigned int ignored); -void screen_consent_ticker_init(unsigned int number_of_steps, - unsigned int interval_ms, - unsigned int check_pin_to_confirm); -void screen_consent_set_interval(unsigned int interval_ms); - -void screen_common_pin_init(unsigned int stack_slot, pin_callback_t end_callback); - -void screen_keyboard_init(char* buffer, unsigned int maxsize, appmain_t validation_callback); - -void debug(unsigned int id, unsigned char* msg); - -void screen_mcu_upgrade_required_init(void); -void screen_consent_run_app_init(void); -void screen_boot_delay_init(void); - -void settings_general_version(appmain_t end_callback); -void settings_general_regulatory(appmain_t end_callback); -void settings_general_serial(appmain_t end_callback); - #endif // HAVE_BOLOS_UX #endif // BOLOS_UX_H From 9e608085139fd21d28b166228372bcfd819be120 Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Fri, 7 Oct 2022 14:30:23 +0200 Subject: [PATCH 12/65] [fix] 'sizeof' differs from 'strlen' --- src/nanos_enter_phrase.c | 2 +- src/nanox_enter_phrase.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/nanos_enter_phrase.c b/src/nanos_enter_phrase.c index 87e06c36..b17d410c 100644 --- a/src/nanos_enter_phrase.c +++ b/src/nanos_enter_phrase.c @@ -237,7 +237,7 @@ void compare_recovery_phrase(void) { cx_hmac_sha512_t ctx; const char key[] = "Bitcoin seed"; - cx_hmac_sha512_init(&ctx, (const uint8_t*) key, sizeof(key)); + cx_hmac_sha512_init(&ctx, (const uint8_t*) key, strlen(key)); cx_hmac((cx_hmac_t*) &ctx, CX_LAST, buffer, 64, buffer, 64); PRINTF("Root key from input:\n%.*H\n", 64, buffer); diff --git a/src/nanox_enter_phrase.c b/src/nanox_enter_phrase.c index 263493f5..faa5bed2 100644 --- a/src/nanox_enter_phrase.c +++ b/src/nanox_enter_phrase.c @@ -411,7 +411,7 @@ uint8_t compare_recovery_phrase(void) { cx_hmac_sha512_t ctx; const char key[] = "Bitcoin seed"; - cx_hmac_sha512_init(&ctx, (const uint8_t*) key, sizeof(key)); + cx_hmac_sha512_init(&ctx, (const uint8_t*) key, strlen(key)); cx_hmac((cx_hmac_t*) &ctx, CX_LAST, buffer, 64, buffer, 64); PRINTF("Root key from input:\n%.*H\n", 64, buffer); From f11a4b541b8b7c16d0d2526865575f94c4bb9539 Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Thu, 13 Oct 2022 11:23:39 +0200 Subject: [PATCH 13/65] [clean] includes and makefile --- Makefile | 7 ++- src/main.c | 5 -- src/nanos_pick_phrase_length.c | 51 ------------------- src/ui.c | 37 ++++++++++++++ src/ui.h | 7 ++- src/ux_common/common.h | 5 +- src/ux_common/common_bip39.h | 5 +- src/ux_common/onboarding_seed_rom_variables.h | 5 +- src/ux_nanos.c | 2 +- src/ux_nanos.h | 11 ++-- src/ux_nanox.h | 11 ++-- 11 files changed, 55 insertions(+), 91 deletions(-) delete mode 100644 src/nanos_pick_phrase_length.c diff --git a/Makefile b/Makefile index 689efadf..449ba6b5 100755 --- a/Makefile +++ b/Makefile @@ -18,6 +18,7 @@ ifeq ($(BOLOS_SDK),) $(error Environment variable BOLOS_SDK is not set) endif + include $(BOLOS_SDK)/Makefile.defines all: default @@ -30,9 +31,9 @@ APPVERSION_N = 1 APPVERSION_P = 0 APPVERSION = "$(APPVERSION_M).$(APPVERSION_N).$(APPVERSION_P)" -APP_LOAD_PARAMS = --appFlags 0x210 $(COMMON_LOAD_PARAMS) --apdu --curve secp256k1 --path "" +APP_LOAD_PARAMS = --appFlags 0x10 $(COMMON_LOAD_PARAMS) --apdu --curve secp256k1 --path "" -ifeq ($(TARGET_NAME),TARGET_NANOS) +ifeq ($(TARGET_NAME), TARGET_NANOS) ICONNAME=icons/nanos_app_recovery_check.gif else ICONNAME=icons/nanox_app_recovery_check.gif @@ -103,12 +104,10 @@ endif CC := $(CLANGPATH)clang CFLAGS += -O3 -Os -Wshadow -Wformat AS := $(GCCPATH)arm-none-eabi-gcc -AFLAGS += LD := $(GCCPATH)arm-none-eabi-gcc LDFLAGS += -O3 -Os LDLIBS += -lm -lgcc -lc -# import rules to compile glyphs(/pone) include $(BOLOS_SDK)/Makefile.glyphs APP_SOURCE_PATH += src diff --git a/src/main.c b/src/main.c index ea83d18b..6e98916f 100644 --- a/src/main.c +++ b/src/main.c @@ -31,16 +31,11 @@ enum UI_STATE { UI_IDLE, UI_TEXT, UI_APPROVAL }; extern enum UI_STATE uiState; #if defined(TARGET_NANOX) || defined(TARGET_NANOS2) -#include "ux_nanox.h" uint8_t compare_recovery_phrase(void); #else -#include "ux_nanos.h" void compare_recovery_phrase(void); #endif -ux_state_t G_ux; -bolos_ux_params_t G_ux_params; - unsigned short io_exchange_al(unsigned char channel, unsigned short tx_len) { switch (channel & ~(IO_FLAGS)) { case CHANNEL_KEYBOARD: diff --git a/src/nanos_pick_phrase_length.c b/src/nanos_pick_phrase_length.c deleted file mode 100644 index 81d643a3..00000000 --- a/src/nanos_pick_phrase_length.c +++ /dev/null @@ -1,51 +0,0 @@ -/******************************************************************************* - * (c) 2016-2022 Ledger SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - ********************************************************************************/ - -#include "ui.h" - -#ifdef TARGET_NANOS - -UX_STEP_CB(restore_3_1_1, bb, G_bolos_ux_context.onboarding_kind = BOLOS_UX_ONBOARDING_NEW_24; - screen_onboarding_4_restore_word_init(RESTORE_WORD_ACTION_FIRST_WORD); - , - { - "Recovery phrase", - "with 24 words", - }); - -UX_STEP_CB(restore_3_1_2, bb, G_bolos_ux_context.onboarding_kind = BOLOS_UX_ONBOARDING_NEW_18; - screen_onboarding_4_restore_word_init(RESTORE_WORD_ACTION_FIRST_WORD); - , - { - "Recovery phrase", - "with 18 words", - }); - -UX_STEP_CB(restore_3_1_3, bb, G_bolos_ux_context.onboarding_kind = BOLOS_UX_ONBOARDING_NEW_12; - screen_onboarding_4_restore_word_init(RESTORE_WORD_ACTION_FIRST_WORD); - , - { - "Recovery phrase", - "with 12 words", - }); - -UX_FLOW(restore_3_1, &restore_3_1_1, &restore_3_1_2, &restore_3_1_3); - -void screen_onboarding_3_restore_init(void) { - ux_flow_init(0, restore_3_1, NULL); -} - -#endif diff --git a/src/ui.c b/src/ui.c index a3c9253b..16f4874a 100644 --- a/src/ui.c +++ b/src/ui.c @@ -1,23 +1,59 @@ +#include #include "ui.h" enum UI_STATE { UI_IDLE, UI_TEXT, UI_APPROVAL }; enum UI_STATE uiState; +ux_state_t G_ux; +bolos_ux_params_t G_ux_params; + #if defined(TARGET_NANOS) +UX_STEP_CB(restore_3_1_1, bb, G_bolos_ux_context.onboarding_kind = BOLOS_UX_ONBOARDING_NEW_24; + screen_onboarding_4_restore_word_init(RESTORE_WORD_ACTION_FIRST_WORD); + , + { + "Recovery phrase", + "with 24 words", + }); + +UX_STEP_CB(restore_3_1_2, bb, G_bolos_ux_context.onboarding_kind = BOLOS_UX_ONBOARDING_NEW_18; + screen_onboarding_4_restore_word_init(RESTORE_WORD_ACTION_FIRST_WORD); + , + { + "Recovery phrase", + "with 18 words", + }); + +UX_STEP_CB(restore_3_1_3, bb, G_bolos_ux_context.onboarding_kind = BOLOS_UX_ONBOARDING_NEW_12; + screen_onboarding_4_restore_word_init(RESTORE_WORD_ACTION_FIRST_WORD); + , + { + "Recovery phrase", + "with 12 words", + }); + +UX_FLOW(restore_3_1, &restore_3_1_1, &restore_3_1_2, &restore_3_1_3); + +void screen_onboarding_3_restore_init(void) { + ux_flow_init(0, restore_3_1, NULL); +} + UX_STEP_VALID(ux_idle_flow_1_step, pbb, screen_onboarding_3_restore_init();, { &C_badge, "Check your", "recovery phrase", }); + UX_STEP_NOCB(ux_idle_flow_3_step, bn, { "Version", APPVERSION, }); + UX_STEP_VALID(ux_idle_flow_4_step, pb, os_sched_exit(-1), @@ -25,6 +61,7 @@ UX_STEP_VALID(ux_idle_flow_4_step, &C_icon_dashboard_x, "Quit", }); + UX_FLOW(ux_idle_flow, &ux_idle_flow_1_step, &ux_idle_flow_3_step, &ux_idle_flow_4_step); #elif defined(TARGET_NANOX) || defined(TARGET_NANOS2) diff --git a/src/ui.h b/src/ui.h index c46cf291..bfeeef44 100644 --- a/src/ui.h +++ b/src/ui.h @@ -14,14 +14,15 @@ * limitations under the License. ********************************************************************************/ -#ifndef _UI_ -#define _UI_ +#pragma once #include #include #include #include +#include "glyphs.h" + #if defined(TARGET_NANOS) #include "ux_nanos.h" #elif defined(TARGET_NANOX) || defined(TARGET_NANOS2) @@ -31,5 +32,3 @@ #define ARRAYLEN(array) (sizeof(array) / sizeof(array[0])) void ui_idle_init(void); - -#endif diff --git a/src/ux_common/common.h b/src/ux_common/common.h index 8bf29935..2be31a6f 100644 --- a/src/ux_common/common.h +++ b/src/ux_common/common.h @@ -14,8 +14,7 @@ * limitations under the License. ********************************************************************************/ -#ifndef COMMON_UX_H -#define COMMON_UX_H +#pragma once #include #include @@ -61,5 +60,3 @@ void screen_onboarding_4_restore_word_init(unsigned int action); #define COMMON_KEYBOARD_INDEX_UNCHANGED (-1UL) #endif // OS_IO_SEPROXYHAL - -#endif // COMMON_UX_H diff --git a/src/ux_common/common_bip39.h b/src/ux_common/common_bip39.h index f629100f..767bfbeb 100644 --- a/src/ux_common/common_bip39.h +++ b/src/ux_common/common_bip39.h @@ -1,6 +1,5 @@ -#ifndef COMMON_BIP39_H -#define COMMON_BIP39_H +#pragma once // BIP39 helpers #include "onboarding_seed_rom_variables.h" @@ -32,5 +31,3 @@ unsigned int bolos_ux_electrum_mnemonic_check(unsigned int version, unsigned int mnemonicLength); #endif - -#endif // COMMON_BIP39 diff --git a/src/ux_common/onboarding_seed_rom_variables.h b/src/ux_common/onboarding_seed_rom_variables.h index 80b29608..2abc1b8b 100644 --- a/src/ux_common/onboarding_seed_rom_variables.h +++ b/src/ux_common/onboarding_seed_rom_variables.h @@ -14,8 +14,7 @@ * limitations under the License. ********************************************************************************/ -#ifndef BUSRV_H -#define BUSRV_H +#pragma once #define BIP39_WORDLIST_LENGTH 11068 #define BIP39_WORDLIST_OFFSETS_LENGTH 2049 @@ -42,5 +41,3 @@ extern unsigned char const WIDE ELECTRUM_SEED_VERSION[ELECTRUM_SEED_VERSION_LENG extern unsigned char const WIDE ELECTRUM_MNEMONIC[ELECTRUM_MNEMONIC_LENGTH]; #endif - -#endif // BUSRV_H diff --git a/src/ux_nanos.c b/src/ux_nanos.c index 8bccad6e..3fbdb6b1 100644 --- a/src/ux_nanos.c +++ b/src/ux_nanos.c @@ -17,7 +17,7 @@ #include "ux_nanos.h" -#ifdef TARGET_NANOS +#if defined(TARGET_NANOS) //#ifdef OS_IO_SEPROXYHAL diff --git a/src/ux_nanos.h b/src/ux_nanos.h index 15c7261d..391a47d6 100644 --- a/src/ux_nanos.h +++ b/src/ux_nanos.h @@ -14,13 +14,12 @@ * limitations under the License. ********************************************************************************/ -#ifndef BOLOS_UX_NANOS_H -#define BOLOS_UX_NANOS_H - -#ifdef HAVE_BOLOS_UX +#pragma once #include "ux_common/common.h" +#if defined(HAVE_BOLOS_UX) && defined(TARGET_NANOS) + typedef unsigned int (*callback_t)(unsigned int); // bolos ux context (not mandatory if redesigning a bolos ux) @@ -89,6 +88,4 @@ void screen_common_keyboard_init(unsigned int stack_slot, extern const bagl_element_t screen_onboarding_word_list_elements[9]; -#endif // HAVE_BOLOS_UX - -#endif // BOLOS_UX_H +#endif // HAVE_BOLOS_UX && TARGET_NANOS diff --git a/src/ux_nanox.h b/src/ux_nanox.h index 498d92a5..3ae2b502 100644 --- a/src/ux_nanox.h +++ b/src/ux_nanox.h @@ -14,13 +14,12 @@ * limitations under the License. ********************************************************************************/ -#ifndef BOLOS_UX_NANOX_H -#define BOLOS_UX_NANOX_H - -#ifdef HAVE_BOLOS_UX +#pragma once #include "ux_common/common.h" +#if defined(HAVE_BOLOS_UX) && (defined(TARGET_NANOX) || defined(TARGET_NANOS2)) + // bolos ux context (not mandatory if redesigning a bolos ux) typedef struct bolos_ux_context { #define BOLOS_UX_ONBOARDING_NEW 1 @@ -107,6 +106,4 @@ void screen_common_keyboard_init(unsigned int stack_slot, // to be included into all flow that needs to go back to the dashboard extern const ux_flow_step_t ux_ob_goto_dashboard_step; -#endif // HAVE_BOLOS_UX - -#endif // BOLOS_UX_H +#endif // HAVE_BOLOS_UX_H && (TARGET_NANOX || TARGET_NANOS2) From 466b69d895676fcb2863e5f92c5746db5153f84c Mon Sep 17 00:00:00 2001 From: Xavier Chapron Date: Thu, 2 Feb 2023 13:52:40 +0100 Subject: [PATCH 14/65] src: Update glyphs.h includes so that they don't use relatives includes This is necessary as this file is automatically generated and it's generation location is going to change --- src/ux_common/common.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ux_common/common.h b/src/ux_common/common.h index 2be31a6f..388b3a79 100644 --- a/src/ux_common/common.h +++ b/src/ux_common/common.h @@ -23,7 +23,7 @@ #include #include -#include "../glyphs.h" +#include "glyphs.h" #ifdef OS_IO_SEPROXYHAL From 39564612e2b4dd24cda4bc4e376d4d4f9338c0c5 Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Thu, 13 Oct 2022 11:18:12 +0200 Subject: [PATCH 15/65] [add] Stax home screen --- Makefile | 32 +++++-- icons/fatstacks_app_recovery_check.gif | Bin 0 -> 1122 bytes src/main.c | 12 ++- src/ui.c | 118 ++++++++++++++++++++++++ src/ui.h | 2 + src/ux_common/common.h | 4 + src/ux_stax.h | 119 +++++++++++++++++++++++++ 7 files changed, 276 insertions(+), 11 deletions(-) create mode 100644 icons/fatstacks_app_recovery_check.gif create mode 100644 src/ux_stax.h diff --git a/Makefile b/Makefile index 449ba6b5..28956142 100755 --- a/Makefile +++ b/Makefile @@ -35,6 +35,8 @@ APP_LOAD_PARAMS = --appFlags 0x10 $(COMMON_LOAD_PARAMS) --apdu --curve secp256k1 ifeq ($(TARGET_NAME), TARGET_NANOS) ICONNAME=icons/nanos_app_recovery_check.gif +else ifeq ($(TARGET_NAME), TARGET_FATSTACKS) + ICONNAME=icons/fatstacks_app_recovery_check.gif else ICONNAME=icons/nanox_app_recovery_check.gif endif @@ -49,7 +51,7 @@ DEFINES += LEDGER_PATCH_VERSION=$(APPVERSION_P) DEFINES += OS_IO_SEPROXYHAL DEFINES += UNUSED\(x\)=\(void\)x -DEFINES += HAVE_BAGL HAVE_UX_FLOW +DEFINES += HAVE_UX_FLOW DEFINES += BOLOS_APP_ICON_SIZE_B=\(9+32\) #DEFINES += HAVE_ELECTRUM @@ -57,17 +59,26 @@ DEFINES += IO_USB_MAX_ENDPOINTS=4 IO_HID_EP_LENGTH=64 DEFINES += HAVE_SPRINTF DEFINES += HAVE_BOLOS_UX +ifneq ($(TARGET_NAME), TARGET_FATSTACKS) + $(info BAGL activated) + DEFINES += HAVE_BAGL +else + $(info BAGL deactivated) +endif + ifeq ($(TARGET_NAME), TARGET_NANOS) DEFINES += IO_SEPROXYHAL_BUFFER_SIZE_B=128 else DEFINES += IO_SEPROXYHAL_BUFFER_SIZE_B=300 - DEFINES += HAVE_GLO096 - DEFINES += BAGL_WIDTH=128 BAGL_HEIGHT=64 - DEFINES += HAVE_BAGL_ELLIPSIS # long label truncation feature - DEFINES += HAVE_BAGL_FONT_OPEN_SANS_REGULAR_11PX - DEFINES += HAVE_BAGL_FONT_OPEN_SANS_EXTRABOLD_11PX - DEFINES += HAVE_BAGL_FONT_OPEN_SANS_LIGHT_16PX - DEFINES += HAVE_KEYBOARD_UX + ifneq ($(TARGET_NAME), TARGET_FATSTACKS) + DEFINES += HAVE_GLO096 + DEFINES += BAGL_WIDTH=128 BAGL_HEIGHT=64 + DEFINES += HAVE_BAGL_ELLIPSIS # long label truncation feature + DEFINES += HAVE_BAGL_FONT_OPEN_SANS_REGULAR_11PX + DEFINES += HAVE_BAGL_FONT_OPEN_SANS_EXTRABOLD_11PX + DEFINES += HAVE_BAGL_FONT_OPEN_SANS_LIGHT_16PX + DEFINES += HAVE_KEYBOARD_UX + endif endif DEBUG = 0 @@ -112,7 +123,10 @@ include $(BOLOS_SDK)/Makefile.glyphs APP_SOURCE_PATH += src -ifneq ($(TARGET_NAME),TARGET_NANOS) +ifeq ($(TARGET_NAME), TARGET_FATSTACKS) + SDK_SOURCE_PATH += lib_nbgl/src + SDK_SOURCE_PATH += lib_ux_fatstacks +else ifneq ($(TARGET_NAME), TARGET_NANOS) SDK_SOURCE_PATH += lib_ux endif diff --git a/icons/fatstacks_app_recovery_check.gif b/icons/fatstacks_app_recovery_check.gif new file mode 100644 index 0000000000000000000000000000000000000000..ec8af4bf07c019b216bd33453e426adfdad35693 GIT binary patch literal 1122 zcmZ?wbhEHb6krfwXkY+=|Ns9h{^ySH4N!1NEJ*~?Hue<-iOJciB??KY>6v-9O7C~? zS5nAKu~iB;^)>JRtPXT0NVp4u-iLDaQ zr4TRV7Ql_oE7k*hM=v=)SHB{$K;KZ$0OTc@LSJ9}N^^7Js*6j4QW5UOYH)E#WkITb zP-=00X;E@2P`NV5ssbzLqSVBa{GyQj{2W*)24v)yucqiS6q^qmz?V9Vygr+M=vuo#mdph)z!(!z`)GV*~QS%)xyc#%*?{b#L>{$ z(ACV**aD{4B|o_|H#M&WrZ)wl*AS;(P=d%U0NU)5T9jFqn&MWJpQ`}&xK$=Dw-~y) z;xrGcHwBkl4Bgyt>eUB2MjsTtNYM=w0;VAl6P|d19C-3i%>$zB`;K7M%r?(Lh`uU@`*{_N?K$B!O9xPR~No!hr=-nf44>XplvE?zi)?(CV< zr%s+Ye(dOx!-ozY*uQV@p5424?%2L<>z2)%Hf~tIZta@Yt5&X9zHI4|#fug$m_Kjs zoY}Kx&X_)J>XgZoCQj(@>+R|8>g;H5Yi((6YHX;ltF5W7s;nq4D=jH5DlEv)%gxEo z%FIYlOHD~mN=%54i;annii`*k3k?Yl3Jmb~^Y!ue^7L?bb9Hfca&)k_v$e6dva~Qa zGc_?bGBnWF)78<|($r8_Q&mw`QdE$ala-N{l9Uh^6BQ8_5)|O)dHOB?VjG91Q|m6i3hOVGF`xRZQjEbG07hGi?*IS* literal 0 HcmV?d00001 diff --git a/src/main.c b/src/main.c index 6e98916f..3ccb7679 100644 --- a/src/main.c +++ b/src/main.c @@ -18,6 +18,10 @@ #include #include +#if defined(HAVE_NBGL) +#include +#endif + #include "ui.h" unsigned char G_io_seproxyhal_spi_buffer[IO_SEPROXYHAL_BUFFER_SIZE_B]; @@ -84,11 +88,11 @@ unsigned char io_event(unsigned char channel __attribute__((unused))) { case SEPROXYHAL_TAG_FINGER_EVENT: UX_FINGER_EVENT(G_io_seproxyhal_spi_buffer); break; - +#if ! defined(HAVE_NBGL) case SEPROXYHAL_TAG_BUTTON_PUSH_EVENT: // for Nano S UX_BUTTON_PUSH_EVENT(G_io_seproxyhal_spi_buffer); break; - +#endif case SEPROXYHAL_TAG_DISPLAY_PROCESSED_EVENT: #if defined(TARGET_NANOS) if ((uiState == UI_TEXT) && @@ -141,7 +145,11 @@ __attribute__((section(".boot"))) int main(void) { // ensure exception will work as planned os_boot(); +#if defined(HAVE_NBGL) + nbgl_objInit(); +#elif defined(HAVE_BAGL) UX_INIT(); +#endif BEGIN_TRY { TRY { diff --git a/src/ui.c b/src/ui.c index 16f4874a..f5d322cc 100644 --- a/src/ui.c +++ b/src/ui.c @@ -1,6 +1,116 @@ #include #include "ui.h" +#if defined(HAVE_NBGL) + +#include +#include +#include +#include + +nbgl_page_t* pageContext; + +void ui_menu_about(); +void recover_page(); + +void releaseContext(void) { + if (pageContext != NULL) { + nbgl_pageRelease(pageContext); + pageContext = NULL; + } +} + +enum { BACK_TOKEN = 0, INFO_TOKEN, NEXT_TOKEN, CANCEL_TOKEN, START_RECOVER_TOKEN, BACK_HOME_TOKEN, QUIT_APP_TOKEN }; + +void pageTouchCallback(int token, uint8_t index) { + (void) index; + PRINTF("LOL"); + if (token == QUIT_APP_TOKEN) { + releaseContext(); + os_sched_exit(-1); + } else if (token == INFO_TOKEN) { + ui_menu_about(); + } else if (token == START_RECOVER_TOKEN) { + recover_page(); + } else if (token == BACK_HOME_TOKEN) { + releaseContext(); + ui_idle_init(); + } +} + +// 'About' menu + +static const char* const infoTypes[] = {"Version", "Recovery Check"}; +static const char* const infoContents[] = {APPVERSION, "(c) 2022 Ledger"}; + +void ui_menu_about() { + nbgl_pageContent_t content = {.title = "Recovery Check infos", .isTouchableTitle = false}; + nbgl_pageNavigationInfo_t nav = {.activePage = 0, + .nbPages = 1, + .navType = NAV_WITH_BUTTONS, + .navWithButtons.quitButton = true, + .navWithButtons.navToken = BACK_HOME_TOKEN, + .tuneId = TUNE_TAP_CASUAL}; + content.type = INFOS_LIST; + content.infosList.nbInfos = 2; + content.infosList.infoTypes = (const char**) infoTypes; + content.infosList.infoContents = (const char**) infoContents; + + releaseContext(); + pageContext = nbgl_pageDrawGenericContent(&pageTouchCallback, &nav, &content); + nbgl_refresh(); +} + +void recover_page() { + + nbgl_layoutDescription_t layoutDescription = { + .modal = false, // not modal (so on plane 0) + .onActionCallback = NULL, // generic callback for all controls + .tapActionText = "Return", // A "tapable" main container is necessary, with this text + .tapActionToken = BACK_HOME_TOKEN, // token to be used when main container is touched + .ticker.tickerCallback = NULL // no ticker + }; + + nbgl_layoutProgressBar_t bar = { + .percentage = 50, + .text = "coucou lol", + .subText = "sub" + }; + + nbgl_layout_t *layout = nbgl_layoutGet(&layoutDescription); + nbgl_layoutAddKeyboard(layout, &bar); + + releaseContext(); + nbgl_layoutDraw(layout); + nbgl_refresh(); +} + +static void display_home_page() { + nbgl_pageInfoDescription_t home = { + /* .centeredInfo.icon = &C_fatstacks_app_recovery_check, */ + .centeredInfo.icon = NULL, + .centeredInfo.text1 = "Recovery Check app", + .centeredInfo.text2 = NULL, + .centeredInfo.text3 = NULL, + .centeredInfo.style = LARGE_CASE_INFO, + .centeredInfo.offsetY = 32, + .topRightStyle = INFO_ICON, + .bottomButtonStyle = QUIT_ICON, + .topRightToken = INFO_TOKEN, + .bottomButtonToken = QUIT_APP_TOKEN, + .footerText = NULL, + .tapActionText = "Tap to check your mnemonic", + .tapActionToken = START_RECOVER_TOKEN, + .tuneId = TUNE_TAP_CASUAL + }; + releaseContext(); + pageContext = nbgl_pageDrawInfo(&pageTouchCallback, NULL, &home); + nbgl_refresh(); +} + +#endif + + enum UI_STATE { UI_IDLE, UI_TEXT, UI_APPROVAL }; enum UI_STATE uiState; @@ -141,7 +251,9 @@ UX_FLOW(ux_idle_flow, &ux_idle_flow_1_step, &ux_idle_flow_3_step, &ux_idle_flow_ #endif + void ui_idle_init(void) { +#if defined(HAVE_BAGL) uiState = UI_IDLE; // reserve a display stack slot if none yet @@ -149,4 +261,10 @@ void ui_idle_init(void) { ux_stack_push(); } ux_flow_init(0, ux_idle_flow, NULL); +#endif + +#if defined(HAVE_NBGL) + display_home_page(); +#endif + } diff --git a/src/ui.h b/src/ui.h index bfeeef44..607a79ca 100644 --- a/src/ui.h +++ b/src/ui.h @@ -27,6 +27,8 @@ #include "ux_nanos.h" #elif defined(TARGET_NANOX) || defined(TARGET_NANOS2) #include "ux_nanox.h" +#elif defined(TARGET_FATSTACKS) +#include "ux_stax.h" #endif #define ARRAYLEN(array) (sizeof(array) / sizeof(array[0])) diff --git a/src/ux_common/common.h b/src/ux_common/common.h index 388b3a79..3caff9f8 100644 --- a/src/ux_common/common.h +++ b/src/ux_common/common.h @@ -44,7 +44,11 @@ #define KEYBOARD_RENDER_WORD \ 3 // callback is called with a -1 when requesting complete word, or the char index else, // returnin 0 implies no char is to be displayed +#if defined(HAVE_NBGL) +typedef const nbgl_obj_t* (*keyboard_callback_t)(unsigned int event, unsigned int value); +#else typedef const bagl_element_t* (*keyboard_callback_t)(unsigned int event, unsigned int value); +#endif void bolos_ux_hslider3_init(unsigned int total_count); void bolos_ux_hslider3_set_current(unsigned int current); diff --git a/src/ux_stax.h b/src/ux_stax.h new file mode 100644 index 00000000..236beb2a --- /dev/null +++ b/src/ux_stax.h @@ -0,0 +1,119 @@ +/******************************************************************************* + * (c) 2016-2022 Ledger SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ********************************************************************************/ + +#pragma once + +#include "ux_common/common.h" + +#if defined(HAVE_BOLOS_UX) && defined(TARGET_FATSTACKS) + +#include +#include + + +/** + * @brief struct to represent a keyboard (@ref KEYBOARD type) + * + */ +typedef struct PACKED__ keyboard_s { + struct nbgl_obj_s; ///< common part + color_t textColor; ///< color set to letters. + color_t borderColor; ///< color set to key borders + bool lettersOnly; ///< if true, only display letter keys and Backspace + bool upperCase; ///< if true, display letter keys in upper case + keyboardMode_t mode; ///< keyboard mode to start with + uint32_t keyMask; ///< mask used to disable some keys in letters only mod. The 26 LSB bits of mask are used, for the 26 letters of a QWERTY keyboard. Bit[0] for Q, Bit[1] for W and so on + keyboardCallback_t callback; ///< function called when an active key is pressed +} keyboard_t; + + +/** + * @brief struct to represent a progress bar (@ref PROGRESS_BAR type) + * @note if withBorder, the stroke of the border is fixed (3 pixels) + */ +typedef struct PACKED__ progress_bar_s { + struct nbgl_obj_s; // common part + bool withBorder; ///< if set to true, a border in black surround the whole object + uint8_t state; ///< state of the progress, in % (from 0 to 100). + color_t foregroundColor; ///< color of the inner progress bar and border (if applicable) +} progress_bar_t; + +/** + * @brief This structure contains info to build a progress bar with info + * + */ +typedef struct { + uint8_t percentage; ///< percentage of completion, from 0 to 100. + char *text; ///< text in black, on top of progress bar + char *subText; ///< text in gray, under progress bar +} layoutKeyboard_t; + + +int nbgl_layoutAddKeyboard(nbgl_layout_t *layout, nbgl_layoutProgressBar_t *barLayout) { + nbgl_layoutInternal_t *layoutInt = (nbgl_layoutInternal_t *)layout; + nbgl_text_area_t *textArea; + nbgl_text_area_t *subTextArea; + nbgl_progress_bar_t *progress; + + LOG_DEBUG(LAYOUT_LOGGER,"nbgl_layoutAddProgressBar():\n"); + if (layout == NULL) + return -1; + // if (barLayout->text != NULL) { + // textArea = (nbgl_text_area_t *)nbgl_objPoolGet(TEXT_AREA,((nbgl_layoutInternal_t *)layout)->layer); + // textArea->textColor=BLACK; + // textArea->text = PIC(barLayout->text); + // textArea->textAlignment = MID_LEFT; + // textArea->fontId = BAGL_FONT_INTER_REGULAR_24px; + // textArea->width = GET_AVAILABLE_WIDTH(((nbgl_layoutInternal_t *)layout)); + // textArea->height = 24*nbgl_getTextNbLines(textArea->text); + // textArea->style = NO_STYLE; + // textArea->alignment = NO_ALIGNMENT; + // textArea->alignmentMarginX = BORDER_MARGIN; + // textArea->alignmentMarginY = BORDER_MARGIN; + // textArea->alignTo = NULL; + // addObjectToLayout(layoutInt,(nbgl_obj_t*)textArea); + // } + progress = (nbgl_progress_bar_t *)nbgl_objPoolGet(PROGRESS_BAR,((nbgl_layoutInternal_t *)layout)->layer); + progress->foregroundColor = BLACK; + progress->withBorder = true; + progress->state = barLayout->percentage; + progress->width = GET_AVAILABLE_WIDTH(((nbgl_layoutInternal_t *)layout)); + progress->height = 32; + progress->alignment = NO_ALIGNMENT; + progress->alignmentMarginX = BORDER_MARGIN; + progress->alignmentMarginY = BORDER_MARGIN; + progress->alignTo = NULL; + addObjectToLayout(layoutInt,(nbgl_obj_t*)progress); + // if (barLayout->subText != NULL) { + // subTextArea = (nbgl_text_area_t *)nbgl_objPoolGet(TEXT_AREA,((nbgl_layoutInternal_t *)layout)->layer); + // subTextArea->textColor=LIGHT_GRAY; + // subTextArea->text = PIC(barLayout->subText); + // subTextArea->textAlignment = MID_LEFT; + // subTextArea->fontId = BAGL_FONT_INTER_REGULAR_24px; + // subTextArea->width = GET_AVAILABLE_WIDTH(((nbgl_layoutInternal_t *)layout)); + // subTextArea->height = 24*nbgl_getTextNbLines(subTextArea->text); + // subTextArea->style = NO_STYLE; + // subTextArea->alignment = NO_ALIGNMENT; + // subTextArea->alignmentMarginX = BORDER_MARGIN; + // subTextArea->alignmentMarginY = BORDER_MARGIN; + // subTextArea->alignTo = NULL; + // addObjectToLayout(layoutInt,(nbgl_obj_t*)subTextArea); + // } + return 0; +} + + +#endif // HAVE_BOLOS_UX && TARGET_NANOS From e91c908bd6578767d089519f433f67425b4787e7 Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Wed, 19 Oct 2022 12:00:49 +0200 Subject: [PATCH 16/65] [add] Word page + char selectors --- Makefile | 2 + src/ui.c | 152 ++++++++++++++++++++++---- src/ui.h | 2 - src/ux_common/common_bip39.h | 28 +++-- src/ux_common/onboarding_seed_bip39.c | 74 +++++++++++-- src/ux_stax.h | 92 ---------------- 6 files changed, 215 insertions(+), 135 deletions(-) diff --git a/Makefile b/Makefile index 28956142..9818d1ef 100755 --- a/Makefile +++ b/Makefile @@ -82,7 +82,9 @@ else endif DEBUG = 0 + ifneq ($(DEBUG), 0) + $(info DEBUG enabled) DEFINES += HAVE_IO_USB HAVE_USB_APDU SDK_SOURCE_PATH += lib_stusb lib_stusb_impl DEFINES += HAVE_PRINTF diff --git a/src/ui.c b/src/ui.c index f5d322cc..cffe1ff4 100644 --- a/src/ui.c +++ b/src/ui.c @@ -8,10 +8,14 @@ #include #include -nbgl_page_t* pageContext; +#include "ux_common/common_bip39.h" -void ui_menu_about(); -void recover_page(); +static nbgl_page_t* pageContext; +static int wordNum; + +void ui_menu_about(void); +void page_keyboard(void); +void display_home_page(void); void releaseContext(void) { if (pageContext != NULL) { @@ -20,26 +24,34 @@ void releaseContext(void) { } } -enum { BACK_TOKEN = 0, INFO_TOKEN, NEXT_TOKEN, CANCEL_TOKEN, START_RECOVER_TOKEN, BACK_HOME_TOKEN, QUIT_APP_TOKEN }; +enum { + BACK_HOME_TOKEN = 0, + BACK_BUTTON_TOKEN, + FIRST_SUGGESTION_TOKEN, + INFO_TOKEN, + QUIT_APP_TOKEN, + START_RECOVER_TOKEN, +}; void pageTouchCallback(int token, uint8_t index) { (void) index; - PRINTF("LOL"); if (token == QUIT_APP_TOKEN) { releaseContext(); os_sched_exit(-1); } else if (token == INFO_TOKEN) { ui_menu_about(); } else if (token == START_RECOVER_TOKEN) { - recover_page(); + wordNum = 0; + page_keyboard(); } else if (token == BACK_HOME_TOKEN) { releaseContext(); ui_idle_init(); } } -// 'About' menu - +/* + * About menu + */ static const char* const infoTypes[] = {"Version", "Recovery Check"}; static const char* const infoContents[] = {APPVERSION, "(c) 2022 Ledger"}; @@ -61,30 +73,122 @@ void ui_menu_about() { nbgl_refresh(); } -void recover_page() { +/* + * Word recover page + */ +static char textToEnter[20]; +static char headerText[48]; +static nbgl_layout_t *layout; +static int textIndex, suggestionIndex, keyboardIndex; +static char *buttonTexts[NB_MAX_SUGGESTION_BUTTONS] = {0}; + +// function called when back or any suggestion button is touched +static void layoutTouchCallback(const int token, uint8_t index) { + if (token == BACK_BUTTON_TOKEN) { + // go back to main screen of app + // TODO: instead, back to previous word + nbgl_layoutRelease(layout); + display_home_page(); + } + else if (token >= FIRST_SUGGESTION_TOKEN) { + // do something with touched button + PRINTF("Selected word is %s\n", buttonTexts[token - FIRST_SUGGESTION_TOKEN]); + // go back to main screen of app + nbgl_layoutRelease(layout); + display_home_page(); + } +} + +// function called when a key of keyboard is touched +static void keyboardCallback(const char touchedKey) { + size_t textLen = 0; + if (touchedKey != BACKSPACE_KEY) { + const size_t previousTextLen = strlen(textToEnter); + textToEnter[previousTextLen] = touchedKey; + textToEnter[previousTextLen+1] = '\0'; + textLen = previousTextLen + 1; + } + else { + const size_t previousTextLen = strlen(textToEnter); + if (previousTextLen == 0) { + return; + } + textToEnter[previousTextLen-1] = '\0'; + textLen = previousTextLen - 1; + } + if (textLen < 2) { + nbgl_layoutUpdateSuggestionButtons(layout, suggestionIndex, 0, buttonTexts); + nbgl_layoutUpdateKeyboard(layout, keyboardIndex, 0x0); + } + else { + const size_t nbMatchingWords = bolos_ux_bip39_fillwith_candidates( + (unsigned char*)textToEnter, + strlen(textToEnter), + buttonTexts + ); + PRINTF("Updating layout with '%d' buttons\nThey will be:\n", nbMatchingWords); + for (size_t i=0; i + +// the biggest word of BIP39 list is 8 char (9 with trailing '\0'), and +// the max number of showed suggestions is NB_MAX_SUGGESTION_BUTTONS +static char wordCandidates[(MAX_WORD_LENGTH + 1) * NB_MAX_SUGGESTION_BUTTONS] = {0}; + +size_t bolos_ux_bip39_fillwith_candidates( + const unsigned char *startingChars, + const size_t startingCharsLenght, + char *outputBuffer[] + ) { + const size_t nbMatchingWords = MIN( + bolos_ux_bip39_get_word_count_starting_with(startingChars, startingCharsLenght), + NB_MAX_SUGGESTION_BUTTONS + ); + PRINTF("There are %d possible suggestions\n", nbMatchingWords); + if (nbMatchingWords == 0) { + return 0; + } + size_t matchingWordIndex = bolos_ux_bip39_get_word_idx_starting_with( + startingChars, + startingCharsLenght + ); + size_t offset = 0; + for (size_t i = 0; i < nbMatchingWords; i++) { + unsigned char *wordDest = (unsigned char *)(&wordCandidates + offset); + const size_t wordSize = bolos_ux_bip39_idx_strcpy(matchingWordIndex, wordDest); + matchingWordIndex++; + offset += wordSize + 1; // + trailing '\0' size + outputBuffer[i] = (char *)wordDest; + } + return nbMatchingWords; +} + +uint32_t bolos_ux_bip39_get_keyboard_mask( + const unsigned char *prefix, + const unsigned int prefixlength + ) { + uint32_t existing_mask = 0; + unsigned char next_letters[MAX_WORD_LENGTH] = {0}; + const size_t nb_letters = bolos_ux_bip39_get_word_next_letters_starting_with(prefix, prefixlength, next_letters); + next_letters[nb_letters] = '\0'; + PRINTF("Next letters are in: %s\n", next_letters); + for (int i = 0; i < ALPHABET_LENGTH; i++) { + for (size_t j = 0; j < nb_letters; j++) { + if (KBD_LETTERS[i] == next_letters[j]) { + existing_mask += 1 << i; + } + } + } + return (-1 ^ existing_mask); +} +#endif diff --git a/src/ux_stax.h b/src/ux_stax.h index 236beb2a..a3cd839f 100644 --- a/src/ux_stax.h +++ b/src/ux_stax.h @@ -24,96 +24,4 @@ #include -/** - * @brief struct to represent a keyboard (@ref KEYBOARD type) - * - */ -typedef struct PACKED__ keyboard_s { - struct nbgl_obj_s; ///< common part - color_t textColor; ///< color set to letters. - color_t borderColor; ///< color set to key borders - bool lettersOnly; ///< if true, only display letter keys and Backspace - bool upperCase; ///< if true, display letter keys in upper case - keyboardMode_t mode; ///< keyboard mode to start with - uint32_t keyMask; ///< mask used to disable some keys in letters only mod. The 26 LSB bits of mask are used, for the 26 letters of a QWERTY keyboard. Bit[0] for Q, Bit[1] for W and so on - keyboardCallback_t callback; ///< function called when an active key is pressed -} keyboard_t; - - -/** - * @brief struct to represent a progress bar (@ref PROGRESS_BAR type) - * @note if withBorder, the stroke of the border is fixed (3 pixels) - */ -typedef struct PACKED__ progress_bar_s { - struct nbgl_obj_s; // common part - bool withBorder; ///< if set to true, a border in black surround the whole object - uint8_t state; ///< state of the progress, in % (from 0 to 100). - color_t foregroundColor; ///< color of the inner progress bar and border (if applicable) -} progress_bar_t; - -/** - * @brief This structure contains info to build a progress bar with info - * - */ -typedef struct { - uint8_t percentage; ///< percentage of completion, from 0 to 100. - char *text; ///< text in black, on top of progress bar - char *subText; ///< text in gray, under progress bar -} layoutKeyboard_t; - - -int nbgl_layoutAddKeyboard(nbgl_layout_t *layout, nbgl_layoutProgressBar_t *barLayout) { - nbgl_layoutInternal_t *layoutInt = (nbgl_layoutInternal_t *)layout; - nbgl_text_area_t *textArea; - nbgl_text_area_t *subTextArea; - nbgl_progress_bar_t *progress; - - LOG_DEBUG(LAYOUT_LOGGER,"nbgl_layoutAddProgressBar():\n"); - if (layout == NULL) - return -1; - // if (barLayout->text != NULL) { - // textArea = (nbgl_text_area_t *)nbgl_objPoolGet(TEXT_AREA,((nbgl_layoutInternal_t *)layout)->layer); - // textArea->textColor=BLACK; - // textArea->text = PIC(barLayout->text); - // textArea->textAlignment = MID_LEFT; - // textArea->fontId = BAGL_FONT_INTER_REGULAR_24px; - // textArea->width = GET_AVAILABLE_WIDTH(((nbgl_layoutInternal_t *)layout)); - // textArea->height = 24*nbgl_getTextNbLines(textArea->text); - // textArea->style = NO_STYLE; - // textArea->alignment = NO_ALIGNMENT; - // textArea->alignmentMarginX = BORDER_MARGIN; - // textArea->alignmentMarginY = BORDER_MARGIN; - // textArea->alignTo = NULL; - // addObjectToLayout(layoutInt,(nbgl_obj_t*)textArea); - // } - progress = (nbgl_progress_bar_t *)nbgl_objPoolGet(PROGRESS_BAR,((nbgl_layoutInternal_t *)layout)->layer); - progress->foregroundColor = BLACK; - progress->withBorder = true; - progress->state = barLayout->percentage; - progress->width = GET_AVAILABLE_WIDTH(((nbgl_layoutInternal_t *)layout)); - progress->height = 32; - progress->alignment = NO_ALIGNMENT; - progress->alignmentMarginX = BORDER_MARGIN; - progress->alignmentMarginY = BORDER_MARGIN; - progress->alignTo = NULL; - addObjectToLayout(layoutInt,(nbgl_obj_t*)progress); - // if (barLayout->subText != NULL) { - // subTextArea = (nbgl_text_area_t *)nbgl_objPoolGet(TEXT_AREA,((nbgl_layoutInternal_t *)layout)->layer); - // subTextArea->textColor=LIGHT_GRAY; - // subTextArea->text = PIC(barLayout->subText); - // subTextArea->textAlignment = MID_LEFT; - // subTextArea->fontId = BAGL_FONT_INTER_REGULAR_24px; - // subTextArea->width = GET_AVAILABLE_WIDTH(((nbgl_layoutInternal_t *)layout)); - // subTextArea->height = 24*nbgl_getTextNbLines(subTextArea->text); - // subTextArea->style = NO_STYLE; - // subTextArea->alignment = NO_ALIGNMENT; - // subTextArea->alignmentMarginX = BORDER_MARGIN; - // subTextArea->alignmentMarginY = BORDER_MARGIN; - // subTextArea->alignTo = NULL; - // addObjectToLayout(layoutInt,(nbgl_obj_t*)subTextArea); - // } - return 0; -} - - #endif // HAVE_BOLOS_UX && TARGET_NANOS From 3f1a9a7dd95da6b3dc49e370fb4b312d5becc66b Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Wed, 19 Oct 2022 14:37:30 +0200 Subject: [PATCH 17/65] [add] Keyboard masking with the first letter (but suggestions with the second only) --- src/ui.c | 52 +++++++++++++-------------- src/ux_common/common.h | 2 ++ src/ux_common/common_bip39.h | 10 +++--- src/ux_common/onboarding_seed_bip39.c | 32 ++++++++--------- 4 files changed, 47 insertions(+), 49 deletions(-) diff --git a/src/ui.c b/src/ui.c index cffe1ff4..2bf1e090 100644 --- a/src/ui.c +++ b/src/ui.c @@ -76,10 +76,10 @@ void ui_menu_about() { /* * Word recover page */ -static char textToEnter[20]; -static char headerText[48]; -static nbgl_layout_t *layout; -static int textIndex, suggestionIndex, keyboardIndex; +static char textToEnter[MAX_WORD_LENGTH + 1] = {0}; +static char headerText[48] = {0}; +static nbgl_layout_t *layout = 0; +static int textIndex, suggestionIndex, keyboardIndex = 0; static char *buttonTexts[NB_MAX_SUGGESTION_BUTTONS] = {0}; // function called when back or any suggestion button is touched @@ -103,42 +103,41 @@ static void layoutTouchCallback(const int token, uint8_t index) { // function called when a key of keyboard is touched static void keyboardCallback(const char touchedKey) { size_t textLen = 0; - if (touchedKey != BACKSPACE_KEY) { - const size_t previousTextLen = strlen(textToEnter); - textToEnter[previousTextLen] = touchedKey; - textToEnter[previousTextLen+1] = '\0'; - textLen = previousTextLen + 1; - } - else { - const size_t previousTextLen = strlen(textToEnter); + uint32_t mask = 0; + const size_t previousTextLen = strlen(textToEnter); + if (touchedKey == BACKSPACE_KEY) { if (previousTextLen == 0) { return; } - textToEnter[previousTextLen-1] = '\0'; + textToEnter[previousTextLen - 1] = '\0'; textLen = previousTextLen - 1; + } else { + textToEnter[previousTextLen] = touchedKey; + textToEnter[previousTextLen + 1] = '\0'; + textLen = previousTextLen + 1; } + if (textLen < 2) { nbgl_layoutUpdateSuggestionButtons(layout, suggestionIndex, 0, buttonTexts); - nbgl_layoutUpdateKeyboard(layout, keyboardIndex, 0x0); - } - else { - const size_t nbMatchingWords = bolos_ux_bip39_fillwith_candidates( - (unsigned char*)textToEnter, + } else { + const size_t nbMatchingWords = bolos_ux_bip39_fill_with_candidates( + (unsigned char *)&(textToEnter[0]), strlen(textToEnter), buttonTexts ); - PRINTF("Updating layout with '%d' buttons\nThey will be:\n", nbMatchingWords); - for (size_t i=0; i 0) { + mask = bolos_ux_bip39_get_keyboard_mask( + (unsigned char *)&(textToEnter[0]), + strlen(textToEnter) + ); + } + nbgl_layoutUpdateKeyboard(layout, keyboardIndex, mask); - nbgl_layoutUpdateEnteredText(layout, textIndex, false, 0, textToEnter, false); + nbgl_layoutUpdateEnteredText(layout, textIndex, false, 0, &(textToEnter[0]), false); nbgl_refresh(); -} + } void page_keyboard(void) { nbgl_layoutDescription_t layoutDescription = { @@ -181,7 +180,6 @@ void page_keyboard(void) { buttonTexts, FIRST_SUGGESTION_TOKEN, TUNE_TAP_CASUAL); - nbgl_layoutDraw(layout); } diff --git a/src/ux_common/common.h b/src/ux_common/common.h index 3caff9f8..886d784d 100644 --- a/src/ux_common/common.h +++ b/src/ux_common/common.h @@ -35,6 +35,8 @@ #define ONBOARDING_WORD_COMPLETION_MAX_ITEMS 8 #define BOLOS_UX_HASH_LENGTH 4 // as on the blue +#define MAX_WORD_LENGTH 8 + #define KEYBOARD_ITEM_VALIDATED \ 1 // callback is called with the entered item index, tmp_element is precharged with element to // be displayed and using the common string buffer as string parameter diff --git a/src/ux_common/common_bip39.h b/src/ux_common/common_bip39.h index 65d9ecd6..14c4bd58 100644 --- a/src/ux_common/common_bip39.h +++ b/src/ux_common/common_bip39.h @@ -16,17 +16,17 @@ unsigned int bolos_ux_bip39_get_word_idx_starting_with(const unsigned char *pref const unsigned int prefixlength); unsigned int bolos_ux_bip39_idx_strcpy(const unsigned int index, unsigned char *buffer); unsigned int bolos_ux_bip39_get_word_count_starting_with(const unsigned char *prefix, - const unsigned int prefixlength); + const unsigned int prefixLength); unsigned int bolos_ux_bip39_get_word_next_letters_starting_with(const unsigned char *prefix, - const unsigned int prefixlength, + const unsigned int prefixLength, unsigned char *next_letters_buffer); #if defined(HAVE_NBGL) -size_t bolos_ux_bip39_fillwith_candidates(const unsigned char *startingChars, - const size_t startingCharsLenght, +size_t bolos_ux_bip39_fill_with_candidates(const unsigned char *startingChars, + const size_t startingCharsLenght, char *outputBuffer[]); uint32_t bolos_ux_bip39_get_keyboard_mask(const unsigned char *prefix, - const unsigned int prefixlength); + const unsigned int prefixLength); #endif #ifdef HAVE_ELECTRUM diff --git a/src/ux_common/onboarding_seed_bip39.c b/src/ux_common/onboarding_seed_bip39.c index d4c17cbb..cc7e4216 100644 --- a/src/ux_common/onboarding_seed_bip39.c +++ b/src/ux_common/onboarding_seed_bip39.c @@ -6,10 +6,8 @@ #include "onboarding_seed_rom_variables.h" #include "common.h" -#define MAX_WORD_LENGTH 8 #define ALPHABET_LENGTH 27 - -static const char KBD_LETTERS[ALPHABET_LENGTH] = "qwertyuiopasdfghjklzxcvbnm"; +#define KBD_LETTERS "qwertyuiopasdfghjklzxcvbnm" // separated function to lower the stack usage when jumping into pbkdf algorithm unsigned int bolos_ux_mnemonic_to_seed_hash_length128(unsigned char* mnemonic, @@ -132,15 +130,15 @@ unsigned int bolos_ux_bip39_idx_strcpy(const unsigned int index, unsigned char* } unsigned int bolos_ux_bip39_get_word_idx_starting_with(const unsigned char* prefix, - const unsigned int prefixlength) { + const unsigned int prefixLength) { unsigned int i; for (i = 0; i < BIP39_WORDLIST_OFFSETS_LENGTH - 1; i++) { unsigned int j = 0; while (j < (unsigned int) (BIP39_WORDLIST_OFFSETS[i + 1] - BIP39_WORDLIST_OFFSETS[i]) && - j < prefixlength && BIP39_WORDLIST[BIP39_WORDLIST_OFFSETS[i] + j] == prefix[j]) { + j < prefixLength && BIP39_WORDLIST[BIP39_WORDLIST_OFFSETS[i] + j] == prefix[j]) { j++; } - if (j == prefixlength) { + if (j == prefixLength) { return i; } } @@ -149,16 +147,16 @@ unsigned int bolos_ux_bip39_get_word_idx_starting_with(const unsigned char* pref } unsigned int bolos_ux_bip39_get_word_count_starting_with(const unsigned char* prefix, - const unsigned int prefixlength) { + const unsigned int prefixLength) { unsigned int i; unsigned int count = 0; for (i = 0; i < BIP39_WORDLIST_OFFSETS_LENGTH - 1; i++) { unsigned int j = 0; while (j < (unsigned int) (BIP39_WORDLIST_OFFSETS[i + 1] - BIP39_WORDLIST_OFFSETS[i]) && - j < prefixlength && BIP39_WORDLIST[BIP39_WORDLIST_OFFSETS[i] + j] == prefix[j]) { + j < prefixLength && BIP39_WORDLIST[BIP39_WORDLIST_OFFSETS[i] + j] == prefix[j]) { j++; } - if (j == prefixlength) { + if (j == prefixLength) { count++; } // don't seek till the end, abort when the prefix is not matched anymore @@ -174,17 +172,17 @@ unsigned int bolos_ux_bip39_get_word_count_starting_with(const unsigned char* pr // algorithm considers the bip39 words are alphabetically ordered in the wordlist unsigned int bolos_ux_bip39_get_word_next_letters_starting_with( const unsigned char* prefix, - const unsigned int prefixlength, + const unsigned int prefixLength, unsigned char* next_letters_buffer) { unsigned int i; unsigned int letter_count = 0; for (i = 0; i < BIP39_WORDLIST_OFFSETS_LENGTH - 1; i++) { unsigned int j = 0; while (j < (unsigned int) (BIP39_WORDLIST_OFFSETS[i + 1] - BIP39_WORDLIST_OFFSETS[i]) && - j < prefixlength && BIP39_WORDLIST[BIP39_WORDLIST_OFFSETS[i] + j] == prefix[j]) { + j < prefixLength && BIP39_WORDLIST[BIP39_WORDLIST_OFFSETS[i] + j] == prefix[j]) { j++; } - if (j == prefixlength) { + if (j == prefixLength) { if (j < (unsigned int) (BIP39_WORDLIST_OFFSETS[i + 1] - BIP39_WORDLIST_OFFSETS[i])) { // j is inc during previous loop, don't touch it unsigned char next_letter = BIP39_WORDLIST[BIP39_WORDLIST_OFFSETS[i] + j]; @@ -217,8 +215,8 @@ unsigned int bolos_ux_bip39_get_word_next_letters_starting_with( // the max number of showed suggestions is NB_MAX_SUGGESTION_BUTTONS static char wordCandidates[(MAX_WORD_LENGTH + 1) * NB_MAX_SUGGESTION_BUTTONS] = {0}; -size_t bolos_ux_bip39_fillwith_candidates( - const unsigned char *startingChars, +size_t bolos_ux_bip39_fill_with_candidates( + const unsigned char * startingChars, const size_t startingCharsLenght, char *outputBuffer[] ) { @@ -247,11 +245,11 @@ size_t bolos_ux_bip39_fillwith_candidates( uint32_t bolos_ux_bip39_get_keyboard_mask( const unsigned char *prefix, - const unsigned int prefixlength + const unsigned int prefixLength ) { uint32_t existing_mask = 0; - unsigned char next_letters[MAX_WORD_LENGTH] = {0}; - const size_t nb_letters = bolos_ux_bip39_get_word_next_letters_starting_with(prefix, prefixlength, next_letters); + unsigned char next_letters[ALPHABET_LENGTH] = {0}; + const size_t nb_letters = bolos_ux_bip39_get_word_next_letters_starting_with(prefix, prefixLength, next_letters); next_letters[nb_letters] = '\0'; PRINTF("Next letters are in: %s\n", next_letters); for (int i = 0; i < ALPHABET_LENGTH; i++) { From 27e8f6dfeea2fe3b01a2a3ba5ced46552a6c0645 Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Wed, 19 Oct 2022 18:19:05 +0200 Subject: [PATCH 18/65] [add] Choosing mnemonic size first --- Makefile | 2 +- src/ui.c | 151 +++++++++++++++++++------- src/ux_common/common.h | 7 +- src/ux_common/onboarding_seed_bip39.c | 1 - src/ux_nanos.h | 8 -- src/ux_nanox.h | 8 -- 6 files changed, 121 insertions(+), 56 deletions(-) diff --git a/Makefile b/Makefile index 9818d1ef..d854619d 100755 --- a/Makefile +++ b/Makefile @@ -27,7 +27,7 @@ all: default APPNAME = "Recovery Check" APPVERSION_M = 1 -APPVERSION_N = 1 +APPVERSION_N = 2 APPVERSION_P = 0 APPVERSION = "$(APPVERSION_M).$(APPVERSION_N).$(APPVERSION_P)" diff --git a/src/ui.c b/src/ui.c index 2bf1e090..3020fbf6 100644 --- a/src/ui.c +++ b/src/ui.c @@ -10,12 +10,18 @@ #include "ux_common/common_bip39.h" +#define HEADER_SIZE 50 + static nbgl_page_t* pageContext; -static int wordNum; +static int current_word = 0; +static char headerText[HEADER_SIZE] = {0}; +static nbgl_layout_t *layout = 0; +static uint8_t mnemonic_size = 0; void ui_menu_about(void); -void page_keyboard(void); +void display_keyboard_page(void); void display_home_page(void); +void display_mnemonic_page(void); void releaseContext(void) { if (pageContext != NULL) { @@ -27,22 +33,22 @@ void releaseContext(void) { enum { BACK_HOME_TOKEN = 0, BACK_BUTTON_TOKEN, + CHOOSE_MNEMONIC_SIZE_TOKEN, FIRST_SUGGESTION_TOKEN, INFO_TOKEN, QUIT_APP_TOKEN, START_RECOVER_TOKEN, }; -void pageTouchCallback(int token, uint8_t index) { - (void) index; +void pageTouchCallback(int token, uint8_t index __attribute__((unused))) { if (token == QUIT_APP_TOKEN) { releaseContext(); os_sched_exit(-1); } else if (token == INFO_TOKEN) { ui_menu_about(); - } else if (token == START_RECOVER_TOKEN) { - wordNum = 0; - page_keyboard(); + } else if (token == CHOOSE_MNEMONIC_SIZE_TOKEN) { + current_word = 1; + display_mnemonic_page(); } else if (token == BACK_HOME_TOKEN) { releaseContext(); ui_idle_init(); @@ -73,35 +79,100 @@ void ui_menu_about() { nbgl_refresh(); } +/* + * Choose mnemonic size page + */ +void mnemonic_dispatcher(const int token, uint8_t index) { + if (token == BACK_BUTTON_TOKEN) { + nbgl_layoutRelease(layout); + display_home_page(); + } else if (token == CHOOSE_MNEMONIC_SIZE_TOKEN) { + switch(index) { + case 0: + mnemonic_size = MNEMONIC_SIZE_12; + break; + case 1: + mnemonic_size = MNEMONIC_SIZE_18; + break; + case 2: + mnemonic_size = MNEMONIC_SIZE_24; + break; + default: + PRINTF("Unexpected index '%d' (max 3)\n", index); + nbgl_layoutRelease(layout); + display_home_page(); + return; + } + nbgl_layoutRelease(layout); + display_keyboard_page(); + } +} + +void display_mnemonic_page() { + nbgl_layoutDescription_t layoutDescription = { + .modal = false, + .onActionCallback = mnemonic_dispatcher + }; + nbgl_layoutRadioChoice_t choices = { + .names = (char*[]){"12 words", "18 words", "24 words"}, + .localized = false, + .nbChoices = 3, + .initChoice = 2, + .token = CHOOSE_MNEMONIC_SIZE_TOKEN + }; + nbgl_layoutCenteredInfo_t centeredInfo = { + .text1 = NULL, + .text2 = headerText, // to use as "header" + .text3 = NULL, + .style = LARGE_CASE_INFO, + .icon = NULL, + .offsetY = 0, + .onTop = true + }; + layout = nbgl_layoutGet(&layoutDescription); + nbgl_layoutAddProgressIndicator(layout, 0, 0, true, BACK_BUTTON_TOKEN, TUNE_TAP_CASUAL); + memset(headerText, 0, HEADER_SIZE); + snprintf(headerText, HEADER_SIZE, "What is the length of your\nrecovery passphrase?"); + nbgl_layoutAddCenteredInfo(layout, ¢eredInfo); + nbgl_layoutAddRadioChoice(layout, &choices); + nbgl_layoutDraw(layout); +} + /* * Word recover page */ static char textToEnter[MAX_WORD_LENGTH + 1] = {0}; -static char headerText[48] = {0}; -static nbgl_layout_t *layout = 0; static int textIndex, suggestionIndex, keyboardIndex = 0; static char *buttonTexts[NB_MAX_SUGGESTION_BUTTONS] = {0}; // function called when back or any suggestion button is touched -static void layoutTouchCallback(const int token, uint8_t index) { +static void keyboard_dispatcher(const int token, uint8_t index __attribute__((unused))) { if (token == BACK_BUTTON_TOKEN) { - // go back to main screen of app - // TODO: instead, back to previous word nbgl_layoutRelease(layout); - display_home_page(); - } - else if (token >= FIRST_SUGGESTION_TOKEN) { + current_word--; + if (current_word <= 0) { + current_word = 0; + display_mnemonic_page(); + } else { + display_keyboard_page(); + } + } else if (token >= FIRST_SUGGESTION_TOKEN) { + nbgl_layoutRelease(layout); // do something with touched button PRINTF("Selected word is %s\n", buttonTexts[token - FIRST_SUGGESTION_TOKEN]); - + current_word++; + if (current_word >= mnemonic_size) { + // TODO: mnemonic completed, now checking the seed. + display_home_page(); + } else { + display_keyboard_page(); + } // go back to main screen of app - nbgl_layoutRelease(layout); - display_home_page(); } } // function called when a key of keyboard is touched -static void keyboardCallback(const char touchedKey) { +static void key_press_callback(const char touchedKey) { size_t textLen = 0; uint32_t mask = 0; const size_t previousTextLen = strlen(textToEnter); @@ -134,22 +205,21 @@ static void keyboardCallback(const char touchedKey) { ); } nbgl_layoutUpdateKeyboard(layout, keyboardIndex, mask); - nbgl_layoutUpdateEnteredText(layout, textIndex, false, 0, &(textToEnter[0]), false); nbgl_refresh(); } -void page_keyboard(void) { +void display_keyboard_page() { nbgl_layoutDescription_t layoutDescription = { .modal = false, - .onActionCallback = &layoutTouchCallback + .onActionCallback = &keyboard_dispatcher }; nbgl_layoutKbd_t kbdInfo = { .lettersOnly = true, // use only letters .upperCase = false, // start with lower case letters .mode = MODE_LETTERS, // start in letters mode .keyMask = 0, // no inactive key - .callback = &keyboardCallback + .callback = &key_press_callback }; nbgl_layoutCenteredInfo_t centeredInfo = { .text1 = NULL, @@ -162,19 +232,25 @@ void page_keyboard(void) { }; strlcpy(textToEnter, "", 1); memset(buttonTexts, 0, NB_MAX_SUGGESTION_BUTTONS); - wordNum++; layout = nbgl_layoutGet(&layoutDescription); nbgl_layoutAddProgressIndicator(layout, 0, 0, true, BACK_BUTTON_TOKEN, TUNE_TAP_CASUAL); - snprintf(headerText, 48, "Enter word no.%d from your \nRecovery Sheet", wordNum); + memset(headerText, 0, HEADER_SIZE); + snprintf( + headerText, + HEADER_SIZE, + "Enter word n. %d/%d from your\nRecovery Sheet", + current_word, + mnemonic_size + ); nbgl_layoutAddCenteredInfo(layout, ¢eredInfo); keyboardIndex = nbgl_layoutAddKeyboard(layout, &kbdInfo); textIndex = nbgl_layoutAddEnteredText(layout, - true, // numbered - wordNum, // number to use - textToEnter, // text to display - false, // not grayed-out - 32); // vertical margin from the buttons + true, // numbered + current_word, // number to use + textToEnter, // text to display + false, // not grayed-out + 32); // vertical margin from the buttons suggestionIndex = nbgl_layoutAddSuggestionButtons(layout, 0, // no used buttons at start-up buttonTexts, @@ -202,7 +278,8 @@ static void display_home_page() { .bottomButtonToken = QUIT_APP_TOKEN, .footerText = NULL, .tapActionText = "Tap to check your mnemonic", - .tapActionToken = START_RECOVER_TOKEN, + .tapActionToken = CHOOSE_MNEMONIC_SIZE_TOKEN, + // .tapActionToken = START_RECOVER_TOKEN, .tuneId = TUNE_TAP_CASUAL }; releaseContext(); @@ -222,7 +299,7 @@ bolos_ux_params_t G_ux_params; #if defined(TARGET_NANOS) -UX_STEP_CB(restore_3_1_1, bb, G_bolos_ux_context.onboarding_kind = BOLOS_UX_ONBOARDING_NEW_24; +UX_STEP_CB(restore_3_1_1, bb, G_bolos_ux_context.onboarding_kind = MNEMONIC_SIZE__24; screen_onboarding_4_restore_word_init(RESTORE_WORD_ACTION_FIRST_WORD); , { @@ -230,7 +307,7 @@ UX_STEP_CB(restore_3_1_1, bb, G_bolos_ux_context.onboarding_kind = BOLOS_UX_ONBO "with 24 words", }); -UX_STEP_CB(restore_3_1_2, bb, G_bolos_ux_context.onboarding_kind = BOLOS_UX_ONBOARDING_NEW_18; +UX_STEP_CB(restore_3_1_2, bb, G_bolos_ux_context.onboarding_kind = MNEMONIC_SIZE_18; screen_onboarding_4_restore_word_init(RESTORE_WORD_ACTION_FIRST_WORD); , { @@ -238,7 +315,7 @@ UX_STEP_CB(restore_3_1_2, bb, G_bolos_ux_context.onboarding_kind = BOLOS_UX_ONBO "with 18 words", }); -UX_STEP_CB(restore_3_1_3, bb, G_bolos_ux_context.onboarding_kind = BOLOS_UX_ONBOARDING_NEW_12; +UX_STEP_CB(restore_3_1_3, bb, G_bolos_ux_context.onboarding_kind = MNEMONIC_SIZE_12; screen_onboarding_4_restore_word_init(RESTORE_WORD_ACTION_FIRST_WORD); , { @@ -297,15 +374,15 @@ const char* number_of_words_getter(unsigned int idx) { void number_of_words_selector(unsigned int idx) { switch (idx) { case 0: - G_bolos_ux_context.onboarding_kind = BOLOS_UX_ONBOARDING_NEW_12; + G_bolos_ux_context.onboarding_kind = MNEMONIC_SIZE_12; screen_onboarding_4_restore_word_init(1 /*entering the first word*/); break; case 1: - G_bolos_ux_context.onboarding_kind = BOLOS_UX_ONBOARDING_NEW_18; + G_bolos_ux_context.onboarding_kind = MNEMONIC_SIZE_18; screen_onboarding_4_restore_word_init(1 /*entering the first word*/); break; case 2: - G_bolos_ux_context.onboarding_kind = BOLOS_UX_ONBOARDING_NEW_24; + G_bolos_ux_context.onboarding_kind = MNEMONIC_SIZE_24; screen_onboarding_4_restore_word_init(1 /*entering the first word*/); break; default: diff --git a/src/ux_common/common.h b/src/ux_common/common.h index 886d784d..33769661 100644 --- a/src/ux_common/common.h +++ b/src/ux_common/common.h @@ -37,7 +37,12 @@ #define MAX_WORD_LENGTH 8 -#define KEYBOARD_ITEM_VALIDATED \ +enum { + MNEMONIC_SIZE_12 = 12, + MNEMONIC_SIZE_18 = 18, + MNEMONIC_SIZE_24 = 24, +}; +#define KEYBOARD_ITEM_VALIDATED \ 1 // callback is called with the entered item index, tmp_element is precharged with element to // be displayed and using the common string buffer as string parameter #define KEYBOARD_RENDER_ITEM \ diff --git a/src/ux_common/onboarding_seed_bip39.c b/src/ux_common/onboarding_seed_bip39.c index cc7e4216..1e1152cb 100644 --- a/src/ux_common/onboarding_seed_bip39.c +++ b/src/ux_common/onboarding_seed_bip39.c @@ -224,7 +224,6 @@ size_t bolos_ux_bip39_fill_with_candidates( bolos_ux_bip39_get_word_count_starting_with(startingChars, startingCharsLenght), NB_MAX_SUGGESTION_BUTTONS ); - PRINTF("There are %d possible suggestions\n", nbMatchingWords); if (nbMatchingWords == 0) { return 0; } diff --git a/src/ux_nanos.h b/src/ux_nanos.h index 391a47d6..75107160 100644 --- a/src/ux_nanos.h +++ b/src/ux_nanos.h @@ -24,14 +24,6 @@ typedef unsigned int (*callback_t)(unsigned int); // bolos ux context (not mandatory if redesigning a bolos ux) typedef struct bolos_ux_context { -#define BOLOS_UX_ONBOARDING_NEW 1 -#define BOLOS_UX_ONBOARDING_NEW_12 12 -#define BOLOS_UX_ONBOARDING_NEW_18 18 -#define BOLOS_UX_ONBOARDING_NEW_24 24 -#define BOLOS_UX_ONBOARDING_RESTORE 2 -#define BOLOS_UX_ONBOARDING_RESTORE_12 12 -#define BOLOS_UX_ONBOARDING_RESTORE_18 18 -#define BOLOS_UX_ONBOARDING_RESTORE_24 24 unsigned int onboarding_kind; #ifdef HAVE_ELECTRUM diff --git a/src/ux_nanox.h b/src/ux_nanox.h index 3ae2b502..8bd8fc2a 100644 --- a/src/ux_nanox.h +++ b/src/ux_nanox.h @@ -22,14 +22,6 @@ // bolos ux context (not mandatory if redesigning a bolos ux) typedef struct bolos_ux_context { -#define BOLOS_UX_ONBOARDING_NEW 1 -#define BOLOS_UX_ONBOARDING_NEW_12 12 -#define BOLOS_UX_ONBOARDING_NEW_18 18 -#define BOLOS_UX_ONBOARDING_NEW_24 24 -#define BOLOS_UX_ONBOARDING_RESTORE 2 -#define BOLOS_UX_ONBOARDING_RESTORE_12 12 -#define BOLOS_UX_ONBOARDING_RESTORE_18 18 -#define BOLOS_UX_ONBOARDING_RESTORE_24 24 unsigned int onboarding_kind; #ifdef HAVE_ELECTRUM From 875c7d3ea3ed0187b32e2b096eca78104eec5838 Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Wed, 19 Oct 2022 18:42:59 +0200 Subject: [PATCH 19/65] [ci][add] Clang static analysis for all target --- .github/workflows/ci-workflow.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index 65092633..525ecb82 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -42,18 +42,25 @@ jobs: job_scan_build: name: Clang Static Analyzer + strategy: + matrix: + include: + - SDK: "$NANOS_SDK" + model: nanos + - SDK: "$NANOX_SDK" + model: nanox + - SDK: "$NANOSP_SDK" + model: nanosp runs-on: ubuntu-latest container: image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder:latest steps: - name: Clone uses: actions/checkout@v2 - - name: Build with Clang Static Analyzer run: | make clean - scan-build --use-cc=clang -analyze-headers -enable-checker security -enable-checker unix -enable-checker valist -o scan-build --status-bugs make default - + scan-build --use-cc=clang -analyze-headers -enable-checker security -enable-checker unix -enable-checker valist -o scan-build --status-bugs make default BOLOS_SDK=${{ matrix.SDK }} - name: Upload scan result uses: actions/upload-artifact@v2 if: failure() From c58682951c041cf2f414520d68a18b200ee6fe87 Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Thu, 20 Oct 2022 14:37:43 +0200 Subject: [PATCH 20/65] [ci][add] Fatstacks compilation --- .github/workflows/ci-workflow.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index 525ecb82..1f03f507 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -40,6 +40,32 @@ jobs: name: apps path: bin/*.elf + fatstacks_build: + name: Build application for Fatstacks + runs-on: ubuntu-latest + container: + image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder:latest + steps: + - name: Clone application + uses: actions/checkout@v2 + - name: Clone SDK + uses: actions/checkout@v2 + with: + repository: LedgerHQ/ledger-secure-sdk + path: ./stax-sdk + ref: master + token: ${{ secrets.STAX_RO }} + - name: Build + run: | + git config --global --add safe.directory "$GITHUB_WORKSPACE" + make BOLOS_SDK=./stax-sdk TARGET=fatstacks + mv bin/app.elf "bin/fatstacks.elf" + - name: Upload app binary + uses: actions/upload-artifact@v2 + with: + name: apps + path: bin/*.elf + job_scan_build: name: Clang Static Analyzer strategy: From db59ac1d7dbbbc689e2261a8a460864058ba9a1b Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Thu, 20 Oct 2022 17:59:59 +0200 Subject: [PATCH 21/65] [add] Mnemonic record & final check with result --- src/ui.c | 104 ++++++++++++++++++++++++-- src/ux_common/common_bip39.h | 11 +-- src/ux_common/onboarding_seed_bip39.c | 40 +++++----- tests/speculos_mnemonic.txt | 3 + 4 files changed, 126 insertions(+), 32 deletions(-) create mode 100644 tests/speculos_mnemonic.txt diff --git a/src/ui.c b/src/ui.c index 3020fbf6..d8979933 100644 --- a/src/ui.c +++ b/src/ui.c @@ -12,16 +12,22 @@ #define HEADER_SIZE 50 +#define MAX_MNEMONIC_LENGTH (MNEMONIC_SIZE_24 * (MAX_WORD_LENGTH+1)) + static nbgl_page_t* pageContext; static int current_word = 0; static char headerText[HEADER_SIZE] = {0}; static nbgl_layout_t *layout = 0; static uint8_t mnemonic_size = 0; +static char mnemonic_buffer[MAX_MNEMONIC_LENGTH] = {0}; +static size_t mnemonic_buffer_length = 0; void ui_menu_about(void); void display_keyboard_page(void); void display_home_page(void); +void display_result_page(const bool result); void display_mnemonic_page(void); +void reset_globals(void); void releaseContext(void) { if (pageContext != NULL) { @@ -47,7 +53,7 @@ void pageTouchCallback(int token, uint8_t index __attribute__((unused))) { } else if (token == INFO_TOKEN) { ui_menu_about(); } else if (token == CHOOSE_MNEMONIC_SIZE_TOKEN) { - current_word = 1; + reset_globals(); display_mnemonic_page(); } else if (token == BACK_HOME_TOKEN) { releaseContext(); @@ -144,12 +150,27 @@ void display_mnemonic_page() { static char textToEnter[MAX_WORD_LENGTH + 1] = {0}; static int textIndex, suggestionIndex, keyboardIndex = 0; static char *buttonTexts[NB_MAX_SUGGESTION_BUTTONS] = {0}; +// the biggest word of BIP39 list is 8 char (9 with trailing '\0'), and +// the max number of showed suggestions is NB_MAX_SUGGESTION_BUTTONS +static char wordCandidates[(MAX_WORD_LENGTH + 1) * NB_MAX_SUGGESTION_BUTTONS] = {0}; +static size_t current_word_length = 0; + // function called when back or any suggestion button is touched static void keyboard_dispatcher(const int token, uint8_t index __attribute__((unused))) { + PRINTF("Current mnemonic is: '%s'\n", mnemonic_buffer); if (token == BACK_BUTTON_TOKEN) { nbgl_layoutRelease(layout); current_word--; + + // removing previous word from mnemonic buffer + if (current_word_length + 1 > mnemonic_buffer_length) { + mnemonic_buffer_length = 0; + } else { + mnemonic_buffer_length -= (current_word_length + 1); + } + memset(&mnemonic_buffer[mnemonic_buffer_length], 0, MAX_MNEMONIC_LENGTH - mnemonic_buffer_length); + if (current_word <= 0) { current_word = 0; display_mnemonic_page(); @@ -158,13 +179,35 @@ static void keyboard_dispatcher(const int token, uint8_t index __attribute__((un } } else if (token >= FIRST_SUGGESTION_TOKEN) { nbgl_layoutRelease(layout); - // do something with touched button + + current_word_length = strlen(buttonTexts[token - FIRST_SUGGESTION_TOKEN]); + PRINTF("Copying word '%s' (size '%d') to 0x%p\n", + buttonTexts[token - FIRST_SUGGESTION_TOKEN], + current_word_length, + &mnemonic_buffer[0] + mnemonic_buffer_length + ); PRINTF("Selected word is %s\n", buttonTexts[token - FIRST_SUGGESTION_TOKEN]); + memcpy(&mnemonic_buffer[0] + mnemonic_buffer_length, + buttonTexts[token - FIRST_SUGGESTION_TOKEN], + current_word_length); + mnemonic_buffer_length += current_word_length; + PRINTF("Current mnemonic --> '%s'\n", mnemonic_buffer); current_word++; - if (current_word >= mnemonic_size) { + + // current_word starts at 1 + if (current_word > mnemonic_size) { + PRINTF("Mnemonic completed! --> '%s' (size %d)\n", mnemonic_buffer, mnemonic_buffer_length); + const bool result = bolos_ux_mnemonic_check( + (unsigned char *)&mnemonic_buffer[0], + mnemonic_buffer_length + ); + // clearing the mnemonic ASAP + memset(&mnemonic_buffer[0], 0, mnemonic_buffer_length); // TODO: mnemonic completed, now checking the seed. - display_home_page(); + display_result_page(result); } else { + mnemonic_buffer[mnemonic_buffer_length++] = ' '; + mnemonic_buffer[mnemonic_buffer_length] = '\0'; display_keyboard_page(); } // go back to main screen of app @@ -187,16 +230,20 @@ static void key_press_callback(const char touchedKey) { textToEnter[previousTextLen + 1] = '\0'; textLen = previousTextLen + 1; } - + PRINTF("Current text is: '%s' (size '%d')\n", textToEnter, textLen); if (textLen < 2) { nbgl_layoutUpdateSuggestionButtons(layout, suggestionIndex, 0, buttonTexts); } else { const size_t nbMatchingWords = bolos_ux_bip39_fill_with_candidates( (unsigned char *)&(textToEnter[0]), strlen(textToEnter), + wordCandidates, buttonTexts ); nbgl_layoutUpdateSuggestionButtons(layout, suggestionIndex, nbMatchingWords, buttonTexts); + for (size_t i=0; i 0) { mask = bolos_ux_bip39_get_keyboard_mask( @@ -279,7 +326,6 @@ static void display_home_page() { .footerText = NULL, .tapActionText = "Tap to check your mnemonic", .tapActionToken = CHOOSE_MNEMONIC_SIZE_TOKEN, - // .tapActionToken = START_RECOVER_TOKEN, .tuneId = TUNE_TAP_CASUAL }; releaseContext(); @@ -287,6 +333,49 @@ static void display_home_page() { nbgl_refresh(); } +/* + * Result page + */ +static char *possible_results[2] = { + "Sorry, this passphrase\nis incorrect.", + "You passphrase\nis correct!" +}; + +static void display_result_page(const bool result) { + nbgl_pageInfoDescription_t home = { + /* .centeredInfo.icon = &C_fatstacks_app_recovery_check, */ + .centeredInfo.icon = NULL, + .centeredInfo.text1 = possible_results[result], + .centeredInfo.text2 = NULL, + .centeredInfo.text3 = NULL, + .centeredInfo.style = LARGE_CASE_INFO, + .centeredInfo.offsetY = 32, + .topRightStyle = NO_BUTTON_STYLE, + .bottomButtonStyle = QUIT_ICON, + .bottomButtonToken = QUIT_APP_TOKEN, + .footerText = NULL, + .tapActionText = "Tap to check another mnemonic", + .tapActionToken = CHOOSE_MNEMONIC_SIZE_TOKEN, + .tuneId = TUNE_TAP_CASUAL + }; + releaseContext(); + pageContext = nbgl_pageDrawInfo(&pageTouchCallback, NULL, &home); + nbgl_refresh(); +} + +/* + * Utils + */ + +static void reset_globals() { + current_word = 1; + memset(mnemonic_buffer, 0, sizeof(mnemonic_buffer) / sizeof(char)); + mnemonic_buffer_length = 0; + mnemonic_size = 0; + current_word_length = 0; + memset(buttonTexts, 0, NB_MAX_SUGGESTION_BUTTONS); +} + #endif @@ -299,7 +388,7 @@ bolos_ux_params_t G_ux_params; #if defined(TARGET_NANOS) -UX_STEP_CB(restore_3_1_1, bb, G_bolos_ux_context.onboarding_kind = MNEMONIC_SIZE__24; +UX_STEP_CB(restore_3_1_1, bb, G_bolos_ux_context.onboarding_kind = MNEMONIC_SIZE_24; screen_onboarding_4_restore_word_init(RESTORE_WORD_ACTION_FIRST_WORD); , { @@ -443,6 +532,7 @@ void ui_idle_init(void) { #endif #if defined(HAVE_NBGL) + reset_globals(); display_home_page(); #endif diff --git a/src/ux_common/common_bip39.h b/src/ux_common/common_bip39.h index 14c4bd58..624cf800 100644 --- a/src/ux_common/common_bip39.h +++ b/src/ux_common/common_bip39.h @@ -4,8 +4,8 @@ // BIP39 helpers #include "onboarding_seed_rom_variables.h" -// return 0 if mnemonic is invalid -unsigned int bolos_ux_mnemonic_check(const unsigned char *mnemonic, unsigned int mnemonicLength); +// whether the mnemonic is invalid or not +bool bolos_ux_mnemonic_check(const unsigned char *mnemonic, unsigned int mnemonicLength); // passphrase will be prefixed with "MNEMONIC" from BIP39, the passphrase content shall start @ 8 void bolos_ux_mnemonic_to_seed(const unsigned char *mnemonic, @@ -22,9 +22,10 @@ unsigned int bolos_ux_bip39_get_word_next_letters_starting_with(const unsigned c unsigned char *next_letters_buffer); #if defined(HAVE_NBGL) -size_t bolos_ux_bip39_fill_with_candidates(const unsigned char *startingChars, - const size_t startingCharsLenght, - char *outputBuffer[]); +size_t bolos_ux_bip39_fill_with_candidates(const unsigned char * startingChars, + const size_t startingCharsLength, + char wordCandidatesBuffer[], + char *wordIndexorBuffer[]); uint32_t bolos_ux_bip39_get_keyboard_mask(const unsigned char *prefix, const unsigned int prefixLength); #endif diff --git a/src/ux_common/onboarding_seed_bip39.c b/src/ux_common/onboarding_seed_bip39.c index 1e1152cb..fa5a3244 100644 --- a/src/ux_common/onboarding_seed_bip39.c +++ b/src/ux_common/onboarding_seed_bip39.c @@ -38,7 +38,7 @@ void bolos_ux_mnemonic_to_seed(unsigned char* mnemonic, // what happen to the second block for a very short seed ? } -unsigned int bolos_ux_mnemonic_check(unsigned char* mnemonic, unsigned int mnemonicLength) { +bool bolos_ux_mnemonic_check(unsigned char* mnemonic, unsigned int mnemonicLength) { unsigned int i, n = 0; unsigned int bi; unsigned char bits[32 + 1]; @@ -51,7 +51,7 @@ unsigned int bolos_ux_mnemonic_check(unsigned char* mnemonic, unsigned int mnemo } n++; if (n != 12 && n != 18 && n != 24) { - return 0; + return false; } memset(bits, 0, sizeof(bits)); i = 0; @@ -63,7 +63,7 @@ unsigned int bolos_ux_mnemonic_check(unsigned char* mnemonic, unsigned int mnemo j = 0; while (i < mnemonicLength && mnemonic[i] != ' ') { if (j >= sizeof(current_word)) { - return 0; + return false; } current_word[j] = mnemonic[i]; current_word_size = j; @@ -90,11 +90,11 @@ unsigned int bolos_ux_mnemonic_check(unsigned char* mnemonic, unsigned int mnemo } } if (k == (unsigned int) (BIP39_WORDLIST_OFFSETS_LENGTH - 1)) { - return 0; + return false; } } if (bi != n * 11) { - return 0; + return false; } bits[32] = bits[n * 4 / 3]; cx_hash_sha256(bits, n * 4 / 3, bits, 32); @@ -110,11 +110,11 @@ unsigned int bolos_ux_mnemonic_check(unsigned char* mnemonic, unsigned int mnemo break; } if ((bits[0] & mask) != (bits[32] & mask)) { - return 0; + return false; } // alright mnemonic is ok - return 1; + return true; } unsigned int bolos_ux_bip39_idx_strcpy(const unsigned int index, unsigned char* buffer) { @@ -158,9 +158,8 @@ unsigned int bolos_ux_bip39_get_word_count_starting_with(const unsigned char* pr } if (j == prefixLength) { count++; - } - // don't seek till the end, abort when the prefix is not matched anymore - else if (count > 0) { + } else if (count > 0) { + // don't seek till the end, abort when the prefix is not matched anymore break; } } @@ -211,33 +210,33 @@ unsigned int bolos_ux_bip39_get_word_next_letters_starting_with( #if defined(HAVE_NBGL) #include -// the biggest word of BIP39 list is 8 char (9 with trailing '\0'), and -// the max number of showed suggestions is NB_MAX_SUGGESTION_BUTTONS -static char wordCandidates[(MAX_WORD_LENGTH + 1) * NB_MAX_SUGGESTION_BUTTONS] = {0}; - size_t bolos_ux_bip39_fill_with_candidates( const unsigned char * startingChars, - const size_t startingCharsLenght, - char *outputBuffer[] + const size_t startingCharsLength, + char wordCandidatesBuffer[], + char *wordIndexorBuffer[] ) { + PRINTF("Calculating nb of words starting with '%s' (size is '%d')\n", startingChars, startingCharsLength); const size_t nbMatchingWords = MIN( - bolos_ux_bip39_get_word_count_starting_with(startingChars, startingCharsLenght), + bolos_ux_bip39_get_word_count_starting_with(startingChars, startingCharsLength), NB_MAX_SUGGESTION_BUTTONS ); + PRINTF("'%d' words start with '%s'\n", nbMatchingWords, startingChars); if (nbMatchingWords == 0) { return 0; } size_t matchingWordIndex = bolos_ux_bip39_get_word_idx_starting_with( startingChars, - startingCharsLenght + startingCharsLength ); size_t offset = 0; for (size_t i = 0; i < nbMatchingWords; i++) { - unsigned char *wordDest = (unsigned char *)(&wordCandidates + offset); + unsigned char * const wordDest = (unsigned char *)(&wordCandidatesBuffer[0] + offset); const size_t wordSize = bolos_ux_bip39_idx_strcpy(matchingWordIndex, wordDest); matchingWordIndex++; + *(wordDest + wordSize) = '\0'; offset += wordSize + 1; // + trailing '\0' size - outputBuffer[i] = (char *)wordDest; + wordIndexorBuffer[i] = (char *)wordDest; } return nbMatchingWords; } @@ -248,6 +247,7 @@ uint32_t bolos_ux_bip39_get_keyboard_mask( ) { uint32_t existing_mask = 0; unsigned char next_letters[ALPHABET_LENGTH] = {0}; + PRINTF("Looking for letter candidates following '%s'\n", prefix); const size_t nb_letters = bolos_ux_bip39_get_word_next_letters_starting_with(prefix, prefixLength, next_letters); next_letters[nb_letters] = '\0'; PRINTF("Next letters are in: %s\n", next_letters); diff --git a/tests/speculos_mnemonic.txt b/tests/speculos_mnemonic.txt new file mode 100644 index 00000000..14dce39e --- /dev/null +++ b/tests/speculos_mnemonic.txt @@ -0,0 +1,3 @@ +glory promote mansion idle axis finger extra february uncover one +trip resource lawn turtle enact monster seven myth punch hobby +comfort wild raise skin From b9f601690d78050ca2e4bac03833b575c23a531e Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Fri, 21 Oct 2022 17:45:19 +0200 Subject: [PATCH 22/65] [fix] word count could be incorrectly initialized --- .gitignore | 1 + src/ui.c | 6 +----- tests/speculos_mnemonic.txt | 3 --- 3 files changed, 2 insertions(+), 8 deletions(-) delete mode 100644 tests/speculos_mnemonic.txt diff --git a/.gitignore b/.gitignore index d50062f9..3d1939cb 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ fuzzing/build/ # Editors .idea/ +*~ # Python *.pyc[cod] diff --git a/src/ui.c b/src/ui.c index d8979933..f259e3aa 100644 --- a/src/ui.c +++ b/src/ui.c @@ -53,7 +53,6 @@ void pageTouchCallback(int token, uint8_t index __attribute__((unused))) { } else if (token == INFO_TOKEN) { ui_menu_about(); } else if (token == CHOOSE_MNEMONIC_SIZE_TOKEN) { - reset_globals(); display_mnemonic_page(); } else if (token == BACK_HOME_TOKEN) { releaseContext(); @@ -115,6 +114,7 @@ void mnemonic_dispatcher(const int token, uint8_t index) { } void display_mnemonic_page() { + reset_globals(); nbgl_layoutDescription_t layoutDescription = { .modal = false, .onActionCallback = mnemonic_dispatcher @@ -158,7 +158,6 @@ static size_t current_word_length = 0; // function called when back or any suggestion button is touched static void keyboard_dispatcher(const int token, uint8_t index __attribute__((unused))) { - PRINTF("Current mnemonic is: '%s'\n", mnemonic_buffer); if (token == BACK_BUTTON_TOKEN) { nbgl_layoutRelease(layout); current_word--; @@ -241,9 +240,6 @@ static void key_press_callback(const char touchedKey) { buttonTexts ); nbgl_layoutUpdateSuggestionButtons(layout, suggestionIndex, nbMatchingWords, buttonTexts); - for (size_t i=0; i 0) { mask = bolos_ux_bip39_get_keyboard_mask( diff --git a/tests/speculos_mnemonic.txt b/tests/speculos_mnemonic.txt deleted file mode 100644 index 14dce39e..00000000 --- a/tests/speculos_mnemonic.txt +++ /dev/null @@ -1,3 +0,0 @@ -glory promote mansion idle axis finger extra february uncover one -trip resource lawn turtle enact monster seven myth punch hobby -comfort wild raise skin From cb7c51d0e4dfa7e79e9c28556ab77bc08ef56d52 Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Thu, 3 Nov 2022 15:46:17 +0100 Subject: [PATCH 23/65] [add] Interface functional testing --- tests/functional/__init__.py | 0 tests/functional/app.py | 22 +++++++ tests/functional/conftest.py | 83 ++++++++++++++++++++++++ tests/functional/requirements.txt | 1 + tests/functional/test_fatstacks.py | 74 +++++++++++++++++++++ tests/functional/utils.py | 23 +++++++ tests/screenshots/correct.png | Bin 0 -> 4513 bytes tests/screenshots/first_12.png | Bin 0 -> 4893 bytes tests/screenshots/first_18.png | Bin 0 -> 4901 bytes tests/screenshots/first_24.png | Bin 0 -> 4911 bytes tests/screenshots/incorrect.png | Bin 0 -> 4757 bytes tests/screenshots/info.png | Bin 0 -> 4913 bytes tests/screenshots/passphrase_length.png | Bin 0 -> 5201 bytes tests/screenshots/welcome.png | Bin 0 -> 4715 bytes 14 files changed, 203 insertions(+) create mode 100644 tests/functional/__init__.py create mode 100644 tests/functional/app.py create mode 100644 tests/functional/conftest.py create mode 100644 tests/functional/requirements.txt create mode 100644 tests/functional/test_fatstacks.py create mode 100644 tests/functional/utils.py create mode 100644 tests/screenshots/correct.png create mode 100644 tests/screenshots/first_12.png create mode 100644 tests/screenshots/first_18.png create mode 100644 tests/screenshots/first_24.png create mode 100644 tests/screenshots/incorrect.png create mode 100644 tests/screenshots/info.png create mode 100644 tests/screenshots/passphrase_length.png create mode 100644 tests/screenshots/welcome.png diff --git a/tests/functional/__init__.py b/tests/functional/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/functional/app.py b/tests/functional/app.py new file mode 100644 index 00000000..8277c616 --- /dev/null +++ b/tests/functional/app.py @@ -0,0 +1,22 @@ +from ragger.firmware.fatstacks.layouts import CancelFooter, ChoiceList, InfoHeader, \ + LetterOnlyKeyboard, NavigationHeader, Suggestions, TappableCenter +from ragger.firmware.fatstacks.screen import MetaScreen + + +class Screen(metaclass=MetaScreen): + layout_center = TappableCenter + layout_choice_list = ChoiceList + layout_keyboard = LetterOnlyKeyboard + layout_suggestions = Suggestions + layout_info = InfoHeader + layout_navigation = NavigationHeader + layout_footer = CancelFooter + + def exit(self): + did_raise = False + try: + self.footer.tap() + except: + did_raise = True + if not did_raise: + raise RuntimeError("The application did not exit at this state") diff --git a/tests/functional/conftest.py b/tests/functional/conftest.py new file mode 100644 index 00000000..222db067 --- /dev/null +++ b/tests/functional/conftest.py @@ -0,0 +1,83 @@ +from pathlib import Path +import pytest + +from ragger import Firmware +from ragger.backend import SpeculosBackend, LedgerCommBackend, LedgerWalletBackend, BackendInterface + + +def __str__(self): # also tried __repr__() + # Attempt to print the 'select' attribute in "pytest -v" output + return self.select + + +APPLICATION = (Path(__file__).parent.parent / "elfs" / "recovery_check.elf").resolve() +BACKENDS = ["speculos", "ledgercomm", "ledgerwallet"] +FIRMWARES = [ + Firmware('fat', '1.0'), +] + +def pytest_addoption(parser): + parser.addoption("--backend", action="store", default="speculos") + # Enable using --'device' in the pytest command line to restrict testing to specific devices + for fw in FIRMWARES: + parser.addoption("--" + fw.device, action="store_true", help="run on physical device only") + + +# Glue to call every test that depends on the firmware once for each required firmware +def pytest_generate_tests(metafunc): + if "firmware" in metafunc.fixturenames: + fw_list = [] + ids = [] + # First pass: enable only demanded firmwares + for fw in FIRMWARES: + if metafunc.config.getoption(fw.device): + fw_list.append(fw) + ids.append(fw.device + " " + fw.version) + # Second pass if no specific firmware demanded: add them all + if not fw_list: + for fw in FIRMWARES: + fw_list.append(fw) + ids.append(fw.device + " " + fw.version) + metafunc.parametrize("firmware", fw_list, ids=ids) + +def prepare_speculos_args(firmware): + speculos_args = [] + # Uncomment line below to enable display + # speculos_args += ["--display", "qt"] + # Compute Exchange App binary + return ([str(APPLICATION)], {"args": speculos_args}) + + +@pytest.fixture(scope="session") +def backend(pytestconfig): + return pytestconfig.getoption("backend") + + +def create_backend(backend: str, firmware: Firmware) -> BackendInterface: + if backend.lower() == "ledgercomm": + return LedgerCommBackend(firmware, interface="hid") + elif backend.lower() == "ledgerwallet": + return LedgerWalletBackend(firmware) + elif backend.lower() == "speculos": + args, kwargs = prepare_speculos_args(firmware) + print(args, kwargs) + return SpeculosBackend(*args, firmware, **kwargs) + else: + raise ValueError(f"Backend '{backend}' is unknown. Valid backends are: {BACKENDS}") + +@pytest.fixture +def client(backend, firmware): + with create_backend(backend, firmware) as b: + yield b + +@pytest.fixture(autouse=True) +def use_only_on_backend(request, backend): + if request.node.get_closest_marker('use_on_backend'): + current_backend = request.node.get_closest_marker('use_on_backend').args[0] + if current_backend != backend: + pytest.skip('skipped on this backend: {}'.format(current_backend)) + +def pytest_configure(config): + config.addinivalue_line( + "markers", "use_only_on_backend(backend): skip test if not on the specified backend", + ) diff --git a/tests/functional/requirements.txt b/tests/functional/requirements.txt new file mode 100644 index 00000000..3a550993 --- /dev/null +++ b/tests/functional/requirements.txt @@ -0,0 +1 @@ +ragger[tests,speculos] diff --git a/tests/functional/test_fatstacks.py b/tests/functional/test_fatstacks.py new file mode 100644 index 00000000..4bcbc715 --- /dev/null +++ b/tests/functional/test_fatstacks.py @@ -0,0 +1,74 @@ +from time import sleep +from pathlib import Path + +from pytest import fixture +from ragger.backend import BackendInterface + +from .app import Screen +from .utils import assert_current_equals, SCREENSHOTS + + +SPECULOS_MNEMONIC = "glory promote mansion idle axis finger extra " \ + "february uncover one trip resource lawn turtle enact monster " \ + "seven myth punch hobby comfort wild raise skin" + +@fixture +def screen(client: BackendInterface): + s = Screen(client, client.firmware) + client.finger_touch(600, 0) + assert_current_equals(client, SCREENSHOTS / "welcome.png") + return s + + +def test_nominal_full_passphrase_check(screen: Screen, client: BackendInterface): + # going to choose mnemonic length + screen.center.tap() + assert_current_equals(client, SCREENSHOTS / "passphrase_length.png") + # choosing 3d (24 words) + screen.choice_list.choose(3) + assert_current_equals(client, SCREENSHOTS / "first_24.png") + for word in SPECULOS_MNEMONIC.split(): + # 4 letters are enough to discriminate the correct word + screen.keyboard.write(word[:4]) + # choosing 1st suggestion + screen.suggestions.choose(1) + sleep(0.1) + assert_current_equals(client, SCREENSHOTS / "correct.png") + screen.exit() + + +def test_check_info_then_leave(screen: Screen, client: BackendInterface): + screen.info.tap() + assert_current_equals(client, SCREENSHOTS / "info.png") + screen.footer.tap() + assert_current_equals(client, SCREENSHOTS / "welcome.png") + screen.exit() + + +def test_nominal_full_passphrase_check_error_wrong_passphrase(screen: Screen, client: BackendInterface): + screen.center.tap() + assert_current_equals(client, SCREENSHOTS / "passphrase_length.png") + # choosing 1st (12 words) + screen.choice_list.choose(1) + assert_current_equals(client, SCREENSHOTS / "first_12.png") + # only the 12 first words + for word in SPECULOS_MNEMONIC.split()[:12]: + screen.keyboard.write(word[:4]) + screen.suggestions.choose(1) + sleep(0.1) + assert_current_equals(client, SCREENSHOTS / "incorrect.png") + screen.exit() + + +def test_check_previous_word(screen: Screen, client: BackendInterface): + screen.center.tap() + screen.choice_list.choose(1) + assert_current_equals(client, SCREENSHOTS / "first_12.png") + tries = ["rand", "ok"] + for word in tries: + screen.keyboard.write(word[:4]) + screen.suggestions.choose(1) + # coming back N time, should bring back to the first word page + for _ in tries: + screen.navigation.tap() + assert_current_equals(client, SCREENSHOTS / "first_12.png") diff --git a/tests/functional/utils.py b/tests/functional/utils.py new file mode 100644 index 00000000..2a102f2b --- /dev/null +++ b/tests/functional/utils.py @@ -0,0 +1,23 @@ +from pathlib import Path +from time import time + +from ragger.backend import BackendInterface + +SCREENSHOTS = (Path(__file__).parent.parent / "screenshots").resolve() + + +def assert_current_equals(client: BackendInterface, existing: Path): + current = client._client.get_screenshot() + assert_equal(current, existing) + + +def assert_equal(image: bytes, existing: Path): + error_file = existing.parent / f'{existing.stem}_{time()}{existing.suffix}' + try: + with existing.open('rb') as filee: + assert image == filee.read(), \ + f"Given bytes does not match image '{existing}'. Check '{error_file}' for comparison" + except: + with error_file.open('wb') as filee: + filee.write(image) + raise diff --git a/tests/screenshots/correct.png b/tests/screenshots/correct.png new file mode 100644 index 0000000000000000000000000000000000000000..b5678b0e5313d0d6c49296d2b3bcbda093bf4607 GIT binary patch literal 4513 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 literal 0 HcmV?d00001 diff --git a/tests/screenshots/first_12.png b/tests/screenshots/first_12.png new file mode 100644 index 0000000000000000000000000000000000000000..9584bf1c7f88016d4230d28fce22c47971915fdf GIT binary patch literal 4893 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 Ygo#=2d>R?D)%F6MadbXSKjoM3@1Sjeod5s; literal 0 HcmV?d00001 diff --git a/tests/screenshots/first_18.png b/tests/screenshots/first_18.png new file mode 100644 index 0000000000000000000000000000000000000000..f39305b24a4c3c8a82f91a2f92d390ab7b927f46 GIT binary patch literal 4901 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 literal 0 HcmV?d00001 diff --git a/tests/screenshots/incorrect.png b/tests/screenshots/incorrect.png new file mode 100644 index 0000000000000000000000000000000000000000..9ac7eb37b0e2d221be1b1be9c587d8c0e0d17e40 GIT binary patch literal 4757 zcmeHLYgiK4x~8&GGcVPIc^kXfn2DMtnwn8fhUKl)6r{Z5V3UZK6z@RUs5O?2Q_5ti zspB04!%He?Xl7}|#tVq1Xyz4BGZ8_=gLBSv&cCyN>>qpo=-+zQde(Zr^?vVK-}`>| zF1kV0x9M&J008Reoz8jy0GoaX05(5Y*{qncbEj1T06VXoKYQ9M?wN3i9aG`7IrUO* zAGtsNZI9;kmHtPO<=;DA`d&s6Bllmtg)_-C5_gP6ZgE=3rz-<4I#X`#N#6Qe3$!;i zV3Vbi+TDGyQ`;Gvuc-jFe)_ib4UKQy_;)=Ays&n3bxppU)iGAx<@8DRL!%>iVpM*74s9juE*8{-n!{xJkS^bfVHe?eVVu z$--5xHcOz*1l@8UOTHYX(+vU~>KA&pnt++UOR8gtLoLl_xmXuhbW_rir3O^G_0_na z;~o48iWd32R;+qJj{a_t51$kW?1mS4_LGUPOMqf zcO})~)CCVO2cqqVk##IbajNDlKV$YOS##AZDv4TEL>J1tHO))%;su�#lq>Y%W2H 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 literal 0 HcmV?d00001 diff --git a/tests/screenshots/passphrase_length.png b/tests/screenshots/passphrase_length.png new file mode 100644 index 0000000000000000000000000000000000000000..51ba5f94616eb20dee69c2efcc6110cd4ab9085c GIT binary patch literal 5201 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 literal 0 HcmV?d00001 From 5dcb8f7c49b05878559449a036c41fa8397b3e0e Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Thu, 3 Nov 2022 16:30:05 +0100 Subject: [PATCH 24/65] [clean] Regroup mnemonic logic in its own module --- src/ui.c | 71 ++++++------------------- src/ux_stax.c | 83 ++++++++++++++++++++++++++++++ src/ux_stax.h | 56 +++++++++++++++++++- tests/functional/test_fatstacks.py | 3 ++ 4 files changed, 157 insertions(+), 56 deletions(-) create mode 100644 src/ux_stax.c diff --git a/src/ui.c b/src/ui.c index f259e3aa..8993e5f2 100644 --- a/src/ui.c +++ b/src/ui.c @@ -15,12 +15,8 @@ #define MAX_MNEMONIC_LENGTH (MNEMONIC_SIZE_24 * (MAX_WORD_LENGTH+1)) static nbgl_page_t* pageContext; -static int current_word = 0; static char headerText[HEADER_SIZE] = {0}; static nbgl_layout_t *layout = 0; -static uint8_t mnemonic_size = 0; -static char mnemonic_buffer[MAX_MNEMONIC_LENGTH] = {0}; -static size_t mnemonic_buffer_length = 0; void ui_menu_about(void); void display_keyboard_page(void); @@ -94,13 +90,13 @@ void mnemonic_dispatcher(const int token, uint8_t index) { } else if (token == CHOOSE_MNEMONIC_SIZE_TOKEN) { switch(index) { case 0: - mnemonic_size = MNEMONIC_SIZE_12; + set_mnemonic_final_size(MNEMONIC_SIZE_12); break; case 1: - mnemonic_size = MNEMONIC_SIZE_18; + set_mnemonic_final_size(MNEMONIC_SIZE_18); break; case 2: - mnemonic_size = MNEMONIC_SIZE_24; + set_mnemonic_final_size(MNEMONIC_SIZE_24); break; default: PRINTF("Unexpected index '%d' (max 3)\n", index); @@ -153,60 +149,30 @@ static char *buttonTexts[NB_MAX_SUGGESTION_BUTTONS] = {0}; // the biggest word of BIP39 list is 8 char (9 with trailing '\0'), and // the max number of showed suggestions is NB_MAX_SUGGESTION_BUTTONS static char wordCandidates[(MAX_WORD_LENGTH + 1) * NB_MAX_SUGGESTION_BUTTONS] = {0}; -static size_t current_word_length = 0; - // function called when back or any suggestion button is touched static void keyboard_dispatcher(const int token, uint8_t index __attribute__((unused))) { if (token == BACK_BUTTON_TOKEN) { nbgl_layoutRelease(layout); - current_word--; - - // removing previous word from mnemonic buffer - if (current_word_length + 1 > mnemonic_buffer_length) { - mnemonic_buffer_length = 0; + if (remove_word_from_mnemonic()) { + display_keyboard_page(); } else { - mnemonic_buffer_length -= (current_word_length + 1); - } - memset(&mnemonic_buffer[mnemonic_buffer_length], 0, MAX_MNEMONIC_LENGTH - mnemonic_buffer_length); - - if (current_word <= 0) { - current_word = 0; display_mnemonic_page(); - } else { - display_keyboard_page(); } } else if (token >= FIRST_SUGGESTION_TOKEN) { nbgl_layoutRelease(layout); - current_word_length = strlen(buttonTexts[token - FIRST_SUGGESTION_TOKEN]); - PRINTF("Copying word '%s' (size '%d') to 0x%p\n", + PRINTF("Selected word is '%s' (size '%d')\n", buttonTexts[token - FIRST_SUGGESTION_TOKEN], - current_word_length, - &mnemonic_buffer[0] + mnemonic_buffer_length + strlen(buttonTexts[token - FIRST_SUGGESTION_TOKEN])); + add_word_in_mnemonic( + buttonTexts[token - FIRST_SUGGESTION_TOKEN], + strlen(buttonTexts[token - FIRST_SUGGESTION_TOKEN]) ); - PRINTF("Selected word is %s\n", buttonTexts[token - FIRST_SUGGESTION_TOKEN]); - memcpy(&mnemonic_buffer[0] + mnemonic_buffer_length, - buttonTexts[token - FIRST_SUGGESTION_TOKEN], - current_word_length); - mnemonic_buffer_length += current_word_length; - PRINTF("Current mnemonic --> '%s'\n", mnemonic_buffer); - current_word++; - // current_word starts at 1 - if (current_word > mnemonic_size) { - PRINTF("Mnemonic completed! --> '%s' (size %d)\n", mnemonic_buffer, mnemonic_buffer_length); - const bool result = bolos_ux_mnemonic_check( - (unsigned char *)&mnemonic_buffer[0], - mnemonic_buffer_length - ); - // clearing the mnemonic ASAP - memset(&mnemonic_buffer[0], 0, mnemonic_buffer_length); - // TODO: mnemonic completed, now checking the seed. - display_result_page(result); + if (is_mnemonic_complete()) { + display_result_page(check_mnemonic()); } else { - mnemonic_buffer[mnemonic_buffer_length++] = ' '; - mnemonic_buffer[mnemonic_buffer_length] = '\0'; display_keyboard_page(); } // go back to main screen of app @@ -231,6 +197,7 @@ static void key_press_callback(const char touchedKey) { } PRINTF("Current text is: '%s' (size '%d')\n", textToEnter, textLen); if (textLen < 2) { + // no suggestion until there is at least 2 characters nbgl_layoutUpdateSuggestionButtons(layout, suggestionIndex, 0, buttonTexts); } else { const size_t nbMatchingWords = bolos_ux_bip39_fill_with_candidates( @@ -283,14 +250,14 @@ void display_keyboard_page() { headerText, HEADER_SIZE, "Enter word n. %d/%d from your\nRecovery Sheet", - current_word, - mnemonic_size + get_current_word_number() + 1, + get_mnemonic_final_size() ); nbgl_layoutAddCenteredInfo(layout, ¢eredInfo); keyboardIndex = nbgl_layoutAddKeyboard(layout, &kbdInfo); textIndex = nbgl_layoutAddEnteredText(layout, true, // numbered - current_word, // number to use + get_current_word_number() + 1, // number to use textToEnter, // text to display false, // not grayed-out 32); // vertical margin from the buttons @@ -364,11 +331,7 @@ static void display_result_page(const bool result) { */ static void reset_globals() { - current_word = 1; - memset(mnemonic_buffer, 0, sizeof(mnemonic_buffer) / sizeof(char)); - mnemonic_buffer_length = 0; - mnemonic_size = 0; - current_word_length = 0; + reset_mnemonic(); memset(buttonTexts, 0, NB_MAX_SUGGESTION_BUTTONS); } diff --git a/src/ux_stax.c b/src/ux_stax.c new file mode 100644 index 00000000..4bb0fdc5 --- /dev/null +++ b/src/ux_stax.c @@ -0,0 +1,83 @@ +#include "ux_stax.h" +#include "ux_common/common_bip39.h" + +#if defined(HAVE_BOLOS_UX) && defined(TARGET_FATSTACKS) + +static buffer_t mnemonic = {0}; + +size_t mnemonic_shrink(const size_t size) { + if (size == 0 || size > mnemonic.length) { + // shrink all + mnemonic.length = 0; + } else { + mnemonic.length -= size; + } + memset(&mnemonic.buffer[mnemonic.length], 0, MAX_MNEMONIC_LENGTH - mnemonic.length); + return mnemonic.length; +} + +void set_mnemonic_final_size(const size_t size) { + mnemonic.final_size = size; +} + +size_t get_mnemonic_final_size() { + return mnemonic.final_size; +} + +size_t get_current_word_number() { + return mnemonic.current_word_index + 1; +} + +void reset_mnemonic() { + memset(&mnemonic, 0, sizeof(mnemonic)); + mnemonic.current_word_index = (size_t)-1; +} + +bool remove_word_from_mnemonic() { + PRINTF("Removing a word, currently there is '%d' of them\n", mnemonic.current_word_index + 1); + if (mnemonic.current_word_index == (size_t)-1) { + return false; + } + const size_t current_length = mnemonic.word_lengths[mnemonic.current_word_index]; + mnemonic.current_word_index--; + // removing previous word from mnemonic buffer (+ 1 blank space) + mnemonic_shrink(current_length + 1); + PRINTF("Number of remaining words in the mnemonic: '%d'\n", mnemonic.current_word_index + 1); + return true; +} + +size_t add_word_in_mnemonic(const char* const buffer, const size_t size) { + if (mnemonic.current_word_index != (size_t)-1) { + // adding an extra white space ' ' between words + mnemonic.buffer[mnemonic.length++] = ' '; + mnemonic.buffer[mnemonic.length] = '\0'; + } + memcpy(&mnemonic.buffer[0] + mnemonic.length, buffer, size); + mnemonic.length += size; + mnemonic.current_word_index++; + mnemonic.word_lengths[mnemonic.current_word_index] = size; + PRINTF("Number of words in the mnemonic: '%d'\n", mnemonic.current_word_index); + PRINTF("Current mnemonic: '%s'\n", &mnemonic.buffer[0]); + return mnemonic.current_word_index; +} + +bool is_mnemonic_complete() { + return ((mnemonic.current_word_index + 1) >= get_mnemonic_final_size()); +} + +bool check_mnemonic() { + PRINTF( + "Checking the following mnemonic: '%s' (size %d)\n", + &mnemonic.buffer[0], + mnemonic.length + ); + const bool result = bolos_ux_mnemonic_check( + (unsigned char *)&mnemonic.buffer[0], + mnemonic.length + ); + // clearing the mnemonic ASAP + reset_mnemonic(); + return result; +} + +#endif diff --git a/src/ux_stax.h b/src/ux_stax.h index a3cd839f..78085eb5 100644 --- a/src/ux_stax.h +++ b/src/ux_stax.h @@ -20,8 +20,60 @@ #if defined(HAVE_BOLOS_UX) && defined(TARGET_FATSTACKS) -#include -#include +#define MAX_MNEMONIC_LENGTH (MNEMONIC_SIZE_24 * (MAX_WORD_LENGTH+1)) +typedef struct buffer { + // the mnemonic passphrase, built over time + char buffer[MAX_MNEMONIC_LENGTH]; + // current length of the mnemonic passphrase + size_t length; + // index of the current word ((size_t)-1 mean there is no word currently) + size_t current_word_index; + // array of every stored word lengths (used for removing them if needed) + size_t word_lengths[MNEMONIC_SIZE_24]; + // expected number of word in the final mnemonic (12 or 18 or 24) + size_t final_size; +} buffer_t; + +/* + * Sets how many words are expected in the mnemonic passphrase + */ +void set_mnemonic_final_size(const size_t size); + +/* + * Returns how many words are expected in the mnemonic passphrase + */ +size_t get_mnemonic_final_size(void); + +/* + * Returns how many words are currently stored in the mnemonic + */ +size_t get_current_word_number(void); + +/* + * Check if the current number of words in the mnemonic fits the expected number of words + */ +bool is_mnemonic_complete(void); + +/* + * Check if the currently stored mnemonic generates the same seed as the current device's one + */ +bool check_mnemonic(void); + +/* + * Erase all information and reset the indexes + */ +void reset_mnemonic(void); + +/* + * Remove the latest word from the passphrase, returns true if there was at least one to remove, else + * false (there was no word) + */ +bool remove_word_from_mnemonic(void); + +/* + * Adds a word in the passphrase, returns how many words are stored in the mnemonic + */ +size_t add_word_in_mnemonic(const char* const buffer, const size_t size); #endif // HAVE_BOLOS_UX && TARGET_NANOS diff --git a/tests/functional/test_fatstacks.py b/tests/functional/test_fatstacks.py index 4bcbc715..05b7b85b 100644 --- a/tests/functional/test_fatstacks.py +++ b/tests/functional/test_fatstacks.py @@ -72,3 +72,6 @@ def test_check_previous_word(screen: Screen, client: BackendInterface): for _ in tries: screen.navigation.tap() assert_current_equals(client, SCREENSHOTS / "first_12.png") + # one more 'back' tap will bring us to the passphrase length choice page + screen.navigation.tap() + assert_current_equals(client, SCREENSHOTS / "passphrase_length.png") From 02fa2f5663a96b5c27815d0030f0afd0f45d41bc Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Thu, 3 Nov 2022 20:02:12 +0100 Subject: [PATCH 25/65] [clean] Linter --- src/main.c | 2 +- src/ui.c | 230 +++++++++++--------------- src/ux_common/common.h | 2 +- src/ux_common/common_bip39.h | 2 +- src/ux_common/onboarding_seed_bip39.c | 42 +++-- src/ux_stax.c | 20 +-- src/ux_stax.h | 6 +- 7 files changed, 134 insertions(+), 170 deletions(-) diff --git a/src/main.c b/src/main.c index 3ccb7679..2f13e2bc 100644 --- a/src/main.c +++ b/src/main.c @@ -88,7 +88,7 @@ unsigned char io_event(unsigned char channel __attribute__((unused))) { case SEPROXYHAL_TAG_FINGER_EVENT: UX_FINGER_EVENT(G_io_seproxyhal_spi_buffer); break; -#if ! defined(HAVE_NBGL) +#if !defined(HAVE_NBGL) case SEPROXYHAL_TAG_BUTTON_PUSH_EVENT: // for Nano S UX_BUTTON_PUSH_EVENT(G_io_seproxyhal_spi_buffer); break; diff --git a/src/ui.c b/src/ui.c index 8993e5f2..0ddde9e5 100644 --- a/src/ui.c +++ b/src/ui.c @@ -12,9 +12,9 @@ #define HEADER_SIZE 50 -#define MAX_MNEMONIC_LENGTH (MNEMONIC_SIZE_24 * (MAX_WORD_LENGTH+1)) +#define MAX_MNEMONIC_LENGTH (MNEMONIC_SIZE_24 * (MAX_WORD_LENGTH + 1)) -static nbgl_page_t* pageContext; +static nbgl_page_t *pageContext; static char headerText[HEADER_SIZE] = {0}; static nbgl_layout_t *layout = 0; @@ -59,8 +59,8 @@ void pageTouchCallback(int token, uint8_t index __attribute__((unused))) { /* * About menu */ -static const char* const infoTypes[] = {"Version", "Recovery Check"}; -static const char* const infoContents[] = {APPVERSION, "(c) 2022 Ledger"}; +static const char *const infoTypes[] = {"Version", "Recovery Check"}; +static const char *const infoContents[] = {APPVERSION, "(c) 2022 Ledger"}; void ui_menu_about() { nbgl_pageContent_t content = {.title = "Recovery Check infos", .isTouchableTitle = false}; @@ -72,8 +72,8 @@ void ui_menu_about() { .tuneId = TUNE_TAP_CASUAL}; content.type = INFOS_LIST; content.infosList.nbInfos = 2; - content.infosList.infoTypes = (const char**) infoTypes; - content.infosList.infoContents = (const char**) infoContents; + content.infosList.infoTypes = (const char **) infoTypes; + content.infosList.infoContents = (const char **) infoContents; releaseContext(); pageContext = nbgl_pageDrawGenericContent(&pageTouchCallback, &nav, &content); @@ -88,21 +88,21 @@ void mnemonic_dispatcher(const int token, uint8_t index) { nbgl_layoutRelease(layout); display_home_page(); } else if (token == CHOOSE_MNEMONIC_SIZE_TOKEN) { - switch(index) { - case 0: - set_mnemonic_final_size(MNEMONIC_SIZE_12); - break; - case 1: - set_mnemonic_final_size(MNEMONIC_SIZE_18); - break; - case 2: - set_mnemonic_final_size(MNEMONIC_SIZE_24); - break; - default: - PRINTF("Unexpected index '%d' (max 3)\n", index); - nbgl_layoutRelease(layout); - display_home_page(); - return; + switch (index) { + case 0: + set_mnemonic_final_size(MNEMONIC_SIZE_12); + break; + case 1: + set_mnemonic_final_size(MNEMONIC_SIZE_18); + break; + case 2: + set_mnemonic_final_size(MNEMONIC_SIZE_24); + break; + default: + PRINTF("Unexpected index '%d' (max 3)\n", index); + nbgl_layoutRelease(layout); + display_home_page(); + return; } nbgl_layoutRelease(layout); display_keyboard_page(); @@ -111,26 +111,20 @@ void mnemonic_dispatcher(const int token, uint8_t index) { void display_mnemonic_page() { reset_globals(); - nbgl_layoutDescription_t layoutDescription = { - .modal = false, - .onActionCallback = mnemonic_dispatcher - }; - nbgl_layoutRadioChoice_t choices = { - .names = (char*[]){"12 words", "18 words", "24 words"}, - .localized = false, - .nbChoices = 3, - .initChoice = 2, - .token = CHOOSE_MNEMONIC_SIZE_TOKEN - }; - nbgl_layoutCenteredInfo_t centeredInfo = { - .text1 = NULL, - .text2 = headerText, // to use as "header" - .text3 = NULL, - .style = LARGE_CASE_INFO, - .icon = NULL, - .offsetY = 0, - .onTop = true - }; + nbgl_layoutDescription_t layoutDescription = {.modal = false, + .onActionCallback = mnemonic_dispatcher}; + nbgl_layoutRadioChoice_t choices = {.names = (char *[]){"12 words", "18 words", "24 words"}, + .localized = false, + .nbChoices = 3, + .initChoice = 2, + .token = CHOOSE_MNEMONIC_SIZE_TOKEN}; + nbgl_layoutCenteredInfo_t centeredInfo = {.text1 = NULL, + .text2 = headerText, // to use as "header" + .text3 = NULL, + .style = LARGE_CASE_INFO, + .icon = NULL, + .offsetY = 0, + .onTop = true}; layout = nbgl_layoutGet(&layoutDescription); nbgl_layoutAddProgressIndicator(layout, 0, 0, true, BACK_BUTTON_TOKEN, TUNE_TAP_CASUAL); memset(headerText, 0, HEADER_SIZE); @@ -165,10 +159,8 @@ static void keyboard_dispatcher(const int token, uint8_t index __attribute__((un PRINTF("Selected word is '%s' (size '%d')\n", buttonTexts[token - FIRST_SUGGESTION_TOKEN], strlen(buttonTexts[token - FIRST_SUGGESTION_TOKEN])); - add_word_in_mnemonic( - buttonTexts[token - FIRST_SUGGESTION_TOKEN], - strlen(buttonTexts[token - FIRST_SUGGESTION_TOKEN]) - ); + add_word_in_mnemonic(buttonTexts[token - FIRST_SUGGESTION_TOKEN], + strlen(buttonTexts[token - FIRST_SUGGESTION_TOKEN])); // current_word starts at 1 if (is_mnemonic_complete()) { display_result_page(check_mnemonic()); @@ -200,69 +192,58 @@ static void key_press_callback(const char touchedKey) { // no suggestion until there is at least 2 characters nbgl_layoutUpdateSuggestionButtons(layout, suggestionIndex, 0, buttonTexts); } else { - const size_t nbMatchingWords = bolos_ux_bip39_fill_with_candidates( - (unsigned char *)&(textToEnter[0]), - strlen(textToEnter), - wordCandidates, - buttonTexts - ); + const size_t nbMatchingWords = + bolos_ux_bip39_fill_with_candidates((unsigned char *) &(textToEnter[0]), + strlen(textToEnter), + wordCandidates, + buttonTexts); nbgl_layoutUpdateSuggestionButtons(layout, suggestionIndex, nbMatchingWords, buttonTexts); } if (textLen > 0) { - mask = bolos_ux_bip39_get_keyboard_mask( - (unsigned char *)&(textToEnter[0]), - strlen(textToEnter) - ); + mask = bolos_ux_bip39_get_keyboard_mask((unsigned char *) &(textToEnter[0]), + strlen(textToEnter)); } nbgl_layoutUpdateKeyboard(layout, keyboardIndex, mask); nbgl_layoutUpdateEnteredText(layout, textIndex, false, 0, &(textToEnter[0]), false); nbgl_refresh(); - } +} void display_keyboard_page() { - nbgl_layoutDescription_t layoutDescription = { - .modal = false, - .onActionCallback = &keyboard_dispatcher - }; - nbgl_layoutKbd_t kbdInfo = { - .lettersOnly = true, // use only letters - .upperCase = false, // start with lower case letters - .mode = MODE_LETTERS, // start in letters mode - .keyMask = 0, // no inactive key - .callback = &key_press_callback - }; - nbgl_layoutCenteredInfo_t centeredInfo = { - .text1 = NULL, - .text2 = headerText, // to use as "header" - .text3 = NULL, - .style = LARGE_CASE_INFO, - .icon = NULL, - .offsetY = 0, - .onTop = true - }; + nbgl_layoutDescription_t layoutDescription = {.modal = false, + .onActionCallback = &keyboard_dispatcher}; + nbgl_layoutKbd_t kbdInfo = {.lettersOnly = true, // use only letters + .upperCase = false, // start with lower case letters + .mode = MODE_LETTERS, // start in letters mode + .keyMask = 0, // no inactive key + .callback = &key_press_callback}; + nbgl_layoutCenteredInfo_t centeredInfo = {.text1 = NULL, + .text2 = headerText, // to use as "header" + .text3 = NULL, + .style = LARGE_CASE_INFO, + .icon = NULL, + .offsetY = 0, + .onTop = true}; strlcpy(textToEnter, "", 1); memset(buttonTexts, 0, NB_MAX_SUGGESTION_BUTTONS); layout = nbgl_layoutGet(&layoutDescription); nbgl_layoutAddProgressIndicator(layout, 0, 0, true, BACK_BUTTON_TOKEN, TUNE_TAP_CASUAL); memset(headerText, 0, HEADER_SIZE); - snprintf( - headerText, - HEADER_SIZE, - "Enter word n. %d/%d from your\nRecovery Sheet", - get_current_word_number() + 1, - get_mnemonic_final_size() - ); + snprintf(headerText, + HEADER_SIZE, + "Enter word n. %d/%d from your\nRecovery Sheet", + get_current_word_number() + 1, + get_mnemonic_final_size()); nbgl_layoutAddCenteredInfo(layout, ¢eredInfo); keyboardIndex = nbgl_layoutAddKeyboard(layout, &kbdInfo); textIndex = nbgl_layoutAddEnteredText(layout, - true, // numbered - get_current_word_number() + 1, // number to use - textToEnter, // text to display - false, // not grayed-out - 32); // vertical margin from the buttons + true, // numbered + get_current_word_number() + 1, // number to use + textToEnter, // text to display + false, // not grayed-out + 32); // vertical margin from the buttons suggestionIndex = nbgl_layoutAddSuggestionButtons(layout, - 0, // no used buttons at start-up + 0, // no used buttons at start-up buttonTexts, FIRST_SUGGESTION_TOKEN, TUNE_TAP_CASUAL); @@ -274,23 +255,21 @@ void display_keyboard_page() { */ static void display_home_page() { - nbgl_pageInfoDescription_t home = { - /* .centeredInfo.icon = &C_fatstacks_app_recovery_check, */ - .centeredInfo.icon = NULL, - .centeredInfo.text1 = "Recovery Check app", - .centeredInfo.text2 = NULL, - .centeredInfo.text3 = NULL, - .centeredInfo.style = LARGE_CASE_INFO, - .centeredInfo.offsetY = 32, - .topRightStyle = INFO_ICON, - .bottomButtonStyle = QUIT_ICON, - .topRightToken = INFO_TOKEN, - .bottomButtonToken = QUIT_APP_TOKEN, - .footerText = NULL, - .tapActionText = "Tap to check your mnemonic", - .tapActionToken = CHOOSE_MNEMONIC_SIZE_TOKEN, - .tuneId = TUNE_TAP_CASUAL - }; + nbgl_pageInfoDescription_t home = {/* .centeredInfo.icon = &C_fatstacks_app_recovery_check, */ + .centeredInfo.icon = NULL, + .centeredInfo.text1 = "Recovery Check app", + .centeredInfo.text2 = NULL, + .centeredInfo.text3 = NULL, + .centeredInfo.style = LARGE_CASE_INFO, + .centeredInfo.offsetY = 32, + .topRightStyle = INFO_ICON, + .bottomButtonStyle = QUIT_ICON, + .topRightToken = INFO_TOKEN, + .bottomButtonToken = QUIT_APP_TOKEN, + .footerText = NULL, + .tapActionText = "Tap to check your mnemonic", + .tapActionToken = CHOOSE_MNEMONIC_SIZE_TOKEN, + .tuneId = TUNE_TAP_CASUAL}; releaseContext(); pageContext = nbgl_pageDrawInfo(&pageTouchCallback, NULL, &home); nbgl_refresh(); @@ -299,28 +278,24 @@ static void display_home_page() { /* * Result page */ -static char *possible_results[2] = { - "Sorry, this passphrase\nis incorrect.", - "You passphrase\nis correct!" -}; +static char *possible_results[2] = {"Sorry, this passphrase\nis incorrect.", + "You passphrase\nis correct!"}; static void display_result_page(const bool result) { - nbgl_pageInfoDescription_t home = { - /* .centeredInfo.icon = &C_fatstacks_app_recovery_check, */ - .centeredInfo.icon = NULL, - .centeredInfo.text1 = possible_results[result], - .centeredInfo.text2 = NULL, - .centeredInfo.text3 = NULL, - .centeredInfo.style = LARGE_CASE_INFO, - .centeredInfo.offsetY = 32, - .topRightStyle = NO_BUTTON_STYLE, - .bottomButtonStyle = QUIT_ICON, - .bottomButtonToken = QUIT_APP_TOKEN, - .footerText = NULL, - .tapActionText = "Tap to check another mnemonic", - .tapActionToken = CHOOSE_MNEMONIC_SIZE_TOKEN, - .tuneId = TUNE_TAP_CASUAL - }; + nbgl_pageInfoDescription_t home = {/* .centeredInfo.icon = &C_fatstacks_app_recovery_check, */ + .centeredInfo.icon = NULL, + .centeredInfo.text1 = possible_results[result], + .centeredInfo.text2 = NULL, + .centeredInfo.text3 = NULL, + .centeredInfo.style = LARGE_CASE_INFO, + .centeredInfo.offsetY = 32, + .topRightStyle = NO_BUTTON_STYLE, + .bottomButtonStyle = QUIT_ICON, + .bottomButtonToken = QUIT_APP_TOKEN, + .footerText = NULL, + .tapActionText = "Tap to check another mnemonic", + .tapActionToken = CHOOSE_MNEMONIC_SIZE_TOKEN, + .tuneId = TUNE_TAP_CASUAL}; releaseContext(); pageContext = nbgl_pageDrawInfo(&pageTouchCallback, NULL, &home); nbgl_refresh(); @@ -337,7 +312,6 @@ static void reset_globals() { #endif - enum UI_STATE { UI_IDLE, UI_TEXT, UI_APPROVAL }; enum UI_STATE uiState; @@ -478,7 +452,6 @@ UX_FLOW(ux_idle_flow, &ux_idle_flow_1_step, &ux_idle_flow_3_step, &ux_idle_flow_ #endif - void ui_idle_init(void) { #if defined(HAVE_BAGL) uiState = UI_IDLE; @@ -494,5 +467,4 @@ void ui_idle_init(void) { reset_globals(); display_home_page(); #endif - } diff --git a/src/ux_common/common.h b/src/ux_common/common.h index 33769661..2ac61a2a 100644 --- a/src/ux_common/common.h +++ b/src/ux_common/common.h @@ -42,7 +42,7 @@ enum { MNEMONIC_SIZE_18 = 18, MNEMONIC_SIZE_24 = 24, }; -#define KEYBOARD_ITEM_VALIDATED \ +#define KEYBOARD_ITEM_VALIDATED \ 1 // callback is called with the entered item index, tmp_element is precharged with element to // be displayed and using the common string buffer as string parameter #define KEYBOARD_RENDER_ITEM \ diff --git a/src/ux_common/common_bip39.h b/src/ux_common/common_bip39.h index 624cf800..8aba2cc0 100644 --- a/src/ux_common/common_bip39.h +++ b/src/ux_common/common_bip39.h @@ -22,7 +22,7 @@ unsigned int bolos_ux_bip39_get_word_next_letters_starting_with(const unsigned c unsigned char *next_letters_buffer); #if defined(HAVE_NBGL) -size_t bolos_ux_bip39_fill_with_candidates(const unsigned char * startingChars, +size_t bolos_ux_bip39_fill_with_candidates(const unsigned char *startingChars, const size_t startingCharsLength, char wordCandidatesBuffer[], char *wordIndexorBuffer[]); diff --git a/src/ux_common/onboarding_seed_bip39.c b/src/ux_common/onboarding_seed_bip39.c index fa5a3244..f45ffe25 100644 --- a/src/ux_common/onboarding_seed_bip39.c +++ b/src/ux_common/onboarding_seed_bip39.c @@ -7,7 +7,7 @@ #include "common.h" #define ALPHABET_LENGTH 27 -#define KBD_LETTERS "qwertyuiopasdfghjklzxcvbnm" +#define KBD_LETTERS "qwertyuiopasdfghjklzxcvbnm" // separated function to lower the stack usage when jumping into pbkdf algorithm unsigned int bolos_ux_mnemonic_to_seed_hash_length128(unsigned char* mnemonic, @@ -210,45 +210,41 @@ unsigned int bolos_ux_bip39_get_word_next_letters_starting_with( #if defined(HAVE_NBGL) #include -size_t bolos_ux_bip39_fill_with_candidates( - const unsigned char * startingChars, - const size_t startingCharsLength, - char wordCandidatesBuffer[], - char *wordIndexorBuffer[] - ) { - PRINTF("Calculating nb of words starting with '%s' (size is '%d')\n", startingChars, startingCharsLength); - const size_t nbMatchingWords = MIN( - bolos_ux_bip39_get_word_count_starting_with(startingChars, startingCharsLength), - NB_MAX_SUGGESTION_BUTTONS - ); +size_t bolos_ux_bip39_fill_with_candidates(const unsigned char* startingChars, + const size_t startingCharsLength, + char wordCandidatesBuffer[], + char* wordIndexorBuffer[]) { + PRINTF("Calculating nb of words starting with '%s' (size is '%d')\n", + startingChars, + startingCharsLength); + const size_t nbMatchingWords = + MIN(bolos_ux_bip39_get_word_count_starting_with(startingChars, startingCharsLength), + NB_MAX_SUGGESTION_BUTTONS); PRINTF("'%d' words start with '%s'\n", nbMatchingWords, startingChars); if (nbMatchingWords == 0) { return 0; } - size_t matchingWordIndex = bolos_ux_bip39_get_word_idx_starting_with( - startingChars, - startingCharsLength - ); + size_t matchingWordIndex = + bolos_ux_bip39_get_word_idx_starting_with(startingChars, startingCharsLength); size_t offset = 0; for (size_t i = 0; i < nbMatchingWords; i++) { - unsigned char * const wordDest = (unsigned char *)(&wordCandidatesBuffer[0] + offset); + unsigned char* const wordDest = (unsigned char*) (&wordCandidatesBuffer[0] + offset); const size_t wordSize = bolos_ux_bip39_idx_strcpy(matchingWordIndex, wordDest); matchingWordIndex++; *(wordDest + wordSize) = '\0'; offset += wordSize + 1; // + trailing '\0' size - wordIndexorBuffer[i] = (char *)wordDest; + wordIndexorBuffer[i] = (char*) wordDest; } return nbMatchingWords; } -uint32_t bolos_ux_bip39_get_keyboard_mask( - const unsigned char *prefix, - const unsigned int prefixLength - ) { +uint32_t bolos_ux_bip39_get_keyboard_mask(const unsigned char* prefix, + const unsigned int prefixLength) { uint32_t existing_mask = 0; unsigned char next_letters[ALPHABET_LENGTH] = {0}; PRINTF("Looking for letter candidates following '%s'\n", prefix); - const size_t nb_letters = bolos_ux_bip39_get_word_next_letters_starting_with(prefix, prefixLength, next_letters); + const size_t nb_letters = + bolos_ux_bip39_get_word_next_letters_starting_with(prefix, prefixLength, next_letters); next_letters[nb_letters] = '\0'; PRINTF("Next letters are in: %s\n", next_letters); for (int i = 0; i < ALPHABET_LENGTH; i++) { diff --git a/src/ux_stax.c b/src/ux_stax.c index 4bb0fdc5..81977432 100644 --- a/src/ux_stax.c +++ b/src/ux_stax.c @@ -30,12 +30,12 @@ size_t get_current_word_number() { void reset_mnemonic() { memset(&mnemonic, 0, sizeof(mnemonic)); - mnemonic.current_word_index = (size_t)-1; + mnemonic.current_word_index = (size_t) -1; } bool remove_word_from_mnemonic() { PRINTF("Removing a word, currently there is '%d' of them\n", mnemonic.current_word_index + 1); - if (mnemonic.current_word_index == (size_t)-1) { + if (mnemonic.current_word_index == (size_t) -1) { return false; } const size_t current_length = mnemonic.word_lengths[mnemonic.current_word_index]; @@ -47,7 +47,7 @@ bool remove_word_from_mnemonic() { } size_t add_word_in_mnemonic(const char* const buffer, const size_t size) { - if (mnemonic.current_word_index != (size_t)-1) { + if (mnemonic.current_word_index != (size_t) -1) { // adding an extra white space ' ' between words mnemonic.buffer[mnemonic.length++] = ' '; mnemonic.buffer[mnemonic.length] = '\0'; @@ -66,15 +66,11 @@ bool is_mnemonic_complete() { } bool check_mnemonic() { - PRINTF( - "Checking the following mnemonic: '%s' (size %d)\n", - &mnemonic.buffer[0], - mnemonic.length - ); - const bool result = bolos_ux_mnemonic_check( - (unsigned char *)&mnemonic.buffer[0], - mnemonic.length - ); + PRINTF("Checking the following mnemonic: '%s' (size %d)\n", + &mnemonic.buffer[0], + mnemonic.length); + const bool result = + bolos_ux_mnemonic_check((unsigned char*) &mnemonic.buffer[0], mnemonic.length); // clearing the mnemonic ASAP reset_mnemonic(); return result; diff --git a/src/ux_stax.h b/src/ux_stax.h index 78085eb5..75cff5c5 100644 --- a/src/ux_stax.h +++ b/src/ux_stax.h @@ -20,7 +20,7 @@ #if defined(HAVE_BOLOS_UX) && defined(TARGET_FATSTACKS) -#define MAX_MNEMONIC_LENGTH (MNEMONIC_SIZE_24 * (MAX_WORD_LENGTH+1)) +#define MAX_MNEMONIC_LENGTH (MNEMONIC_SIZE_24 * (MAX_WORD_LENGTH + 1)) typedef struct buffer { // the mnemonic passphrase, built over time @@ -66,8 +66,8 @@ bool check_mnemonic(void); void reset_mnemonic(void); /* - * Remove the latest word from the passphrase, returns true if there was at least one to remove, else - * false (there was no word) + * Remove the latest word from the passphrase, returns true if there was at least one to remove, + * else false (there was no word) */ bool remove_word_from_mnemonic(void); From d8dd2c7c623835541a105ce947a06ff4776f4cdc Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Tue, 8 Nov 2022 11:05:43 +0100 Subject: [PATCH 26/65] [clean] Adapt code to new 'official' page layouts --- icons/fatstacks_app_recovery_check.gif | Bin 1122 -> 246 bytes src/ui.c | 113 ++++++++++-------- tests/functional/app.py | 9 +- ...st_fatstacks.py => test_fatstacks_full.py} | 26 ---- tests/functional/test_fatstacks_options.py | 52 ++++++++ tests/functional/utils.py | 3 +- tests/screenshots/first_12.png | Bin 4893 -> 4985 bytes tests/screenshots/first_18.png | Bin 4901 -> 4995 bytes tests/screenshots/first_24.png | Bin 4911 -> 5008 bytes tests/screenshots/welcome.png | Bin 4715 -> 5276 bytes 10 files changed, 119 insertions(+), 84 deletions(-) rename tests/functional/{test_fatstacks.py => test_fatstacks_full.py} (65%) create mode 100644 tests/functional/test_fatstacks_options.py diff --git a/icons/fatstacks_app_recovery_check.gif b/icons/fatstacks_app_recovery_check.gif index ec8af4bf07c019b216bd33453e426adfdad35693..07c7d1b2bf3471cc1b1be15f872786cec5ddbc0a 100644 GIT binary patch literal 246 zcmV6v-9O7C~? zS5nAKu~iB;^)>JRtPXT0NVp4u-iLDaQ zr4TRV7Ql_oE7k*hM=v=)SHB{$K;KZ$0OTc@LSJ9}N^^7Js*6j4QW5UOYH)E#WkITb zP-=00X;E@2P`NV5ssbzLqSVBa{GyQj{2W*)24v)yucqiS6q^qmz?V9Vygr+M=vuo#mdph)z!(!z`)GV*~QS%)xyc#%*?{b#L>{$ z(ACV**aD{4B|o_|H#M&WrZ)wl*AS;(P=d%U0NU)5T9jFqn&MWJpQ`}&xK$=Dw-~y) z;xrGcHwBkl4Bgyt>eUB2MjsTtNYM=w0;VAl6P|d19C-3i%>$zB`;K7M%r?(Lh`uU@`*{_N?K$B!O9xPR~No!hr=-nf44>XplvE?zi)?(CV< zr%s+Ye(dOx!-ozY*uQV@p5424?%2L<>z2)%Hf~tIZta@Yt5&X9zHI4|#fug$m_Kjs zoY}Kx&X_)J>XgZoCQj(@>+R|8>g;H5Yi((6YHX;ltF5W7s;nq4D=jH5DlEv)%gxEo z%FIYlOHD~mN=%54i;annii`*k3k?Yl3Jmb~^Y!ue^7L?bb9Hfca&)k_v$e6dva~Qa zGc_?bGBnWF)78<|($r8_Q&mw`QdE$ala-N{l9Uh^6BQ8_5)|O)dHOB?VjG91Q|m6i3hOVGF`xRZQjEbG07hGi?*IS* diff --git a/src/ui.c b/src/ui.c index 0ddde9e5..b8ca8328 100644 --- a/src/ui.c +++ b/src/ui.c @@ -3,6 +3,7 @@ #if defined(HAVE_NBGL) +#include #include #include #include @@ -18,19 +19,13 @@ static nbgl_page_t *pageContext; static char headerText[HEADER_SIZE] = {0}; static nbgl_layout_t *layout = 0; -void ui_menu_about(void); -void display_keyboard_page(void); -void display_home_page(void); -void display_result_page(const bool result); -void display_mnemonic_page(void); -void reset_globals(void); +static void display_keyboard_page(void); +static void display_home_page(void); +static void display_result_page(const bool result); +static void display_mnemonic_page(void); +static void reset_globals(void); +static bool onInfos(uint8_t page, nbgl_pageContent_t *content); -void releaseContext(void) { - if (pageContext != NULL) { - nbgl_pageRelease(pageContext); - pageContext = NULL; - } -} enum { BACK_HOME_TOKEN = 0, @@ -42,48 +37,59 @@ enum { START_RECOVER_TOKEN, }; -void pageTouchCallback(int token, uint8_t index __attribute__((unused))) { + +static void releaseContext(void) { + if (pageContext != NULL) { + nbgl_pageRelease(pageContext); + pageContext = NULL; + } +} + + +static void onHome(void) { + releaseContext(); + ui_idle_init(); +} + + +static void pageTouchCallback(int token, uint8_t index __attribute__((unused))) { if (token == QUIT_APP_TOKEN) { releaseContext(); os_sched_exit(-1); } else if (token == INFO_TOKEN) { - ui_menu_about(); + nbgl_useCaseSettings("Recovery Check infos", 0, 1, false, onHome, onInfos, NULL); } else if (token == CHOOSE_MNEMONIC_SIZE_TOKEN) { display_mnemonic_page(); } else if (token == BACK_HOME_TOKEN) { - releaseContext(); - ui_idle_init(); + onHome(); } } + /* * About menu */ static const char *const infoTypes[] = {"Version", "Recovery Check"}; static const char *const infoContents[] = {APPVERSION, "(c) 2022 Ledger"}; -void ui_menu_about() { - nbgl_pageContent_t content = {.title = "Recovery Check infos", .isTouchableTitle = false}; - nbgl_pageNavigationInfo_t nav = {.activePage = 0, - .nbPages = 1, - .navType = NAV_WITH_BUTTONS, - .navWithButtons.quitButton = true, - .navWithButtons.navToken = BACK_HOME_TOKEN, - .tuneId = TUNE_TAP_CASUAL}; - content.type = INFOS_LIST; - content.infosList.nbInfos = 2; - content.infosList.infoTypes = (const char **) infoTypes; - content.infosList.infoContents = (const char **) infoContents; - releaseContext(); - pageContext = nbgl_pageDrawGenericContent(&pageTouchCallback, &nav, &content); - nbgl_refresh(); +static bool onInfos(uint8_t page, nbgl_pageContent_t *content) { + if (page == 0) { + content->type = INFOS_LIST; + content->infosList.nbInfos = 2; + content->infosList.infoTypes = (const char **) infoTypes; + content->infosList.infoContents = (const char **) infoContents; + } else { + return false; + } + return true; } + /* * Choose mnemonic size page */ -void mnemonic_dispatcher(const int token, uint8_t index) { +static void mnemonic_dispatcher(const int token, uint8_t index) { if (token == BACK_BUTTON_TOKEN) { nbgl_layoutRelease(layout); display_home_page(); @@ -109,7 +115,8 @@ void mnemonic_dispatcher(const int token, uint8_t index) { } } -void display_mnemonic_page() { + +static void display_mnemonic_page() { reset_globals(); nbgl_layoutDescription_t layoutDescription = {.modal = false, .onActionCallback = mnemonic_dispatcher}; @@ -134,6 +141,7 @@ void display_mnemonic_page() { nbgl_layoutDraw(layout); } + /* * Word recover page */ @@ -144,7 +152,7 @@ static char *buttonTexts[NB_MAX_SUGGESTION_BUTTONS] = {0}; // the max number of showed suggestions is NB_MAX_SUGGESTION_BUTTONS static char wordCandidates[(MAX_WORD_LENGTH + 1) * NB_MAX_SUGGESTION_BUTTONS] = {0}; -// function called when back or any suggestion button is touched + static void keyboard_dispatcher(const int token, uint8_t index __attribute__((unused))) { if (token == BACK_BUTTON_TOKEN) { nbgl_layoutRelease(layout); @@ -155,22 +163,20 @@ static void keyboard_dispatcher(const int token, uint8_t index __attribute__((un } } else if (token >= FIRST_SUGGESTION_TOKEN) { nbgl_layoutRelease(layout); - PRINTF("Selected word is '%s' (size '%d')\n", buttonTexts[token - FIRST_SUGGESTION_TOKEN], strlen(buttonTexts[token - FIRST_SUGGESTION_TOKEN])); add_word_in_mnemonic(buttonTexts[token - FIRST_SUGGESTION_TOKEN], strlen(buttonTexts[token - FIRST_SUGGESTION_TOKEN])); - // current_word starts at 1 if (is_mnemonic_complete()) { display_result_page(check_mnemonic()); } else { display_keyboard_page(); } - // go back to main screen of app } } + // function called when a key of keyboard is touched static void key_press_callback(const char touchedKey) { size_t textLen = 0; @@ -208,7 +214,8 @@ static void key_press_callback(const char touchedKey) { nbgl_refresh(); } -void display_keyboard_page() { + +static void display_keyboard_page() { nbgl_layoutDescription_t layoutDescription = {.modal = false, .onActionCallback = &keyboard_dispatcher}; nbgl_layoutKbd_t kbdInfo = {.lettersOnly = true, // use only letters @@ -225,13 +232,12 @@ void display_keyboard_page() { .onTop = true}; strlcpy(textToEnter, "", 1); memset(buttonTexts, 0, NB_MAX_SUGGESTION_BUTTONS); - layout = nbgl_layoutGet(&layoutDescription); nbgl_layoutAddProgressIndicator(layout, 0, 0, true, BACK_BUTTON_TOKEN, TUNE_TAP_CASUAL); memset(headerText, 0, HEADER_SIZE); snprintf(headerText, HEADER_SIZE, - "Enter word n. %d/%d from your\nRecovery Sheet", + "Enter word n. %d/%d from your\nrecovery passphrase", get_current_word_number() + 1, get_mnemonic_final_size()); nbgl_layoutAddCenteredInfo(layout, ¢eredInfo); @@ -250,24 +256,23 @@ void display_keyboard_page() { nbgl_layoutDraw(layout); } + /* * Home page */ - static void display_home_page() { - nbgl_pageInfoDescription_t home = {/* .centeredInfo.icon = &C_fatstacks_app_recovery_check, */ - .centeredInfo.icon = NULL, - .centeredInfo.text1 = "Recovery Check app", + nbgl_pageInfoDescription_t home = {.centeredInfo.icon = &C_fatstacks_app_recovery_check, + .centeredInfo.text1 = "Recovery Check", .centeredInfo.text2 = NULL, .centeredInfo.text3 = NULL, .centeredInfo.style = LARGE_CASE_INFO, .centeredInfo.offsetY = 32, - .topRightStyle = INFO_ICON, - .bottomButtonStyle = QUIT_ICON, - .topRightToken = INFO_TOKEN, - .bottomButtonToken = QUIT_APP_TOKEN, + .topRightStyle = QUIT_ICON, + .bottomButtonStyle = INFO_ICON, + .topRightToken = QUIT_APP_TOKEN, + .bottomButtonToken = INFO_TOKEN, .footerText = NULL, - .tapActionText = "Tap to check your mnemonic", + .tapActionText = "Tap to check if your\nrecovery passphrase is valid", .tapActionToken = CHOOSE_MNEMONIC_SIZE_TOKEN, .tuneId = TUNE_TAP_CASUAL}; releaseContext(); @@ -275,15 +280,16 @@ static void display_home_page() { nbgl_refresh(); } + /* * Result page */ static char *possible_results[2] = {"Sorry, this passphrase\nis incorrect.", - "You passphrase\nis correct!"}; + "You recovery passphrase\nis correct!"}; + static void display_result_page(const bool result) { - nbgl_pageInfoDescription_t home = {/* .centeredInfo.icon = &C_fatstacks_app_recovery_check, */ - .centeredInfo.icon = NULL, + nbgl_pageInfoDescription_t home = {.centeredInfo.icon = &C_fatstacks_app_recovery_check, .centeredInfo.text1 = possible_results[result], .centeredInfo.text2 = NULL, .centeredInfo.text3 = NULL, @@ -301,15 +307,16 @@ static void display_result_page(const bool result) { nbgl_refresh(); } + /* * Utils */ - static void reset_globals() { reset_mnemonic(); memset(buttonTexts, 0, NB_MAX_SUGGESTION_BUTTONS); } + #endif enum UI_STATE { UI_IDLE, UI_TEXT, UI_APPROVAL }; diff --git a/tests/functional/app.py b/tests/functional/app.py index 8277c616..2a219cd3 100644 --- a/tests/functional/app.py +++ b/tests/functional/app.py @@ -1,4 +1,4 @@ -from ragger.firmware.fatstacks.layouts import CancelFooter, ChoiceList, InfoHeader, \ +from ragger.firmware.fatstacks.layouts import ChoiceList, ExitHeader, ExitFooter, InfoFooter, \ LetterOnlyKeyboard, NavigationHeader, Suggestions, TappableCenter from ragger.firmware.fatstacks.screen import MetaScreen @@ -8,14 +8,15 @@ class Screen(metaclass=MetaScreen): layout_choice_list = ChoiceList layout_keyboard = LetterOnlyKeyboard layout_suggestions = Suggestions - layout_info = InfoHeader + layout_exit_button = ExitHeader layout_navigation = NavigationHeader - layout_footer = CancelFooter + layout_info = InfoFooter + layout_quit_info = ExitFooter def exit(self): did_raise = False try: - self.footer.tap() + self.exit_button.tap() except: did_raise = True if not did_raise: diff --git a/tests/functional/test_fatstacks.py b/tests/functional/test_fatstacks_full.py similarity index 65% rename from tests/functional/test_fatstacks.py rename to tests/functional/test_fatstacks_full.py index 05b7b85b..db60c084 100644 --- a/tests/functional/test_fatstacks.py +++ b/tests/functional/test_fatstacks_full.py @@ -1,5 +1,4 @@ from time import sleep -from pathlib import Path from pytest import fixture from ragger.backend import BackendInterface @@ -37,14 +36,6 @@ def test_nominal_full_passphrase_check(screen: Screen, client: BackendInterface) screen.exit() -def test_check_info_then_leave(screen: Screen, client: BackendInterface): - screen.info.tap() - assert_current_equals(client, SCREENSHOTS / "info.png") - screen.footer.tap() - assert_current_equals(client, SCREENSHOTS / "welcome.png") - screen.exit() - - def test_nominal_full_passphrase_check_error_wrong_passphrase(screen: Screen, client: BackendInterface): screen.center.tap() assert_current_equals(client, SCREENSHOTS / "passphrase_length.png") @@ -58,20 +49,3 @@ def test_nominal_full_passphrase_check_error_wrong_passphrase(screen: Screen, cl sleep(0.1) assert_current_equals(client, SCREENSHOTS / "incorrect.png") screen.exit() - - -def test_check_previous_word(screen: Screen, client: BackendInterface): - screen.center.tap() - screen.choice_list.choose(1) - assert_current_equals(client, SCREENSHOTS / "first_12.png") - tries = ["rand", "ok"] - for word in tries: - screen.keyboard.write(word[:4]) - screen.suggestions.choose(1) - # coming back N time, should bring back to the first word page - for _ in tries: - screen.navigation.tap() - assert_current_equals(client, SCREENSHOTS / "first_12.png") - # one more 'back' tap will bring us to the passphrase length choice page - screen.navigation.tap() - assert_current_equals(client, SCREENSHOTS / "passphrase_length.png") diff --git a/tests/functional/test_fatstacks_options.py b/tests/functional/test_fatstacks_options.py new file mode 100644 index 00000000..a4b3e35a --- /dev/null +++ b/tests/functional/test_fatstacks_options.py @@ -0,0 +1,52 @@ +from pytest import fixture +from ragger.backend import BackendInterface + +from .app import Screen +from .utils import assert_current_equals, SCREENSHOTS + + +SPECULOS_MNEMONIC = "glory promote mansion idle axis finger extra " \ + "february uncover one trip resource lawn turtle enact monster " \ + "seven myth punch hobby comfort wild raise skin" + +@fixture +def screen(client: BackendInterface): + s = Screen(client, client.firmware) + client.finger_touch(600, 0) + assert_current_equals(client, SCREENSHOTS / "welcome.png") + return s + + +def test_check_info_then_leave(screen: Screen, client: BackendInterface): + screen.info.tap() + assert_current_equals(client, SCREENSHOTS / "info.png") + screen.quit_info.tap() + assert_current_equals(client, SCREENSHOTS / "welcome.png") + screen.exit() + + +def test_check_all_passphrase_lengths(screen: Screen, client: BackendInterface): + screen.center.tap() + assert_current_equals(client, SCREENSHOTS / "passphrase_length.png") + for choice, length in [(1, 12), (2, 18), (3, 24)]: + screen.choice_list.choose(choice) + assert_current_equals(client, SCREENSHOTS / f"first_{length}.png") + screen.navigation.tap() + assert_current_equals(client, SCREENSHOTS / "passphrase_length.png") + + +def test_check_previous_word(screen: Screen, client: BackendInterface): + screen.center.tap() + screen.choice_list.choose(1) + assert_current_equals(client, SCREENSHOTS / "first_12.png") + tries = ["rand", "ok"] + for word in tries: + screen.keyboard.write(word[:4]) + screen.suggestions.choose(1) + # coming back N time, should bring back to the first word page + for _ in tries: + screen.navigation.tap() + assert_current_equals(client, SCREENSHOTS / "first_12.png") + # one more 'back' tap will bring us to the passphrase length choice page + screen.navigation.tap() + assert_current_equals(client, SCREENSHOTS / "passphrase_length.png") diff --git a/tests/functional/utils.py b/tests/functional/utils.py index 2a102f2b..4056f211 100644 --- a/tests/functional/utils.py +++ b/tests/functional/utils.py @@ -12,7 +12,8 @@ def assert_current_equals(client: BackendInterface, existing: Path): def assert_equal(image: bytes, existing: Path): - error_file = existing.parent / f'{existing.stem}_{time()}{existing.suffix}' + error_file = existing.parent.parent / 'screenshots-tmp' / f'{existing.stem}_{time()}{existing.suffix}' + error_file.parent.mkdir(parents=True, exist_ok=True) try: with existing.open('rb') as filee: assert image == filee.read(), \ diff --git a/tests/screenshots/first_12.png b/tests/screenshots/first_12.png index 9584bf1c7f88016d4230d28fce22c47971915fdf..618a3f60bbc160fd137f1bff52c97566fd282da8 100644 GIT binary patch delta 3365 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 delta 3259 zcmV;s3`Fz!CY>gbBRdl0Nkl(#BFv4SiAOiWbVXdn( zj%5PEXM%Lq@OV6K015urdI12|5|e=kP=BZ$tYH``j|iMb)nGkqrCGo>^@62Sz)qEd z^J9z)!P)qty5n$(jWmqS7Cs z&@5oTdVl82jqkutRsWFcvsuaoY*YJB)vFb-S?yo}Y$lVz2bzDzpVkZTpJ5ml{@=&r zQEf?1=DA@jZ@~9=b7AP|vVHx*>3*_))m}eqcM5g#8P+fi%N?YI&D;Fz4}N^Y%Hiw@ zC(zLh>&Zu7y7yVkPmTEcgCFNd&j*KLSh?re!J?gfhDBu?&bgk?T>pckCkW7`bT`9V zxkSWkbW%NC<+8SkGDUnf2b*Fn}_ z9NFyGOEYtewD3CXmt@c7p>8eg3{M+2>6*9Z_009J@V?B?=zN`?A987Ls?Ya5c!ss} zTE_eHN9Ee^veR>c<^P_(?E2MNUZg#nrRL?Kg?wyyot=)_q;t=#uwf4$qQhFiEx6^a{&SZ%t7V3wfa68P=J6+@;R6pOsMOdT_(H zW^UU4b$Y(=V7l&RSar5?xyjV~2iogu+UUybDfgtG?B5Q(eS1Y6-36!sX{d@JwjJa&2JLR{ho#a18Ptz3m zy1Rx|;sUy0Z^m>!(st&2$nx~IQVY6Wu&Icxx_%)?M?{C~vdmXkROQBw4Q#ZR>t4eG;s_RW z&GhQs$**D28wcKcLD#~oF~fr1sr-wRVFx3Be@j?O=|8Y|LDSz&IQJYI`K$!1Hb>M3 z3x8yxpDo$j@|==jl{MXcU%#W)p1soCF9^0JFUU2l+8kZmto(DwoSPX|UsnEMn3cbl zd--W!|IXdqkF5Er(K9!UwZ^eqY2~XeY?hl7!@>ls7a#xl((#q`trz9kFh&yh+oH=`@yeI`*p!KuhjB2H3w8t zu3>fL;~$7#^T+1CzS_dmN-y4i?_;f;yRBdM4J)>c+&~vhF3W7X;k1ERs$72 zu^0X=%gAW9aw~a!)*Opd60E8g=2mlw`FIEB*Qfou3}2uBb=-1FjB@NYry9Y3YGcg| zXzqA8_Gz~jH>3ahf?sX;+3AlQ(u>14CPle%w3MH29KFpS14W-Ueg3cC39Pw4|^Emj zdJ)X9fE@nvGZ4cJlc51;7{;G}|NgaJ27q<$_xqy-;~Lkv#{HF3LX*(}Gk+}&!!Qg( z>!pYxSbXL{qCF&(DYFBgcMLe0tT!_hB_R zZc*su||VFRokORd-ArSf1Y8Du4|nBGRNprPLnG; zr{Bm_Gu?q}?$Z0~@!JgRmY#VpWHLTG9h1PRAfn5IYa+~}8l}F{Oz!A%_>Kwm?#{D5!oy??aPHjF8rywNy zdi>Tuz^9XvuYR6kIb1p1+-%y;cDRq5G-x|_eIM5J>c;awo{v8tsektUAkbXb!Fo3h zN1dzDK_5PFdV6ncfJ3~W6*?a*=i~7kbWr~gtR_3q$8YEs{`>Hu=icC;_ZV&~4wL!^ z_=ZoWEw?q)zjt(Qh&t7{+P$)EHNP+S^zU!0^*vzZ3u6B>^uRrU1{BeC>|5uA8 zs;%bP+53)Ec|`kjwQ4(rcaFQ|wuTC)WBnMd@5e_@=?d$3Yj+oAD{c4ZQ93q^yYXAX zn(yn}SUoJm`3CJ_0h;_%q*e=XwOwoH(0;uP5JRx&g}WK}D(1#fPVB4~xjCE}79BCY zV%ae>Ec+RI*niIL!1vb-lc51M8ixD*-g+4T*16yBkFECn>>kE7u5qt9u2Vvj(E%}k ze_AiV{zTsui4r8P@rob#B!UTeuoCEFebh z8P@0|rRGOdldC!J>)UGF`d3(-9@}=GkYZ!*vd7%{lN-DLs3__PWP@}u-bkmT=R;5EGJg& z^S&#UP6=PvIC^NrYcVePesd1b9r%K~u{+&4zy*(-u3Tr!w+vR3yGHAnJAhBWV_o*{ z%~pCiWvQw5MfcbCt$7ROT(c^?*{!~3sBi9@jkB(AKVEzI72;KChV|a5S3X+ZRk-lr zv4dXiZpN#vZ*Q*f*-&qW)ub9r^#}SoZp|gBzt;VXc-CCDJ&u|T_s&IkAckMy$1y&h z=AZr~<+692w<1^^qwSX)ZJmAv`{9(3!}M@@rLs=$g}x3mERN*pr6@C#fd@4c81DCb z>tz5~=YGFGS}?A0jmxi-pAt2H{xb~2Fbu7i0%i#o5aUnl1psQ;Ja>LP9>*@p9%fi@ zetXXJyShs7^9*ZY>C2V)Iq3BD=N>P#08gcdTUe=T>w9ar%B9C$Ggi%Yub`{q>|}=Z z#q>J#6|PKASNVM7&W+V--3v)y{*j+&SPP&0HjKmJiJ5p0?(}ejG5vs&gQX=I}L7jz*(|?z`)?THl)E&W&}Z z_xfwCZZqrK^IGo>R?To!wym7`!|OkKexr8C-$LE1F{V?wQ-SUG>gy9ED5hsymyRVUzI(I}tJ5 t@AuZr0I*8Izd{OdET@E%aR0)|nUbzZSw(({ncg|lo%1OY>mANIcL zwk-pSpCQs!!{hO|0VMcO>kR-{OOrteP=BL#u!do%JR)!!RfF}cm1Y6^)C-nQ0Y9n~ ztZ}U#k4K$Bfs?5bEVTlPI6PQt1(b4Zu+$1D=)hp96;Reu!BQ)rxI==aR=^R;2TQGh zgA@*yS^>uy3TEZdYuV)wC^p0Lf8GE`D;6v>AmC^vPYG|s%$@PB^RETF7of4z)m0R6F7b~7lxj0+nOI-`QQsb=x_?vQLv8Cjmf`(B#c{4x1;XBSR& ziyS`-w{JS~E6bi?y&duEKW2wvcsqy}ezwA=_jCjHZPM;1q|_tqKCGL+9~Pd{UtXHm zzyAKxEZDk{>mT;VM<_0GhV?WI!}R0pb>sN+;p@cb?Pq@%MmfUA53wDawD36Vw`Aou zH1u&Jmn|F&A0Ko2;QL;h*FD!i!g!mX(fK+(-@1`2_4c|y1<$ZPu4U}|@wamAc-ZN= z!16y&A9nrjEN{{;Ps@)_=_U*Lm@|HvUf%xGKBb=0xqDW4Vv7&aVXbZ*25Xp6@bPae zdQr;CrHX%F?l8Vvp9fh%A9wJey)O0A94cZX3+gUd*HrU;nCtb?^%xo7euRcE&D^wo z<;U$QkDu<=uAJwn?@J*czj<8v=jp#LKju07 zaz0q|of+}cL~LY1mkZX*+fiQpi^%o27ulY<5SM*37yr_By}3(IN?qBo;J8C&_cnaJ z!@~W0*qz@r`hGba-#*e`%e1?%x+<&e_PjmFDc7WHo{=>_RK~C1!V=zX#YTR- zXVy>=(p|91ntL&(^O3eQ=OWwF+fFU`<@>?P?TC0**Kg$8JEFsVS>}rss&Zk+4xY4^ z>#kt|aRdvxW_mSt@--}a0Wky%h#^=& z48ITS|9(N*!f*Ru`PW%i{=)3#yM6r&bXzxXVIOylwfwLPKQjFT8OwKD^8~Kn-{lAV zde1e-Nx>alf455!tbeWi^ITt(v3$>!cUy1e=FN>>#B>|J?)93xt$6}B?eD^0hu(e9 zycE9=Yt?7<_`iO<|H>P;{_MxYz0?p}$C~@Wf}1qAgZpl&aN}`1c>DGQ zUXwMf|5-j1KK|+EHGc+f%~-!5h5I(Ilu~>BF_DkQ<7>y??z45ne!1tS2id*x3$N+b z2-cFhS;qRw$oO#ldbDjl99M2$eba@W=9~01UDYao^{}OOu-n(P6M|LSnz_L3(_8Df zg^ZeGy&ivV0=Rr7=bHQUVD}EP%%b~uogZGEZ{ExF8?=va>EYAgQr~)?CQ9w#^r3hW ztnYewGc4$Q^Pbi(8&6lPrr))#xld)s?I2@L32R&brTF+v?dWr~L&thO;MT`{{mAl3 z;z28aQ|leNgNzB*l7)k5yb`hLZeNEhzIpX^d)-jC8}`!eYuX9HTIJ(~d`I8y_Wy_P z!rN^91AevRXZMXhz1x~Mp7L#ew>@Bj4<3lF$* zX0d)U`1Y`K--yjU?A(&``h8h*pRXHsn}23xrnfIVhh=wg{oO9eqWgE#S^C1I38n5< zeDe-9PQ%AbG(LVcw(up|+l^cAvvR{qDZ7KWK3>y4nPKs-txG)JE(vwJVf@G%77#thYrg>sG*hTc5GdRx~x{W8bfQcjbVozp+b zR5Lw*YaY_S*JIlZ>wn|t{E6RA)BSzkG7LktkHnf|Jsme|o*VA@o~b-*zMzHoZ-v9` zZHRs2?pecn8{nY*oK7H@|M|G5d;G|6<*L9tma^)+e-9dWL$ErTN!6U%d>l?eNc8pn zTmKC{os@jFd4}cfN^x_uX$MZ2(YYb&RO4#b%C^<~y4=&hzODAR$hh0quwJX(Z`aqK^)>#uzpww)VvB04 zd3H8FkSdSpcx)cp9Y{U5m?O_9&{I5u@HsET9){jH`^)f&V!J-$gX5g!s3r9Jzvo3N|oEa7!F}-40 znHiSjj9)l@&aL3rI-VAFPhUt<@okf-n_Z->W^_FW0Hnt=bIh<8%7Faeo+wI#2Z5&_|EEu*(%RoZU3p-?vw@@}bqxrD}Dn zyoUAmoHqO#`1vP;DvaJ_mm6xezgK&npRQJ`TeTV1b)L|t=8GDYU2T7RaKT#L8h%Sy zZ6WD(a&(wNVYOzx7B$8EV>6rrL|{ujjYsC6sf`s_ka-*%&?_fWi5>iYL%PW<2axL_Am|;cptv{~B&q1e;KX>~<8}M}Wa0@$CZGUg=cKPUW*Njzj-z(^ul|8X9V$1@YtkHJGf0AKrmH68bDV+#7X zv$wj_$(sAyWZb#2ZgpSlqbUaS4BIquw8 zXL_x_*6KF1zdi5uufeJrj>;Bx?fECts&7#<(-j{&_w~h^o;|$&q30L+r(jjC)$46@ z;z~YE19AFYrl6I@88<6`qE@$Gg4JZl=BDnz)226O+&2t6gY|JWx`Mf{a)2xN>6EZZ ztzOgDb2n?ekIn~KZ_{xS_a(!gU|r<>vyjCrT#7}mmG8~*x>c)lrmZ#yt5vPu#;rNY zYIUAJ-p86x_;d#^@)30=Utg^0Dtkn;wp}>7yo6Nwf6bY@z_Ori*F|(6udh33(B?Y7 zbdZ&nU&rSgXZm5>`K#}L&G}+_$<-Au!n)utpVIQHHn-ey*ya?lZHCqAsGS?P)kWCV zR33LcT+N|tzP?+>N-uTiGp4P~u;Ap!|0aDKlutE=AmjiYc0U3F(SGn2swJQ0Tb u{oZ;T09HBhuaE+i<&+SUp$NejjQ;{(`%w=%KQ$2m0000+yf-+`T~{vp+Ovy=}CNd7Lg- z;*zdEi&@o(08hkm|cDfnX68#-_>T*W^@u{Z^V(&z?ovc712bCS7RlpnJyJFwkJ*FMOA`y)}>Nee&nZ&YbEN z*?(ppU$y5~mOaB7?D5M#X1lH%?8Gx)TjA4tya3x4Y4Zjt^$MGJ>*mM9%qHFQ)_neR z|64O-?Lsbp*`2RYT;vSvt?Rn+$Jgh<{^!HbiO=9`XMaXH!q*S64U07MI?I=2;W9Mz zbs^`?>zqTp0NvED!VZ#<5;)Jz&oG@73go3aCSkQ}7 z7GA2@^M8c?)6Th*74&rl4_fO|Z_S}17P6qz1?!$_9*4O;A6>7W{^Lhz_|{BK+ZTS^ zUUL6A-8HN_Tlt9ndwkL1b2V*n<@J(#(kgql!}#3iVv`7FoGw^(RJ)Gee?+JDm}%V9 z^=YohDd8!6ogX*+W+vOH&7sTq5|KUldP5nFZnLJp3I6RyiV-&mn4H+F1bqkY`> z8Ws>ou;A28ulg>24U4mJ;4K$)ExZ~tEI2!r|8UVAh+&44p#dF#@Uw(RIpQ}F-rV?i z6HYzHM!qV+s?8C#!OX96KhKst+wxqJU>$3``+j~$tv!38sb3ImOWu%cShYF2wpsY+ zj=457taDlTH((b2TJHI${ro$3Q(v;=s|L^9FxC=fx6;B_o7pT6Cx!$ME%_}v3P0ayyDA%w~E0e!kkw(~e%e{rY1qoVzVw_YEsG^E8eoSXF%v zxu4UV@gp-I;IG$f`BAj?Fk85IhZ=0I{eOLW=QcNuYQ;5wtYgjm8P`M2|B2wiBmL_; z$DP;g9BZ6-r(icEShbz`LiLPOEnfdSk9+RJTDVfDTKAh1tYeK&F{Tc%lg|`RxOn3U zvV9 z#?N2-J%J_nXRIA-`RL;pnOcU*+uUJw5EtE>8C-oIpM5Mmf86u5<%+-H&e4l3K=EyE z=5;hPENXMPoy}as0%8ajRheF~hih0=;`$EznPE{1o9Pvsx#$kWFvGLK2xtMa2`K@S zpaCdD@u&3yOzOJs^Y7Q|)o@9GlQ`)Nt6wXCx6{qAqzagFt{IkE0W(e%tieSq*R6no z)14CP6fosP!ICOq-ib~L0pn%g;azGn2syX%_zd``3CI0G4?^pRX2- zYh2?RcP*!clK}`be_HCguIswiOA$k`_|AdE3H@ok0B~!s*Q@m+Br(GRVr-dV_0JUE zpAYXx%6?XSea+9%Umrj2!)j{WL-kjx<^uHJ-=jC|{1A2C*756CSnDQZ4F-Lywnq*2 zbNp7=%?3kk8+Xe!tib>Wt><_GIsfnDHh2G(2j!~35ldNh zj^BgET@kF4%%o~gZN3htASC*E{MJ9f$CHwuR?n~;t`s*lo3^tR_jQv7ZRbJ`CivIBkn z4&BVZiw`~Z1_!;z@K|t|)IY#Cd@^mht)c$Cqf-cDQ7iBAL_vb-6HjKORyM#60*Qv42ungB5^b8Bo|d_G@W z?fKe0jB8xu-g8{1gp&aXF@OHFUVz)_y6*9Kyk4)G!_^$IvdY`D`7+#XeFx02uIy7Q zxCS$<>$~dQ>O5@WYRs^J7`10ugOik+A5BfJ=JeOM)wuPqu)MuTY&&`#&95t6{z~OX zy%|>5b>mAabuO|DhW@%F2aP-PpV!lNeebrlKkiejN3|K&*KPWKuz%~iIvaX0^wHza zta1enXE#mOckpgj-nAOKRjnSCuVD?gX~S;=zyD-Vh0&X=azm}w_io$y?QXStRGVSl zXM?^qzo=2!-PZREZdj{F!|xJSyO8ubIl9k94KDAVwR%+kF5&oFgw>k$xv0^9u)@%A zs5@Mz>#G;6wx0?2yniCgg;o2!Kb1d&Na}Dnu_=dZ&JKZVZhDXk+ z+-J+T3|5o7M(db5fG@ve-S+OyR?cwBQd8}V?w{>j^A^gfW>t8zTYaCQzNznQTy=fV z%M52u6_bB4<+m7U~X z==(6kq9jKzhcd&H!3Z=HjPCh-wq6E+WuDLHs|Dj4*SM^cKM*#5pRVh=u4}y%FiEh0 z7=KzX08qo`x%2DwD!U|mm|?;7?K#sw)m4JkGpw1VKd!{@LC3E@_4q*x@OJcYGb>eX zeIM;s`RH+%j8${pD>zkgb~3~IVSF9>4p*kft9-q2r^afv?gvR<{(;pqteH>#Hiw%^#TWJ#EQ@=TTsWRp&q& z%;9UE91TVZ-FKI3wZ1jSof_*(@Ac1GJto%o%xk?jST(~@+03fF|3q5#C2D4T#fOvo z`DTsJ9zOp#^BcV@Se0w_IczRm$+vMJF2BkYw6Hk+W<}J0>aizSO;&7b>JGfEIxyqD z!>}<}U#~`2F!ik*;0|`35;m#TXZm{WX7$(6`5?=!+E3!X$*?6@H#vT0vUrDEvBK2Qjid9IkShOZ z&eRQ-1$}O8qWgM(-9dvkck){YS!n)!d>=UD595=+`+hX%hw&{}cen}bhO2x@^Y7Z! z<&MJ^r+{@ctX6yN)VQr~!mg(Bxc%X34qfy8T{~8KsZTy*TFVRzE-t;feS!tox2IXB z>Yd*>3b$~o?rdg;lc51S5=QrYK3gvXz#<3!6;gn*oDu?)F$lpH@qe_0ix=Pjbh-cl N002ovPDHLkV1kSj*FyjR diff --git a/tests/screenshots/first_24.png b/tests/screenshots/first_24.png index 10f59b06dbbd974dd44b3bdfcb7f0d12d6a6516f..21faf3768989f33513e17573abcc492951edf7df 100644 GIT binary patch delta 3387 zcmV-B4aD-VCXgqPBRdmUNklkR-{OOqi7P=BL#u!do%JR)!!RfF}cm1Y6^)C-nQ0Y9n~ ztZ}U#k4K$Bfs?5bEVTlPI6PQt1(b4Zu+$1D=)hp96;Reu!BQ)rxI==aR=^R;2TQGh zgA@*yS^>uy3TEZdYuV)wC^p0LFK>XO6$_Rb5OB1T9|>>Puz({KUUVNv%7;XttXT4w zHE0%akV7sU)hyr$NBz?r%>s%$@E<>G7Eso)KVC+&fPxPH0e+eVlv3kIBx@E>M5SLs zp;^H1>iwE87rp~Os``smpUqM(V4vE*s$Q*t-D(F5U^kPY2b+KXwBCT*?RFc6Vd4Ki z9*=5Uax%{+w(=7=el-_{o^IRpFj%wYufMf^H+TFMI&s<=)-Vjq9pnhRpYxA@@#~}Q z7|u}j9&?45)<`xc&m9O}| zm+3Zte6Ec0*RghQQ2_QyvkC~SuH^tOMl@wV78eoy#1@mW|XIcW7{ zd3*M4(!%4c-;!$UN3ARz7*9K%-Zd}N>z?Z$VZ6=H=zN{3AG8kG+c6M1!`iyovGCX5 z%C+NRr}O{iKTjWa{q8Jp(l1ZTk5B0)Da`7R$s@1Kl8P?nZ zTIwlV=*52#lisfn`*qNjZ*B*jy=|>rNUFId3wLP#jGx>NJo#WWrZOsyI z<)^6O%QQEI|2kD)xG`5pGpst#>~WK+@tfFdHSKM+^<+oS4qu;#vHtO!$Av#n|8w~<&tc8U*4ti3yi5@rS;XZ&65eJmlPUGcn{F?%J4F~gC8 zAAgs2=ud*DIKcv92o?}Suz(nT9@hW;Xtaf&^S|;pyR7_C*~@qP`s=W^Zr;K^?ig$N zVHf_&^w(r8->t|mh+hB0%h$gsbE6mB!S#2$6v6t}%3sp;H5tqITzR+kR&L(h=tWGo zVa@Lx`+B!EPvEBgosRhXJ(q9XxAB|y@qbeMJgimk)#LyB@%}4s-1>Jv9`2=v*gDqS zUo5yub33^2mI^l>w}ZEDPvA9K!}_1~zru5R7|lJ%!Y^4`KlB>=Hm{UYd;KwykH_O{ z$KUR=b;EwS=cWhQz3~gL>D36=; z$5VRv^jp-o-lvIDJ2-tPUIgpA9!`b@op0XL`eozkiq-VHwl(*u?6@6d%tyl7)_*BJ z{-k#FXS74ddOhIQ$9(;jCRj@r4yN%+#HPD_9j^H1)z|HHL)~uJ zOSi9SCj@Jij~DVCeYe~HAHEBx+4=|kYRBK*H~RE$Yfi{bCvCx}Wruw`IQQ43>dA$p zn{)8;@#2P3cbne->m3#zaOKQm{bca%VduUPn|s)~CFk|~vgSTtH|#e5%w$Y&Uw977 z?%?{nU64ih@20c#g-a7k-L3fM9crA0kC$kC{Az6BOSHEex87&vhLuuw2XB46rhPKQ z;$K^rc)DE@>UP8Uku@wJhG6kOW+n8`8Wz5RB8C|j|1!Oza~9o!7-qAP2txt0kO)Kp zlaL5ALH@MffLX&Zy#D-nJQ{8Z(1}iGSmRm&Jasq2k}6thYrg>sG*< z?miOg6fmcwU`Z9QtfP;F0Lzmh2q+t)qo3BW$?Jz_`56yz9cEZS48a0o3_el;Da?~0 z2TdGw@vG|p{{7o}6M!%N`RAY3n_z|oxAisvtaHELA1xTy zxW+Z^Rz4B}lW_<#e`skKhG7_5Z$%8j;xi`_9s1LH1K`pgk4NiGNMeQs#Mm>#8h=uF zzCS!~Df?dW@iAXRf4u#;H>jJYnj0{FeUE-(mxrkHvQ8hr!d^ES>uu1dYDd)D zk-Tr{%`>dGb&b<6bG$9(G+Eg>{gX^J(*wBXA^m$jw#~3Uf4|#@!ICPG(Xyr#2slQxFn;J%8(;;L}OTSDR;8PFISXn@u~|iu?GK20hPRUxzha z-FW`tdHeI0e`?a0cwz4^fD{d!pg6fyo(==0!lKAyipC-sJ4H93Gj zenYqL=i)=py}&`QG2B+1CiPG79q&wgE^DZN?daSPb*gc-Yh~MNeqHYAU*A^yTV&jA zYgn(uZLS(E&9Z!~K45y$t~C-0%0t zUcda^Ul`Z8#=YjaJ`w_xaR@Mf_|tj=E@v2q+wJyvJZcVCbHvIjKcCIF;cEN)#tiGs zUbTXAFvB{(v(Byh;Tf*R3=4=+dxrJ)A*JS9Q+(msIL3vb+ubaZ3&wci}(Jr|te;?b&|0POWa$W>_De)8~zU!!Xo&qThx- zdfbIwuAt%Urpf-ky_%H|t%fdDt6SwYtheX1;n%>=Ul~+k^d`I9P^$jyz2A4G(nrGA8b=R}cx}c7UvJLgc>rJVFm|Oo1zhmV z>B@DU`If*4qHE&i#IWv|wE08kennBm|SO2r_^CYZ!)M7+P-y z%n~dh#-G+30MxL1?)-Q>%5KRo%&_46e)*)|)hfZ}8P>wqA6Me%pwq{nyZxXIcshEx zg`KLlzqfX~eDt_$#;UpR6?9dcADLl&FeiWEt z)j5&g=I}K?9KDSadhD*(YJY2vJ2%#uUhA*5y3Oow&wKrAuxf^*vV~oH{))8fThz>S z#fQ#)eX*vW9$tUw`Gx)|Se0w_I&Dr|$){-`PQS|(w6ZwkW<`J0>h?>pn(Wxz)E#)* zbYjMR!>}`0A6KI*nENURxPqTP5;m#TYx;WbW{vmJ=RwxnbezO}$*?C_7dd|xvUr6{ zvB3wu5c071$X(B zmS45G<&MKPr+{rUtX4 z$FWR6cubJ)8lKPR10ccwwO#;#t&>p*QGaR&YZ!*gBLbIEHCV4&X%=uyy z8rSOieAXEhxR@HjQY+vT#e=0*z*))$ORazt6%3YI0p}_eEVTkoS0q?!1(a}pu+$1D zqg&^;dH=3picDzx=FOz`4r)^D>$RoT&Ie;HO!@S!(y$n7ut<13sfY=-qRxvqaPx!5uOO!zwSS-DVB=<3P( z@*G>FmDkz6B-O4TwX(8fyd2oPTi&MkGq=COc$r_(`8riEbRDp_eIRm%wfADj%8yUV zb>L;^^Z)ff&tG=?>Z~u)FPr7ZO}fj*$`Vd{!>e1B*tZVba*4O{ zCTjRLEluISPSsZ)%+=8htIn1!H<=oLh`q0-O|G_HZ0|YX>vmYLmCbl_9R;h7YS+=n zk2t_K>gVfo`s9?bv5zypuD(dF=4-9S)77!@gQb6$?dyXjjL!RgwT^h3A{O!#w>u?#EZin@)+_IN{FLQczg6cfMT4^O z#n)$E6;AdG?KZoX;p-DNE@Tg@b3jMIDsfxGWLJJ)1wVY()w#}Hp2>?8x?Q8Fj|<#< zcFj! zfADt+ODX+p7H??&FBC34$40&?!K%#>b->E6SLkO;_O?8iBv@rF{o-Q3`)j+Ue;U}9 zydl@HYIAfQv+)-lb8Ti=ecAZ?V>bSS-0M&K`b)Q#eq_r}O`dsRtS!#nN*iBoWwSh- z7#1d2y_j64`I?ONXKsAj>5I2CdVTW+fBw2LH*RxDYRLJ2DZAzr^H1-OpXVDN-21a1 zV|UgNd&gS(!LLu-yI_Y`YWoDu{O@#wy*nv zm0Eckr3qG5??dkEG-v+E$_M!K`P_aK?LEvkE?!ZS&9%K;OMcwurctf9hE-Ope_v(1 z{>y$6xNl+PG3(t^3UhR$22?jHLst z^O-`2i#MMz9(aWlpGF0)VKq?U6MNfEa>BRi;<`!Zj=^aeas1nPE|z=@o~$1?UdMFvGKf z2etx51Ph4qPwNF(Gz`Q0&(G(x;gSHI=yZlPt`)#bcQY)h0+#eO!%{0?MMuG!T(olC z3Yh5blu)ODB^?Dzs(^JJoe~196O(}mCmf89{qRib0&@7x&p-?_lYt0m82|kD-+!%_0brZI zzrSY-#x<^Sjk}jqLX$BFGk+}&!!Qg(>!pYxSbXL{qC@|*UI4hY=kwWm5t5i;0Wpru zu*PQ!ug{0qBjtT!_hGd(Zc*cvs<{B;_qX(hT_2*(+d6;!3TxeDtjVA+ z)%K{#o_uWR-7~Dob&bnk=9pZ{X>w)f@*A0I<~wlBUHbQW?3-bI{C}Lk@ZmIH-`69< zFjV_UtU1<8xmoklaIa&g@~ruWR*v5ayV+!jW8)sVhBXis&eiCk4q{*Z2LuS}aj*HP6n*J5uEl?a$Tj5NZB#+#|O&R5%^$$7Fp! zK5|KSSjR`ZyC_>}yFX9TabVno-xAh*Uzf(}VHvJBXb%g}+>W3{{jTsgYqxKAIa*|T>qp8W&obmd$8n^xxmY3IvZAY)8 z`E{ksU#WbnH^Ul+VSY)a&PA5V&>xqi(6}rAc|C2{_ikJJ?LM`7RGVRa+@`Mwe}`eH zv!N$LmmYUzl`CjCyJ@n%lXtW7uGP@3YW1jm4QsMZ8-5M^`jtTyMsKpp4YgX|yKU!} zyVdGZZH9H94f@jjqDEzRTi+hsuvU+T-x5~4kn}z|y3a)oF7KYTdQ|?FaQ+oxwPw99 zYK$MOFf^@7#*GvS_Be`L9^YM=LAsdP&CzQ)l*Bi@T~!}ptOcPDo z&IxXK$o+S zr2bm>GvZZq+4eYUGTfMp?m!IxfFI}hc$$Ctla$-uao&nxagMfMZnSmz73_yoLQd1e z<(0}hxfl9A%&<64jg6sR`OuwtE1iNQgD@$Ll#IHf;ufOzo zp#^v;J>1GlRa@UjyHzed?v}A?u6qSt73W80SYOPqL*L=b^n8`CH}29{t=7Ge^yQz} zJ;Pf0e7-U<{}?R#0r+08fBCqRg*nm3m3`EmPS#xCCgU!R^{D$^m$rUjzV@^& z5BB4P8CIPGX)=eed2%!vC3N52uGRY19CvA~E4|lWYxP)I-=5d{*I?BQM`bIk_WBiR z)t9K5`4t~J_w~)1pFO<)(DNJpQ?M%6>V4Q;xRNjPKwN&6Inl=AjGGlvf2+qY!D_N% zOH+5?W!Hfj_YK3rV12wAUBS{sWJx&sXp!A5mBG_05`JWshjq zz8go^FCkU_t2s+ISf1#8Y!ltb`|An~+Fa+i4zkhu`}jI=<{!qLzx)1b&KL7ruI_LX z)(uzrlGfj~rOO?~7Uu-}W>~HE+NE(@-Gp6D<#ETu)f~F!`@46n^ip>|W7^9M3ob6b zxnqI_*Y``ay6T;OaTIQ$tL_|ThLhm|I}*n5_xIO&82~mZ_*+N;&gGO4ld%WE7mWV_ X^Q)Queu&i$00000NkvXXu0mjfIVkMU diff --git a/tests/screenshots/welcome.png b/tests/screenshots/welcome.png index 96e9e85fbd97e8e3a82afd3d6d0912c5527cd1ca..c119bd4879bc354973a85b4ab8e1e196ea824490 100644 GIT binary patch literal 5276 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 F{{VLfJEs5u literal 4715 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 From ce096d9b1da8141c92f787ff1f399aef9176db54 Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Tue, 8 Nov 2022 17:39:50 +0100 Subject: [PATCH 27/65] [clean] Separating constant declaration to function declaration + removing some unused include --- .gitignore | 10 ++++----- src/constants.h | 43 ++++++++++++++++++++++++++++++++++++ src/nanos_enter_phrase.c | 2 ++ src/nanox_enter_phrase.c | 1 + src/ui.c | 21 +++--------------- src/ux_common/common.h | 30 ------------------------- src/ux_common/common_bip39.h | 2 +- src/ux_nanos_keyboard.c | 1 + src/ux_nanox_keyboard.c | 1 + src/ux_stax.c | 24 ++++++++++++++++---- src/ux_stax.h | 16 ++------------ 11 files changed, 78 insertions(+), 73 deletions(-) create mode 100644 src/constants.h diff --git a/.gitignore b/.gitignore index 3d1939cb..83c86665 100644 --- a/.gitignore +++ b/.gitignore @@ -6,14 +6,12 @@ debug/ dep/ obj/ elfs/ +build/ +*.elf # Unit tests and code coverage -unit-tests/build/ -unit-tests/coverage/ -unit-tests/coverage.info - -# Fuzzing -fuzzing/build/ +tests/unit/coverage/ +tests/unit/coverage.info # Editors diff --git a/src/constants.h b/src/constants.h new file mode 100644 index 00000000..6df9e923 --- /dev/null +++ b/src/constants.h @@ -0,0 +1,43 @@ +/******************************************************************************* + * (c) 2016-2022 Ledger SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ********************************************************************************/ + +#pragma once + +#define ONBOARDING_WORD_COMPLETION_MAX_ITEMS 8 +#define BOLOS_UX_HASH_LENGTH 4 // as on the blue + +#define MAX_WORD_LENGTH 8 + +enum { + MNEMONIC_SIZE_12 = 12, + MNEMONIC_SIZE_18 = 18, + MNEMONIC_SIZE_24 = 24, +}; + +#define KEYBOARD_ITEM_VALIDATED \ + 1 // callback is called with the entered item index, tmp_element is precharged with element to + // be displayed and using the common string buffer as string parameter +#define KEYBOARD_RENDER_ITEM \ + 2 // callback is called with the element index, tmp_element is precharged with element to be + // displayed and using the common string buffer as string parameter +#define KEYBOARD_RENDER_WORD \ + 3 // callback is called with a -1 when requesting complete word, or the char index else, + // returnin 0 implies no char is to be displayed + +#define RESTORE_WORD_ACTION_REENTER_WORD 0 +#define RESTORE_WORD_ACTION_FIRST_WORD 1 + +#define COMMON_KEYBOARD_INDEX_UNCHANGED (-1UL) diff --git a/src/nanos_enter_phrase.c b/src/nanos_enter_phrase.c index b17d410c..8b5b7060 100644 --- a/src/nanos_enter_phrase.c +++ b/src/nanos_enter_phrase.c @@ -14,7 +14,9 @@ * limitations under the License. ********************************************************************************/ +#include "constants.h" #include "ui.h" +#include "glyphs.h" #ifdef TARGET_NANOS diff --git a/src/nanox_enter_phrase.c b/src/nanox_enter_phrase.c index faa5bed2..b7a22c3d 100644 --- a/src/nanox_enter_phrase.c +++ b/src/nanox_enter_phrase.c @@ -14,6 +14,7 @@ * limitations under the License. ********************************************************************************/ +#include "constants.h" #include "ui.h" #if defined(TARGET_NANOX) || defined(TARGET_NANOS2) diff --git a/src/ui.c b/src/ui.c index b8ca8328..373b6fd2 100644 --- a/src/ui.c +++ b/src/ui.c @@ -1,5 +1,8 @@ #include + +#include "constants.h" #include "ui.h" +#include "glyphs.h" #if defined(HAVE_NBGL) @@ -13,8 +16,6 @@ #define HEADER_SIZE 50 -#define MAX_MNEMONIC_LENGTH (MNEMONIC_SIZE_24 * (MAX_WORD_LENGTH + 1)) - static nbgl_page_t *pageContext; static char headerText[HEADER_SIZE] = {0}; static nbgl_layout_t *layout = 0; @@ -37,7 +38,6 @@ enum { START_RECOVER_TOKEN, }; - static void releaseContext(void) { if (pageContext != NULL) { nbgl_pageRelease(pageContext); @@ -45,13 +45,11 @@ static void releaseContext(void) { } } - static void onHome(void) { releaseContext(); ui_idle_init(); } - static void pageTouchCallback(int token, uint8_t index __attribute__((unused))) { if (token == QUIT_APP_TOKEN) { releaseContext(); @@ -65,14 +63,12 @@ static void pageTouchCallback(int token, uint8_t index __attribute__((unused))) } } - /* * About menu */ static const char *const infoTypes[] = {"Version", "Recovery Check"}; static const char *const infoContents[] = {APPVERSION, "(c) 2022 Ledger"}; - static bool onInfos(uint8_t page, nbgl_pageContent_t *content) { if (page == 0) { content->type = INFOS_LIST; @@ -85,7 +81,6 @@ static bool onInfos(uint8_t page, nbgl_pageContent_t *content) { return true; } - /* * Choose mnemonic size page */ @@ -115,7 +110,6 @@ static void mnemonic_dispatcher(const int token, uint8_t index) { } } - static void display_mnemonic_page() { reset_globals(); nbgl_layoutDescription_t layoutDescription = {.modal = false, @@ -141,7 +135,6 @@ static void display_mnemonic_page() { nbgl_layoutDraw(layout); } - /* * Word recover page */ @@ -152,7 +145,6 @@ static char *buttonTexts[NB_MAX_SUGGESTION_BUTTONS] = {0}; // the max number of showed suggestions is NB_MAX_SUGGESTION_BUTTONS static char wordCandidates[(MAX_WORD_LENGTH + 1) * NB_MAX_SUGGESTION_BUTTONS] = {0}; - static void keyboard_dispatcher(const int token, uint8_t index __attribute__((unused))) { if (token == BACK_BUTTON_TOKEN) { nbgl_layoutRelease(layout); @@ -176,7 +168,6 @@ static void keyboard_dispatcher(const int token, uint8_t index __attribute__((un } } - // function called when a key of keyboard is touched static void key_press_callback(const char touchedKey) { size_t textLen = 0; @@ -214,7 +205,6 @@ static void key_press_callback(const char touchedKey) { nbgl_refresh(); } - static void display_keyboard_page() { nbgl_layoutDescription_t layoutDescription = {.modal = false, .onActionCallback = &keyboard_dispatcher}; @@ -256,7 +246,6 @@ static void display_keyboard_page() { nbgl_layoutDraw(layout); } - /* * Home page */ @@ -280,14 +269,12 @@ static void display_home_page() { nbgl_refresh(); } - /* * Result page */ static char *possible_results[2] = {"Sorry, this passphrase\nis incorrect.", "You recovery passphrase\nis correct!"}; - static void display_result_page(const bool result) { nbgl_pageInfoDescription_t home = {.centeredInfo.icon = &C_fatstacks_app_recovery_check, .centeredInfo.text1 = possible_results[result], @@ -307,7 +294,6 @@ static void display_result_page(const bool result) { nbgl_refresh(); } - /* * Utils */ @@ -316,7 +302,6 @@ static void reset_globals() { memset(buttonTexts, 0, NB_MAX_SUGGESTION_BUTTONS); } - #endif enum UI_STATE { UI_IDLE, UI_TEXT, UI_APPROVAL }; diff --git a/src/ux_common/common.h b/src/ux_common/common.h index 2ac61a2a..03081b5d 100644 --- a/src/ux_common/common.h +++ b/src/ux_common/common.h @@ -16,15 +16,8 @@ #pragma once -#include -#include #include -#include -#include - -#include "glyphs.h" - #ifdef OS_IO_SEPROXYHAL #ifndef SPRINTF @@ -32,25 +25,6 @@ #define SPRINTF(strbuf, ...) snprintf((char*) (strbuf), sizeof(strbuf), __VA_ARGS__) #endif -#define ONBOARDING_WORD_COMPLETION_MAX_ITEMS 8 -#define BOLOS_UX_HASH_LENGTH 4 // as on the blue - -#define MAX_WORD_LENGTH 8 - -enum { - MNEMONIC_SIZE_12 = 12, - MNEMONIC_SIZE_18 = 18, - MNEMONIC_SIZE_24 = 24, -}; -#define KEYBOARD_ITEM_VALIDATED \ - 1 // callback is called with the entered item index, tmp_element is precharged with element to - // be displayed and using the common string buffer as string parameter -#define KEYBOARD_RENDER_ITEM \ - 2 // callback is called with the element index, tmp_element is precharged with element to be - // displayed and using the common string buffer as string parameter -#define KEYBOARD_RENDER_WORD \ - 3 // callback is called with a -1 when requesting complete word, or the char index else, - // returnin 0 implies no char is to be displayed #if defined(HAVE_NBGL) typedef const nbgl_obj_t* (*keyboard_callback_t)(unsigned int event, unsigned int value); #else @@ -64,10 +38,6 @@ void bolos_ux_hslider3_previous(void); // all screens void screen_onboarding_3_restore_init(void); -#define RESTORE_WORD_ACTION_REENTER_WORD 0 -#define RESTORE_WORD_ACTION_FIRST_WORD 1 void screen_onboarding_4_restore_word_init(unsigned int action); -#define COMMON_KEYBOARD_INDEX_UNCHANGED (-1UL) - #endif // OS_IO_SEPROXYHAL diff --git a/src/ux_common/common_bip39.h b/src/ux_common/common_bip39.h index 8aba2cc0..127be91e 100644 --- a/src/ux_common/common_bip39.h +++ b/src/ux_common/common_bip39.h @@ -1,6 +1,6 @@ - #pragma once +#include // BIP39 helpers #include "onboarding_seed_rom_variables.h" diff --git a/src/ux_nanos_keyboard.c b/src/ux_nanos_keyboard.c index e630c853..75c83859 100644 --- a/src/ux_nanos_keyboard.c +++ b/src/ux_nanos_keyboard.c @@ -14,6 +14,7 @@ * limitations under the License. ********************************************************************************/ +#include "constants.h" #include "ui.h" #ifdef TARGET_NANOS diff --git a/src/ux_nanox_keyboard.c b/src/ux_nanox_keyboard.c index f8bfcfde..7f0f922e 100644 --- a/src/ux_nanox_keyboard.c +++ b/src/ux_nanox_keyboard.c @@ -14,6 +14,7 @@ * limitations under the License. ********************************************************************************/ +#include "constants.h" #include "ui.h" #if defined(TARGET_NANOX) || defined(TARGET_NANOS2) diff --git a/src/ux_stax.c b/src/ux_stax.c index 81977432..1fa14b7e 100644 --- a/src/ux_stax.c +++ b/src/ux_stax.c @@ -1,8 +1,24 @@ +#include +#include + #include "ux_stax.h" #include "ux_common/common_bip39.h" #if defined(HAVE_BOLOS_UX) && defined(TARGET_FATSTACKS) +typedef struct buffer { + // the mnemonic passphrase, built over time + char buffer[MAX_MNEMONIC_LENGTH]; + // current length of the mnemonic passphrase + size_t length; + // index of the current word ((size_t)-1 mean there is no word currently) + size_t current_word_index; + // array of every stored word lengths (used for removing them if needed) + size_t word_lengths[MNEMONIC_SIZE_24]; + // expected number of word in the final mnemonic (12 or 18 or 24) + size_t final_size; +} buffer_t; + static buffer_t mnemonic = {0}; size_t mnemonic_shrink(const size_t size) { @@ -34,7 +50,7 @@ void reset_mnemonic() { } bool remove_word_from_mnemonic() { - PRINTF("Removing a word, currently there is '%d' of them\n", mnemonic.current_word_index + 1); + PRINTF("Removing a word, currently there is '%ld' of them\n", mnemonic.current_word_index + 1); if (mnemonic.current_word_index == (size_t) -1) { return false; } @@ -42,7 +58,7 @@ bool remove_word_from_mnemonic() { mnemonic.current_word_index--; // removing previous word from mnemonic buffer (+ 1 blank space) mnemonic_shrink(current_length + 1); - PRINTF("Number of remaining words in the mnemonic: '%d'\n", mnemonic.current_word_index + 1); + PRINTF("Number of remaining words in the mnemonic: '%ld'\n", mnemonic.current_word_index + 1); return true; } @@ -56,7 +72,7 @@ size_t add_word_in_mnemonic(const char* const buffer, const size_t size) { mnemonic.length += size; mnemonic.current_word_index++; mnemonic.word_lengths[mnemonic.current_word_index] = size; - PRINTF("Number of words in the mnemonic: '%d'\n", mnemonic.current_word_index); + PRINTF("Number of words in the mnemonic: '%ld'\n", mnemonic.current_word_index); PRINTF("Current mnemonic: '%s'\n", &mnemonic.buffer[0]); return mnemonic.current_word_index; } @@ -66,7 +82,7 @@ bool is_mnemonic_complete() { } bool check_mnemonic() { - PRINTF("Checking the following mnemonic: '%s' (size %d)\n", + PRINTF("Checking the following mnemonic: '%s' (size %ld)\n", &mnemonic.buffer[0], mnemonic.length); const bool result = diff --git a/src/ux_stax.h b/src/ux_stax.h index 75cff5c5..24a12d53 100644 --- a/src/ux_stax.h +++ b/src/ux_stax.h @@ -16,25 +16,13 @@ #pragma once -#include "ux_common/common.h" +#include +#include "constants.h" #if defined(HAVE_BOLOS_UX) && defined(TARGET_FATSTACKS) #define MAX_MNEMONIC_LENGTH (MNEMONIC_SIZE_24 * (MAX_WORD_LENGTH + 1)) -typedef struct buffer { - // the mnemonic passphrase, built over time - char buffer[MAX_MNEMONIC_LENGTH]; - // current length of the mnemonic passphrase - size_t length; - // index of the current word ((size_t)-1 mean there is no word currently) - size_t current_word_index; - // array of every stored word lengths (used for removing them if needed) - size_t word_lengths[MNEMONIC_SIZE_24]; - // expected number of word in the final mnemonic (12 or 18 or 24) - size_t final_size; -} buffer_t; - /* * Sets how many words are expected in the mnemonic passphrase */ From ec323d7e7d0fc5a091054246c44ab71882ee5c61 Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Tue, 8 Nov 2022 18:58:27 +0100 Subject: [PATCH 28/65] [test] Unit test of fatstacks mnemonic management --- .github/workflows/ci-workflow.yml | 47 +++++++++-- .github/workflows/lint-workflow.yml | 3 +- src/nanox_enter_phrase.c | 14 ++-- src/ui.c | 30 +++---- src/ux_stax.c | 17 +++- src/ux_stax.h | 4 + tests/unit/CMakeLists.txt | 51 ++++++++++++ tests/unit/README.md | 36 +++++++++ tests/unit/mocks/os.h | 17 ++++ tests/unit/test_stax_mnemonic.c | 118 ++++++++++++++++++++++++++++ 10 files changed, 304 insertions(+), 33 deletions(-) create mode 100644 tests/unit/CMakeLists.txt create mode 100644 tests/unit/README.md create mode 100644 tests/unit/mocks/os.h create mode 100644 tests/unit/test_stax_mnemonic.c diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index 1f03f507..a78408df 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -11,6 +11,39 @@ on: - '.github/workflows/*.yml' jobs: + unittesting: + name: C unit testing + runs-on: ubuntu-latest + steps: + - name: Clone + uses: actions/checkout@v3 + - name: Install cmocka + run: | + sudo apt update + sudo apt install libcmocka-dev lcov + - name: Compile the tests + run: | + cd tests/unit/ + rm -rf build/ + cmake -B build -H. + make -C build + - name: Run the tests + run: | + cd tests/unit/ + CTEST_OUTPUT_ON_FAILURE=1 make -C build test + - name: Generate code coverage + run: | + cd tests/unit/ + lcov --directory . -b "$(realpath build/)" --capture --initial -o coverage.base + lcov --rc lcov_branch_coverage=1 --directory . -b "$(realpath build/)" --capture -o coverage.capture + lcov --directory . -b "$(realpath build/)" --add-tracefile coverage.base --add-tracefile coverage.capture -o coverage.info + lcov --directory . -b "$(realpath build/)" --remove coverage.info '*/unit-tests/*' -o coverage.info + genhtml coverage.info -o coverage + - uses: actions/upload-artifact@v3 + with: + name: code-coverage + path: tests/unit/coverage + nano_build: name: Build application for NanoS, X and S+ strategy: @@ -27,7 +60,7 @@ jobs: image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder:latest steps: - name: Clone - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Build run: | @@ -35,7 +68,7 @@ jobs: make BOLOS_SDK=${{ matrix.SDK }} mv bin/app.elf "bin/${{ matrix.model }}.elf" - name: Upload app binary - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: apps path: bin/*.elf @@ -47,9 +80,9 @@ jobs: image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder:latest steps: - name: Clone application - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Clone SDK - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: LedgerHQ/ledger-secure-sdk path: ./stax-sdk @@ -61,7 +94,7 @@ jobs: make BOLOS_SDK=./stax-sdk TARGET=fatstacks mv bin/app.elf "bin/fatstacks.elf" - name: Upload app binary - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: apps path: bin/*.elf @@ -82,13 +115,13 @@ jobs: image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder:latest steps: - name: Clone - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Build with Clang Static Analyzer run: | make clean scan-build --use-cc=clang -analyze-headers -enable-checker security -enable-checker unix -enable-checker valist -o scan-build --status-bugs make default BOLOS_SDK=${{ matrix.SDK }} - name: Upload scan result - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 if: failure() with: name: scan-build diff --git a/.github/workflows/lint-workflow.yml b/.github/workflows/lint-workflow.yml index d708d468..d0396f55 100644 --- a/.github/workflows/lint-workflow.yml +++ b/.github/workflows/lint-workflow.yml @@ -35,7 +35,8 @@ jobs: with: fetch-depth: 0 - name: Check misspellings - uses: codespell-project/actions-codespell@2391250ab05295bddd51e36a8c6295edb6343b0e + uses: codespell-project/actions-codespell@master with: builtin: clear,rare check_filenames: true + ignore_words_list: ontop diff --git a/src/nanox_enter_phrase.c b/src/nanox_enter_phrase.c index b7a22c3d..08a776be 100644 --- a/src/nanox_enter_phrase.c +++ b/src/nanox_enter_phrase.c @@ -227,8 +227,8 @@ void screen_onboarding_4_restore_word_display_word_selection(void) { ux_stack_display(0); } -const bagl_element_t* screen_onboarding_4_restore_word_keyboard_callback(unsigned int event, - unsigned int value) { +const bagl_element_t* screen_onboarding_4_restore_word_keyboard_callback(const unsigned int event, + const unsigned int value) { switch (event) { case KEYBOARD_ITEM_VALIDATED: // depending on the chosen class, interpret the click @@ -273,7 +273,6 @@ const bagl_element_t* screen_onboarding_4_restore_word_keyboard_callback(unsigne x:57 w:14 iconw: 11 */ - value = 3; G_ux.tmp_element.component.width = C_icon_backspace.width; G_ux.tmp_element.component.x += 1 + G_ux.tmp_element.component.width / 2 - C_icon_backspace.width / 2; @@ -476,8 +475,9 @@ void screen_onboarding_4_restore_word_validate(void) { } } -unsigned int screen_onboarding_4_restore_word_select_button(unsigned int button_mask, - unsigned int button_mask_counter) { +unsigned int screen_onboarding_4_restore_word_select_button( + const unsigned int button_mask, + const unsigned int button_mask_counter) { UNUSED(button_mask_counter); switch (button_mask) { case BUTTON_EVT_FAST | BUTTON_LEFT: @@ -515,8 +515,8 @@ unsigned int screen_onboarding_4_restore_word_select_button(unsigned int button_ return 0; } -unsigned int screen_onboarding_4_restore_word_intro_button(unsigned int button_mask, - unsigned int button_mask_counter) { +unsigned int screen_onboarding_4_restore_word_intro_button(const unsigned int button_mask, + const unsigned int button_mask_counter) { UNUSED(button_mask_counter); switch (button_mask) { case BUTTON_EVT_RELEASED | BUTTON_LEFT | BUTTON_RIGHT: { diff --git a/src/ui.c b/src/ui.c index 373b6fd2..09a31d97 100644 --- a/src/ui.c +++ b/src/ui.c @@ -27,7 +27,6 @@ static void display_mnemonic_page(void); static void reset_globals(void); static bool onInfos(uint8_t page, nbgl_pageContent_t *content); - enum { BACK_HOME_TOKEN = 0, BACK_BUTTON_TOKEN, @@ -250,20 +249,21 @@ static void display_keyboard_page() { * Home page */ static void display_home_page() { - nbgl_pageInfoDescription_t home = {.centeredInfo.icon = &C_fatstacks_app_recovery_check, - .centeredInfo.text1 = "Recovery Check", - .centeredInfo.text2 = NULL, - .centeredInfo.text3 = NULL, - .centeredInfo.style = LARGE_CASE_INFO, - .centeredInfo.offsetY = 32, - .topRightStyle = QUIT_ICON, - .bottomButtonStyle = INFO_ICON, - .topRightToken = QUIT_APP_TOKEN, - .bottomButtonToken = INFO_TOKEN, - .footerText = NULL, - .tapActionText = "Tap to check if your\nrecovery passphrase is valid", - .tapActionToken = CHOOSE_MNEMONIC_SIZE_TOKEN, - .tuneId = TUNE_TAP_CASUAL}; + nbgl_pageInfoDescription_t home = { + .centeredInfo.icon = &C_fatstacks_app_recovery_check, + .centeredInfo.text1 = "Recovery Check", + .centeredInfo.text2 = NULL, + .centeredInfo.text3 = NULL, + .centeredInfo.style = LARGE_CASE_INFO, + .centeredInfo.offsetY = 32, + .topRightStyle = QUIT_ICON, + .bottomButtonStyle = INFO_ICON, + .topRightToken = QUIT_APP_TOKEN, + .bottomButtonToken = INFO_TOKEN, + .footerText = NULL, + .tapActionText = "Tap to check if your\nrecovery passphrase is valid", + .tapActionToken = CHOOSE_MNEMONIC_SIZE_TOKEN, + .tuneId = TUNE_TAP_CASUAL}; releaseContext(); pageContext = nbgl_pageDrawInfo(&pageTouchCallback, NULL, &home); nbgl_refresh(); diff --git a/src/ux_stax.c b/src/ux_stax.c index 1fa14b7e..3653c846 100644 --- a/src/ux_stax.c +++ b/src/ux_stax.c @@ -72,16 +72,21 @@ size_t add_word_in_mnemonic(const char* const buffer, const size_t size) { mnemonic.length += size; mnemonic.current_word_index++; mnemonic.word_lengths[mnemonic.current_word_index] = size; - PRINTF("Number of words in the mnemonic: '%ld'\n", mnemonic.current_word_index); + PRINTF("Number of words in the mnemonic: '%ld'\n", get_current_word_number()); PRINTF("Current mnemonic: '%s'\n", &mnemonic.buffer[0]); - return mnemonic.current_word_index; + return get_current_word_number(); } bool is_mnemonic_complete() { - return ((mnemonic.current_word_index + 1) >= get_mnemonic_final_size()); + return (mnemonic.final_size == 0 + ? false + : (mnemonic.current_word_index + 1) >= get_mnemonic_final_size()); } bool check_mnemonic() { + if (!is_mnemonic_complete()) { + return false; + } PRINTF("Checking the following mnemonic: '%s' (size %ld)\n", &mnemonic.buffer[0], mnemonic.length); @@ -92,4 +97,10 @@ bool check_mnemonic() { return result; } +#if defined(TEST) +char* get_mnemonic() { + return mnemonic.buffer; +} +#endif + #endif diff --git a/src/ux_stax.h b/src/ux_stax.h index 24a12d53..21698dd3 100644 --- a/src/ux_stax.h +++ b/src/ux_stax.h @@ -64,4 +64,8 @@ bool remove_word_from_mnemonic(void); */ size_t add_word_in_mnemonic(const char* const buffer, const size_t size); +#if defined(TEST) +char* get_mnemonic(); +#endif + #endif // HAVE_BOLOS_UX && TARGET_NANOS diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt new file mode 100644 index 00000000..2dd63719 --- /dev/null +++ b/tests/unit/CMakeLists.txt @@ -0,0 +1,51 @@ +cmake_minimum_required(VERSION 3.10) + +if(${CMAKE_VERSION} VERSION_LESS 3.10) + cmake_policy(VERSION ${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION}) +endif() + +# project information +project(unit_tests + VERSION 0.1 + DESCRIPTION "Unit tests for the Recovery Check application" + LANGUAGES C) + + +# guard against bad build-type strings +if (NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE "Debug") +endif() + +include(CTest) +ENABLE_TESTING() + +# specify C standard +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED True) +set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -Wall -pedantic -g -O0 --coverage") + +set(GCC_COVERAGE_LINK_FLAGS "--coverage -lgcov") +set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} ${GCC_COVERAGE_LINK_FLAGS}") +set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${GCC_COVERAGE_LINK_FLAGS}") + +# guard against in-source builds +if(${CMAKE_SOURCE_DIR} STREQUAL ${CMAKE_BINARY_DIR}) + message(FATAL_ERROR "In-source builds not allowed. Please make a new directory (called a build \ + directory) and run CMake from there. You may need to remove CMakeCache.txt.") +endif() + +add_compile_definitions(WIDE=) +add_compile_definitions(TEST) +add_compile_definitions(HAVE_BOLOS_UX) +add_compile_definitions(TARGET_FATSTACKS) +add_compile_definitions(OS_IO_SEPROXYHAL) + +include_directories(../../src ./mocks/) + +add_executable(test_stax_mnemonic test_stax_mnemonic.c) + +add_library(stax SHARED ../../src/ux_common/onboarding_seed_rom_variables.c ../../src/ux_stax.c) + +target_link_libraries(test_stax_mnemonic PUBLIC cmocka gcov stax) + +add_test(test_stax_mnemonic test_stax_mnemonic) diff --git a/tests/unit/README.md b/tests/unit/README.md new file mode 100644 index 00000000..01b67bbc --- /dev/null +++ b/tests/unit/README.md @@ -0,0 +1,36 @@ +# Unit tests + +## Prerequisite + +Be sure to have installed: + +- CMake >= 3.10 +- CMocka >= 1.1.5 + +and for code coverage generation: + +- lcov >= 1.14 + +## Overview + +In `unit-tests` folder, compile with + +```sh +cmake -Bbuild -H. && make -C build +``` + +and run tests with + +```sh +CTEST_OUTPUT_ON_FAILURE=1 make -C build test +``` + +## Generate code coverage + +Just execute in `unit-tests` folder + +```sh +./gen_coverage.sh +``` + +it will output `coverage.total` and `coverage/` folder with HTML details (in `coverage/index.html`). diff --git a/tests/unit/mocks/os.h b/tests/unit/mocks/os.h new file mode 100644 index 00000000..195c2395 --- /dev/null +++ b/tests/unit/mocks/os.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include +#include +#include + +#define PRINTF printf + + +bool bolos_ux_mnemonic_check(const unsigned char* buffer, unsigned int length) { + const char* expected_mnemonic = "list of random words which actually are the mnemonic"; + printf("Comparing strings under size '%d'\n", length); + printf(" - expected: '%s'\n", expected_mnemonic); + printf(" - given: '%s'\n", buffer); + return (strncmp(expected_mnemonic, (const char *)buffer, length) == 0); +} diff --git a/tests/unit/test_stax_mnemonic.c b/tests/unit/test_stax_mnemonic.c new file mode 100644 index 00000000..d45c4575 --- /dev/null +++ b/tests/unit/test_stax_mnemonic.c @@ -0,0 +1,118 @@ +#include +/* #include */ +#include +#include +/* #include */ +#include +#include + +#include "ux_stax.h" + + +static int setup(void **state __attribute__((unused))) { + // resets the whole buffer with initial values + reset_mnemonic(); + return 0; +} + +static void test_mnemonic_init(void **state __attribute__((unused))) { + assert_int_equal(get_mnemonic_final_size(), 0); + assert_int_equal(get_current_word_number(), 0); + assert_string_equal(get_mnemonic(), ""); + assert_false(is_mnemonic_complete()); + assert_false(check_mnemonic()); +} + +static void test_mnemonic_final_size(void **state __attribute__((unused))) { + const size_t test = 9; + set_mnemonic_final_size(test); + assert_int_equal(get_mnemonic_final_size(), test); +} + +static void test_add_word_in_mnemonic(void **state __attribute__((unused))) { + const char* word = "hello"; + assert_int_equal(get_current_word_number(), 0); + assert_string_equal(get_mnemonic(), ""); + + assert_int_equal(add_word_in_mnemonic(word, 5), 1); + assert_int_equal(get_current_word_number(), 1); + assert_string_equal(get_mnemonic(), word); +} + +static void test_remove_word_from_mnemonic_none(void **state __attribute__((unused))) { + assert_false(remove_word_from_mnemonic()); +} + +static void test_remove_word_from_mnemonic(void **state __attribute__((unused))) { + const char* word = "list"; + assert_int_equal(get_current_word_number(), 0); + assert_string_equal(get_mnemonic(), ""); + + add_word_in_mnemonic(word, 4); + assert_int_equal(get_current_word_number(), 1); + assert_string_equal(get_mnemonic(), word); + + assert_true(remove_word_from_mnemonic()); + assert_int_equal(get_current_word_number(), 0); + assert_string_equal(get_mnemonic(), ""); +} + +static void test_is_mnemonic_complete(void **state __attribute__((unused))) { + set_mnemonic_final_size(2); + assert_false(is_mnemonic_complete()); + + const char* word = "list"; + assert_int_equal(add_word_in_mnemonic(word, 4), 1); + assert_false(is_mnemonic_complete()); + + assert_int_equal(add_word_in_mnemonic(word, 4), 2); + assert_true(is_mnemonic_complete()); + + set_mnemonic_final_size(3); + assert_false(is_mnemonic_complete()); + + set_mnemonic_final_size(1); + assert_true(is_mnemonic_complete()); + + set_mnemonic_final_size(0); + assert_false(is_mnemonic_complete()); +} + +static void test_check_mnemonic_nok(void **state __attribute__((unused))) { + assert_false(check_mnemonic()); + + set_mnemonic_final_size(1); + assert_false(check_mnemonic()); + + assert_int_equal(add_word_in_mnemonic("word", 4), 1); + assert_string_equal(get_mnemonic(), "word"); + assert_false(check_mnemonic()); +} + +static void test_check_mnemonic_ok(void **state __attribute__((unused))) { + const char* const mnemonic[] = {"list", "of", "random", "words", "which", "actually", "are", "the", "mnemonic"}; + int i = 0; + size_t mnemonic_size = (sizeof(mnemonic) / sizeof(char*)); + set_mnemonic_final_size(mnemonic_size); + + for (i = 0; i < mnemonic_size; i++) { + assert_int_equal(add_word_in_mnemonic(mnemonic[i], strlen(mnemonic[i])), i + 1); + } + + assert_true(check_mnemonic()); +} + + +int main() { + const struct CMUnitTest tests[] = { + cmocka_unit_test_setup_teardown(test_mnemonic_init, setup, NULL), + cmocka_unit_test_setup_teardown(test_mnemonic_final_size, setup, NULL), + cmocka_unit_test_setup_teardown(test_add_word_in_mnemonic, setup, NULL), + cmocka_unit_test_setup_teardown(test_remove_word_from_mnemonic_none, setup, NULL), + cmocka_unit_test_setup_teardown(test_remove_word_from_mnemonic, setup, NULL), + cmocka_unit_test_setup_teardown(test_is_mnemonic_complete, setup, NULL), + cmocka_unit_test_setup_teardown(test_check_mnemonic_nok, setup, NULL), + cmocka_unit_test_setup_teardown(test_check_mnemonic_ok, setup, NULL), + }; + return cmocka_run_group_tests(tests, NULL, NULL); +} From 9c8ff62a07db4023e7d00dd004053685da504191 Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Thu, 17 Nov 2022 13:56:23 +0100 Subject: [PATCH 29/65] [update] Using official icon --- .github/workflows/ci-workflow.yml | 2 +- Makefile | 2 +- glyphs/fatstacks_recovery_check_64px.gif | Bin 0 -> 258 bytes icons/fatstacks_app_recovery_check.gif | Bin 246 -> 0 bytes icons/fatstacks_recovery_check_32px.gif | Bin 0 -> 139 bytes src/ui.c | 8 ++++---- tests/screenshots/correct.png | Bin 4513 -> 5088 bytes tests/screenshots/incorrect.png | Bin 4757 -> 5372 bytes tests/screenshots/welcome.png | Bin 5276 -> 5348 bytes 9 files changed, 6 insertions(+), 6 deletions(-) create mode 100644 glyphs/fatstacks_recovery_check_64px.gif delete mode 100644 icons/fatstacks_app_recovery_check.gif create mode 100644 icons/fatstacks_recovery_check_32px.gif diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index a78408df..474bceaf 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -86,7 +86,7 @@ jobs: with: repository: LedgerHQ/ledger-secure-sdk path: ./stax-sdk - ref: master + ref: v0.4.5-ci token: ${{ secrets.STAX_RO }} - name: Build run: | diff --git a/Makefile b/Makefile index d854619d..271c5c1a 100755 --- a/Makefile +++ b/Makefile @@ -36,7 +36,7 @@ APP_LOAD_PARAMS = --appFlags 0x10 $(COMMON_LOAD_PARAMS) --apdu --curve secp256k1 ifeq ($(TARGET_NAME), TARGET_NANOS) ICONNAME=icons/nanos_app_recovery_check.gif else ifeq ($(TARGET_NAME), TARGET_FATSTACKS) - ICONNAME=icons/fatstacks_app_recovery_check.gif + ICONNAME=icons/fatstacks_recovery_check_32px.gif else ICONNAME=icons/nanox_app_recovery_check.gif endif diff --git a/glyphs/fatstacks_recovery_check_64px.gif b/glyphs/fatstacks_recovery_check_64px.gif new file mode 100644 index 0000000000000000000000000000000000000000..af4d15f4cb90b7781d8d5d20743ec6a8e6de1fdd GIT binary patch literal 258 zcmV+d0sa0*Nk%w1VL$*t0Pq0-00030|NkNR1ONa4001li0000$06+i$0@;j@smtvT zqnxzbi?iOm`wxcV$N{FAoa)N9YpJI0OrzV2)Orh_@(u*_Y=>ZwgiI=z%%<~+!4-|W zp)g0)S%2Bt7TfjSV&E_jicYK7?6y*BpmNC?c;GnIM(Oct-*{o?ch_YhSd&-yWaiVv z$cOZ3HzElsQaQPBc*aPWsj$iNmz7B55K3k?cXbNsG74DM*g6Z_s90JHLgVTBO8MK% zYHR5c{I~jP<(jNKcHHdzd=_2&fha3Rq9@Foq=om5IUHH(Bnj1t7Pdy(2ISTpKTlt8 IUr_)6J7?B@SpWb4 literal 0 HcmV?d00001 diff --git a/icons/fatstacks_app_recovery_check.gif b/icons/fatstacks_app_recovery_check.gif deleted file mode 100644 index 07c7d1b2bf3471cc1b1be15f872786cec5ddbc0a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 246 zcmV5I2s3T#w(S>D+q9H>T8-_r o%hR_Q9ZXzzL&nnQRA}CJo?h+p8>V~pM4z?aPuhJWh=IWx05=FZ+5i9m literal 0 HcmV?d00001 diff --git a/src/ui.c b/src/ui.c index 09a31d97..4472390f 100644 --- a/src/ui.c +++ b/src/ui.c @@ -250,7 +250,7 @@ static void display_keyboard_page() { */ static void display_home_page() { nbgl_pageInfoDescription_t home = { - .centeredInfo.icon = &C_fatstacks_app_recovery_check, + .centeredInfo.icon = &C_fatstacks_recovery_check_64px, .centeredInfo.text1 = "Recovery Check", .centeredInfo.text2 = NULL, .centeredInfo.text3 = NULL, @@ -272,11 +272,11 @@ static void display_home_page() { /* * Result page */ -static char *possible_results[2] = {"Sorry, this passphrase\nis incorrect.", - "You recovery passphrase\nis correct!"}; +static char *possible_results[2] = {"Sorry, this recovery\npassphrase is\nincorrect.", + "Your recovery\npassphrase is\ncorrect!"}; static void display_result_page(const bool result) { - nbgl_pageInfoDescription_t home = {.centeredInfo.icon = &C_fatstacks_app_recovery_check, + nbgl_pageInfoDescription_t home = {.centeredInfo.icon = &C_fatstacks_recovery_check_64px, .centeredInfo.text1 = possible_results[result], .centeredInfo.text2 = NULL, .centeredInfo.text3 = NULL, diff --git a/tests/screenshots/correct.png b/tests/screenshots/correct.png index b5678b0e5313d0d6c49296d2b3bcbda093bf4607..5721cddd90e51a1f54a3a83150b4ca60e3ff6a7c 100644 GIT binary patch literal 5088 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 F_jf(?`UC&~ literal 4513 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 diff --git a/tests/screenshots/incorrect.png b/tests/screenshots/incorrect.png index 9ac7eb37b0e2d221be1b1be9c587d8c0e0d17e40..29e1f1a8cc206da7e3b6eb8978fd8a457a5b460d 100644 GIT binary patch literal 5372 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 delta 3916 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 JmUsq*{Wk!D)@c9$ From 8a40c96aed606b218ed229f4da1b53f3da0ee765 Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Tue, 6 Dec 2022 10:28:33 +0100 Subject: [PATCH 30/65] [fix] Device locking management --- .github/workflows/ci-workflow.yml | 1 + src/main.c | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index 474bceaf..8e514e93 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -102,6 +102,7 @@ jobs: job_scan_build: name: Clang Static Analyzer strategy: + fail-fast: false matrix: include: - SDK: "$NANOS_SDK" diff --git a/src/main.c b/src/main.c index 2f13e2bc..f68239ad 100644 --- a/src/main.c +++ b/src/main.c @@ -107,6 +107,8 @@ unsigned char io_event(unsigned char channel __attribute__((unused))) { } #elif defined(TARGET_NANOX) || defined(TARGET_NANOS2) UX_DISPLAYED_EVENT({}); +#elif defined(HAVE_NBGL) + UX_DEFAULT_EVENT(); #endif break; From e90f5a281c4389fc5f21e822c64713b3509639e6 Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Fri, 16 Dec 2022 11:04:32 +0100 Subject: [PATCH 31/65] [update] SDK 1.0.0-rc6 --- .github/workflows/ci-workflow.yml | 2 +- Makefile | 1 + src/ui.c | 9 +++++---- tests/functional/conftest.py | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index 8e514e93..6ce1fbff 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -86,7 +86,7 @@ jobs: with: repository: LedgerHQ/ledger-secure-sdk path: ./stax-sdk - ref: v0.4.5-ci + ref: fatstacks_1.0.0-rc6 token: ${{ secrets.STAX_RO }} - name: Build run: | diff --git a/Makefile b/Makefile index 271c5c1a..d7d7c253 100755 --- a/Makefile +++ b/Makefile @@ -64,6 +64,7 @@ ifneq ($(TARGET_NAME), TARGET_FATSTACKS) DEFINES += HAVE_BAGL else $(info BAGL deactivated) + DEFINES += NBGL_KEYBOARD endif ifeq ($(TARGET_NAME), TARGET_NANOS) diff --git a/src/ui.c b/src/ui.c index 4472390f..3d6a430a 100644 --- a/src/ui.c +++ b/src/ui.c @@ -11,6 +11,7 @@ #include #include #include +#include #include "ux_common/common_bip39.h" @@ -259,7 +260,7 @@ static void display_home_page() { .topRightStyle = QUIT_ICON, .bottomButtonStyle = INFO_ICON, .topRightToken = QUIT_APP_TOKEN, - .bottomButtonToken = INFO_TOKEN, + .bottomButtonsToken = INFO_TOKEN, .footerText = NULL, .tapActionText = "Tap to check if your\nrecovery passphrase is valid", .tapActionToken = CHOOSE_MNEMONIC_SIZE_TOKEN, @@ -276,7 +277,7 @@ static char *possible_results[2] = {"Sorry, this recovery\npassphrase is\nincorr "Your recovery\npassphrase is\ncorrect!"}; static void display_result_page(const bool result) { - nbgl_pageInfoDescription_t home = {.centeredInfo.icon = &C_fatstacks_recovery_check_64px, + nbgl_pageInfoDescription_t page = {.centeredInfo.icon = &C_fatstacks_recovery_check_64px, .centeredInfo.text1 = possible_results[result], .centeredInfo.text2 = NULL, .centeredInfo.text3 = NULL, @@ -284,13 +285,13 @@ static void display_result_page(const bool result) { .centeredInfo.offsetY = 32, .topRightStyle = NO_BUTTON_STYLE, .bottomButtonStyle = QUIT_ICON, - .bottomButtonToken = QUIT_APP_TOKEN, + .bottomButtonsToken = QUIT_APP_TOKEN, .footerText = NULL, .tapActionText = "Tap to check another mnemonic", .tapActionToken = CHOOSE_MNEMONIC_SIZE_TOKEN, .tuneId = TUNE_TAP_CASUAL}; releaseContext(); - pageContext = nbgl_pageDrawInfo(&pageTouchCallback, NULL, &home); + pageContext = nbgl_pageDrawInfo(&pageTouchCallback, NULL, &page); nbgl_refresh(); } diff --git a/tests/functional/conftest.py b/tests/functional/conftest.py index 222db067..33dffb93 100644 --- a/tests/functional/conftest.py +++ b/tests/functional/conftest.py @@ -1,7 +1,7 @@ from pathlib import Path import pytest -from ragger import Firmware +from ragger.firmware import Firmware from ragger.backend import SpeculosBackend, LedgerCommBackend, LedgerWalletBackend, BackendInterface From 1e50cc1aa9ce535227b71d8d3a02da139ca94b7c Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Mon, 19 Dec 2022 09:56:37 +0100 Subject: [PATCH 32/65] [clean] Better separation between Nano/BAGL code and Stax/NBGL code --- src/{ => fatstacks}/ui.c | 214 ++++---------------- src/fatstacks/ui.h | 7 + src/{ux_stax.c => fatstacks/ux_fatstacks.c} | 4 +- src/{ux_stax.h => fatstacks/ux_fatstacks.h} | 0 src/main.c | 11 +- src/{ => nano}/nanos_enter_phrase.c | 2 +- src/{ => nano}/nanox_enter_phrase.c | 0 src/nano/ui.c | 158 +++++++++++++++ src/{ => nano}/ui.h | 11 +- src/{ => nano}/ux_nanos.c | 0 src/{ => nano}/ux_nanos.h | 2 +- src/{ => nano}/ux_nanos_keyboard.c | 0 src/{ => nano}/ux_nanox.c | 0 src/{ => nano}/ux_nanox.h | 0 src/{ => nano}/ux_nanox_keyboard.c | 0 15 files changed, 216 insertions(+), 193 deletions(-) rename src/{ => fatstacks}/ui.c (72%) create mode 100644 src/fatstacks/ui.h rename src/{ux_stax.c => fatstacks/ux_fatstacks.c} (98%) rename src/{ux_stax.h => fatstacks/ux_fatstacks.h} (100%) rename src/{ => nano}/nanos_enter_phrase.c (100%) rename src/{ => nano}/nanox_enter_phrase.c (100%) create mode 100644 src/nano/ui.c rename src/{ => nano}/ui.h (85%) rename src/{ => nano}/ux_nanos.c (100%) rename src/{ => nano}/ux_nanos.h (98%) rename src/{ => nano}/ux_nanos_keyboard.c (100%) rename src/{ => nano}/ux_nanox.c (100%) rename src/{ => nano}/ux_nanox.h (100%) rename src/{ => nano}/ux_nanox_keyboard.c (100%) diff --git a/src/ui.c b/src/fatstacks/ui.c similarity index 72% rename from src/ui.c rename to src/fatstacks/ui.c index 3d6a430a..5deb8b36 100644 --- a/src/ui.c +++ b/src/fatstacks/ui.c @@ -1,7 +1,7 @@ -#include +#include +#include #include "constants.h" -#include "ui.h" #include "glyphs.h" #if defined(HAVE_NBGL) @@ -13,7 +13,9 @@ #include #include -#include "ux_common/common_bip39.h" +#include "../ux_common/common_bip39.h" +#include "ui.h" +#include "ux_fatstacks.h" #define HEADER_SIZE 50 @@ -38,6 +40,17 @@ enum { START_RECOVER_TOKEN, }; +static char *buttonTexts[NB_MAX_SUGGESTION_BUTTONS] = {0}; + +/* + * Utils + */ +static void reset_globals() { + reset_mnemonic(); + memset(buttonTexts, 0, NB_MAX_SUGGESTION_BUTTONS); +} + + static void releaseContext(void) { if (pageContext != NULL) { nbgl_pageRelease(pageContext); @@ -50,17 +63,9 @@ static void onHome(void) { ui_idle_init(); } -static void pageTouchCallback(int token, uint8_t index __attribute__((unused))) { - if (token == QUIT_APP_TOKEN) { - releaseContext(); - os_sched_exit(-1); - } else if (token == INFO_TOKEN) { - nbgl_useCaseSettings("Recovery Check infos", 0, 1, false, onHome, onInfos, NULL); - } else if (token == CHOOSE_MNEMONIC_SIZE_TOKEN) { - display_mnemonic_page(); - } else if (token == BACK_HOME_TOKEN) { - onHome(); - } +static void onQuit(void) { + releaseContext(); + os_sched_exit(-1); } /* @@ -140,7 +145,6 @@ static void display_mnemonic_page() { */ static char textToEnter[MAX_WORD_LENGTH + 1] = {0}; static int textIndex, suggestionIndex, keyboardIndex = 0; -static char *buttonTexts[NB_MAX_SUGGESTION_BUTTONS] = {0}; // the biggest word of BIP39 list is 8 char (9 with trailing '\0'), and // the max number of showed suggestions is NB_MAX_SUGGESTION_BUTTONS static char wordCandidates[(MAX_WORD_LENGTH + 1) * NB_MAX_SUGGESTION_BUTTONS] = {0}; @@ -247,9 +251,22 @@ static void display_keyboard_page() { } /* - * Home page + * Home page & dispatcher */ +static void pageTouchCallback(int token, uint8_t index __attribute__((unused))) { + if (token == QUIT_APP_TOKEN) { + onQuit(); + } else if (token == INFO_TOKEN) { + nbgl_useCaseSettings("Recovery Check infos", 0, 1, false, onHome, onInfos, NULL); + } else if (token == CHOOSE_MNEMONIC_SIZE_TOKEN) { + display_mnemonic_page(); + } else if (token == BACK_HOME_TOKEN) { + onHome(); + } +} + static void display_home_page() { + reset_globals(); nbgl_pageInfoDescription_t home = { .centeredInfo.icon = &C_fatstacks_recovery_check_64px, .centeredInfo.text1 = "Recovery Check", @@ -277,6 +294,7 @@ static char *possible_results[2] = {"Sorry, this recovery\npassphrase is\nincorr "Your recovery\npassphrase is\ncorrect!"}; static void display_result_page(const bool result) { + reset_globals(); nbgl_pageInfoDescription_t page = {.centeredInfo.icon = &C_fatstacks_recovery_check_64px, .centeredInfo.text1 = possible_results[result], .centeredInfo.text2 = NULL, @@ -296,168 +314,10 @@ static void display_result_page(const bool result) { } /* - * Utils + * Public function */ -static void reset_globals() { - reset_mnemonic(); - memset(buttonTexts, 0, NB_MAX_SUGGESTION_BUTTONS); -} - -#endif - -enum UI_STATE { UI_IDLE, UI_TEXT, UI_APPROVAL }; - -enum UI_STATE uiState; - -ux_state_t G_ux; -bolos_ux_params_t G_ux_params; - -#if defined(TARGET_NANOS) - -UX_STEP_CB(restore_3_1_1, bb, G_bolos_ux_context.onboarding_kind = MNEMONIC_SIZE_24; - screen_onboarding_4_restore_word_init(RESTORE_WORD_ACTION_FIRST_WORD); - , - { - "Recovery phrase", - "with 24 words", - }); - -UX_STEP_CB(restore_3_1_2, bb, G_bolos_ux_context.onboarding_kind = MNEMONIC_SIZE_18; - screen_onboarding_4_restore_word_init(RESTORE_WORD_ACTION_FIRST_WORD); - , - { - "Recovery phrase", - "with 18 words", - }); - -UX_STEP_CB(restore_3_1_3, bb, G_bolos_ux_context.onboarding_kind = MNEMONIC_SIZE_12; - screen_onboarding_4_restore_word_init(RESTORE_WORD_ACTION_FIRST_WORD); - , - { - "Recovery phrase", - "with 12 words", - }); - -UX_FLOW(restore_3_1, &restore_3_1_1, &restore_3_1_2, &restore_3_1_3); - -void screen_onboarding_3_restore_init(void) { - ux_flow_init(0, restore_3_1, NULL); -} - -UX_STEP_VALID(ux_idle_flow_1_step, pbb, screen_onboarding_3_restore_init();, - { - &C_badge, - "Check your", - "recovery phrase", - }); - -UX_STEP_NOCB(ux_idle_flow_3_step, - bn, - { - "Version", - APPVERSION, - }); - -UX_STEP_VALID(ux_idle_flow_4_step, - pb, - os_sched_exit(-1), - { - &C_icon_dashboard_x, - "Quit", - }); - -UX_FLOW(ux_idle_flow, &ux_idle_flow_1_step, &ux_idle_flow_3_step, &ux_idle_flow_4_step); - -#elif defined(TARGET_NANOX) || defined(TARGET_NANOS2) - -////////////////////////////////////////////////////////////////////// - -const char* const number_of_words_getter_values[] = { - "12 words", - "18 words", - "24 words", - "Back", -}; - -const char* number_of_words_getter(unsigned int idx) { - if (idx < ARRAYLEN(number_of_words_getter_values)) { - return number_of_words_getter_values[idx]; - } - return NULL; -} - -void number_of_words_selector(unsigned int idx) { - switch (idx) { - case 0: - G_bolos_ux_context.onboarding_kind = MNEMONIC_SIZE_12; - screen_onboarding_4_restore_word_init(1 /*entering the first word*/); - break; - case 1: - G_bolos_ux_context.onboarding_kind = MNEMONIC_SIZE_18; - screen_onboarding_4_restore_word_init(1 /*entering the first word*/); - break; - case 2: - G_bolos_ux_context.onboarding_kind = MNEMONIC_SIZE_24; - screen_onboarding_4_restore_word_init(1 /*entering the first word*/); - break; - default: - ui_idle_init(); - } -} - -////////////////////////////////////////////////////////////////////// - -UX_STEP_VALID(ux_instruction_step, - nnn, - ux_menulist_init(0, number_of_words_getter, number_of_words_selector), - { - "Select the number", - "of words written on", - "your Recovery Sheet", - }); - -UX_FLOW(ux_instruction_flow, &ux_instruction_step); - -////////////////////////////////////////////////////////////////////// - -UX_STEP_VALID(ux_idle_flow_1_step, - pbb, - ux_flow_init(0, ux_instruction_flow, NULL), - { - &C_badge, - "Check your", - "recovery phrase", - }); -UX_STEP_NOCB(ux_idle_flow_3_step, - bn, - { - "Version", - APPVERSION, - }); -UX_STEP_VALID(ux_idle_flow_4_step, - pb, - os_sched_exit(-1), - { - &C_icon_dashboard_x, - "Quit", - }); -UX_FLOW(ux_idle_flow, &ux_idle_flow_1_step, &ux_idle_flow_3_step, &ux_idle_flow_4_step); - -#endif - void ui_idle_init(void) { -#if defined(HAVE_BAGL) - uiState = UI_IDLE; - - // reserve a display stack slot if none yet - if (G_ux.stack_count == 0) { - ux_stack_push(); - } - ux_flow_init(0, ux_idle_flow, NULL); -#endif - -#if defined(HAVE_NBGL) - reset_globals(); display_home_page(); -#endif } + +#endif diff --git a/src/fatstacks/ui.h b/src/fatstacks/ui.h new file mode 100644 index 00000000..09b3fc71 --- /dev/null +++ b/src/fatstacks/ui.h @@ -0,0 +1,7 @@ +#pragma once + +#if defined(HAVE_NBGL) + +void ui_idle_init(void); + +#endif diff --git a/src/ux_stax.c b/src/fatstacks/ux_fatstacks.c similarity index 98% rename from src/ux_stax.c rename to src/fatstacks/ux_fatstacks.c index 3653c846..a58f5d7b 100644 --- a/src/ux_stax.c +++ b/src/fatstacks/ux_fatstacks.c @@ -1,8 +1,8 @@ #include #include -#include "ux_stax.h" -#include "ux_common/common_bip39.h" +#include "ux_fatstacks.h" +#include "../ux_common/common_bip39.h" #if defined(HAVE_BOLOS_UX) && defined(TARGET_FATSTACKS) diff --git a/src/ux_stax.h b/src/fatstacks/ux_fatstacks.h similarity index 100% rename from src/ux_stax.h rename to src/fatstacks/ux_fatstacks.h diff --git a/src/main.c b/src/main.c index f68239ad..dec8520c 100644 --- a/src/main.c +++ b/src/main.c @@ -16,14 +16,16 @@ #include #include +#include #include #if defined(HAVE_NBGL) -#include +#include "fatstacks/ui.h" +#else +#include "nano/ui.h" #endif -#include "ui.h" - +bolos_ux_params_t G_ux_params; unsigned char G_io_seproxyhal_spi_buffer[IO_SEPROXYHAL_BUFFER_SIZE_B]; static unsigned int current_text_pos; // parsing cursor in the text to display @@ -142,8 +144,9 @@ __attribute__((section(".boot"))) int main(void) { current_text_pos = 0; text_y = 60; +#if defined(HAVE_BAGL) uiState = UI_IDLE; - +#endif // ensure exception will work as planned os_boot(); diff --git a/src/nanos_enter_phrase.c b/src/nano/nanos_enter_phrase.c similarity index 100% rename from src/nanos_enter_phrase.c rename to src/nano/nanos_enter_phrase.c index 8b5b7060..1de55e7e 100644 --- a/src/nanos_enter_phrase.c +++ b/src/nano/nanos_enter_phrase.c @@ -15,8 +15,8 @@ ********************************************************************************/ #include "constants.h" -#include "ui.h" #include "glyphs.h" +#include "ui.h" #ifdef TARGET_NANOS diff --git a/src/nanox_enter_phrase.c b/src/nano/nanox_enter_phrase.c similarity index 100% rename from src/nanox_enter_phrase.c rename to src/nano/nanox_enter_phrase.c diff --git a/src/nano/ui.c b/src/nano/ui.c new file mode 100644 index 00000000..d272ab5c --- /dev/null +++ b/src/nano/ui.c @@ -0,0 +1,158 @@ +#include + +#if defined(HAVE_BAGL) + +#include "constants.h" +#include "glyphs.h" +#include "ui.h" + +enum UI_STATE { UI_IDLE, UI_TEXT, UI_APPROVAL }; + +enum UI_STATE uiState; + +ux_state_t G_ux; + +#if defined(TARGET_NANOS) + +UX_STEP_CB(restore_3_1_1, bb, G_bolos_ux_context.onboarding_kind = MNEMONIC_SIZE_24; + screen_onboarding_4_restore_word_init(RESTORE_WORD_ACTION_FIRST_WORD); + , + { + "Recovery phrase", + "with 24 words", + }); + +UX_STEP_CB(restore_3_1_2, bb, G_bolos_ux_context.onboarding_kind = MNEMONIC_SIZE_18; + screen_onboarding_4_restore_word_init(RESTORE_WORD_ACTION_FIRST_WORD); + , + { + "Recovery phrase", + "with 18 words", + }); + +UX_STEP_CB(restore_3_1_3, bb, G_bolos_ux_context.onboarding_kind = MNEMONIC_SIZE_12; + screen_onboarding_4_restore_word_init(RESTORE_WORD_ACTION_FIRST_WORD); + , + { + "Recovery phrase", + "with 12 words", + }); + +UX_FLOW(restore_3_1, &restore_3_1_1, &restore_3_1_2, &restore_3_1_3); + +void screen_onboarding_3_restore_init(void) { + ux_flow_init(0, restore_3_1, NULL); +} + +UX_STEP_VALID(ux_idle_flow_1_step, pbb, screen_onboarding_3_restore_init();, + { + &C_badge, + "Check your", + "recovery phrase", + }); + +UX_STEP_NOCB(ux_idle_flow_3_step, + bn, + { + "Version", + APPVERSION, + }); + +UX_STEP_VALID(ux_idle_flow_4_step, + pb, + os_sched_exit(-1), + { + &C_icon_dashboard_x, + "Quit", + }); + +UX_FLOW(ux_idle_flow, &ux_idle_flow_1_step, &ux_idle_flow_3_step, &ux_idle_flow_4_step); + +#elif defined(TARGET_NANOX) || defined(TARGET_NANOS2) + +////////////////////////////////////////////////////////////////////// + +const char* const number_of_words_getter_values[] = { + "12 words", + "18 words", + "24 words", + "Back", +}; + +const char* number_of_words_getter(unsigned int idx) { + if (idx < ARRAYLEN(number_of_words_getter_values)) { + return number_of_words_getter_values[idx]; + } + return NULL; +} + +void number_of_words_selector(unsigned int idx) { + switch (idx) { + case 0: + G_bolos_ux_context.onboarding_kind = MNEMONIC_SIZE_12; + screen_onboarding_4_restore_word_init(1 /*entering the first word*/); + break; + case 1: + G_bolos_ux_context.onboarding_kind = MNEMONIC_SIZE_18; + screen_onboarding_4_restore_word_init(1 /*entering the first word*/); + break; + case 2: + G_bolos_ux_context.onboarding_kind = MNEMONIC_SIZE_24; + screen_onboarding_4_restore_word_init(1 /*entering the first word*/); + break; + default: + ui_idle_init(); + } +} + +////////////////////////////////////////////////////////////////////// + +UX_STEP_VALID(ux_instruction_step, + nnn, + ux_menulist_init(0, number_of_words_getter, number_of_words_selector), + { + "Select the number", + "of words written on", + "your Recovery Sheet", + }); + +UX_FLOW(ux_instruction_flow, &ux_instruction_step); + +////////////////////////////////////////////////////////////////////// + +UX_STEP_VALID(ux_idle_flow_1_step, + pbb, + ux_flow_init(0, ux_instruction_flow, NULL), + { + &C_badge, + "Check your", + "recovery phrase", + }); +UX_STEP_NOCB(ux_idle_flow_3_step, + bn, + { + "Version", + APPVERSION, + }); +UX_STEP_VALID(ux_idle_flow_4_step, + pb, + os_sched_exit(-1), + { + &C_icon_dashboard_x, + "Quit", + }); +UX_FLOW(ux_idle_flow, &ux_idle_flow_1_step, &ux_idle_flow_3_step, &ux_idle_flow_4_step); + +#endif + +void ui_idle_init(void) { + uiState = UI_IDLE; + + // reserve a display stack slot if none yet + if (G_ux.stack_count == 0) { + ux_stack_push(); + } + ux_flow_init(0, ux_idle_flow, NULL); +} + +#endif diff --git a/src/ui.h b/src/nano/ui.h similarity index 85% rename from src/ui.h rename to src/nano/ui.h index 06ea744a..3c52de44 100644 --- a/src/ui.h +++ b/src/nano/ui.h @@ -16,19 +16,14 @@ #pragma once -#include -#include -#include -#include - -#include "glyphs.h" +#if defined(HAVE_BAGL) #if defined(TARGET_NANOS) #include "ux_nanos.h" #elif defined(TARGET_NANOX) || defined(TARGET_NANOS2) #include "ux_nanox.h" -#elif defined(TARGET_FATSTACKS) -#include "ux_stax.h" #endif void ui_idle_init(void); + +#endif diff --git a/src/ux_nanos.c b/src/nano/ux_nanos.c similarity index 100% rename from src/ux_nanos.c rename to src/nano/ux_nanos.c diff --git a/src/ux_nanos.h b/src/nano/ux_nanos.h similarity index 98% rename from src/ux_nanos.h rename to src/nano/ux_nanos.h index 75107160..87eb3a71 100644 --- a/src/ux_nanos.h +++ b/src/nano/ux_nanos.h @@ -16,7 +16,7 @@ #pragma once -#include "ux_common/common.h" +#include "../ux_common/common.h" #if defined(HAVE_BOLOS_UX) && defined(TARGET_NANOS) diff --git a/src/ux_nanos_keyboard.c b/src/nano/ux_nanos_keyboard.c similarity index 100% rename from src/ux_nanos_keyboard.c rename to src/nano/ux_nanos_keyboard.c diff --git a/src/ux_nanox.c b/src/nano/ux_nanox.c similarity index 100% rename from src/ux_nanox.c rename to src/nano/ux_nanox.c diff --git a/src/ux_nanox.h b/src/nano/ux_nanox.h similarity index 100% rename from src/ux_nanox.h rename to src/nano/ux_nanox.h diff --git a/src/ux_nanox_keyboard.c b/src/nano/ux_nanox_keyboard.c similarity index 100% rename from src/ux_nanox_keyboard.c rename to src/nano/ux_nanox_keyboard.c From c883bab4ac092d38a0b2b22b3c0d35a60ee6294f Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Mon, 19 Dec 2022 11:38:43 +0100 Subject: [PATCH 33/65] [clean] Using SDK use case for the home & result pages --- src/fatstacks/ui.c | 68 +++++++++++------------------------- src/fatstacks/ux_fatstacks.c | 8 ++--- 2 files changed, 24 insertions(+), 52 deletions(-) diff --git a/src/fatstacks/ui.c b/src/fatstacks/ui.c index 5deb8b36..fc3dc9b1 100644 --- a/src/fatstacks/ui.c +++ b/src/fatstacks/ui.c @@ -31,12 +31,9 @@ static void reset_globals(void); static bool onInfos(uint8_t page, nbgl_pageContent_t *content); enum { - BACK_HOME_TOKEN = 0, - BACK_BUTTON_TOKEN, + BACK_BUTTON_TOKEN = 0, CHOOSE_MNEMONIC_SIZE_TOKEN, FIRST_SUGGESTION_TOKEN, - INFO_TOKEN, - QUIT_APP_TOKEN, START_RECOVER_TOKEN, }; @@ -253,38 +250,21 @@ static void display_keyboard_page() { /* * Home page & dispatcher */ -static void pageTouchCallback(int token, uint8_t index __attribute__((unused))) { - if (token == QUIT_APP_TOKEN) { - onQuit(); - } else if (token == INFO_TOKEN) { - nbgl_useCaseSettings("Recovery Check infos", 0, 1, false, onHome, onInfos, NULL); - } else if (token == CHOOSE_MNEMONIC_SIZE_TOKEN) { - display_mnemonic_page(); - } else if (token == BACK_HOME_TOKEN) { - onHome(); - } + +static void display_settings_page() { + nbgl_useCaseSettings("Recovery Check infos", 0, 1, false, onHome, onInfos, NULL); } static void display_home_page() { reset_globals(); - nbgl_pageInfoDescription_t home = { - .centeredInfo.icon = &C_fatstacks_recovery_check_64px, - .centeredInfo.text1 = "Recovery Check", - .centeredInfo.text2 = NULL, - .centeredInfo.text3 = NULL, - .centeredInfo.style = LARGE_CASE_INFO, - .centeredInfo.offsetY = 32, - .topRightStyle = QUIT_ICON, - .bottomButtonStyle = INFO_ICON, - .topRightToken = QUIT_APP_TOKEN, - .bottomButtonsToken = INFO_TOKEN, - .footerText = NULL, - .tapActionText = "Tap to check if your\nrecovery passphrase is valid", - .tapActionToken = CHOOSE_MNEMONIC_SIZE_TOKEN, - .tuneId = TUNE_TAP_CASUAL}; - releaseContext(); - pageContext = nbgl_pageDrawInfo(&pageTouchCallback, NULL, &home); - nbgl_refresh(); + nbgl_useCaseHomeExt("Recovery Check", + &C_fatstacks_recovery_check_64px, + "Verify the validity of\nyour recovery passphrase", + true, + "Check your passphrase", + display_mnemonic_page, + display_settings_page, + onQuit); } /* @@ -295,22 +275,14 @@ static char *possible_results[2] = {"Sorry, this recovery\npassphrase is\nincorr static void display_result_page(const bool result) { reset_globals(); - nbgl_pageInfoDescription_t page = {.centeredInfo.icon = &C_fatstacks_recovery_check_64px, - .centeredInfo.text1 = possible_results[result], - .centeredInfo.text2 = NULL, - .centeredInfo.text3 = NULL, - .centeredInfo.style = LARGE_CASE_INFO, - .centeredInfo.offsetY = 32, - .topRightStyle = NO_BUTTON_STYLE, - .bottomButtonStyle = QUIT_ICON, - .bottomButtonsToken = QUIT_APP_TOKEN, - .footerText = NULL, - .tapActionText = "Tap to check another mnemonic", - .tapActionToken = CHOOSE_MNEMONIC_SIZE_TOKEN, - .tuneId = TUNE_TAP_CASUAL}; - releaseContext(); - pageContext = nbgl_pageDrawInfo(&pageTouchCallback, NULL, &page); - nbgl_refresh(); + nbgl_useCaseHomeExt(possible_results[result], + &C_fatstacks_recovery_check_64px, + "", + false, + "Check another passphrase", + display_mnemonic_page, + NULL, + onQuit); } /* diff --git a/src/fatstacks/ux_fatstacks.c b/src/fatstacks/ux_fatstacks.c index a58f5d7b..41779905 100644 --- a/src/fatstacks/ux_fatstacks.c +++ b/src/fatstacks/ux_fatstacks.c @@ -50,7 +50,7 @@ void reset_mnemonic() { } bool remove_word_from_mnemonic() { - PRINTF("Removing a word, currently there is '%ld' of them\n", mnemonic.current_word_index + 1); + PRINTF("Removing a word, currently there is '%d' of them\n", mnemonic.current_word_index + 1); if (mnemonic.current_word_index == (size_t) -1) { return false; } @@ -58,7 +58,7 @@ bool remove_word_from_mnemonic() { mnemonic.current_word_index--; // removing previous word from mnemonic buffer (+ 1 blank space) mnemonic_shrink(current_length + 1); - PRINTF("Number of remaining words in the mnemonic: '%ld'\n", mnemonic.current_word_index + 1); + PRINTF("Number of remaining words in the mnemonic: '%d'\n", mnemonic.current_word_index + 1); return true; } @@ -72,7 +72,7 @@ size_t add_word_in_mnemonic(const char* const buffer, const size_t size) { mnemonic.length += size; mnemonic.current_word_index++; mnemonic.word_lengths[mnemonic.current_word_index] = size; - PRINTF("Number of words in the mnemonic: '%ld'\n", get_current_word_number()); + PRINTF("Number of words in the mnemonic: '%d'\n", get_current_word_number()); PRINTF("Current mnemonic: '%s'\n", &mnemonic.buffer[0]); return get_current_word_number(); } @@ -87,7 +87,7 @@ bool check_mnemonic() { if (!is_mnemonic_complete()) { return false; } - PRINTF("Checking the following mnemonic: '%s' (size %ld)\n", + PRINTF("Checking the following mnemonic: '%s' (size %d)\n", &mnemonic.buffer[0], mnemonic.length); const bool result = From dfff7ffc6afc5e6b63c6789f605e435eb7d50420 Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Mon, 19 Dec 2022 17:27:44 +0100 Subject: [PATCH 34/65] [clean] Using SDK use case for choice selection page --- src/fatstacks/ui.c | 83 ++++++++++++++++++---------------------------- 1 file changed, 33 insertions(+), 50 deletions(-) diff --git a/src/fatstacks/ui.c b/src/fatstacks/ui.c index fc3dc9b1..5df62445 100644 --- a/src/fatstacks/ui.c +++ b/src/fatstacks/ui.c @@ -19,7 +19,6 @@ #define HEADER_SIZE 50 -static nbgl_page_t *pageContext; static char headerText[HEADER_SIZE] = {0}; static nbgl_layout_t *layout = 0; @@ -27,41 +26,25 @@ static void display_keyboard_page(void); static void display_home_page(void); static void display_result_page(const bool result); static void display_mnemonic_page(void); -static void reset_globals(void); -static bool onInfos(uint8_t page, nbgl_pageContent_t *content); enum { - BACK_BUTTON_TOKEN = 0, + BACK_BUTTON_TOKEN = FIRST_USER_TOKEN, CHOOSE_MNEMONIC_SIZE_TOKEN, FIRST_SUGGESTION_TOKEN, START_RECOVER_TOKEN, }; -static char *buttonTexts[NB_MAX_SUGGESTION_BUTTONS] = {0}; - /* * Utils */ +static char *buttonTexts[NB_MAX_SUGGESTION_BUTTONS] = {0}; + static void reset_globals() { reset_mnemonic(); - memset(buttonTexts, 0, NB_MAX_SUGGESTION_BUTTONS); + memset(buttonTexts, 0, sizeof(buttonTexts[0]) * NB_MAX_SUGGESTION_BUTTONS); } - -static void releaseContext(void) { - if (pageContext != NULL) { - nbgl_pageRelease(pageContext); - pageContext = NULL; - } -} - -static void onHome(void) { - releaseContext(); - ui_idle_init(); -} - -static void onQuit(void) { - releaseContext(); +static void on_quit(void) { os_sched_exit(-1); } @@ -71,16 +54,15 @@ static void onQuit(void) { static const char *const infoTypes[] = {"Version", "Recovery Check"}; static const char *const infoContents[] = {APPVERSION, "(c) 2022 Ledger"}; -static bool onInfos(uint8_t page, nbgl_pageContent_t *content) { +static bool on_infos(uint8_t page, nbgl_pageContent_t *content) { if (page == 0) { content->type = INFOS_LIST; content->infosList.nbInfos = 2; content->infosList.infoTypes = (const char **) infoTypes; content->infosList.infoContents = (const char **) infoContents; - } else { - return false; + return true; } - return true; + return false; } /* @@ -112,29 +94,30 @@ static void mnemonic_dispatcher(const int token, uint8_t index) { } } +static const char *const passphraseLength[] = {"12 words", "18 words", "24 words"}; + +static bool on_passphrase_length_selector(uint8_t page, nbgl_pageContent_t *content) { + if (page == 0) { + content->type = CHOICES_LIST; + content->choicesList.names = (char **) passphraseLength; + content->choicesList.localized = false; + content->choicesList.nbChoices = 3; + content->choicesList.initChoice = 2; + content->choicesList.token = CHOOSE_MNEMONIC_SIZE_TOKEN; + return true; + } + return false; +} + static void display_mnemonic_page() { reset_globals(); - nbgl_layoutDescription_t layoutDescription = {.modal = false, - .onActionCallback = mnemonic_dispatcher}; - nbgl_layoutRadioChoice_t choices = {.names = (char *[]){"12 words", "18 words", "24 words"}, - .localized = false, - .nbChoices = 3, - .initChoice = 2, - .token = CHOOSE_MNEMONIC_SIZE_TOKEN}; - nbgl_layoutCenteredInfo_t centeredInfo = {.text1 = NULL, - .text2 = headerText, // to use as "header" - .text3 = NULL, - .style = LARGE_CASE_INFO, - .icon = NULL, - .offsetY = 0, - .onTop = true}; - layout = nbgl_layoutGet(&layoutDescription); - nbgl_layoutAddProgressIndicator(layout, 0, 0, true, BACK_BUTTON_TOKEN, TUNE_TAP_CASUAL); - memset(headerText, 0, HEADER_SIZE); - snprintf(headerText, HEADER_SIZE, "What is the length of your\nrecovery passphrase?"); - nbgl_layoutAddCenteredInfo(layout, ¢eredInfo); - nbgl_layoutAddRadioChoice(layout, &choices); - nbgl_layoutDraw(layout); + nbgl_useCaseSettings("Select the length of your\nrecovery passphrase", + 0, + 1, + true, + display_home_page, + on_passphrase_length_selector, + mnemonic_dispatcher); } /* @@ -252,7 +235,7 @@ static void display_keyboard_page() { */ static void display_settings_page() { - nbgl_useCaseSettings("Recovery Check infos", 0, 1, false, onHome, onInfos, NULL); + nbgl_useCaseSettings("Recovery Check infos", 0, 1, false, display_home_page, on_infos, NULL); } static void display_home_page() { @@ -264,7 +247,7 @@ static void display_home_page() { "Check your passphrase", display_mnemonic_page, display_settings_page, - onQuit); + on_quit); } /* @@ -282,7 +265,7 @@ static void display_result_page(const bool result) { "Check another passphrase", display_mnemonic_page, NULL, - onQuit); + on_quit); } /* From 4f1817b95ede2c18c25c5bf39bb6e00a542bf5bd Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Mon, 19 Dec 2022 19:24:17 +0100 Subject: [PATCH 35/65] [test] Adapting the tests to new SDK and code --- .gitignore | 1 + tests/functional/app.py | 22 ++++++++++++++------- tests/functional/test_fatstacks_full.py | 4 ++-- tests/functional/test_fatstacks_options.py | 6 +++--- tests/screenshots/correct.png | Bin 5088 -> 6310 bytes tests/screenshots/first_12.png | Bin 4985 -> 4985 bytes tests/screenshots/first_18.png | Bin 4995 -> 4995 bytes tests/screenshots/first_24.png | Bin 5008 -> 5008 bytes tests/screenshots/incorrect.png | Bin 5372 -> 6608 bytes tests/screenshots/info.png | Bin 4913 -> 5132 bytes tests/screenshots/passphrase_length.png | Bin 5201 -> 5432 bytes tests/screenshots/welcome.png | Bin 5348 -> 6994 bytes 12 files changed, 21 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index 83c86665..328434cf 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ __pycache__/ *.egg-info/ .eggs/ .python-version +*-tmp/ # Doxygen doc/html diff --git a/tests/functional/app.py b/tests/functional/app.py index 2a219cd3..13b0d152 100644 --- a/tests/functional/app.py +++ b/tests/functional/app.py @@ -1,22 +1,30 @@ -from ragger.firmware.fatstacks.layouts import ChoiceList, ExitHeader, ExitFooter, InfoFooter, \ - LetterOnlyKeyboard, NavigationHeader, Suggestions, TappableCenter +from ragger.firmware.fatstacks.layouts import ExitFooter, _Layout, LetterOnlyKeyboard, \ + NavigationHeader, Suggestions +from ragger.firmware.fatstacks.use_cases import UseCaseHomeExt from ragger.firmware.fatstacks.screen import MetaScreen +class CustomChoiceList(_Layout): + + def choose(self, index: int): + assert 1 <= index <= 6, "Choice index must be in [1, 6]" + x, y = (200, 130) + diff = 80 + self.client.finger_touch(x, y + (index - 1)*diff) + + class Screen(metaclass=MetaScreen): - layout_center = TappableCenter - layout_choice_list = ChoiceList + layout_choice_list = CustomChoiceList layout_keyboard = LetterOnlyKeyboard layout_suggestions = Suggestions - layout_exit_button = ExitHeader layout_navigation = NavigationHeader - layout_info = InfoFooter layout_quit_info = ExitFooter + use_case_home = UseCaseHomeExt def exit(self): did_raise = False try: - self.exit_button.tap() + self.home.quit() except: did_raise = True if not did_raise: diff --git a/tests/functional/test_fatstacks_full.py b/tests/functional/test_fatstacks_full.py index db60c084..c567853f 100644 --- a/tests/functional/test_fatstacks_full.py +++ b/tests/functional/test_fatstacks_full.py @@ -21,7 +21,7 @@ def screen(client: BackendInterface): def test_nominal_full_passphrase_check(screen: Screen, client: BackendInterface): # going to choose mnemonic length - screen.center.tap() + screen.home.action() assert_current_equals(client, SCREENSHOTS / "passphrase_length.png") # choosing 3d (24 words) screen.choice_list.choose(3) @@ -37,7 +37,7 @@ def test_nominal_full_passphrase_check(screen: Screen, client: BackendInterface) def test_nominal_full_passphrase_check_error_wrong_passphrase(screen: Screen, client: BackendInterface): - screen.center.tap() + screen.home.action() assert_current_equals(client, SCREENSHOTS / "passphrase_length.png") # choosing 1st (12 words) screen.choice_list.choose(1) diff --git a/tests/functional/test_fatstacks_options.py b/tests/functional/test_fatstacks_options.py index a4b3e35a..f2d26e11 100644 --- a/tests/functional/test_fatstacks_options.py +++ b/tests/functional/test_fatstacks_options.py @@ -18,7 +18,7 @@ def screen(client: BackendInterface): def test_check_info_then_leave(screen: Screen, client: BackendInterface): - screen.info.tap() + screen.home.info() assert_current_equals(client, SCREENSHOTS / "info.png") screen.quit_info.tap() assert_current_equals(client, SCREENSHOTS / "welcome.png") @@ -26,7 +26,7 @@ def test_check_info_then_leave(screen: Screen, client: BackendInterface): def test_check_all_passphrase_lengths(screen: Screen, client: BackendInterface): - screen.center.tap() + screen.home.action() assert_current_equals(client, SCREENSHOTS / "passphrase_length.png") for choice, length in [(1, 12), (2, 18), (3, 24)]: screen.choice_list.choose(choice) @@ -36,7 +36,7 @@ def test_check_all_passphrase_lengths(screen: Screen, client: BackendInterface): def test_check_previous_word(screen: Screen, client: BackendInterface): - screen.center.tap() + screen.home.action() screen.choice_list.choose(1) assert_current_equals(client, SCREENSHOTS / "first_12.png") tries = ["rand", "ok"] diff --git a/tests/screenshots/correct.png b/tests/screenshots/correct.png index 5721cddd90e51a1f54a3a83150b4ca60e3ff6a7c..68c9a5cb7d76ecb6e1fd82a526b3dba4b023b93f 100644 GIT binary patch literal 6310 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 Hu|@t5w;RL< literal 5088 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 F_jf(?`UC&~ diff --git a/tests/screenshots/first_12.png b/tests/screenshots/first_12.png index 618a3f60bbc160fd137f1bff52c97566fd282da8..487ff9f249551e3a52d5559e5cbb362f6cff7111 100644 GIT binary patch delta 262 zcmV+h0r~# za56Q5rB*-@hX+fofKrYPmRbP?9T+UN0?IlnSZW0ncSx|*3OGXfV5t>wkix-IE8sXo z!K@s5ExY^y#b#Lk%NyWm#e!u91RSm8DdEi;7I37(i|*q{IY|V{Dp-G6gJuB-Ipo4o z%>s^a)PI_zSwL|I{>RUn1(bE{ub0s*prFHlfuCjprPTNp$(jWeQR$CRXcq9hdVl82 zh3~+Rs{SF>XS0+G*r)cNs#hyux7xu1*iDlG2*a}=0?G%oAPPkZ6xk&~@yt7ovH$=8 M07*qoM6N<$f;KI8^Z)<= delta 262 zcmV+h0r~#E6oD-sTVAr0)A8}SmRnf9*;VM0w+@= zSZW0nad@!Q3Ml2+V5t>Q(1F2HE1;~Sf~8hKafbv;t$-tx50+X12PqsZwE~Va6wJz@ z*RsnWP;7?fzq|pCRxDU%K)}&To)X@yVF5=fyy!lTl#@iDtXT4wHE0%akV7sU)hyr$ zNByTcngtYh;D7wASwLCG{(2eB0t!0(7x-xwP)dzok*rxj5taT3g=PW2tM_NVT=)+B zsOle5eKt$EfPHHJsd}{ncB>sMfZdY;37fN40?G%Ia0oWDAPPkZ6c^$_@&C7*4*&oF M07*qoM6N<$g47XlE&u=k diff --git a/tests/screenshots/first_18.png b/tests/screenshots/first_18.png index 03d9813e73a29a057d195ce70757dcd32d6c2644..a657042c79ffed9fd06a990cbe34e0ba14a274bd 100644 GIT binary patch delta 262 zcmV+h0r~!eCxa)Du9J`m4u5I~YZ!*gBLb&UHCWGDX%?_gy8rSOac+?pb zIGGy3QY)Z{!-J((KqyxP zU{(&jmR8^9CG2P zW&uYy>VM7AETFgp|L14T0?In}*UM-YP|)GOz)!P)QfmB)WX%GKsPsoDGz<7$y+8Bi z!gt_DRsWFcvsuao>{I(s)vFb-TkT*0>?V^@2*a}=0@DYxDGEjj6e8?V5Be93uK)l5 M07*qoM6N<$g1g{$4*&oF delta 261 zcmV+g0s8)fCxa)Du79I;u!do%JR)!!RfF}cm1Y6^)C-nQ0Y9n~tZ}U#k4K$Bfs?5b zEVTlPI6PQt1(b4Zu+$1D=)hp96;Reu!BQ)rxI==aR=^R;2TQGhgA@*yS^>uy3TEZd zYuV)wC^p0Lf8GE`D;6v>AmC^vPYG|s%$@PB^RETF7of4z)m0R3Ft0;f?mSkGE%7O+pfVCfX_qe{UV*Xr?j)EN{w znHs@TE1-zOgQZqLDaQs&t$>0K43=5}WgQhPwE~JeBv@(%9HD%$)CxFA;b5s1aGarF zRt~+EUH*V#Gc5n|1~^)=V3`2{M=SY}@MaAQI8xz7_i?0rNCe6%SbtfAW&sB|uy3TEZd zYuV)wC^p0LFK>XO6$_Rb5OB1T9|>>Puz({KUUVNv%7;XttXT4wHE0%akV7sU)hyr$ zNBz?r%>s%$@E<>G7Eso)KVC+&fPxPH0e+eVlv3kIBx@E>M5SLsp;^H1>iwE87rp~O zs``smpUqM(V4vE*s$Q*t-D(F5U^kP|2b;520^tXfAObqGI|@Mw6rUnr_IWV?9{>OV M07*qoM6N<$f?4)%A^-pY diff --git a/tests/screenshots/incorrect.png b/tests/screenshots/incorrect.png index 29e1f1a8cc206da7e3b6eb8978fd8a457a5b460d..3dc1492cbd593dd8a8785672688716b366fd0c92 100644 GIT binary patch literal 6608 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 fBZQwTFYFxy=ONFs5AwoLu@Pp2?UVd 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 KYc9~+5Cr_P_fwSz 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 diff --git a/tests/screenshots/welcome.png b/tests/screenshots/welcome.png index 1009264fcc527b2bf05ca6f11a0a259b688f01bd..b92d35460e4d5dca71f3059a1015096e44e94c5b 100644 GIT binary patch literal 6994 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 From d51da62170002563c79bafa0485e5f672c1aebfe Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Mon, 16 Jan 2023 15:19:10 +0100 Subject: [PATCH 36/65] [fix] Necessary load flag for Fatstacks --- Makefile | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index d7d7c253..c0bcf023 100755 --- a/Makefile +++ b/Makefile @@ -60,11 +60,12 @@ DEFINES += HAVE_SPRINTF DEFINES += HAVE_BOLOS_UX ifneq ($(TARGET_NAME), TARGET_FATSTACKS) - $(info BAGL activated) + $(info Using BAGL) DEFINES += HAVE_BAGL else - $(info BAGL deactivated) + $(info Usgin NBGL) DEFINES += NBGL_KEYBOARD + APP_LOAD_PARAMS += --appFlags 0x200 endif ifeq ($(TARGET_NAME), TARGET_NANOS) From f0daef4cca443a761eb68fccc10ba574114e21cf Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Wed, 18 Jan 2023 16:31:13 +0100 Subject: [PATCH 37/65] [clean] Adapt UI flow to design charts --- glyphs/stax_recovery_64px.gif | Bin 0 -> 218 bytes ..._64px.gif => stax_recovery_check_64px.gif} | Bin src/fatstacks/passphrase_length_screen.c | 83 +++++++++ src/fatstacks/passphrase_length_screen.h | 12 ++ src/fatstacks/ui.c | 167 +++++++++++------- tests/functional/app.py | 9 +- tests/functional/conftest.py | 107 ++++++----- tests/functional/test_fatstacks_full.py | 6 +- tests/functional/test_fatstacks_options.py | 6 +- tests/screenshots/correct.png | Bin 6310 -> 6608 bytes tests/screenshots/first_12.png | Bin 4985 -> 4893 bytes tests/screenshots/first_18.png | Bin 4995 -> 4901 bytes tests/screenshots/first_24.png | Bin 5008 -> 4911 bytes tests/screenshots/incorrect.png | Bin 6608 -> 6532 bytes tests/screenshots/passphrase_length.png | Bin 5432 -> 5842 bytes tests/screenshots/welcome.png | Bin 6994 -> 7944 bytes 16 files changed, 273 insertions(+), 117 deletions(-) create mode 100644 glyphs/stax_recovery_64px.gif rename glyphs/{fatstacks_recovery_check_64px.gif => stax_recovery_check_64px.gif} (100%) create mode 100644 src/fatstacks/passphrase_length_screen.c create mode 100644 src/fatstacks/passphrase_length_screen.h diff --git a/glyphs/stax_recovery_64px.gif b/glyphs/stax_recovery_64px.gif new file mode 100644 index 0000000000000000000000000000000000000000..7854f092e1335538e49f46b3a67843792648e039 GIT binary patch literal 218 zcmZ?wbhEHbbYO5`_`t{j1poj4SNzEWVlgQG=l0A^Oi%SqOwUZt=1Wh^%}um5&@(Xw zK?WU=5|BOyrpZ11D^I`WUp!~at?teD_Wb5=dvt@I zzRtb(dit+ +#include "../glyphs.h" + +#if defined(TARGET_FATSTACKS) + +#include + +#define UPPER_MARGIN 4 +#define BUTTON_RADIUS 6 +#define BUTTON_DIAMETER 80 + +nbgl_image_t *passphrase_length_set_icon() { + nbgl_image_t *image = (nbgl_image_t *) nbgl_objPoolGet(IMAGE, 0); + image->foregroundColor = BLACK; + image->buffer = &C_stax_recovery_64px; + image->bpp = NBGL_BPP_1; + image->alignmentMarginX = 0; + image->alignmentMarginY = 148; + image->alignment = TOP_MIDDLE; + image->alignTo = NULL; + return image; +} + +nbgl_text_area_t *passphrase_length_set_title(nbgl_obj_t *align_to) { + nbgl_text_area_t *textArea = (nbgl_text_area_t *) nbgl_objPoolGet(TEXT_AREA, 0); + textArea->textColor = BLACK; + textArea->text = "How long is your\nRecovery Phrase?"; + textArea->textAlignment = CENTER; + textArea->fontId = BAGL_FONT_INTER_REGULAR_32px; + textArea->width = SCREEN_WIDTH - 2 * BORDER_MARGIN; + textArea->height = nbgl_getTextHeight(textArea->fontId, textArea->text); + textArea->style = NO_STYLE; + textArea->alignment = BOTTOM_MIDDLE; + textArea->alignTo = align_to; + textArea->alignmentMarginX = 0; + textArea->alignmentMarginY = BORDER_MARGIN; + return textArea; +} + +void passphrase_length_configure_buttons(nbgl_button_t **buttons, + const size_t size, + nbgl_touchCallback_t callback) { + nbgl_button_t *button; + for (uint8_t i = 0; i < size; i++) { + button = buttons[i]; + button->innerColor = WHITE; + button->borderColor = LIGHT_GRAY; + button->foregroundColor = BLACK; + button->width = SCREEN_WIDTH - 2 * BORDER_MARGIN; + button->height = BUTTON_DIAMETER; + button->radius = BUTTON_RADIUS; + button->fontId = BAGL_FONT_INTER_SEMIBOLD_24px; + button->icon = NULL; + button->localized = true; + button->alignmentMarginX = 0; + button->alignmentMarginY = (button->height + 8) * i + BORDER_MARGIN; + button->alignment = BOTTOM_MIDDLE; + button->alignTo = NULL; + button->touchMask = (1 << TOUCHED); + button->touchCallback = callback; + } +} + +nbgl_button_t *passphrase_length_set_back_button(nbgl_touchCallback_t callback) { + nbgl_button_t *button = (nbgl_button_t *) nbgl_objPoolGet(BUTTON, 0); + button->innerColor = WHITE; + button->borderColor = WHITE; + button->foregroundColor = BLACK; + button->width = BUTTON_DIAMETER; + button->height = BUTTON_DIAMETER; + button->radius = BUTTON_RADIUS; + button->text = NULL; + button->icon = &C_leftArrow32px; + button->alignmentMarginX = 0; + button->alignmentMarginY = UPPER_MARGIN; + button->alignment = TOP_LEFT; + button->alignTo = NULL; + button->touchMask = (1 << TOUCHED); + button->touchCallback = callback; + return button; +} + +#endif diff --git a/src/fatstacks/passphrase_length_screen.h b/src/fatstacks/passphrase_length_screen.h new file mode 100644 index 00000000..bc8e4e1a --- /dev/null +++ b/src/fatstacks/passphrase_length_screen.h @@ -0,0 +1,12 @@ +#pragma once + +#if defined(TARGET_FATSTACKS) + +nbgl_image_t *passphrase_length_set_icon(void); +nbgl_text_area_t *passphrase_length_set_title(nbgl_obj_t *align_to); +void passphrase_length_configure_buttons(nbgl_button_t **buttons, + const size_t size, + nbgl_touchCallback_t callback); +nbgl_button_t *passphrase_length_set_back_button(nbgl_touchCallback_t callback); + +#endif diff --git a/src/fatstacks/ui.c b/src/fatstacks/ui.c index 5df62445..716e08b7 100644 --- a/src/fatstacks/ui.c +++ b/src/fatstacks/ui.c @@ -16,22 +16,25 @@ #include "../ux_common/common_bip39.h" #include "ui.h" #include "ux_fatstacks.h" +#include "passphrase_length_screen.h" #define HEADER_SIZE 50 +static nbgl_page_t *pageContext; + static char headerText[HEADER_SIZE] = {0}; static nbgl_layout_t *layout = 0; static void display_keyboard_page(void); static void display_home_page(void); static void display_result_page(const bool result); -static void display_mnemonic_page(void); enum { BACK_BUTTON_TOKEN = FIRST_USER_TOKEN, CHOOSE_MNEMONIC_SIZE_TOKEN, FIRST_SUGGESTION_TOKEN, START_RECOVER_TOKEN, + RESULT_TOKEN, }; /* @@ -68,56 +71,67 @@ static bool on_infos(uint8_t page, nbgl_pageContent_t *content) { /* * Choose mnemonic size page */ -static void mnemonic_dispatcher(const int token, uint8_t index) { - if (token == BACK_BUTTON_TOKEN) { +enum { + ICON_INDEX = 0, + TEXT_INDEX, + BUTTON_12_INDEX, + BUTTON_18_INDEX, + BUTTON_24_INDEX, + BACK_BUTTON_INDEX, + NB_CHILDREN +}; + +#define NB_BUTTONS 3 + +static char *passphraseLength[] = {"12 words", "18 words", "24 words"}; +static void passphrase_length_callback(nbgl_obj_t *obj, nbgl_touchType_t eventType) { + nbgl_obj_t **screenChildren = nbgl_screenGetElements(0); + if (eventType != TOUCHED) { + return; + } + io_seproxyhal_play_tune(TUNE_TAP_CASUAL); + if (obj == screenChildren[BUTTON_12_INDEX]) { + set_mnemonic_final_size(MNEMONIC_SIZE_12); + } else if (obj == screenChildren[BUTTON_18_INDEX]) { + set_mnemonic_final_size(MNEMONIC_SIZE_18); + } else if (obj == screenChildren[BUTTON_24_INDEX]) { + set_mnemonic_final_size(MNEMONIC_SIZE_24); + } else if (obj == screenChildren[BACK_BUTTON_INDEX]) { nbgl_layoutRelease(layout); display_home_page(); - } else if (token == CHOOSE_MNEMONIC_SIZE_TOKEN) { - switch (index) { - case 0: - set_mnemonic_final_size(MNEMONIC_SIZE_12); - break; - case 1: - set_mnemonic_final_size(MNEMONIC_SIZE_18); - break; - case 2: - set_mnemonic_final_size(MNEMONIC_SIZE_24); - break; - default: - PRINTF("Unexpected index '%d' (max 3)\n", index); - nbgl_layoutRelease(layout); - display_home_page(); - return; - } - nbgl_layoutRelease(layout); - display_keyboard_page(); + return; } + nbgl_layoutRelease(layout); + display_keyboard_page(); } -static const char *const passphraseLength[] = {"12 words", "18 words", "24 words"}; +static void passphrase_length_page(void) { + nbgl_obj_t **screenChildren; -static bool on_passphrase_length_selector(uint8_t page, nbgl_pageContent_t *content) { - if (page == 0) { - content->type = CHOICES_LIST; - content->choicesList.names = (char **) passphraseLength; - content->choicesList.localized = false; - content->choicesList.nbChoices = 3; - content->choicesList.initChoice = 2; - content->choicesList.token = CHOOSE_MNEMONIC_SIZE_TOKEN; - return true; - } - return false; -} + // 3 buttons + icon + text + subText + nbgl_screenSet(&screenChildren, 6, NULL); -static void display_mnemonic_page() { - reset_globals(); - nbgl_useCaseSettings("Select the length of your\nrecovery passphrase", - 0, - 1, - true, - display_home_page, - on_passphrase_length_selector, - mnemonic_dispatcher); + screenChildren[ICON_INDEX] = (nbgl_obj_t *) passphrase_length_set_icon(); + screenChildren[TEXT_INDEX] = + (nbgl_obj_t *) passphrase_length_set_title(screenChildren[ICON_INDEX]); + + // create nb words buttons + nbgl_objPoolGetArray(BUTTON, NB_BUTTONS, 0, (nbgl_obj_t **) &screenChildren[BUTTON_12_INDEX]); + passphrase_length_configure_buttons((nbgl_button_t **) &screenChildren[BUTTON_12_INDEX], + NB_BUTTONS, + (nbgl_touchCallback_t) &passphrase_length_callback); + ((nbgl_button_t *) screenChildren[BUTTON_12_INDEX])->text = passphraseLength[0]; + ((nbgl_button_t *) screenChildren[BUTTON_18_INDEX])->text = passphraseLength[1]; + ((nbgl_button_t *) screenChildren[BUTTON_24_INDEX])->text = passphraseLength[2]; + ((nbgl_button_t *) screenChildren[BUTTON_24_INDEX])->borderColor = BLACK; + ((nbgl_button_t *) screenChildren[BUTTON_24_INDEX])->innerColor = BLACK; + ((nbgl_button_t *) screenChildren[BUTTON_24_INDEX])->foregroundColor = WHITE; + + // create back button + screenChildren[BACK_BUTTON_INDEX] = (nbgl_obj_t *) passphrase_length_set_back_button( + (nbgl_touchCallback_t) &passphrase_length_callback); + + nbgl_screenRedraw(); } /* @@ -135,7 +149,7 @@ static void keyboard_dispatcher(const int token, uint8_t index __attribute__((un if (remove_word_from_mnemonic()) { display_keyboard_page(); } else { - display_mnemonic_page(); + passphrase_length_page(); } } else if (token >= FIRST_SUGGESTION_TOKEN) { nbgl_layoutRelease(layout); @@ -169,7 +183,7 @@ static void key_press_callback(const char touchedKey) { textLen = previousTextLen + 1; } PRINTF("Current text is: '%s' (size '%d')\n", textToEnter, textLen); - if (textLen < 2) { + if (textLen == 0) { // no suggestion until there is at least 2 characters nbgl_layoutUpdateSuggestionButtons(layout, suggestionIndex, 0, buttonTexts); } else { @@ -207,26 +221,26 @@ static void display_keyboard_page() { strlcpy(textToEnter, "", 1); memset(buttonTexts, 0, NB_MAX_SUGGESTION_BUTTONS); layout = nbgl_layoutGet(&layoutDescription); - nbgl_layoutAddProgressIndicator(layout, 0, 0, true, BACK_BUTTON_TOKEN, TUNE_TAP_CASUAL); memset(headerText, 0, HEADER_SIZE); snprintf(headerText, HEADER_SIZE, - "Enter word n. %d/%d from your\nrecovery passphrase", + "Enter word n. %d/%d from your\nRecovery Sheet", get_current_word_number() + 1, get_mnemonic_final_size()); + nbgl_layoutAddProgressIndicator(layout, 0, 0, true, BACK_BUTTON_TOKEN, TUNE_TAP_CASUAL); nbgl_layoutAddCenteredInfo(layout, ¢eredInfo); keyboardIndex = nbgl_layoutAddKeyboard(layout, &kbdInfo); + suggestionIndex = nbgl_layoutAddSuggestionButtons(layout, + 0, // no used buttons at start-up + buttonTexts, + FIRST_SUGGESTION_TOKEN, + TUNE_TAP_CASUAL); textIndex = nbgl_layoutAddEnteredText(layout, true, // numbered get_current_word_number() + 1, // number to use textToEnter, // text to display false, // not grayed-out 32); // vertical margin from the buttons - suggestionIndex = nbgl_layoutAddSuggestionButtons(layout, - 0, // no used buttons at start-up - buttonTexts, - FIRST_SUGGESTION_TOKEN, - TUNE_TAP_CASUAL); nbgl_layoutDraw(layout); } @@ -241,11 +255,12 @@ static void display_settings_page() { static void display_home_page() { reset_globals(); nbgl_useCaseHomeExt("Recovery Check", - &C_fatstacks_recovery_check_64px, - "Verify the validity of\nyour recovery passphrase", - true, - "Check your passphrase", - display_mnemonic_page, + &C_stax_recovery_check_64px, + "This app lets you enter a\nSecret Recovery Phrase and\ntest if it matches " + "the one\npresent on this Ledger Stax", + false, + "Start check", + passphrase_length_page, display_settings_page, on_quit); } @@ -253,19 +268,35 @@ static void display_home_page() { /* * Result page */ -static char *possible_results[2] = {"Sorry, this recovery\npassphrase is\nincorrect.", - "Your recovery\npassphrase is\ncorrect!"}; +static char *possible_results[2][2] = { + {"Incorrect Secret\nRecovery Phrase", + "The Recovery Phrase you have\nentered doesn't match the one\npresent on this Ledger Stax."}, + {"Correct Secret\nRecovery Phrase", + "The Recovery Phrase you have\nentered matches the one\npresent on this Ledger Stax."}}; +static const nbgl_icon_details_t *icons[2] = {&C_warning64px, &C_round_check_64px}; + +static void result_callback(int token __attribute__((unused)), + uint8_t index __attribute__((unused))) { + display_home_page(); +} static void display_result_page(const bool result) { reset_globals(); - nbgl_useCaseHomeExt(possible_results[result], - &C_fatstacks_recovery_check_64px, - "", - false, - "Check another passphrase", - display_mnemonic_page, - NULL, - on_quit); + nbgl_pageInfoDescription_t info = {.centeredInfo.icon = icons[result], + .centeredInfo.text1 = possible_results[result][0], + .centeredInfo.text2 = possible_results[result][1], + .centeredInfo.text3 = NULL, + .centeredInfo.style = LARGE_CASE_INFO, + .centeredInfo.offsetY = -16, + .footerText = "Tap to dismiss", + .footerToken = RESULT_TOKEN, + .bottomButtonStyle = NO_BUTTON_STYLE, + .tapActionText = NULL, + .topRightStyle = NO_BUTTON_STYLE, + .actionButtonText = NULL, + .tuneId = TUNE_TAP_CASUAL}; + pageContext = nbgl_pageDrawInfo(&result_callback, NULL, &info); + nbgl_refresh(); } /* diff --git a/tests/functional/app.py b/tests/functional/app.py index 13b0d152..f8b4dd1b 100644 --- a/tests/functional/app.py +++ b/tests/functional/app.py @@ -1,14 +1,14 @@ -from ragger.firmware.fatstacks.layouts import ExitFooter, _Layout, LetterOnlyKeyboard, \ +from ragger.firmware.stax.layouts import ExitFooter, CenteredFooter, _Layout, LetterOnlyKeyboard, \ NavigationHeader, Suggestions -from ragger.firmware.fatstacks.use_cases import UseCaseHomeExt -from ragger.firmware.fatstacks.screen import MetaScreen +from ragger.firmware.stax.use_cases import UseCaseHomeExt +from ragger.firmware.stax.screen import MetaScreen class CustomChoiceList(_Layout): def choose(self, index: int): assert 1 <= index <= 6, "Choice index must be in [1, 6]" - x, y = (200, 130) + x, y = (200, 430) diff = 80 self.client.finger_touch(x, y + (index - 1)*diff) @@ -19,6 +19,7 @@ class Screen(metaclass=MetaScreen): layout_suggestions = Suggestions layout_navigation = NavigationHeader layout_quit_info = ExitFooter + layout_dismiss = CenteredFooter use_case_home = UseCaseHomeExt def exit(self): diff --git a/tests/functional/conftest.py b/tests/functional/conftest.py index 33dffb93..c2d6961d 100644 --- a/tests/functional/conftest.py +++ b/tests/functional/conftest.py @@ -1,5 +1,6 @@ -from pathlib import Path import pytest +from pathlib import Path +from typing import Optional from ragger.firmware import Firmware from ragger.backend import SpeculosBackend, LedgerCommBackend, LedgerWalletBackend, BackendInterface @@ -12,15 +13,39 @@ def __str__(self): # also tried __repr__() APPLICATION = (Path(__file__).parent.parent / "elfs" / "recovery_check.elf").resolve() BACKENDS = ["speculos", "ledgercomm", "ledgerwallet"] +DEVICES = ["nanos", "nanox", "nanosp", "stax", "all"] FIRMWARES = [ - Firmware('fat', '1.0'), + Firmware('stax', '1.0'), ] + def pytest_addoption(parser): - parser.addoption("--backend", action="store", default="speculos") - # Enable using --'device' in the pytest command line to restrict testing to specific devices - for fw in FIRMWARES: - parser.addoption("--" + fw.device, action="store_true", help="run on physical device only") + parser.addoption("--device", choices=DEVICES, required=True) + parser.addoption("--backend", choices=BACKENDS, default="speculos") + parser.addoption("--display", action="store_true", default=False) + parser.addoption("--golden_run", action="store_true", default=False) + parser.addoption("--log_apdu_file", action="store", default=None) + + +@pytest.fixture(scope="session") +def backend_name(pytestconfig): + return pytestconfig.getoption("backend") + + +@pytest.fixture(scope="session") +def display(pytestconfig): + return pytestconfig.getoption("display") + + +@pytest.fixture(scope="session") +def golden_run(pytestconfig): + return pytestconfig.getoption("golden_run") + + +@pytest.fixture(scope="session") +def log_apdu_file(pytestconfig): + filename = pytestconfig.getoption("log_apdu_file") + return Path(filename).resolve() if filename is not None else None # Glue to call every test that depends on the firmware once for each required firmware @@ -28,54 +53,56 @@ def pytest_generate_tests(metafunc): if "firmware" in metafunc.fixturenames: fw_list = [] ids = [] - # First pass: enable only demanded firmwares - for fw in FIRMWARES: - if metafunc.config.getoption(fw.device): - fw_list.append(fw) - ids.append(fw.device + " " + fw.version) - # Second pass if no specific firmware demanded: add them all - if not fw_list: + device = metafunc.config.getoption("device") + backend_name = metafunc.config.getoption("backend") + if device == "all": + if backend_name != "speculos": + raise ValueError("Invalid device parameter on this backend") + # Add all supported firmwares for fw in FIRMWARES: fw_list.append(fw) ids.append(fw.device + " " + fw.version) - metafunc.parametrize("firmware", fw_list, ids=ids) - -def prepare_speculos_args(firmware): - speculos_args = [] - # Uncomment line below to enable display - # speculos_args += ["--display", "qt"] - # Compute Exchange App binary - return ([str(APPLICATION)], {"args": speculos_args}) - - -@pytest.fixture(scope="session") -def backend(pytestconfig): - return pytestconfig.getoption("backend") + else: + # Enable firmware for demanded device + for fw in FIRMWARES: + if device == fw.device: + fw_list.append(fw) + ids.append(fw.device + " " + fw.version) + metafunc.parametrize("firmware", fw_list, ids=ids, scope="session") -def create_backend(backend: str, firmware: Firmware) -> BackendInterface: - if backend.lower() == "ledgercomm": - return LedgerCommBackend(firmware, interface="hid") - elif backend.lower() == "ledgerwallet": - return LedgerWalletBackend(firmware) - elif backend.lower() == "speculos": - args, kwargs = prepare_speculos_args(firmware) - print(args, kwargs) - return SpeculosBackend(*args, firmware, **kwargs) +def prepare_speculos_args(firmware: Firmware, display: bool): + speculos_args = [] + if display: + speculos_args += ["--display", "qt"] + return ([APPLICATION], {"args": speculos_args}) + + +def create_backend(backend_name: str, firmware: Firmware, display: bool, log_apdu_file: Optional[Path]): + if backend_name.lower() == "ledgercomm": + return LedgerCommBackend(firmware=firmware, interface="hid", log_apdu_file=log_apdu_file) + elif backend_name.lower() == "ledgerwallet": + return LedgerWalletBackend(firmware=firmware, log_apdu_file=log_apdu_file) + elif backend_name.lower() == "speculos": + args, kwargs = prepare_speculos_args(firmware, display) + return SpeculosBackend(*args, firmware=firmware, log_apdu_file=log_apdu_file, **kwargs) else: - raise ValueError(f"Backend '{backend}' is unknown. Valid backends are: {BACKENDS}") + raise ValueError(f"Backend '{backend_name}' is unknown. Valid backends are: {BACKENDS}") + @pytest.fixture -def client(backend, firmware): - with create_backend(backend, firmware) as b: +def client(backend_name, firmware, display, log_apdu_file): + with create_backend(backend_name, firmware, display, log_apdu_file) as b: yield b + @pytest.fixture(autouse=True) -def use_only_on_backend(request, backend): +def use_only_on_backend(request, backend_name: str): if request.node.get_closest_marker('use_on_backend'): current_backend = request.node.get_closest_marker('use_on_backend').args[0] if current_backend != backend: - pytest.skip('skipped on this backend: {}'.format(current_backend)) + pytest.skip(f'skipped on this backend: "{current_backend}"') + def pytest_configure(config): config.addinivalue_line( diff --git a/tests/functional/test_fatstacks_full.py b/tests/functional/test_fatstacks_full.py index c567853f..35a58530 100644 --- a/tests/functional/test_fatstacks_full.py +++ b/tests/functional/test_fatstacks_full.py @@ -24,7 +24,7 @@ def test_nominal_full_passphrase_check(screen: Screen, client: BackendInterface) screen.home.action() assert_current_equals(client, SCREENSHOTS / "passphrase_length.png") # choosing 3d (24 words) - screen.choice_list.choose(3) + screen.choice_list.choose(1) assert_current_equals(client, SCREENSHOTS / "first_24.png") for word in SPECULOS_MNEMONIC.split(): # 4 letters are enough to discriminate the correct word @@ -33,6 +33,7 @@ def test_nominal_full_passphrase_check(screen: Screen, client: BackendInterface) screen.suggestions.choose(1) sleep(0.1) assert_current_equals(client, SCREENSHOTS / "correct.png") + screen.dismiss.tap() # exit the result screen to the home page screen.exit() @@ -40,7 +41,7 @@ def test_nominal_full_passphrase_check_error_wrong_passphrase(screen: Screen, cl screen.home.action() assert_current_equals(client, SCREENSHOTS / "passphrase_length.png") # choosing 1st (12 words) - screen.choice_list.choose(1) + screen.choice_list.choose(3) assert_current_equals(client, SCREENSHOTS / "first_12.png") # only the 12 first words for word in SPECULOS_MNEMONIC.split()[:12]: @@ -48,4 +49,5 @@ def test_nominal_full_passphrase_check_error_wrong_passphrase(screen: Screen, cl screen.suggestions.choose(1) sleep(0.1) assert_current_equals(client, SCREENSHOTS / "incorrect.png") + screen.dismiss.tap() # exit the result screen to the home page screen.exit() diff --git a/tests/functional/test_fatstacks_options.py b/tests/functional/test_fatstacks_options.py index f2d26e11..bfc820cc 100644 --- a/tests/functional/test_fatstacks_options.py +++ b/tests/functional/test_fatstacks_options.py @@ -28,7 +28,7 @@ def test_check_info_then_leave(screen: Screen, client: BackendInterface): def test_check_all_passphrase_lengths(screen: Screen, client: BackendInterface): screen.home.action() assert_current_equals(client, SCREENSHOTS / "passphrase_length.png") - for choice, length in [(1, 12), (2, 18), (3, 24)]: + for choice, length in [(1, 24), (2, 18), (3, 12)]: screen.choice_list.choose(choice) assert_current_equals(client, SCREENSHOTS / f"first_{length}.png") screen.navigation.tap() @@ -38,7 +38,7 @@ def test_check_all_passphrase_lengths(screen: Screen, client: BackendInterface): def test_check_previous_word(screen: Screen, client: BackendInterface): screen.home.action() screen.choice_list.choose(1) - assert_current_equals(client, SCREENSHOTS / "first_12.png") + assert_current_equals(client, SCREENSHOTS / "first_24.png") tries = ["rand", "ok"] for word in tries: screen.keyboard.write(word[:4]) @@ -46,7 +46,7 @@ def test_check_previous_word(screen: Screen, client: BackendInterface): # coming back N time, should bring back to the first word page for _ in tries: screen.navigation.tap() - assert_current_equals(client, SCREENSHOTS / "first_12.png") + assert_current_equals(client, SCREENSHOTS / "first_24.png") # one more 'back' tap will bring us to the passphrase length choice page screen.navigation.tap() assert_current_equals(client, SCREENSHOTS / "passphrase_length.png") diff --git a/tests/screenshots/correct.png b/tests/screenshots/correct.png index 68c9a5cb7d76ecb6e1fd82a526b3dba4b023b93f..a44e0f746d22999983707991508fc3600d0b38c7 100644 GIT binary patch literal 6608 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 literal 6310 zcmeHMc{tR2+jnHFp$(-pIznZs>=KStj!+?EjAd-u!(gbfWk@AUCtJv#guyV$Iy4m7 z#y*p6Y*}ZFWg24}-qBO%xz2T+>$%=@z0aTTf4}c?{eJiN{@nNH{(SG>)7v-o_<2Qn z+1S|l^?$!=!p8Q?b2c{iEN*tz%%xj(MQm(G@9AIFxpV*3;;3I`-H>!Cvb#QjkDB}XHW}kbI*&u%H%LEM z(B(*CK(}O*ZFSI7Q@O3OO8|k;IXO!t;E(2l!k3ARoXBZLk{k|sK|0UP9_-^acu4k1 zkCd4M_HKJH__dL2x;Ij(9d=5tguIx8T(&X;lWp0!%PLL;&SZcV@_&uWhFPP0 zfghv1H2zY~S^giR6ib0pPU#;bmXDF{BkMul-pv--Ebj(&ifl*B(l&NzWO|LaQS61r z9}tYQS2j#)y$u;1RhC`nsU{XTN3&qL4)*q)VsLR)=gLX^gvXOgM@(nzyZJZB$$PJ< zo({;_)RO}I3Gn)|;&w#f2*oo3K&)cI8HyqLdxre$94W^$g)+VwkiT#D&a)IK$8X&f z{~i`r6sK^MX@igP_vj$jc8ZfJUhjnWtk4nhyzu!_;C=c5**QHwjA;MDDR6gom6Nii z9S#}$O7n`kS$Lv|?3}^KQ@gZyT-cs_QwPdHf;gnsHKih0@ku0L6rozwC46H-3_hP6K0iKR%wM{2ZbGv3RZ~5y>E`YznE@+ z9eluHn_!pLL#T?@9O*v?KJdoqJ;Gq%H?(-K^ZS#^Z3^mx6lt@4IHYa-mO;Q*r+FW+ zSAi(E@JGuUx?M}fhL#3h3hWJxA4%XSqFpiK79IhCqHk0Q{^_~DaqxdDYyL^DZTr%B zyOU>W>N{jI9kM};k`m;=>;=cabJogl_B>gn!CG_{a{Vu9Enh^^t+MdgdYICyQqOO-K!cqwNzfZt}+$b9ku#*t`vSVVz8WA)R#8uIG7wVZVwMg|q}swXD{`K*Oq{M8o3Hm%wk|&79C3}$z`SoD%827xpPaRkxy<`RvNu%& zy^7eIfwy<9uWY!L{Ox6-rsAJjuw$$FfKk6dO0cKZnL5AGI&gw6ZS4_s+!g5A? zjAfiQ=nAjg$>${!&w;pw<${~+3|Q>I5hA>a<9*k%J_HrQk-La!bxLzlvlvC{moPo{GOxboKGdycco(`+8Rcsv(DQV#)lK~VEZ*!+Z(w8qk>V4Grp%Q1&c{&N<2qy;pb8DL18 z;&6{^{G|tHuo}DxSun~xa3%;PcDNMPys1An+hvec9jGZNzewaJaLVNKSUQi?L;#(+ z6uf)V(>H-4C@5lV1vFzuZb~~yP?<7MGzUk)q!4o+UgX0cxu2zbj*yRgK`u$iQYqwV zS~J2jQw=%L*oGcErt@@4!`nx(ZJITA1PRYE+P&wQy51;)Xtmtx+U-Z677mx5pXfIl zX4)tU;@q63r(89h4Bushm!A59-B%fsP?$1&86#Ic7gDmKF_=c`&FBEHcy>%VXl5T6QyfGSf z?AyE`1(vJR3Nh+kpM}r5y_vqQOcF|8+JH)d(-uPpsA;gi;}eV8G^>DI{7ZL6-86>V zLQ%d(*B#FlaQJ&UFpV_VtBM53ee>3C<3DPL1MDwdyV!^nK2pg0SK|3Qy%D7JSaZ8A z?ADt8z8~DBCK++*6_1K-#J@C&X`@Lz<6&C^m!GTpw4!Hvp7$oiQfVs-qgZlza`W4C z(K|1h8=##cdH|0g)rJ#U(bBs#o52jBmHJ|LUGvU9imQNcmMWHXRVXYGj}b##9)t&1 zC=h#fDVQz&iFn(4DqFz$_2XX}cDOWBvc8geR5S@x`0(Z#3@xdn$|k~T%gxpB+fLzT zjq+ewV2U@tdw%OPkF^T0!juE`cIGyGx-8bPq2502y~}0TM6{$koKz1Ykb^cR3}KFCwxi1SaxwEC?xokS9*wl^J-|#)#Bg*!%7+9wzCre zl|0AGu+}QuzHdwoFX%n%9ZyD}iTr8sA}U&--ex-$+g#^2;oA2t*bP&fI`98!k{cw! z7#X&y82$UmvJi9@K+~3I`xUrJ%4h}?x;vHIg386Me zpGkOW4~*u}`iIOr{c7#AqtX75O_$FOQ%lBYo;J2wVfIiOtmdtpyX?GFMAre)yD5Pt zFE2C>QJDBm3Llf~Y_&EDzN0{_*Ly%2%Avg*G>@;w*T#i1;u~dgLk?^2Q%f`BhGAYk zlzqjQM+oyo=emW^9hEJ&;F74}`tMLLny(bs)Brh2!ABFi*5Pg8s+!8f=6W+j?+)B6 zY*hZKuzptg`^o5jCTY5QOc)mU*Sh+5biVE)pxtubwCgS_ZZ9-WMn-2-+qnM}6IGFk zZe!;2_fp+`Bwf~Ovkcx?Kp!`@8%nxUhZDVSp;u*tM$NE;cLFCEI!$C&P2+_7$#grN5-4zVYbBL4&16EFmf(l!+!D?3?1iwtD zkelwMJLQRICZ4sZI>eNcK%FMXi%-#hz+%cn>=%)L~}9iev5tbAr|$s z<2Hq4rVi=~nW#y=7Tb45K!K%9S#>$q_tuX`W>li3Zs=m~i!d8`2~^s!pLd4e>Ew}$ z@jcF7kTR*mbmzJKDZU1954QX5R8J~=qJJgTVjod6jKx7zi-wZZk2 zkW7h>tmcsEAZ}8&wvEDd14dA*a5dLJ9!c;6tx#J39vKxDO{cmOS52`~RhXe4qO->5aeh{7Tn=47!uHQHKy32icSo2a3=-OE{A= zY6D*nkn5o7oCf*hhn93pyL{jxvIWSK`}OR-1{cnNO#s+=IZJB}gTgFlrXzXMR$u7# z+$l?Q(&IePf!U#^wbX-2cXUel9z#T;(fz!h<1hAA!}lI?nG+*QU^jqj-+Gp(h-?MY z((QqZDsK`57o)84!IHzEczkW>#7sUH>p@RR?F*A#TfDg%$5KI=K5M5!?OE>#=`P@^ zH@za>Z1H^>PMYvyrMH?SIMRH1>SokKmW0j?LGLep*rI-R?=E*oAZbEq8KlYmgD?8G z4@Je}xwJ!xOv*m2w0kH7F`=9JHN?^{8#tEIHH(=fn)$6O)Y@4LR?rNtCtu7_RC z3Ax470IGhxlnsC^_(dqk_RQ|?ngALK`fRbC8O3#7QP5Ii@4R8_1|A{oEWLGUC3ZUb z@<&JC8%b6tnWbBWZFsAph(<0s=uVp>W_MsQ&7(z8LBpX{p~Vz5($V#Ft3mDdVgh>d z%}L;IMzPIZfRG(;|MEN*VKl9ZdImq~Qr@lU$kRJ(40FcI`;sqp>{K`cOQ45FFAc^@ zUgq_mX}hxEIYVKOtGfxh6Rk^>KqW4YD2boTv{i82ipFnA%Y@es9&4A@xc^q9gtm@N zQ_;W#rKG(%Kk^tZEd#|uOhM3Hv0GMxH39_B%v%E#U$j|_`~7ZVh%-Jl73r| zf@w-$%wVwEF+JJEuZ^vEQ2Dc152upAj6+Ftv(>LqtvJOh1FQN*$t~~IrbOfGt&YiW zt5Pt06etk~sa_`zw7Y;N65DrFUx!X_d9NinPqTM#I>wldbzIftEb2Tk^;+7iZWRjs zwX$H~5xKz&sMSuQ$-P2_#j7Y%9}%~dQS<;uGL@R=yVEKN-Q9TQ1J2}wau|ph4JVgX z@}0qS4N-=2eYI)zu&t~4rlJxEE(VEM^q=2(x`f}V<~za zHh-MWygSz02J~xL7$^^94qt{j&#feQFVjDIGj;=~YRw5E8tM(hj5*2KgF;Z?G7df@s19;FlUjWfv=o8S+B%5B$qyl2zJ( z_Vd@?2TnkNn;0W$O+xRLSZzI-_FaT--Uf>c~;M zUoPI{NzxJCu8RM4PgbhQuvV5ac#Z8C*0ofcvt68TFVT0ZC1g9l=l)W{fhRBodVl$M zUa*WtEAe@jcG{8V5^iJ0n^VQc7fbA1g-1-Nb*0t{e+RNdLj;cx9Ay*iN86i9SUqGe8RmrH)r8B zShK2yTA9|YKzhc+b-RyMH8@2KY-k+(kk4=-H)az65-b+BA_`PMSez?9`l!=Q6vg=6 zq*#1t>x}0J${)8dA;+1Ov)1W$(b4{-_vF4n( z)~j(o3f$T`olhs8vbL?CL*;tkDdZ(>AkkUre9sLBd5LNYWwy+a{zP@2?pnNQ32Z6;0epD0| z@It~>ryw!3m7NXR#^o16`jb~m>_!Q%Wv?``EN20oYk>Tw{` z*>$pY**SQ!%fih_{|s(YG`@pB6nqZvJQRCN|5sd2L8I}2W742DFy+N<`5NDg65?$< z!XhuPE-=?5QHN2-MaL@8t4X_NBf?#tECXA5vF1%cEiPp_!hARc7GXQuN9l}*J~qcx zKD?sMK?Ws}@X+0tc*hcnhG@I7XtHmj^Br%mE@f2hG3bX3O&7kdPb${jyFuMimEriC zb?>%U&+0oyz;&k?sd3I08}1X~)C|-%wU9pFo`3pIg>5=BrqqL;s<_=}f{}{vBz>*+ znVp?I#B1&mT$YJRI3INPhUnX!gAVx-4WBL=D&yRU+pi{Fj67TyMO?sS(2 zmf=pgPNkB8+?8PPl2fbj)j;5_1kWV(ZG0H*P+x-xXU{L_E_9$gqq5E0m5(ZvsAHp zZr#_=oSB1&Z}0NQMGWQf-~8G9kDzd0cY5&wz}J>FK_jze1Zy;>L8VreieUZ%#?&jF zlYFmyi-T`rq3-r!ESLMQ?z8OhjdbcyU;dwZ`#-ec4$#gitVHR;@)y?s2R8j{H?Njl Hu|@t5w;RL< diff --git a/tests/screenshots/first_12.png b/tests/screenshots/first_12.png index 487ff9f249551e3a52d5559e5cbb362f6cff7111..b38f39305af71364094a6526c9731b6ade979eb9 100644 GIT binary patch delta 3264 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 diff --git a/tests/screenshots/first_18.png b/tests/screenshots/first_18.png index a657042c79ffed9fd06a990cbe34e0ba14a274bd..beecef6459c26b307d16debb8f322974bd1887d0 100644 GIT binary patch delta 3273 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 Hu0mjf)2h+i delta 3370 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 XAk>-u(?2Un00000NkvXXu0mjfBRuVD delta 3382 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 M07*qoM6N<$g66_G3;+NC diff --git a/tests/screenshots/incorrect.png b/tests/screenshots/incorrect.png index 3dc1492cbd593dd8a8785672688716b366fd0c92..b04171a950544964213d2123dbb86ec01d2870ef 100644 GIT binary patch literal 6532 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 literal 6608 zcmeHMdpML^+t)!tlx*jiN|J4mG?L?%Vz=e+62^H%#Ds>N8AVPJ_EwZbVv1tSj0QQ* zgrqPuVTQp>!ybnQ<1_|?`NrP6{q6U9zw5o;@4NP&-+#}&)_T@@)_t$vZ{7FrNwc-K z5EGFX5fBg%yZqxtI{|@R*#ZK)pY7kxZ_&4Dt`rcE%e#Eh^lD_`+!*nf(jO5?|1i=< z>uIg`nMG@+>fXF}w)Mov2T*sy)jhkroP@7M@o2nl4hzqt3M6yl{m=qGXteIWwqN>? z8Rm{`g5bMDz@y2KUD|s^Q;&gvIPkyh+CCp6cn7h`p~VBrKW@0#wI(oMs$GrBZRM(f z4+U4*gV;|9+)bKxH{YD)E)s9%PVFw|$!fSJ2>$G0KX_Wb?~+wFV8`?l|7ozN=2zlt z{z!yPg~x+j4UU&-GP#>xRJpj3YCuiB`C-4P`MD1U5q(5bnwf?&P?mDEHd;U8r3&~o ziWmSoU+tVAm>B&uO3b11V`!EBIZF)WqH?ne=N~*@Vd(>^rH(nhgu_5tOWUfGMg{zr zvO`%R8+Iv=WPq~PpB4f>2PO%MnoC5SS*z$vD}l5gDuse(J^_F8NFi%?d*w%B;vd%R z8NOBW&6>H!w_x9_$zS-E=^qx~$zP7h7y}BAz#LEU-Rrq5XhMhMO?ceJ$in)Qw9{>W zgyPHIJp8z^KYVNq1Y8qtAXjLlRFcYg+z-rlz$iGpF9B%})`mdImSYsyLAqTcGS8S5DN2~Z`oGraYQ=J^l$ z^9FZ+I|{d6r;EK7-|D*b$NtYGA`y6^%Wq#{R{!=WbDBPAg@GI)629727C>6>jOP#T zIB;Lc%6^8eqN4`}0)(k1$64=FO%QAs4sh&q8QUxR{yQSFx3-ZV4(2d&7nbc-*(Cfs=qBSP=pQ{y-zBiLoMYID+08>|n@14i#1*1%QhdjHDnj^kv^jo^r? zrM7-M z(Z;x9S^i4PucRKi-Ja6w+_K`-R?Y-mGExRcgPZbEjyI2kj|9yHG`dtjg23uKVwwi- zEACBK%I7hz!*d|SE9<{(x;*+G$_TOxO1Nah3OrMHO|rGLl6>Knl&i3`#$aK+RT1T? z`c&eTz3Gy&uGLu(B6yjYO_#DsxD+EMiEwN_3=}7xI2+KB!FP*RD8E4xC|+ZrAJB0; z-XqyNaLfC--KDz_VtXeGO;!uoFYPWYNxLQXSD*hKWcx2C;~S3u1NP2{@AbHAG|%DA zs%CXgJOrgJkA@Yj4&fS}HDDkuLTK&i5{Z-+SFoc@`Xaois_Q&XGYMn+IFRMfmnV^i+bH)h$)bM-pNsQ{xI&^$BU{ zmY_2zuNKN;^42U{AFDUql<%#@AP@M|t-I3d!3J+}qrzNk4=UCbyU-;uoJsDPFMdO* zsk|>t;-ZTbBNXCdQ(*lW?EdHPP=or-fT1F%FAh=7>oA81yl-^lTPWJ@3MMF6IiWnH zL04*SS|Wtr80jbGiHOssPecVVe)Q@0o_k!6k!R%KIK^aOC?^`;P=h@o2(?>#1Xj8g zx9*zyWcbNH#w&Ts=qE%Ux7(gI;c8x-q;<1LTAkwcZ6IHT!6~t&#qN4;gYk`i)C#3r z!ApY@(edkd=@yZw4x{+NU1Udu5gAd{C*G=zjL1T>;E>{ubfA!ts0`d!tJ%NG&F7vJ zeL`hWbm(^ZholFqZdrB4P1pBAI~5P4iw-Z^SVW5Ni7~!m150VmXL;)UKAEFjYxMW5 z>5g<kxd4jpEiYtUmdZSS+(S{-+B~0J58o!0H!M9k+muV zHqHM1x-$zD0~b3?Q2c7h78K=6{p5#2x?+Q3iZdqVRl!;Wn-8$H{oTujB!e>2v z-P$SstH)hqHAp>?uKELhJl9BMaG4f*s7xces6OO<9Iaw3z*EXs&9F<{(^#!X)@!bM zHE!Nhsn`?s_FO+}sC4ObonzCsX9V&}`wP(7+4~RpL~|$*e;|nYIdkp%#@g$d;R6mq z+|DfG*&{x+?!8%(NPl5Iu@63e&T5t^yZ;z&h8vhM#_OR2~fu?J)y_Gpy!B%d~ z&+S58%@`|YGY#nI`Rr-c>xWQY?9)xv(*-iqH&^pTT?@)g#?rVI1zt`!V-sqJI}0$P zE?b&`F|O_BTyGa*R~^o^qlP~0Wga5OXn5RA514nU=MkYV)_OolqpsrRq3W8YK6UR! z!iY`an`ynO-^PPaDI<$W#_}N}YZ2cCl-fRqE>_p^cjd&8R-BrSb5QaD9VFmXljM|< zgr_W|7VeV?{QY&>gG->$xtNC|C=Gq<;y6$Nv!1&aVWI{+j5LxCu3TSRuUF*o&xS3X zUT5w3bP-A0j9YbxLM4yjehc*+d7-MY4zQ0JCp*x$2J6xaj{aJQ#yx@dU#NK{O#YT+ zek%#+?rkpQn7pZI*LRGag!JEEL3G4<#DWZtuDo-IC8Y7)FLg5^58AZ%0M4uI-6=WR z)##_B(d9kx`U&RpYTN|1tU*M5eS5Ae$-j$f8`b58oy6Tcp9kqU6^n>w$dKX-uXEf0 zezda$l7@y>T?irVC|Hh~BEV>e2ku|qsGR|sWpRX!Pa+DFH|BfFqTuD5o%->k)LIx| zB2-NM{lsFV5lZ@CcSnN=j~fu)(>Gw~7`0wNqQ~WT*^z^_(lRg!MoL+2!~KCQP6wL6 zhHbL1SA}y!VuY{Vdqkf`pe8B(xU>jhc%Zkskh9xk$YS0FIWY&lBKr%;-U1!+zi+f1 zHvjRrM}Bu3TRAURuLuHb3N#con0{C_iAv%EERX^dl1S0-0qcOQ^(Df~JsbQb#!I;! z1mjqz?L&P`RhCE&$gQO|>{R?f^(Ou4#=*mn@@8|A_Z&t-H^9Yl#rT<9(>UEqxS}}4 z!(17hSIe@wF}L0BD#msTC3GDJBW|pjGOQ$HO~|IjQS+EdhI65?_=igo-y->cDDM7! zu>XI;zh=*WpEur{_@UqGa=2Tw4tFf043+%QZTk>HmL*?ov1T_H;9~N?q6B-xgDu+K zAUDi&5%bfEX^894XOPQM8NM|)#Uy~Q5`KP=Qwu984|b@>1qE@ z5+rB_HJ)cFwOMAG$gYX3TQXFK8!AD|bwE=F44|gVzL1bir@7`K05&u3FVi$T=5Q`#wS2>yGwt(mL^apJpFo=w)z|JNb$aSfa5!`nV5N z-5#6F290hUVnMfIPoP+P%^k_<;YNcn3 zqrI*tPtj&{f3i3!h-EW49%`%PuVu^uv551s-gxWP#d{GK4U8K>iAP*8)|Ytix;a z-B$>{lZ}&VUJqbTyNB~*qxMDI?{}*A1@ywfveyT$gx^mtXsCa?D5i*P>%fV(J_5*LSBoD zQ+n!UsMBOj7wbda*cfzb6ALijjw}vwDVFry3zf0r=D})2Po?)!#z*#pa>STiHefET zK<+G;^O!h`SH!OjJA7{wpc?K6_g=hzWedp7b>uxWa6~^EW74M5 z4BAr5@8x8vCO5^iTBt2{9<#HXK{N!p(Bf=t+yHu+`_LfVf2()XoSv#T8} z~_@zy0QVwMTO9^;!;L1GqQA@?k#=OhP>9b+yK%&y`l!woMo_QBUn z(ecP&4<$wv-sCaI%JF<$mY=nC!tfj1K0P!{{wYXc@}$*x5c3iR|@1U@io(@Eg}jT zO2DK9GlF<^D;IPFL(&JWQSa$tz30~P-b&DRLG?;bc2Ib#(ZRPfCguhLLLww+h53lp zg7VW8>TSc!+^S;!gn~1D!kdEKpZYOKIZ#B@lQP6cF**vqL?1Vd3B3FI>s!NlvXMMH_ zxBR62XVYcoytObI;d>ja2i*9~Y&Y5l7ewlAs@ZHBTUM$GtAGo=a~oz%NcWeY*0IV@ z{0t#FFkS}ec8|x$>!_6Q^&=ni9|k0W)~C}4mgg)_@pH217HG*rF3)*Xx-SUj<=#nY zaq5q6J{S;<>{qBAS4ar6NEupT^u7Gz z@f6D6ZuG90#b;H9G9$N-ZEl$=P{kYd7AZ?i{%1P5u*ZGB66ygJ}E$A=x^^r#$N z{EICiA#CLw?)9Me#}ykWA>XPlGRKeJ+s9G4iULPfv#HuxY1<^87GoisYI~Z$3ldcTxX@c=-{d=0-)M9gIsT281!ge|qVB%WF)0p*=C| zHteqHZM~tG3{0VHTN3-cT;Xkfl>uP=^QX0q?B*&{?3Zi$md|#LAbG zCywJD9iuQNw6kjI2_WYtc57{v-oWFcX!)~X8|;`W_!H?%QofuKSjX8w=0-Q|w$R`8 fBZQwTFY^Gfv8!r@S`w6~ z5JE{?MJ*8|5lc}bVyO_Zyh&%yd^2jDCTPXq*nUWf_tG^Z`rbMzT&3G8I|_?8?f^1`b-Ta5w`v!%cVFt--!AmVb7chDE;Q}IT{@W| ze4}?wVog_~@HNr)h^MUO1(ZVG7xm$w^~i~~`>e*K+KngE^>&|xXL`7(X_9zH7W>b-A+##zb$sdS*w2Qejxg+P$@80NFrn2S>NX>)BZET7|!&+d% zLVINW5;}D~*q=UB)g(Gy+^*&rG7=aW;w6E`%s{xhQ2W26j0}A#yI86cV(*@g3gW-bVPVcomBDy0W+~|yO!7sVu=*{PXDzY=|D7uSRaTj-&t&=X_cL1=63&1q*v8q}hJi8aa>S?2arfo^PgIAPs-9NVSGXqL z)rh_ds}!wkVIVw8{mK^<(2f-%-l{>ZJ^LcdSI&HDC5e-(%QqfO`>+!os1fIjsIA1< zjE)t!Nwuf+(@1Zv=G8%>?Iv61lc)vFg+1oB8T^o}66ov0w29o$rVwRMmDRDtk;tN( zt3H8BZlEwKJJO)HeASmd7*~Gbp&|84qW3WlS#9=iz>C$~LkT~}b{-X>*ZEhNiTM_i z(q?vczb6W^zMM2nbLr|Mp5lsTHKl<8ZQAPg$J5UNmKOZy!ouA^Z@V|oKd9y3JJ~Gs zH%R^y^=OmGZQ*NgOFI<-vlIo>FsHe<7W1QdnM<%VRgeNLx7RMWZ7O7SXmCX+mvgXK zXLjf6bd7rs9(rjVOtqTGa*0?=Z6oK#-8|--d%%JoUzH2Y{QS-h1Rn1?6CojgY1VB$ zrP*JYagwzwWM>ZkS*Aa7?h<#%Z(vy%H*S_vw}Z{JFnNxiUUKL!_OdWkUj9B7K?P5? z>LKjD1{PH@a{WFX2O$QQkIv+S&CCn< zQ0vdDsdTBBB@@q{XzsYDWd=SUpuE7d z@uBlKPEATOYDZyr7p6#~3v_3IbEyk+n8COL|GeL%%zqiaQB$4lU}_-cv;yo6%>b*g zE;yu*IqH<)&gQfzKsEf$1}->JIwqkQisEAO3zVTPhFyO^|2Z?28LYNg$mER67NaX2 zIn6b}x5TAYtwS>0N=di-AUvx|$70w;B`QIR;y(;jPdBY!XD}){tX5jG2=>8sZ;bKU zXXe|EG0Ff{Ri2l2O4Kt~2F+lFhQ==pq-#69-;TBCds{fkNW1RIscwp|-0E3akHRIX zSnMftc^jtN!S1y#ZeX6?2vwzrckC?jRyGJTIyFk9yonrV>gm>d%1@rY{&G@HYw&&y z>1J|OeMb@+Q}r57ojnu{r1%Z41Vq=o^?M013X4#VxY{}Dp+L42bX9cYlv>Dxplu4q_-*wOyC=p}?hI+vuoh1#D=?1xw&QW^6m+%92jV2Q}FGUB+-`vE`SZZ)fHFxdGyddhUBKQq+rLV3OnnnEvHAkgHp)Z^m7Fswzj{ zUJ8UOy%Mk3@9;>ev-!H(J^vYukz2g7^^@5r0sFOhnzTm6E$@POt!Y6Q0vH5HwI|#< zEj=Ac;90JB=FyIWJcM^K^c%yrnw3IABjQ6AAw@D-=MxCC5^GU%-m$ksg;Cw1A5KZ~ zE^gC1umax}Ems=D*OGizGI@PT#@4p);^t*mTR-zU@+JQF9S2`E@|fMq090Vxcy8&? z?>&vx;-zCV(g(*mrt0CcaP^ZV#0b*{rOG#Q=_XY+?-jw$;>%{$3A17i_qG}KD@#^) z9WcFl#SK){$DTTfr2Am8i|QwokdPLK8&jdBYhyK@m+s-~*RJ~wdzH^ntWy*s!niU)oVpW`C=)D70}2!jfMH91BNX})7GuS%MIcK4|6@LSfpxg6?<+^ zWwN~txCTA*=%dDn*I;{Ij`RbgJub`z+oa-@8LW>N!gK+QF0$LD)tqtV<9z;sF}U~B zN1EPl;xqX&twIk#$T**SgyuWWq4rK<7vra!7-7gydV)lZ z8^)=)Wxk+F%?nw!=5-%-6sC|=vx6w92Byw}5qEvBoirwV>T9q;_msDo=16#ol7oCh zJ{Ey|g9L;lgy7p^BdFvs4 zrz(jV9%r*$6PQnRN;bXYiex2y@7X-U>>Vy zHQ{D-Y|nSy(Cs30yzqKVZIk$us&|d)93hi`o-YxJLs6_XF00{^uh7-;T*QV!=q|k0 z>iv9iVI#m0`fO}fUlBp!+P!Cbeen`~qa*T*Rt56)5yt$n%QBCA;{?O5VlLK9HcAnthPHGB*s6V4lB-V0QW`Mi z;c`mK%ZBXK#HBq`O*^-_gXSCm+D0p#>o;rAIlWuGD&}~gk4+K}u2)N-l^LRK|por`?BK^9kE?r_pMC z%*!3d5~+(_`x)cLK6A=$<}fpJ;!Lj3>Kc5=(kVECb^Tp|A}HL_czo}qWf!HHnV~00 zxal^=5?YiYyzFHVrLmoMP80@p{r-6JFXcnC0*N|HW0ztA#UE;~F}1Uk1)W!NXguIj zj&~E6>Wo;A0y>&A21Z|@HWWZ1Ls9mXIg{H48AgLOEzUmA&x-YZ)n^Z`J{lqe`e8Wd z(pyaGjjD_&&ABc1MMGPOe_4chh!oRUziffo*GWKs8xb##Lk85gYP5HJ()}&MJfu3} zr0e-rnOD1yNSGH-K_9g9Ug4GXWMP%wRW%2GDF?*zPN?n?`T4;P9$?L1b}k3O|3K^+ z5MFz)`O9VqOS`$kRcsf7<0{2VJUxkmUJeSnn%(!pPAh%Gc8KDaN<-$(3^3t+Enu#iP@yBH(gh`A_}eG**XJt3QHI;|t$7d~ zjD26j8iF6H^7ds5{U=h_*$ZZbk;;||Ut8-)$yugT&-k_*6y<>T>)Wvu z@38$t8$I67WWmdzWo9$J>}zogc2C~ZX{f6~e9N1NbV}N;()B4j(^7HGGiUm4K#_B= z@TV1rIY6@ijy?^ghJ8X9)o?Jx+X zv+^<3#D~Nu~hXc{dNK)*ef%^Ucg%He{)?%^HVVc0-bcbC7@q>D#+)eQUBRhwD~r;{O2hpvr@4 zq&=14>-${WK-%+HVj^j>vdp7BG)Ki9z3lUYg+*NAFF2q@8-lVTHsH$gwj0yG#;gdL zyhxe8#^=!g87vF;mcSb<0PpY2Yh?YtNlJ;mjox0PYpB-4Z>&r}NA3K6Wg36K`3Dh~ z7UUk+|AUDw8E7wU-bq)I&&yX5eBXN-Hg~H7?;F$U19$k$%BP1#2&eTH@V-=RJyEhP zQSoZYWRgL^4}@Qz%*~k!vX6Bm+*;kVv}w=fsLqR}x;i{z6h>3I@4JXJ9a4;?sJWpO zu*a$L5V_pYv_5EC?toF;Asz^nVgos0$;u8|1+dmB0-;uZazr5KvA%wSSRc(@8jv-R zHU@hEp`A#4pR{#{_3z%LoKVSAUB<4S&Lepdb delta 3846 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 XzqDBpEzIc>;$wTo@p7$|Z^C~AEQjr; diff --git a/tests/screenshots/welcome.png b/tests/screenshots/welcome.png index b92d35460e4d5dca71f3059a1015096e44e94c5b..aef905c901c3fe7325933b0f4d9c072d0ac4363c 100644 GIT binary patch literal 7944 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 literal 6994 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 Date: Thu, 19 Jan 2023 10:47:43 +0100 Subject: [PATCH 38/65] [clean] Renaming 'fatstacks' to 'stax' --- Makefile | 2 +- ..._check_32px.gif => stax_recovery_check_32px.gif} | Bin src/main.c | 2 +- src/{fatstacks => stax}/passphrase_length_screen.c | 0 src/{fatstacks => stax}/passphrase_length_screen.h | 0 src/{fatstacks => stax}/ui.c | 6 +++--- src/{fatstacks => stax}/ui.h | 0 src/{fatstacks/ux_fatstacks.c => stax/ux_stax.c} | 10 +++++----- src/{fatstacks/ux_fatstacks.h => stax/ux_stax.h} | 0 tests/unit/CMakeLists.txt | 2 +- tests/unit/test_stax_mnemonic.c | 2 +- 11 files changed, 12 insertions(+), 12 deletions(-) rename icons/{fatstacks_recovery_check_32px.gif => stax_recovery_check_32px.gif} (100%) rename src/{fatstacks => stax}/passphrase_length_screen.c (100%) rename src/{fatstacks => stax}/passphrase_length_screen.h (100%) rename src/{fatstacks => stax}/ui.c (99%) rename src/{fatstacks => stax}/ui.h (100%) rename src/{fatstacks/ux_fatstacks.c => stax/ux_stax.c} (89%) rename src/{fatstacks/ux_fatstacks.h => stax/ux_stax.h} (100%) diff --git a/Makefile b/Makefile index c0bcf023..002bb234 100755 --- a/Makefile +++ b/Makefile @@ -36,7 +36,7 @@ APP_LOAD_PARAMS = --appFlags 0x10 $(COMMON_LOAD_PARAMS) --apdu --curve secp256k1 ifeq ($(TARGET_NAME), TARGET_NANOS) ICONNAME=icons/nanos_app_recovery_check.gif else ifeq ($(TARGET_NAME), TARGET_FATSTACKS) - ICONNAME=icons/fatstacks_recovery_check_32px.gif + ICONNAME=icons/stax_recovery_check_32px.gif else ICONNAME=icons/nanox_app_recovery_check.gif endif diff --git a/icons/fatstacks_recovery_check_32px.gif b/icons/stax_recovery_check_32px.gif similarity index 100% rename from icons/fatstacks_recovery_check_32px.gif rename to icons/stax_recovery_check_32px.gif diff --git a/src/main.c b/src/main.c index dec8520c..d6b067b6 100644 --- a/src/main.c +++ b/src/main.c @@ -20,7 +20,7 @@ #include #if defined(HAVE_NBGL) -#include "fatstacks/ui.h" +#include "stax/ui.h" #else #include "nano/ui.h" #endif diff --git a/src/fatstacks/passphrase_length_screen.c b/src/stax/passphrase_length_screen.c similarity index 100% rename from src/fatstacks/passphrase_length_screen.c rename to src/stax/passphrase_length_screen.c diff --git a/src/fatstacks/passphrase_length_screen.h b/src/stax/passphrase_length_screen.h similarity index 100% rename from src/fatstacks/passphrase_length_screen.h rename to src/stax/passphrase_length_screen.h diff --git a/src/fatstacks/ui.c b/src/stax/ui.c similarity index 99% rename from src/fatstacks/ui.c rename to src/stax/ui.c index 716e08b7..64c855d8 100644 --- a/src/fatstacks/ui.c +++ b/src/stax/ui.c @@ -14,9 +14,9 @@ #include #include "../ux_common/common_bip39.h" -#include "ui.h" -#include "ux_fatstacks.h" -#include "passphrase_length_screen.h" +#include "./ui.h" +#include "./ux_stax.h" +#include "./passphrase_length_screen.h" #define HEADER_SIZE 50 diff --git a/src/fatstacks/ui.h b/src/stax/ui.h similarity index 100% rename from src/fatstacks/ui.h rename to src/stax/ui.h diff --git a/src/fatstacks/ux_fatstacks.c b/src/stax/ux_stax.c similarity index 89% rename from src/fatstacks/ux_fatstacks.c rename to src/stax/ux_stax.c index 41779905..e0033e02 100644 --- a/src/fatstacks/ux_fatstacks.c +++ b/src/stax/ux_stax.c @@ -1,8 +1,8 @@ #include #include -#include "ux_fatstacks.h" #include "../ux_common/common_bip39.h" +#include "./ux_stax.h" #if defined(HAVE_BOLOS_UX) && defined(TARGET_FATSTACKS) @@ -50,7 +50,7 @@ void reset_mnemonic() { } bool remove_word_from_mnemonic() { - PRINTF("Removing a word, currently there is '%d' of them\n", mnemonic.current_word_index + 1); + PRINTF("Removing a word, currently there is '%ld' of them\n", mnemonic.current_word_index + 1); if (mnemonic.current_word_index == (size_t) -1) { return false; } @@ -58,7 +58,7 @@ bool remove_word_from_mnemonic() { mnemonic.current_word_index--; // removing previous word from mnemonic buffer (+ 1 blank space) mnemonic_shrink(current_length + 1); - PRINTF("Number of remaining words in the mnemonic: '%d'\n", mnemonic.current_word_index + 1); + PRINTF("Number of remaining words in the mnemonic: '%ld'\n", mnemonic.current_word_index + 1); return true; } @@ -72,7 +72,7 @@ size_t add_word_in_mnemonic(const char* const buffer, const size_t size) { mnemonic.length += size; mnemonic.current_word_index++; mnemonic.word_lengths[mnemonic.current_word_index] = size; - PRINTF("Number of words in the mnemonic: '%d'\n", get_current_word_number()); + PRINTF("Number of words in the mnemonic: '%ld'\n", get_current_word_number()); PRINTF("Current mnemonic: '%s'\n", &mnemonic.buffer[0]); return get_current_word_number(); } @@ -87,7 +87,7 @@ bool check_mnemonic() { if (!is_mnemonic_complete()) { return false; } - PRINTF("Checking the following mnemonic: '%s' (size %d)\n", + PRINTF("Checking the following mnemonic: '%s' (size %ld)\n", &mnemonic.buffer[0], mnemonic.length); const bool result = diff --git a/src/fatstacks/ux_fatstacks.h b/src/stax/ux_stax.h similarity index 100% rename from src/fatstacks/ux_fatstacks.h rename to src/stax/ux_stax.h diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 2dd63719..f57e41e8 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -44,7 +44,7 @@ include_directories(../../src ./mocks/) add_executable(test_stax_mnemonic test_stax_mnemonic.c) -add_library(stax SHARED ../../src/ux_common/onboarding_seed_rom_variables.c ../../src/ux_stax.c) +add_library(stax SHARED ../../src/ux_common/onboarding_seed_rom_variables.c ../../src/stax/ux_stax.c) target_link_libraries(test_stax_mnemonic PUBLIC cmocka gcov stax) diff --git a/tests/unit/test_stax_mnemonic.c b/tests/unit/test_stax_mnemonic.c index d45c4575..5df33791 100644 --- a/tests/unit/test_stax_mnemonic.c +++ b/tests/unit/test_stax_mnemonic.c @@ -6,7 +6,7 @@ #include #include -#include "ux_stax.h" +#include "stax/ux_stax.h" static int setup(void **state __attribute__((unused))) { From 745809f55a2c017dd762481bb0f458f5add9c239 Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Fri, 20 Jan 2023 11:39:02 +0100 Subject: [PATCH 39/65] [fix] C file include architecture rework --- .github/workflows/ci-workflow.yml | 9 +++++---- .github/workflows/codeql-workflow.yml | 2 +- src/main.c | 16 ++++------------ src/nano/nanos_enter_phrase.c | 9 ++++++--- src/nano/nanox_enter_phrase.c | 7 +++++-- src/nano/{ui.c => ui_nano.c} | 6 +++--- src/nano/ux_nanos.h | 2 ++ src/stax/ui.h | 7 ------- src/stax/{ui.c => ui_stax.c} | 6 +++--- src/{nano => }/ui.h | 13 ++++++++----- 10 files changed, 37 insertions(+), 40 deletions(-) rename src/nano/{ui.c => ui_nano.c} (98%) delete mode 100644 src/stax/ui.h rename src/stax/{ui.c => ui_stax.c} (99%) rename src/{nano => }/ui.h (90%) diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index 6ce1fbff..1aee7b53 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -47,6 +47,7 @@ jobs: nano_build: name: Build application for NanoS, X and S+ strategy: + fail-fast: false matrix: include: - SDK: "$NANOS_SDK" @@ -73,8 +74,8 @@ jobs: name: apps path: bin/*.elf - fatstacks_build: - name: Build application for Fatstacks + stax_build: + name: Build application for Stax runs-on: ubuntu-latest container: image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder:latest @@ -86,13 +87,13 @@ jobs: with: repository: LedgerHQ/ledger-secure-sdk path: ./stax-sdk - ref: fatstacks_1.0.0-rc6 + ref: fatstacks_1.0.0-rc7 token: ${{ secrets.STAX_RO }} - name: Build run: | git config --global --add safe.directory "$GITHUB_WORKSPACE" make BOLOS_SDK=./stax-sdk TARGET=fatstacks - mv bin/app.elf "bin/fatstacks.elf" + mv bin/app.elf "bin/stax.elf" - name: Upload app binary uses: actions/upload-artifact@v3 with: diff --git a/.github/workflows/codeql-workflow.yml b/.github/workflows/codeql-workflow.yml index 197a7cd3..f323b73b 100644 --- a/.github/workflows/codeql-workflow.yml +++ b/.github/workflows/codeql-workflow.yml @@ -21,7 +21,7 @@ jobs: language: [ 'cpp' ] runs-on: ubuntu-latest container: - image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder:latest + image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-legacy:latest steps: - name: Clone diff --git a/src/main.c b/src/main.c index d6b067b6..271e5e3b 100644 --- a/src/main.c +++ b/src/main.c @@ -19,10 +19,10 @@ #include #include -#if defined(HAVE_NBGL) -#include "stax/ui.h" -#else -#include "nano/ui.h" +#include "ui.h" + +#if defined(HAVE_BAGL) +extern enum UI_STATE uiState; #endif bolos_ux_params_t G_ux_params; @@ -34,14 +34,6 @@ static unsigned int text_y; // current location of the displayed text // UI currently displayed enum UI_STATE { UI_IDLE, UI_TEXT, UI_APPROVAL }; -extern enum UI_STATE uiState; - -#if defined(TARGET_NANOX) || defined(TARGET_NANOS2) -uint8_t compare_recovery_phrase(void); -#else -void compare_recovery_phrase(void); -#endif - unsigned short io_exchange_al(unsigned char channel, unsigned short tx_len) { switch (channel & ~(IO_FLAGS)) { case CHANNEL_KEYBOARD: diff --git a/src/nano/nanos_enter_phrase.c b/src/nano/nanos_enter_phrase.c index 1de55e7e..e89453c0 100644 --- a/src/nano/nanos_enter_phrase.c +++ b/src/nano/nanos_enter_phrase.c @@ -14,11 +14,14 @@ * limitations under the License. ********************************************************************************/ -#include "constants.h" -#include "glyphs.h" +#include +#include + +#include "../constants.h" +#include "../glyphs.h" #include "ui.h" -#ifdef TARGET_NANOS +#if defined(TARGET_NANOS) // allow to edit back any entered word #define RESTORE_WORD_MAX_BACKWARD_STEPS 24 diff --git a/src/nano/nanox_enter_phrase.c b/src/nano/nanox_enter_phrase.c index 08a776be..139b6761 100644 --- a/src/nano/nanox_enter_phrase.c +++ b/src/nano/nanox_enter_phrase.c @@ -14,8 +14,11 @@ * limitations under the License. ********************************************************************************/ -#include "constants.h" -#include "ui.h" +#include +#include + +#include "../constants.h" +#include "../ui.h" #if defined(TARGET_NANOX) || defined(TARGET_NANOS2) diff --git a/src/nano/ui.c b/src/nano/ui_nano.c similarity index 98% rename from src/nano/ui.c rename to src/nano/ui_nano.c index d272ab5c..9130598a 100644 --- a/src/nano/ui.c +++ b/src/nano/ui_nano.c @@ -1,10 +1,10 @@ #include +#include "../ui.h" #if defined(HAVE_BAGL) -#include "constants.h" -#include "glyphs.h" -#include "ui.h" +#include "../constants.h" +#include "../glyphs.h" enum UI_STATE { UI_IDLE, UI_TEXT, UI_APPROVAL }; diff --git a/src/nano/ux_nanos.h b/src/nano/ux_nanos.h index 87eb3a71..994ceda4 100644 --- a/src/nano/ux_nanos.h +++ b/src/nano/ux_nanos.h @@ -80,4 +80,6 @@ void screen_common_keyboard_init(unsigned int stack_slot, extern const bagl_element_t screen_onboarding_word_list_elements[9]; +void compare_recovery_phrase(void); + #endif // HAVE_BOLOS_UX && TARGET_NANOS diff --git a/src/stax/ui.h b/src/stax/ui.h deleted file mode 100644 index 09b3fc71..00000000 --- a/src/stax/ui.h +++ /dev/null @@ -1,7 +0,0 @@ -#pragma once - -#if defined(HAVE_NBGL) - -void ui_idle_init(void); - -#endif diff --git a/src/stax/ui.c b/src/stax/ui_stax.c similarity index 99% rename from src/stax/ui.c rename to src/stax/ui_stax.c index 64c855d8..c2c00602 100644 --- a/src/stax/ui.c +++ b/src/stax/ui_stax.c @@ -1,8 +1,8 @@ #include #include -#include "constants.h" -#include "glyphs.h" +#include "../constants.h" +#include "../glyphs.h" #if defined(HAVE_NBGL) @@ -14,7 +14,7 @@ #include #include "../ux_common/common_bip39.h" -#include "./ui.h" +#include "../ui.h" #include "./ux_stax.h" #include "./passphrase_length_screen.h" diff --git a/src/nano/ui.h b/src/ui.h similarity index 90% rename from src/nano/ui.h rename to src/ui.h index 3c52de44..425d8b3e 100644 --- a/src/nano/ui.h +++ b/src/ui.h @@ -16,14 +16,17 @@ #pragma once -#if defined(HAVE_BAGL) +#include #if defined(TARGET_NANOS) -#include "ux_nanos.h" + +#include "./ux_nanos.h" + #elif defined(TARGET_NANOX) || defined(TARGET_NANOS2) -#include "ux_nanox.h" -#endif -void ui_idle_init(void); +#include "./ux_nanox.h" #endif + +// NanoS, S+, X and Stax +void ui_idle_init(void); From e784bacdcbf0fe69c844003a7156e58486050bae Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Tue, 7 Mar 2023 15:45:14 +0100 Subject: [PATCH 40/65] [ci][update] rc7 -> rc9 --- .github/workflows/ci-workflow.yml | 3 +-- .github/workflows/codeql-workflow.yml | 13 +++++++++++-- Makefile | 10 +++++----- src/nano/nanos_enter_phrase.c | 4 ++-- src/nano/ui_nano.c | 6 +++--- src/stax/passphrase_length_screen.c | 5 ++--- src/stax/passphrase_length_screen.h | 2 +- src/stax/ui_stax.c | 11 ++++++----- src/stax/ux_stax.c | 2 +- src/stax/ux_stax.h | 2 +- tests/unit/CMakeLists.txt | 2 +- tests/unit/test_stax_mnemonic.c | 2 -- 12 files changed, 34 insertions(+), 28 deletions(-) diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index 1aee7b53..1c3983f9 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -87,8 +87,7 @@ jobs: with: repository: LedgerHQ/ledger-secure-sdk path: ./stax-sdk - ref: fatstacks_1.0.0-rc7 - token: ${{ secrets.STAX_RO }} + ref: fatstacks_1.0.0-rc9 - name: Build run: | git config --global --add safe.directory "$GITHUB_WORKSPACE" diff --git a/.github/workflows/codeql-workflow.yml b/.github/workflows/codeql-workflow.yml index f323b73b..54c50f27 100644 --- a/.github/workflows/codeql-workflow.yml +++ b/.github/workflows/codeql-workflow.yml @@ -15,8 +15,17 @@ jobs: analyse: name: Analyse strategy: + fail-fast: false matrix: - sdk: [ "$NANOS_SDK", "$NANOX_SDK", "$NANOSP_SDK" ] + include: + - SDK: "$NANOS_SDK" + name: nanos + - SDK: "$NANOX_SDK" + name: nanox + - SDK: "$NANOSP_SDK" + name: nanos2 + - SDK: "$STAX_SDK" + name: stax #'cpp' covers C and C++ language: [ 'cpp' ] runs-on: ubuntu-latest @@ -36,7 +45,7 @@ jobs: # CodeQL will create the database during the compilation - name: Build run: | - make BOLOS_SDK=${{ matrix.sdk }} + make BOLOS_SDK=${{ matrix.SDK }} TARGET=${{ matrix.name }} - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 diff --git a/Makefile b/Makefile index 002bb234..44ab13ca 100755 --- a/Makefile +++ b/Makefile @@ -35,7 +35,7 @@ APP_LOAD_PARAMS = --appFlags 0x10 $(COMMON_LOAD_PARAMS) --apdu --curve secp256k1 ifeq ($(TARGET_NAME), TARGET_NANOS) ICONNAME=icons/nanos_app_recovery_check.gif -else ifeq ($(TARGET_NAME), TARGET_FATSTACKS) +else ifeq ($(TARGET_NAME), TARGET_STAX) ICONNAME=icons/stax_recovery_check_32px.gif else ICONNAME=icons/nanox_app_recovery_check.gif @@ -59,7 +59,7 @@ DEFINES += IO_USB_MAX_ENDPOINTS=4 IO_HID_EP_LENGTH=64 DEFINES += HAVE_SPRINTF DEFINES += HAVE_BOLOS_UX -ifneq ($(TARGET_NAME), TARGET_FATSTACKS) +ifneq ($(TARGET_NAME), TARGET_STAX) $(info Using BAGL) DEFINES += HAVE_BAGL else @@ -72,7 +72,7 @@ ifeq ($(TARGET_NAME), TARGET_NANOS) DEFINES += IO_SEPROXYHAL_BUFFER_SIZE_B=128 else DEFINES += IO_SEPROXYHAL_BUFFER_SIZE_B=300 - ifneq ($(TARGET_NAME), TARGET_FATSTACKS) + ifneq ($(TARGET_NAME), TARGET_STAX) DEFINES += HAVE_GLO096 DEFINES += BAGL_WIDTH=128 BAGL_HEIGHT=64 DEFINES += HAVE_BAGL_ELLIPSIS # long label truncation feature @@ -127,9 +127,9 @@ include $(BOLOS_SDK)/Makefile.glyphs APP_SOURCE_PATH += src -ifeq ($(TARGET_NAME), TARGET_FATSTACKS) +ifeq ($(TARGET_NAME), TARGET_STAX) SDK_SOURCE_PATH += lib_nbgl/src - SDK_SOURCE_PATH += lib_ux_fatstacks + SDK_SOURCE_PATH += lib_ux_stax else ifneq ($(TARGET_NAME), TARGET_NANOS) SDK_SOURCE_PATH += lib_ux endif diff --git a/src/nano/nanos_enter_phrase.c b/src/nano/nanos_enter_phrase.c index e89453c0..f9258f28 100644 --- a/src/nano/nanos_enter_phrase.c +++ b/src/nano/nanos_enter_phrase.c @@ -17,8 +17,8 @@ #include #include -#include "../constants.h" -#include "../glyphs.h" +#include "constants.h" +#include "glyphs.h" #include "ui.h" #if defined(TARGET_NANOS) diff --git a/src/nano/ui_nano.c b/src/nano/ui_nano.c index 9130598a..f34f2c53 100644 --- a/src/nano/ui_nano.c +++ b/src/nano/ui_nano.c @@ -1,10 +1,10 @@ #include -#include "../ui.h" +#include "ui.h" #if defined(HAVE_BAGL) -#include "../constants.h" -#include "../glyphs.h" +#include "constants.h" +#include "glyphs.h" enum UI_STATE { UI_IDLE, UI_TEXT, UI_APPROVAL }; diff --git a/src/stax/passphrase_length_screen.c b/src/stax/passphrase_length_screen.c index b823c95c..4ac46472 100644 --- a/src/stax/passphrase_length_screen.c +++ b/src/stax/passphrase_length_screen.c @@ -1,12 +1,11 @@ #include -#include "../glyphs.h" +#include "glyphs.h" -#if defined(TARGET_FATSTACKS) +#if defined(TARGET_STAX) #include #define UPPER_MARGIN 4 -#define BUTTON_RADIUS 6 #define BUTTON_DIAMETER 80 nbgl_image_t *passphrase_length_set_icon() { diff --git a/src/stax/passphrase_length_screen.h b/src/stax/passphrase_length_screen.h index bc8e4e1a..2c680a1d 100644 --- a/src/stax/passphrase_length_screen.h +++ b/src/stax/passphrase_length_screen.h @@ -1,6 +1,6 @@ #pragma once -#if defined(TARGET_FATSTACKS) +#if defined(TARGET_STAX) nbgl_image_t *passphrase_length_set_icon(void); nbgl_text_area_t *passphrase_length_set_title(nbgl_obj_t *align_to); diff --git a/src/stax/ui_stax.c b/src/stax/ui_stax.c index c2c00602..a84b2d8f 100644 --- a/src/stax/ui_stax.c +++ b/src/stax/ui_stax.c @@ -1,8 +1,8 @@ #include #include -#include "../constants.h" -#include "../glyphs.h" +#include "constants.h" +#include "glyphs.h" #if defined(HAVE_NBGL) @@ -78,6 +78,7 @@ enum { BUTTON_18_INDEX, BUTTON_24_INDEX, BACK_BUTTON_INDEX, + KBD_TEXT_TOKEN, NB_CHILDREN }; @@ -198,7 +199,7 @@ static void key_press_callback(const char touchedKey) { mask = bolos_ux_bip39_get_keyboard_mask((unsigned char *) &(textToEnter[0]), strlen(textToEnter)); } - nbgl_layoutUpdateKeyboard(layout, keyboardIndex, mask); + nbgl_layoutUpdateKeyboard(layout, keyboardIndex, mask, false, LOWER_CASE); nbgl_layoutUpdateEnteredText(layout, textIndex, false, 0, &(textToEnter[0]), false); nbgl_refresh(); } @@ -207,7 +208,6 @@ static void display_keyboard_page() { nbgl_layoutDescription_t layoutDescription = {.modal = false, .onActionCallback = &keyboard_dispatcher}; nbgl_layoutKbd_t kbdInfo = {.lettersOnly = true, // use only letters - .upperCase = false, // start with lower case letters .mode = MODE_LETTERS, // start in letters mode .keyMask = 0, // no inactive key .callback = &key_press_callback}; @@ -240,7 +240,8 @@ static void display_keyboard_page() { get_current_word_number() + 1, // number to use textToEnter, // text to display false, // not grayed-out - 32); // vertical margin from the buttons + 32, // vertical margin from the buttons + KBD_TEXT_TOKEN); nbgl_layoutDraw(layout); } diff --git a/src/stax/ux_stax.c b/src/stax/ux_stax.c index e0033e02..8a6ced5c 100644 --- a/src/stax/ux_stax.c +++ b/src/stax/ux_stax.c @@ -4,7 +4,7 @@ #include "../ux_common/common_bip39.h" #include "./ux_stax.h" -#if defined(HAVE_BOLOS_UX) && defined(TARGET_FATSTACKS) +#if defined(HAVE_BOLOS_UX) && defined(TARGET_STAX) typedef struct buffer { // the mnemonic passphrase, built over time diff --git a/src/stax/ux_stax.h b/src/stax/ux_stax.h index 21698dd3..d4ee5288 100644 --- a/src/stax/ux_stax.h +++ b/src/stax/ux_stax.h @@ -19,7 +19,7 @@ #include #include "constants.h" -#if defined(HAVE_BOLOS_UX) && defined(TARGET_FATSTACKS) +#if defined(HAVE_BOLOS_UX) && defined(TARGET_STAX) #define MAX_MNEMONIC_LENGTH (MNEMONIC_SIZE_24 * (MAX_WORD_LENGTH + 1)) diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index f57e41e8..907c8b02 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -37,7 +37,7 @@ endif() add_compile_definitions(WIDE=) add_compile_definitions(TEST) add_compile_definitions(HAVE_BOLOS_UX) -add_compile_definitions(TARGET_FATSTACKS) +add_compile_definitions(TARGET_STAX) add_compile_definitions(OS_IO_SEPROXYHAL) include_directories(../../src ./mocks/) diff --git a/tests/unit/test_stax_mnemonic.c b/tests/unit/test_stax_mnemonic.c index 5df33791..c3df8c65 100644 --- a/tests/unit/test_stax_mnemonic.c +++ b/tests/unit/test_stax_mnemonic.c @@ -1,8 +1,6 @@ #include -/* #include */ #include #include -/* #include */ #include #include From f44d0f3fe7f539d24ac799dde5e15254920060ef Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Tue, 7 Mar 2023 17:05:36 +0100 Subject: [PATCH 41/65] [ci][add] Using reusable workflows --- .../workflows/build_and_functional_tests.yml | 59 ++++++++ .github/workflows/ci-workflow.yml | 129 ------------------ .github/workflows/guidelines_enforcer.yml | 23 ++++ 3 files changed, 82 insertions(+), 129 deletions(-) create mode 100644 .github/workflows/build_and_functional_tests.yml delete mode 100644 .github/workflows/ci-workflow.yml create mode 100644 .github/workflows/guidelines_enforcer.yml diff --git a/.github/workflows/build_and_functional_tests.yml b/.github/workflows/build_and_functional_tests.yml new file mode 100644 index 00000000..bd909ab9 --- /dev/null +++ b/.github/workflows/build_and_functional_tests.yml @@ -0,0 +1,59 @@ +name: Build, test (unit, functional) and scan the application + +on: + workflow_dispatch: + push: + branches: + - master + - main + - develop + pull_request: + +jobs: + build_application: + name: Build application using the reusable workflow + uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_build.yml@v1 + with: + upload_app_binaries_artifact: compiled_app_binaries + + ragger_tests: + name: Run ragger tests using the reusable workflow + needs: build_application + uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_ragger_tests.yml@v1 + with: + download_app_binaries_artifact: compiled_app_binaries + test_dir: tests/functional + run_for_devices: '["stax"]' + + unittesting: + name: C unit testing + runs-on: ubuntu-latest + steps: + - name: Clone + uses: actions/checkout@v3 + - name: Install cmocka + run: | + sudo apt update + sudo apt install libcmocka-dev lcov + - name: Compile the tests + run: | + cd tests/unit/ + rm -rf build/ + cmake -B build -H. + make -C build + - name: Run the tests + run: | + cd tests/unit/ + CTEST_OUTPUT_ON_FAILURE=1 make -C build test + - name: Generate code coverage + run: | + cd tests/unit/ + lcov --directory . -b "$(realpath build/)" --capture --initial -o coverage.base + lcov --rc lcov_branch_coverage=1 --directory . -b "$(realpath build/)" --capture -o coverage.capture + lcov --directory . -b "$(realpath build/)" --add-tracefile coverage.base --add-tracefile coverage.capture -o coverage.info + lcov --directory . -b "$(realpath build/)" --remove coverage.info '*/unit-tests/*' -o coverage.info + genhtml coverage.info -o coverage + - uses: actions/upload-artifact@v3 + with: + name: code-coverage + path: tests/unit/coverage diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml deleted file mode 100644 index 1c3983f9..00000000 --- a/.github/workflows/ci-workflow.yml +++ /dev/null @@ -1,129 +0,0 @@ -name: Compilation & tests - -on: - workflow_dispatch: - push: - branches: - - master - - develop - pull_request: - paths-ignore: - - '.github/workflows/*.yml' - -jobs: - unittesting: - name: C unit testing - runs-on: ubuntu-latest - steps: - - name: Clone - uses: actions/checkout@v3 - - name: Install cmocka - run: | - sudo apt update - sudo apt install libcmocka-dev lcov - - name: Compile the tests - run: | - cd tests/unit/ - rm -rf build/ - cmake -B build -H. - make -C build - - name: Run the tests - run: | - cd tests/unit/ - CTEST_OUTPUT_ON_FAILURE=1 make -C build test - - name: Generate code coverage - run: | - cd tests/unit/ - lcov --directory . -b "$(realpath build/)" --capture --initial -o coverage.base - lcov --rc lcov_branch_coverage=1 --directory . -b "$(realpath build/)" --capture -o coverage.capture - lcov --directory . -b "$(realpath build/)" --add-tracefile coverage.base --add-tracefile coverage.capture -o coverage.info - lcov --directory . -b "$(realpath build/)" --remove coverage.info '*/unit-tests/*' -o coverage.info - genhtml coverage.info -o coverage - - uses: actions/upload-artifact@v3 - with: - name: code-coverage - path: tests/unit/coverage - - nano_build: - name: Build application for NanoS, X and S+ - strategy: - fail-fast: false - matrix: - include: - - SDK: "$NANOS_SDK" - model: nanos - - SDK: "$NANOX_SDK" - model: nanox - - SDK: "$NANOSP_SDK" - model: nanosp - runs-on: ubuntu-latest - container: - image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder:latest - steps: - - name: Clone - uses: actions/checkout@v3 - - - name: Build - run: | - git config --global --add safe.directory "$GITHUB_WORKSPACE" - make BOLOS_SDK=${{ matrix.SDK }} - mv bin/app.elf "bin/${{ matrix.model }}.elf" - - name: Upload app binary - uses: actions/upload-artifact@v3 - with: - name: apps - path: bin/*.elf - - stax_build: - name: Build application for Stax - runs-on: ubuntu-latest - container: - image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder:latest - steps: - - name: Clone application - uses: actions/checkout@v3 - - name: Clone SDK - uses: actions/checkout@v3 - with: - repository: LedgerHQ/ledger-secure-sdk - path: ./stax-sdk - ref: fatstacks_1.0.0-rc9 - - name: Build - run: | - git config --global --add safe.directory "$GITHUB_WORKSPACE" - make BOLOS_SDK=./stax-sdk TARGET=fatstacks - mv bin/app.elf "bin/stax.elf" - - name: Upload app binary - uses: actions/upload-artifact@v3 - with: - name: apps - path: bin/*.elf - - job_scan_build: - name: Clang Static Analyzer - strategy: - fail-fast: false - matrix: - include: - - SDK: "$NANOS_SDK" - model: nanos - - SDK: "$NANOX_SDK" - model: nanox - - SDK: "$NANOSP_SDK" - model: nanosp - runs-on: ubuntu-latest - container: - image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder:latest - steps: - - name: Clone - uses: actions/checkout@v3 - - name: Build with Clang Static Analyzer - run: | - make clean - scan-build --use-cc=clang -analyze-headers -enable-checker security -enable-checker unix -enable-checker valist -o scan-build --status-bugs make default BOLOS_SDK=${{ matrix.SDK }} - - name: Upload scan result - uses: actions/upload-artifact@v3 - if: failure() - with: - name: scan-build - path: scan-build diff --git a/.github/workflows/guidelines_enforcer.yml b/.github/workflows/guidelines_enforcer.yml new file mode 100644 index 00000000..fdaf9f27 --- /dev/null +++ b/.github/workflows/guidelines_enforcer.yml @@ -0,0 +1,23 @@ +name: Ensure compliance with Ledger guidelines + +# This workflow is mandatory in all applications +# It calls a reusable workflow guidelines_enforcer developed by Ledger's internal developer team. +# The successful completion of the reusable workflow is a mandatory step for an app to be available on the Ledger +# application store. +# +# More information on the guidelines can be found in the repository: +# LedgerHQ/ledger-app-workflows/ + +on: + workflow_dispatch: + push: + branches: + - master + - main + - develop + pull_request: + +jobs: + guidelines_enforcer: + name: Call Ledger guidelines_enforcer + uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_guidelines_enforcer.yml@v1 From 8e18ae54842dd38c5fbb932f83a982733d6b4268 Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Wed, 8 Mar 2023 09:09:34 +0100 Subject: [PATCH 42/65] [update] Using ragger default conftest.py --- tests/functional/conftest.py | 126 ++++----------------- tests/functional/test_fatstacks_full.py | 23 ++-- tests/functional/test_fatstacks_options.py | 30 ++--- tests/functional/utils.py | 4 +- 4 files changed, 44 insertions(+), 139 deletions(-) diff --git a/tests/functional/conftest.py b/tests/functional/conftest.py index c2d6961d..d27e59c2 100644 --- a/tests/functional/conftest.py +++ b/tests/functional/conftest.py @@ -1,110 +1,30 @@ -import pytest -from pathlib import Path -from typing import Optional +from pytest import fixture +from ragger.backend import BackendInterface +from ragger.conftest import configuration -from ragger.firmware import Firmware -from ragger.backend import SpeculosBackend, LedgerCommBackend, LedgerWalletBackend, BackendInterface +from .app import Screen +from .utils import assert_current_equals, SCREENSHOTS +########################### +### CONFIGURATION START ### +########################### -def __str__(self): # also tried __repr__() - # Attempt to print the 'select' attribute in "pytest -v" output - return self.select +# You can configure optional parameters by overriding the value of ragger.configuration.OPTIONAL_CONFIGURATION +# Please refer to ragger/conftest/configuration.py for their descriptions and accepted values +######################### +### CONFIGURATION END ### +######################### -APPLICATION = (Path(__file__).parent.parent / "elfs" / "recovery_check.elf").resolve() -BACKENDS = ["speculos", "ledgercomm", "ledgerwallet"] -DEVICES = ["nanos", "nanox", "nanosp", "stax", "all"] -FIRMWARES = [ - Firmware('stax', '1.0'), -] +# Pull all features from the base ragger conftest using the overridden configuration +pytest_plugins = ("ragger.conftest.base_conftest", ) +from time import sleep -def pytest_addoption(parser): - parser.addoption("--device", choices=DEVICES, required=True) - parser.addoption("--backend", choices=BACKENDS, default="speculos") - parser.addoption("--display", action="store_true", default=False) - parser.addoption("--golden_run", action="store_true", default=False) - parser.addoption("--log_apdu_file", action="store", default=None) - - -@pytest.fixture(scope="session") -def backend_name(pytestconfig): - return pytestconfig.getoption("backend") - - -@pytest.fixture(scope="session") -def display(pytestconfig): - return pytestconfig.getoption("display") - - -@pytest.fixture(scope="session") -def golden_run(pytestconfig): - return pytestconfig.getoption("golden_run") - - -@pytest.fixture(scope="session") -def log_apdu_file(pytestconfig): - filename = pytestconfig.getoption("log_apdu_file") - return Path(filename).resolve() if filename is not None else None - - -# Glue to call every test that depends on the firmware once for each required firmware -def pytest_generate_tests(metafunc): - if "firmware" in metafunc.fixturenames: - fw_list = [] - ids = [] - device = metafunc.config.getoption("device") - backend_name = metafunc.config.getoption("backend") - if device == "all": - if backend_name != "speculos": - raise ValueError("Invalid device parameter on this backend") - # Add all supported firmwares - for fw in FIRMWARES: - fw_list.append(fw) - ids.append(fw.device + " " + fw.version) - else: - # Enable firmware for demanded device - for fw in FIRMWARES: - if device == fw.device: - fw_list.append(fw) - ids.append(fw.device + " " + fw.version) - metafunc.parametrize("firmware", fw_list, ids=ids, scope="session") - - -def prepare_speculos_args(firmware: Firmware, display: bool): - speculos_args = [] - if display: - speculos_args += ["--display", "qt"] - return ([APPLICATION], {"args": speculos_args}) - - -def create_backend(backend_name: str, firmware: Firmware, display: bool, log_apdu_file: Optional[Path]): - if backend_name.lower() == "ledgercomm": - return LedgerCommBackend(firmware=firmware, interface="hid", log_apdu_file=log_apdu_file) - elif backend_name.lower() == "ledgerwallet": - return LedgerWalletBackend(firmware=firmware, log_apdu_file=log_apdu_file) - elif backend_name.lower() == "speculos": - args, kwargs = prepare_speculos_args(firmware, display) - return SpeculosBackend(*args, firmware=firmware, log_apdu_file=log_apdu_file, **kwargs) - else: - raise ValueError(f"Backend '{backend_name}' is unknown. Valid backends are: {BACKENDS}") - - -@pytest.fixture -def client(backend_name, firmware, display, log_apdu_file): - with create_backend(backend_name, firmware, display, log_apdu_file) as b: - yield b - - -@pytest.fixture(autouse=True) -def use_only_on_backend(request, backend_name: str): - if request.node.get_closest_marker('use_on_backend'): - current_backend = request.node.get_closest_marker('use_on_backend').args[0] - if current_backend != backend: - pytest.skip(f'skipped on this backend: "{current_backend}"') - - -def pytest_configure(config): - config.addinivalue_line( - "markers", "use_only_on_backend(backend): skip test if not on the specified backend", - ) +@fixture +def screen(backend: BackendInterface): + s = Screen(backend, backend.firmware) + backend.finger_touch(600, 0) + sleep(1) + assert_current_equals(backend, SCREENSHOTS / "welcome.png") + return s diff --git a/tests/functional/test_fatstacks_full.py b/tests/functional/test_fatstacks_full.py index 35a58530..a1d43607 100644 --- a/tests/functional/test_fatstacks_full.py +++ b/tests/functional/test_fatstacks_full.py @@ -11,43 +11,36 @@ "february uncover one trip resource lawn turtle enact monster " \ "seven myth punch hobby comfort wild raise skin" -@fixture -def screen(client: BackendInterface): - s = Screen(client, client.firmware) - client.finger_touch(600, 0) - assert_current_equals(client, SCREENSHOTS / "welcome.png") - return s - -def test_nominal_full_passphrase_check(screen: Screen, client: BackendInterface): +def test_nominal_full_passphrase_check(screen: Screen, backend: BackendInterface): # going to choose mnemonic length screen.home.action() - assert_current_equals(client, SCREENSHOTS / "passphrase_length.png") + assert_current_equals(backend, SCREENSHOTS / "passphrase_length.png") # choosing 3d (24 words) screen.choice_list.choose(1) - assert_current_equals(client, SCREENSHOTS / "first_24.png") + assert_current_equals(backend, SCREENSHOTS / "first_24.png") for word in SPECULOS_MNEMONIC.split(): # 4 letters are enough to discriminate the correct word screen.keyboard.write(word[:4]) # choosing 1st suggestion screen.suggestions.choose(1) sleep(0.1) - assert_current_equals(client, SCREENSHOTS / "correct.png") + assert_current_equals(backend, SCREENSHOTS / "correct.png") screen.dismiss.tap() # exit the result screen to the home page screen.exit() -def test_nominal_full_passphrase_check_error_wrong_passphrase(screen: Screen, client: BackendInterface): +def test_nominal_full_passphrase_check_error_wrong_passphrase(screen: Screen, backend: BackendInterface): screen.home.action() - assert_current_equals(client, SCREENSHOTS / "passphrase_length.png") + assert_current_equals(backend, SCREENSHOTS / "passphrase_length.png") # choosing 1st (12 words) screen.choice_list.choose(3) - assert_current_equals(client, SCREENSHOTS / "first_12.png") + assert_current_equals(backend, SCREENSHOTS / "first_12.png") # only the 12 first words for word in SPECULOS_MNEMONIC.split()[:12]: screen.keyboard.write(word[:4]) screen.suggestions.choose(1) sleep(0.1) - assert_current_equals(client, SCREENSHOTS / "incorrect.png") + assert_current_equals(backend, SCREENSHOTS / "incorrect.png") screen.dismiss.tap() # exit the result screen to the home page screen.exit() diff --git a/tests/functional/test_fatstacks_options.py b/tests/functional/test_fatstacks_options.py index bfc820cc..23867e53 100644 --- a/tests/functional/test_fatstacks_options.py +++ b/tests/functional/test_fatstacks_options.py @@ -9,36 +9,28 @@ "february uncover one trip resource lawn turtle enact monster " \ "seven myth punch hobby comfort wild raise skin" -@fixture -def screen(client: BackendInterface): - s = Screen(client, client.firmware) - client.finger_touch(600, 0) - assert_current_equals(client, SCREENSHOTS / "welcome.png") - return s - - -def test_check_info_then_leave(screen: Screen, client: BackendInterface): +def test_check_info_then_leave(screen: Screen, backend: BackendInterface): screen.home.info() - assert_current_equals(client, SCREENSHOTS / "info.png") + assert_current_equals(backend, SCREENSHOTS / "info.png") screen.quit_info.tap() - assert_current_equals(client, SCREENSHOTS / "welcome.png") + assert_current_equals(backend, SCREENSHOTS / "welcome.png") screen.exit() -def test_check_all_passphrase_lengths(screen: Screen, client: BackendInterface): +def test_check_all_passphrase_lengths(screen: Screen, backend: BackendInterface): screen.home.action() - assert_current_equals(client, SCREENSHOTS / "passphrase_length.png") + assert_current_equals(backend, SCREENSHOTS / "passphrase_length.png") for choice, length in [(1, 24), (2, 18), (3, 12)]: screen.choice_list.choose(choice) - assert_current_equals(client, SCREENSHOTS / f"first_{length}.png") + assert_current_equals(backend, SCREENSHOTS / f"first_{length}.png") screen.navigation.tap() - assert_current_equals(client, SCREENSHOTS / "passphrase_length.png") + assert_current_equals(backend, SCREENSHOTS / "passphrase_length.png") -def test_check_previous_word(screen: Screen, client: BackendInterface): +def test_check_previous_word(screen: Screen, backend: BackendInterface): screen.home.action() screen.choice_list.choose(1) - assert_current_equals(client, SCREENSHOTS / "first_24.png") + assert_current_equals(backend, SCREENSHOTS / "first_24.png") tries = ["rand", "ok"] for word in tries: screen.keyboard.write(word[:4]) @@ -46,7 +38,7 @@ def test_check_previous_word(screen: Screen, client: BackendInterface): # coming back N time, should bring back to the first word page for _ in tries: screen.navigation.tap() - assert_current_equals(client, SCREENSHOTS / "first_24.png") + assert_current_equals(backend, SCREENSHOTS / "first_24.png") # one more 'back' tap will bring us to the passphrase length choice page screen.navigation.tap() - assert_current_equals(client, SCREENSHOTS / "passphrase_length.png") + assert_current_equals(backend, SCREENSHOTS / "passphrase_length.png") diff --git a/tests/functional/utils.py b/tests/functional/utils.py index 4056f211..dfc018d9 100644 --- a/tests/functional/utils.py +++ b/tests/functional/utils.py @@ -6,8 +6,8 @@ SCREENSHOTS = (Path(__file__).parent.parent / "screenshots").resolve() -def assert_current_equals(client: BackendInterface, existing: Path): - current = client._client.get_screenshot() +def assert_current_equals(backend: BackendInterface, existing: Path): + current = backend._client.get_screenshot() assert_equal(current, existing) From 3a7521971a85e481598dba87a820726e6742a02b Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Wed, 8 Mar 2023 09:17:46 +0100 Subject: [PATCH 43/65] [clean] Comply with guidelines --- ...tests.yml => build_tests_and_analysis.yml} | 0 .github/workflows/guidelines_enforcer.yml | 8 -------- Makefile | 1 - glyphs/icon_bat_ovl_charging.gif | Bin 53 -> 47 bytes src/nano/ux_nanos.h | 4 ++-- src/nano/ux_nanox.h | 4 ++-- src/stax/ux_stax.c | 2 +- src/stax/ux_stax.h | 4 ++-- tests/screenshots/welcome.png | Bin 7944 -> 7961 bytes tests/unit/CMakeLists.txt | 1 - 10 files changed, 7 insertions(+), 17 deletions(-) rename .github/workflows/{build_and_functional_tests.yml => build_tests_and_analysis.yml} (100%) diff --git a/.github/workflows/build_and_functional_tests.yml b/.github/workflows/build_tests_and_analysis.yml similarity index 100% rename from .github/workflows/build_and_functional_tests.yml rename to .github/workflows/build_tests_and_analysis.yml diff --git a/.github/workflows/guidelines_enforcer.yml b/.github/workflows/guidelines_enforcer.yml index fdaf9f27..5ab28b9f 100644 --- a/.github/workflows/guidelines_enforcer.yml +++ b/.github/workflows/guidelines_enforcer.yml @@ -1,13 +1,5 @@ name: Ensure compliance with Ledger guidelines -# This workflow is mandatory in all applications -# It calls a reusable workflow guidelines_enforcer developed by Ledger's internal developer team. -# The successful completion of the reusable workflow is a mandatory step for an app to be available on the Ledger -# application store. -# -# More information on the guidelines can be found in the repository: -# LedgerHQ/ledger-app-workflows/ - on: workflow_dispatch: push: diff --git a/Makefile b/Makefile index 44ab13ca..1669cfe3 100755 --- a/Makefile +++ b/Makefile @@ -57,7 +57,6 @@ DEFINES += BOLOS_APP_ICON_SIZE_B=\(9+32\) #DEFINES += HAVE_ELECTRUM DEFINES += IO_USB_MAX_ENDPOINTS=4 IO_HID_EP_LENGTH=64 DEFINES += HAVE_SPRINTF -DEFINES += HAVE_BOLOS_UX ifneq ($(TARGET_NAME), TARGET_STAX) $(info Using BAGL) diff --git a/glyphs/icon_bat_ovl_charging.gif b/glyphs/icon_bat_ovl_charging.gif index 680947163d14dc6cbfcf41860e4c0b285cfc44b7..c7b7d6a1d005f5c048cd43e2ca032be662081848 100644 GIT binary patch delta 32 lcmXrjmvZ-Xv#?BLW?*CZzyJdO|NmF~$-=6_GT#!5i6B|pB*o+)j25SHaWeA7> diff --git a/src/nano/ux_nanos.h b/src/nano/ux_nanos.h index 994ceda4..d94e60a8 100644 --- a/src/nano/ux_nanos.h +++ b/src/nano/ux_nanos.h @@ -18,7 +18,7 @@ #include "../ux_common/common.h" -#if defined(HAVE_BOLOS_UX) && defined(TARGET_NANOS) +#if defined(TARGET_NANOS) typedef unsigned int (*callback_t)(unsigned int); @@ -82,4 +82,4 @@ extern const bagl_element_t screen_onboarding_word_list_elements[9]; void compare_recovery_phrase(void); -#endif // HAVE_BOLOS_UX && TARGET_NANOS +#endif // TARGET_NANOS diff --git a/src/nano/ux_nanox.h b/src/nano/ux_nanox.h index 8bd8fc2a..d10259d9 100644 --- a/src/nano/ux_nanox.h +++ b/src/nano/ux_nanox.h @@ -18,7 +18,7 @@ #include "ux_common/common.h" -#if defined(HAVE_BOLOS_UX) && (defined(TARGET_NANOX) || defined(TARGET_NANOS2)) +#if (defined(TARGET_NANOX) || defined(TARGET_NANOS2)) // bolos ux context (not mandatory if redesigning a bolos ux) typedef struct bolos_ux_context { @@ -98,4 +98,4 @@ void screen_common_keyboard_init(unsigned int stack_slot, // to be included into all flow that needs to go back to the dashboard extern const ux_flow_step_t ux_ob_goto_dashboard_step; -#endif // HAVE_BOLOS_UX_H && (TARGET_NANOX || TARGET_NANOS2) +#endif // (TARGET_NANOX || TARGET_NANOS2) diff --git a/src/stax/ux_stax.c b/src/stax/ux_stax.c index 8a6ced5c..04053629 100644 --- a/src/stax/ux_stax.c +++ b/src/stax/ux_stax.c @@ -4,7 +4,7 @@ #include "../ux_common/common_bip39.h" #include "./ux_stax.h" -#if defined(HAVE_BOLOS_UX) && defined(TARGET_STAX) +#if defined(TARGET_STAX) typedef struct buffer { // the mnemonic passphrase, built over time diff --git a/src/stax/ux_stax.h b/src/stax/ux_stax.h index d4ee5288..1bd981c5 100644 --- a/src/stax/ux_stax.h +++ b/src/stax/ux_stax.h @@ -19,7 +19,7 @@ #include #include "constants.h" -#if defined(HAVE_BOLOS_UX) && defined(TARGET_STAX) +#if defined(TARGET_STAX) #define MAX_MNEMONIC_LENGTH (MNEMONIC_SIZE_24 * (MAX_WORD_LENGTH + 1)) @@ -68,4 +68,4 @@ size_t add_word_in_mnemonic(const char* const buffer, const size_t size); char* get_mnemonic(); #endif -#endif // HAVE_BOLOS_UX && TARGET_NANOS +#endif // TARGET_NANOS diff --git a/tests/screenshots/welcome.png b/tests/screenshots/welcome.png index aef905c901c3fe7325933b0f4d9c072d0ac4363c..34a689979779946a088ed382543a75ff67841fec 100644 GIT binary patch delta 3549 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 delta 3531 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 ZyYDuSfBnnWSZ)vRG0-=;Rd^E}{2!i;9Wej^ diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 907c8b02..3c5f93d9 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -36,7 +36,6 @@ endif() add_compile_definitions(WIDE=) add_compile_definitions(TEST) -add_compile_definitions(HAVE_BOLOS_UX) add_compile_definitions(TARGET_STAX) add_compile_definitions(OS_IO_SEPROXYHAL) From ce31523bffdc03a03f03c5fb55970a0f3f9e7126 Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Thu, 9 Mar 2023 09:29:13 +0100 Subject: [PATCH 44/65] [clean] Using ragger.navigation feature for functional testing --- tests/functional/app.py | 24 ++--- tests/functional/conftest.py | 23 ++-- tests/functional/navigator.py | 62 +++++++++++ .../check_all_passphrase_lengths/00000.png | 1 + .../check_all_passphrase_lengths/00001.png | 1 + .../check_all_passphrase_lengths/00002.png | 1 + .../check_all_passphrase_lengths/00003.png | 1 + .../check_all_passphrase_lengths/00004.png | 1 + .../check_all_passphrase_lengths/00005.png | 1 + .../check_all_passphrase_lengths/00006.png | 1 + .../check_all_passphrase_lengths/00007.png | 1 + .../stax/check_info_then_leave/00000.png | 1 + .../stax/check_info_then_leave/00001.png | 1 + .../stax/check_info_then_leave/00002.png | 1 + .../stax/check_previous_word/00000.png | 1 + .../stax/check_previous_word/00001.png | 1 + .../stax/check_previous_word/00002.png | 1 + .../stax/check_previous_word/00003.png | Bin 0 -> 4610 bytes .../stax/check_previous_word/00004.png | 1 + .../stax/check_previous_word/00005.png | Bin 0 -> 4594 bytes .../stax/check_previous_word/00006.png | 1 + .../stax/check_previous_word/00007.png | 1 + .../stax/check_previous_word/00008.png | 1 + .../stax/check_previous_word/00009.png | 1 + .../stax/check_previous_word/00010.png | 1 + .../snapshots/stax}/correct.png | Bin tests/functional/snapshots/stax/first_12.png | Bin 0 -> 4889 bytes tests/functional/snapshots/stax/first_18.png | Bin 0 -> 4897 bytes tests/functional/snapshots/stax/first_24.png | Bin 0 -> 4906 bytes .../snapshots/stax}/incorrect.png | Bin .../snapshots/stax}/info.png | Bin .../00000.png | 1 + .../00000.png | 1 + .../snapshots/stax}/passphrase_length.png | Bin tests/functional/snapshots/stax/second_24.png | Bin 0 -> 4956 bytes tests/functional/snapshots/stax/third_24.png | Bin 0 -> 5047 bytes .../snapshots/stax}/welcome.png | Bin tests/functional/test_fatstacks_full.py | 99 +++++++++++------- tests/functional/test_fatstacks_options.py | 89 +++++++++------- tests/functional/utils.py | 27 ++--- tests/screenshots/first_12.png | Bin 4893 -> 0 bytes tests/screenshots/first_18.png | Bin 4901 -> 0 bytes tests/screenshots/first_24.png | Bin 4911 -> 0 bytes 43 files changed, 230 insertions(+), 116 deletions(-) create mode 100644 tests/functional/navigator.py create mode 120000 tests/functional/snapshots/stax/check_all_passphrase_lengths/00000.png create mode 120000 tests/functional/snapshots/stax/check_all_passphrase_lengths/00001.png create mode 120000 tests/functional/snapshots/stax/check_all_passphrase_lengths/00002.png create mode 120000 tests/functional/snapshots/stax/check_all_passphrase_lengths/00003.png create mode 120000 tests/functional/snapshots/stax/check_all_passphrase_lengths/00004.png create mode 120000 tests/functional/snapshots/stax/check_all_passphrase_lengths/00005.png create mode 120000 tests/functional/snapshots/stax/check_all_passphrase_lengths/00006.png create mode 120000 tests/functional/snapshots/stax/check_all_passphrase_lengths/00007.png create mode 120000 tests/functional/snapshots/stax/check_info_then_leave/00000.png create mode 120000 tests/functional/snapshots/stax/check_info_then_leave/00001.png create mode 120000 tests/functional/snapshots/stax/check_info_then_leave/00002.png create mode 120000 tests/functional/snapshots/stax/check_previous_word/00000.png create mode 120000 tests/functional/snapshots/stax/check_previous_word/00001.png create mode 120000 tests/functional/snapshots/stax/check_previous_word/00002.png create mode 100644 tests/functional/snapshots/stax/check_previous_word/00003.png create mode 120000 tests/functional/snapshots/stax/check_previous_word/00004.png create mode 100644 tests/functional/snapshots/stax/check_previous_word/00005.png create mode 120000 tests/functional/snapshots/stax/check_previous_word/00006.png create mode 120000 tests/functional/snapshots/stax/check_previous_word/00007.png create mode 120000 tests/functional/snapshots/stax/check_previous_word/00008.png create mode 120000 tests/functional/snapshots/stax/check_previous_word/00009.png create mode 120000 tests/functional/snapshots/stax/check_previous_word/00010.png rename tests/{screenshots => functional/snapshots/stax}/correct.png (100%) create mode 100644 tests/functional/snapshots/stax/first_12.png create mode 100644 tests/functional/snapshots/stax/first_18.png create mode 100644 tests/functional/snapshots/stax/first_24.png rename tests/{screenshots => functional/snapshots/stax}/incorrect.png (100%) rename tests/{screenshots => functional/snapshots/stax}/info.png (100%) create mode 120000 tests/functional/snapshots/stax/nominal_full_passphrase_check_error_wrong_passphrase/00000.png create mode 120000 tests/functional/snapshots/stax/nominal_full_passphrase_check_ok/00000.png rename tests/{screenshots => functional/snapshots/stax}/passphrase_length.png (100%) create mode 100644 tests/functional/snapshots/stax/second_24.png create mode 100644 tests/functional/snapshots/stax/third_24.png rename tests/{screenshots => functional/snapshots/stax}/welcome.png (100%) delete mode 100644 tests/screenshots/first_12.png delete mode 100644 tests/screenshots/first_18.png delete mode 100644 tests/screenshots/first_24.png diff --git a/tests/functional/app.py b/tests/functional/app.py index f8b4dd1b..47e363eb 100644 --- a/tests/functional/app.py +++ b/tests/functional/app.py @@ -1,6 +1,6 @@ -from ragger.firmware.stax.layouts import ExitFooter, CenteredFooter, _Layout, LetterOnlyKeyboard, \ +from ragger.firmware.stax.layouts import CenteredFooter, _Layout, LetterOnlyKeyboard, \ NavigationHeader, Suggestions -from ragger.firmware.stax.use_cases import UseCaseHomeExt +from ragger.firmware.stax.use_cases import UseCaseHomeExt, UseCaseSettings from ragger.firmware.stax.screen import MetaScreen @@ -13,20 +13,18 @@ def choose(self, index: int): self.client.finger_touch(x, y + (index - 1)*diff) -class Screen(metaclass=MetaScreen): +class StaxScreen(metaclass=MetaScreen): + # choosing the length a the passphrase. 3 choices layout_choice_list = CustomChoiceList + # entering words layout_keyboard = LetterOnlyKeyboard + # word suggestions. 4 choices layout_suggestions = Suggestions + # going back to the previous screen layout_navigation = NavigationHeader - layout_quit_info = ExitFooter + # Dismiss the final, result screen ("your passphrase is correct / not correct) to go back to the welcome screen layout_dismiss = CenteredFooter + # classic welcome screen with tappable center use_case_home = UseCaseHomeExt - - def exit(self): - did_raise = False - try: - self.home.quit() - except: - did_raise = True - if not did_raise: - raise RuntimeError("The application did not exit at this state") + # classic settings screen + use_case_settings = UseCaseSettings diff --git a/tests/functional/conftest.py b/tests/functional/conftest.py index d27e59c2..a140d9e8 100644 --- a/tests/functional/conftest.py +++ b/tests/functional/conftest.py @@ -1,9 +1,11 @@ +from pathlib import Path from pytest import fixture from ragger.backend import BackendInterface from ragger.conftest import configuration +from ragger.firmware import Firmware + +from .navigator import StaxNavigator -from .app import Screen -from .utils import assert_current_equals, SCREENSHOTS ########################### ### CONFIGURATION START ### @@ -19,12 +21,15 @@ # Pull all features from the base ragger conftest using the overridden configuration pytest_plugins = ("ragger.conftest.base_conftest", ) -from time import sleep +FUNCTIONAL_TESTS_DIR = Path("tests/functional/").resolve() + + +@fixture(scope="session") +def functional_test_directory() -> Path: + yield FUNCTIONAL_TESTS_DIR + @fixture -def screen(backend: BackendInterface): - s = Screen(backend, backend.firmware) - backend.finger_touch(600, 0) - sleep(1) - assert_current_equals(backend, SCREENSHOTS / "welcome.png") - return s +def navigator(backend: BackendInterface, firmware: Firmware) -> StaxNavigator: + navigator = StaxNavigator(backend, firmware) + yield navigator diff --git a/tests/functional/navigator.py b/tests/functional/navigator.py new file mode 100644 index 00000000..c89579af --- /dev/null +++ b/tests/functional/navigator.py @@ -0,0 +1,62 @@ +from enum import auto, Enum +from functools import partial +from ragger.navigator.navigator import Navigator +from time import sleep + +from .app import StaxScreen + + +class NavInsID(Enum): + # generic instructions + WAIT = auto() + TOUCH = auto() + # home screen + HOME_TO_SETTINGS = auto() + HOME_TO_QUIT = auto() + HOME_TO_CHECK = auto() + # settings + SETTINGS_TO_HOME = auto() + # Recovery phrase length choice + LENGTH_CHOOSE_24 = auto() + LENGTH_CHOOSE_18 = auto() + LENGTH_CHOOSE_12 = auto() + LENGTH_TO_PREVIOUS = auto() + # option with a keyboard (enter a word of the passphrase) + KEYBOARD_TO_PREVIOUS = auto() + KEYBOARD_WRITE = auto() + KEYBOARD_SELECT_SUGGESTION = auto() + # result screen, one action: going back to home screen + RESULT_TO_HOME = auto() + + +class StaxNavigator(Navigator): + + def __init__(self, backend, firmware): + self.screen = StaxScreen(backend, firmware) + + callbacks = { + NavInsID.WAIT: sleep, + NavInsID.TOUCH: backend.finger_touch, + NavInsID.HOME_TO_SETTINGS: self.screen.home.settings, + NavInsID.HOME_TO_QUIT: self.screen.home.quit, + NavInsID.HOME_TO_CHECK: self.screen.home.action, + NavInsID.SETTINGS_TO_HOME: self.screen.settings.single_page_exit, + NavInsID.LENGTH_CHOOSE_24: partial(self.screen.choice_list.choose, 1), + NavInsID.LENGTH_CHOOSE_18: partial(self.screen.choice_list.choose, 2), + NavInsID.LENGTH_CHOOSE_12: partial(self.screen.choice_list.choose, 3), + NavInsID.LENGTH_TO_PREVIOUS: self.screen.navigation.tap, + NavInsID.KEYBOARD_TO_PREVIOUS: self.screen.navigation.tap, + NavInsID.KEYBOARD_WRITE: self._write, + NavInsID.KEYBOARD_SELECT_SUGGESTION: self.screen.suggestions.choose, + NavInsID.RESULT_TO_HOME: self.screen.dismiss.tap + } + super().__init__(backend, firmware, callbacks) #, golden_run=True) + + def _write(self, characters: str): + # keyboard write is not an exact science on Ragger for now. The instruction together with + # the `wait_for_screen_change` function can get messy, as the write performs multiple + # `finger_touch` and the screen change several time, so `navigate_and_compare` could think + # the screen can be compared, when it has not reached its last state yet. + # Adding extra time after writing to have a better chance to get the expected screen + self.screen.keyboard.write(characters) + sleep(0.3) diff --git a/tests/functional/snapshots/stax/check_all_passphrase_lengths/00000.png b/tests/functional/snapshots/stax/check_all_passphrase_lengths/00000.png new file mode 120000 index 00000000..45a96fe4 --- /dev/null +++ b/tests/functional/snapshots/stax/check_all_passphrase_lengths/00000.png @@ -0,0 +1 @@ +../welcome.png \ No newline at end of file diff --git a/tests/functional/snapshots/stax/check_all_passphrase_lengths/00001.png b/tests/functional/snapshots/stax/check_all_passphrase_lengths/00001.png new file mode 120000 index 00000000..0bc6085b --- /dev/null +++ b/tests/functional/snapshots/stax/check_all_passphrase_lengths/00001.png @@ -0,0 +1 @@ +../passphrase_length.png \ No newline at end of file diff --git a/tests/functional/snapshots/stax/check_all_passphrase_lengths/00002.png b/tests/functional/snapshots/stax/check_all_passphrase_lengths/00002.png new file mode 120000 index 00000000..3bb0c985 --- /dev/null +++ b/tests/functional/snapshots/stax/check_all_passphrase_lengths/00002.png @@ -0,0 +1 @@ +../first_24.png \ No newline at end of file diff --git a/tests/functional/snapshots/stax/check_all_passphrase_lengths/00003.png b/tests/functional/snapshots/stax/check_all_passphrase_lengths/00003.png new file mode 120000 index 00000000..0bc6085b --- /dev/null +++ b/tests/functional/snapshots/stax/check_all_passphrase_lengths/00003.png @@ -0,0 +1 @@ +../passphrase_length.png \ No newline at end of file diff --git a/tests/functional/snapshots/stax/check_all_passphrase_lengths/00004.png b/tests/functional/snapshots/stax/check_all_passphrase_lengths/00004.png new file mode 120000 index 00000000..9f68a777 --- /dev/null +++ b/tests/functional/snapshots/stax/check_all_passphrase_lengths/00004.png @@ -0,0 +1 @@ +../first_18.png \ No newline at end of file diff --git a/tests/functional/snapshots/stax/check_all_passphrase_lengths/00005.png b/tests/functional/snapshots/stax/check_all_passphrase_lengths/00005.png new file mode 120000 index 00000000..0bc6085b --- /dev/null +++ b/tests/functional/snapshots/stax/check_all_passphrase_lengths/00005.png @@ -0,0 +1 @@ +../passphrase_length.png \ No newline at end of file diff --git a/tests/functional/snapshots/stax/check_all_passphrase_lengths/00006.png b/tests/functional/snapshots/stax/check_all_passphrase_lengths/00006.png new file mode 120000 index 00000000..61e7b110 --- /dev/null +++ b/tests/functional/snapshots/stax/check_all_passphrase_lengths/00006.png @@ -0,0 +1 @@ +../first_12.png \ No newline at end of file diff --git a/tests/functional/snapshots/stax/check_all_passphrase_lengths/00007.png b/tests/functional/snapshots/stax/check_all_passphrase_lengths/00007.png new file mode 120000 index 00000000..0bc6085b --- /dev/null +++ b/tests/functional/snapshots/stax/check_all_passphrase_lengths/00007.png @@ -0,0 +1 @@ +../passphrase_length.png \ No newline at end of file diff --git a/tests/functional/snapshots/stax/check_info_then_leave/00000.png b/tests/functional/snapshots/stax/check_info_then_leave/00000.png new file mode 120000 index 00000000..45a96fe4 --- /dev/null +++ b/tests/functional/snapshots/stax/check_info_then_leave/00000.png @@ -0,0 +1 @@ +../welcome.png \ No newline at end of file diff --git a/tests/functional/snapshots/stax/check_info_then_leave/00001.png b/tests/functional/snapshots/stax/check_info_then_leave/00001.png new file mode 120000 index 00000000..126b82a0 --- /dev/null +++ b/tests/functional/snapshots/stax/check_info_then_leave/00001.png @@ -0,0 +1 @@ +../info.png \ No newline at end of file diff --git a/tests/functional/snapshots/stax/check_info_then_leave/00002.png b/tests/functional/snapshots/stax/check_info_then_leave/00002.png new file mode 120000 index 00000000..45a96fe4 --- /dev/null +++ b/tests/functional/snapshots/stax/check_info_then_leave/00002.png @@ -0,0 +1 @@ +../welcome.png \ No newline at end of file diff --git a/tests/functional/snapshots/stax/check_previous_word/00000.png b/tests/functional/snapshots/stax/check_previous_word/00000.png new file mode 120000 index 00000000..45a96fe4 --- /dev/null +++ b/tests/functional/snapshots/stax/check_previous_word/00000.png @@ -0,0 +1 @@ +../welcome.png \ No newline at end of file diff --git a/tests/functional/snapshots/stax/check_previous_word/00001.png b/tests/functional/snapshots/stax/check_previous_word/00001.png new file mode 120000 index 00000000..0bc6085b --- /dev/null +++ b/tests/functional/snapshots/stax/check_previous_word/00001.png @@ -0,0 +1 @@ +../passphrase_length.png \ No newline at end of file diff --git a/tests/functional/snapshots/stax/check_previous_word/00002.png b/tests/functional/snapshots/stax/check_previous_word/00002.png new file mode 120000 index 00000000..3bb0c985 --- /dev/null +++ b/tests/functional/snapshots/stax/check_previous_word/00002.png @@ -0,0 +1 @@ +../first_24.png \ No newline at end of file diff --git a/tests/functional/snapshots/stax/check_previous_word/00003.png b/tests/functional/snapshots/stax/check_previous_word/00003.png new file mode 100644 index 0000000000000000000000000000000000000000..08ba93fe0066b0577b31099ba1795f2b6780572e GIT binary patch literal 4610 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 UlLZc_W;p=Z0q^~ldn3~S36Ac}3IG5A literal 0 HcmV?d00001 diff --git a/tests/functional/snapshots/stax/check_previous_word/00006.png b/tests/functional/snapshots/stax/check_previous_word/00006.png new file mode 120000 index 00000000..c449407f --- /dev/null +++ b/tests/functional/snapshots/stax/check_previous_word/00006.png @@ -0,0 +1 @@ +../third_24.png \ No newline at end of file diff --git a/tests/functional/snapshots/stax/check_previous_word/00007.png b/tests/functional/snapshots/stax/check_previous_word/00007.png new file mode 120000 index 00000000..f5978ebe --- /dev/null +++ b/tests/functional/snapshots/stax/check_previous_word/00007.png @@ -0,0 +1 @@ +../second_24.png \ No newline at end of file diff --git a/tests/functional/snapshots/stax/check_previous_word/00008.png b/tests/functional/snapshots/stax/check_previous_word/00008.png new file mode 120000 index 00000000..3bb0c985 --- /dev/null +++ b/tests/functional/snapshots/stax/check_previous_word/00008.png @@ -0,0 +1 @@ +../first_24.png \ No newline at end of file diff --git a/tests/functional/snapshots/stax/check_previous_word/00009.png b/tests/functional/snapshots/stax/check_previous_word/00009.png new file mode 120000 index 00000000..0bc6085b --- /dev/null +++ b/tests/functional/snapshots/stax/check_previous_word/00009.png @@ -0,0 +1 @@ +../passphrase_length.png \ No newline at end of file diff --git a/tests/functional/snapshots/stax/check_previous_word/00010.png b/tests/functional/snapshots/stax/check_previous_word/00010.png new file mode 120000 index 00000000..45a96fe4 --- /dev/null +++ b/tests/functional/snapshots/stax/check_previous_word/00010.png @@ -0,0 +1 @@ +../welcome.png \ No newline at end of file diff --git a/tests/screenshots/correct.png b/tests/functional/snapshots/stax/correct.png similarity index 100% rename from tests/screenshots/correct.png rename to tests/functional/snapshots/stax/correct.png diff --git a/tests/functional/snapshots/stax/first_12.png b/tests/functional/snapshots/stax/first_12.png new file mode 100644 index 0000000000000000000000000000000000000000..293399f34fa7218260584c138b716de568329d2b GIT binary patch literal 4889 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≤ulHYYVSej8rB>0s z^kEC{A(N9)XmNh!os;|GawXPY=eC)xbLY0)s%z~6exdP;iSKEQ{LP-H!QcfFSV6Fk zE4!2vh-@z|J*9|v?umT21{v!3Sb61B+`$e}8-mfiUXN0Ul6EaMxjnT233yUsQPa-_ zX=Spq4_V`=^d7PoY-V}{it9@sh8|ggiI=O1M%a${ipT#Z09Q}hivK(Awz4ds$ulxb)iYBBc_Fg>ucLL*hTu~+nXoU>)17c zrc5i>JFJbwDH8hPpu~c#sM5SRvG7;;;WaPtZd0bKowxbBJ6rzpIh|%n>B%@1S>H z6f1cS%UOf+#3fZ#DA=ohyn2mt(5c6qhpEcGexM=PcG%M5uTZ43+iAIrm0TH?)WqF{ zMSWg;X|x=7e(#{zU(dNxEl$$B>WXaJu@DMdwg)-BQl&JI&Tr29Q9~(OKz!WNLed(ZaMM zBz*(Tqr+lvxL+2(kr4(?z{I_K2qAKJQ-ncQ0=)aD(PE9NEOri^Pzdy8G)21ZfHsz? zjw!Bz_k^z$@jOKEJz+Iq+7jOY5v_n3&Bp~m-wrgZUvoPQsEBq&w9D^iTv!Kh}gU9@yE*WsynV6cO--^BqH*HXqO{`36<3E_`& zD>NsyW7#2>Xl)awomg zu(x8^SSxE~yYO{jiQLb_ij|E9h>kzz&z&a|=0f87l^=)!9KrLWo;3-ds1gR9rhv zX71>*nWha=lTlBU&fw&p)T1suZ1&IAw4l$nK3Q5Ha({q*NBV04dv= zq&NKKrBC;&%SZ1@Ep3fVix%}4a)KW0DFJq~6MjVHfrL*68tbEg?7hl)0hXfhxG)$m zrF$Y<^+loxg}bJOMOqP_%STaEvsCu>K-(@&*o>jLYz)&(z4vV3k=&9xCkov=WjeiZ zDRJXP`zWRKFzCRwRvGx&9l>-r6ba3lHo@iSU7Dx-K(iRdwDxq~Qzu(#(@cy-En>9# zS0nFcI;DJE5{-Zo^QJb+3kmQ(K`&Pm7p3pXMtlXs{%;QI@*oL%+5z+qxhgfPQHoSL z)y5R02x6E!SAT78l2t2o2Yp5H*LhRM*Joe%`1cbCHBVXmH4 z{?Z-ok&|^N{Vxp?Rv6`vr&F3}>KIap2U!a-%sXWPOeE}klXdUp^@9y<8}}23_6Lt} z8iJEWs7Xj8F2FMv*!#f5YfU=-;@Rx3Qfll`T8|PlpqjyGi>2@(WEKnPRpZw;)T1=U zKq=nbGX|8h9~K$zErJs!d*{lvyEMNK8dE$-0jkU*@lVGHI&zZ74fIGJS`b{ zgh^2#?2$?B{@|ISS<+0X+`O#@5tvjTn$~%CqVNVd;>;oP78q^oBl=+hy_&T%KC8`Hn5BlUM3u@4`5i^=A;adnUmwFYby}{P4sp~wr48~gNj*k@ZhG>pHUkyHZh3UjM;E^^ z-gyawGqtossB#Q?ue>|PBegK!=rV5GVh~ki+FZrZ?h@O?*HEB{nF-#A+~q>Gdnvs) z7mdI%+C9uj0$cD}RHB4E1mlZr@Mzc-H6kd4o;KA(E0rD!B+4VUEoJHJ$mdu*#u^9(@6~*)uG|zV;v@3;OIBZrw zNLKHdJ({zJ)ACy8QuN|r*x!%WX7dm$j6jO=6q;rVp@f*cq`ALFDLmQiLZItHk{)*6 zJbtGai7E^Jl82zRM*pbU;6D+;C$v1B|DJSpk`*2ibm@)6H+^cW5#Q_j+$!Yqxzu^& zNS$Qai8cPgbQ63i`nKa`O>ik_$nQ#wDIJp|MAemTZW12v6$aQgd4=)sp*`7Xo{yvA zHS!di0O!1@a&0>E`@`*@4q(bRd5bN4g=PHTh38)$$@~x4Do2}c=-RZ09{Ep>eEx5Y bhGrEXM9>_cQ(V=~zyK$X`yQ)35}ElAx{`X- literal 0 HcmV?d00001 diff --git a/tests/screenshots/incorrect.png b/tests/functional/snapshots/stax/incorrect.png similarity index 100% rename from tests/screenshots/incorrect.png rename to tests/functional/snapshots/stax/incorrect.png diff --git a/tests/screenshots/info.png b/tests/functional/snapshots/stax/info.png similarity index 100% rename from tests/screenshots/info.png rename to tests/functional/snapshots/stax/info.png diff --git a/tests/functional/snapshots/stax/nominal_full_passphrase_check_error_wrong_passphrase/00000.png b/tests/functional/snapshots/stax/nominal_full_passphrase_check_error_wrong_passphrase/00000.png new file mode 120000 index 00000000..c228d034 --- /dev/null +++ b/tests/functional/snapshots/stax/nominal_full_passphrase_check_error_wrong_passphrase/00000.png @@ -0,0 +1 @@ +../incorrect.png \ No newline at end of file diff --git a/tests/functional/snapshots/stax/nominal_full_passphrase_check_ok/00000.png b/tests/functional/snapshots/stax/nominal_full_passphrase_check_ok/00000.png new file mode 120000 index 00000000..eeacb4c2 --- /dev/null +++ b/tests/functional/snapshots/stax/nominal_full_passphrase_check_ok/00000.png @@ -0,0 +1 @@ +../correct.png \ No newline at end of file diff --git a/tests/screenshots/passphrase_length.png b/tests/functional/snapshots/stax/passphrase_length.png similarity index 100% rename from tests/screenshots/passphrase_length.png rename to tests/functional/snapshots/stax/passphrase_length.png diff --git a/tests/functional/snapshots/stax/second_24.png b/tests/functional/snapshots/stax/second_24.png new file mode 100644 index 0000000000000000000000000000000000000000..e3643f43684b49bc834988b88a0613bca98eba9f GIT binary patch literal 4956 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 Hu-JbAaVqI9 literal 0 HcmV?d00001 diff --git a/tests/screenshots/welcome.png b/tests/functional/snapshots/stax/welcome.png similarity index 100% rename from tests/screenshots/welcome.png rename to tests/functional/snapshots/stax/welcome.png diff --git a/tests/functional/test_fatstacks_full.py b/tests/functional/test_fatstacks_full.py index a1d43607..dffe1a9c 100644 --- a/tests/functional/test_fatstacks_full.py +++ b/tests/functional/test_fatstacks_full.py @@ -1,10 +1,7 @@ -from time import sleep +from ragger.navigator import NavIns -from pytest import fixture -from ragger.backend import BackendInterface - -from .app import Screen -from .utils import assert_current_equals, SCREENSHOTS +from .navigator import NavInsID, StaxNavigator +from .utils import format_instructions SPECULOS_MNEMONIC = "glory promote mansion idle axis finger extra " \ @@ -12,35 +9,65 @@ "seven myth punch hobby comfort wild raise skin" -def test_nominal_full_passphrase_check(screen: Screen, backend: BackendInterface): - # going to choose mnemonic length - screen.home.action() - assert_current_equals(backend, SCREENSHOTS / "passphrase_length.png") - # choosing 3d (24 words) - screen.choice_list.choose(1) - assert_current_equals(backend, SCREENSHOTS / "first_24.png") +def test_nominal_full_passphrase_check_ok(navigator: StaxNavigator, functional_test_directory: str): + # instructions to go the the keyboard + instructions = [ + NavInsID.HOME_TO_CHECK, + NavInsID.LENGTH_CHOOSE_24, + NavIns(NavInsID.WAIT, args=(0.5, )) + ] + # instruction to write the words for word in SPECULOS_MNEMONIC.split(): - # 4 letters are enough to discriminate the correct word - screen.keyboard.write(word[:4]) - # choosing 1st suggestion - screen.suggestions.choose(1) - sleep(0.1) - assert_current_equals(backend, SCREENSHOTS / "correct.png") - screen.dismiss.tap() # exit the result screen to the home page - screen.exit() - - -def test_nominal_full_passphrase_check_error_wrong_passphrase(screen: Screen, backend: BackendInterface): - screen.home.action() - assert_current_equals(backend, SCREENSHOTS / "passphrase_length.png") - # choosing 1st (12 words) - screen.choice_list.choose(3) - assert_current_equals(backend, SCREENSHOTS / "first_12.png") - # only the 12 first words + instructions += [ + NavIns(NavInsID.KEYBOARD_WRITE, args=(word[:4], )), + NavIns(NavInsID.KEYBOARD_SELECT_SUGGESTION, args=(1, )), + ] + instructions.append(NavIns(NavInsID.WAIT, args=(1, ))) + instructions = format_instructions(instructions) + # running the instruction to go to result screen + navigator.navigate(instructions) + + # now that the 24 words have been written, we check the resulting screen + # should be correct + + instructions = format_instructions([ + NavInsID.RESULT_TO_HOME, + ]) + + navigator.navigate_and_compare(functional_test_directory, + "nominal_full_passphrase_check_ok", + instructions, + screen_change_before_first_instruction=True, + screen_change_after_last_instruction=False) + + +def test_nominal_full_passphrase_check_error_wrong_passphrase(navigator: StaxNavigator, functional_test_directory: str): + # instructions to go the the keyboard + instructions = [ + NavInsID.HOME_TO_CHECK, + NavInsID.LENGTH_CHOOSE_12, + NavIns(NavInsID.WAIT, args=(0.5, )) + ] + # instruction to write the words for word in SPECULOS_MNEMONIC.split()[:12]: - screen.keyboard.write(word[:4]) - screen.suggestions.choose(1) - sleep(0.1) - assert_current_equals(backend, SCREENSHOTS / "incorrect.png") - screen.dismiss.tap() # exit the result screen to the home page - screen.exit() + instructions += [ + NavIns(NavInsID.KEYBOARD_WRITE, args=(word[:4], )), + NavIns(NavInsID.KEYBOARD_SELECT_SUGGESTION, args=(1, )), + ] + instructions.append(NavIns(NavInsID.WAIT, args=(1, ))) + instructions = format_instructions(instructions) + # running the instruction to go to result screen + navigator.navigate(instructions) + + # now that the 12 words have been written, we check the resulting screen + # should be incorrect + + instructions = format_instructions([ + NavInsID.RESULT_TO_HOME, + ]) + + navigator.navigate_and_compare(functional_test_directory, + "nominal_full_passphrase_check_error_wrong_passphrase", + instructions, + screen_change_before_first_instruction=True, + screen_change_after_last_instruction=False) diff --git a/tests/functional/test_fatstacks_options.py b/tests/functional/test_fatstacks_options.py index 23867e53..f70b4896 100644 --- a/tests/functional/test_fatstacks_options.py +++ b/tests/functional/test_fatstacks_options.py @@ -1,44 +1,59 @@ -from pytest import fixture -from ragger.backend import BackendInterface +from ragger.navigator import NavIns -from .app import Screen -from .utils import assert_current_equals, SCREENSHOTS +from .navigator import NavInsID, StaxNavigator +from .utils import format_instructions SPECULOS_MNEMONIC = "glory promote mansion idle axis finger extra " \ "february uncover one trip resource lawn turtle enact monster " \ "seven myth punch hobby comfort wild raise skin" -def test_check_info_then_leave(screen: Screen, backend: BackendInterface): - screen.home.info() - assert_current_equals(backend, SCREENSHOTS / "info.png") - screen.quit_info.tap() - assert_current_equals(backend, SCREENSHOTS / "welcome.png") - screen.exit() - - -def test_check_all_passphrase_lengths(screen: Screen, backend: BackendInterface): - screen.home.action() - assert_current_equals(backend, SCREENSHOTS / "passphrase_length.png") - for choice, length in [(1, 24), (2, 18), (3, 12)]: - screen.choice_list.choose(choice) - assert_current_equals(backend, SCREENSHOTS / f"first_{length}.png") - screen.navigation.tap() - assert_current_equals(backend, SCREENSHOTS / "passphrase_length.png") - - -def test_check_previous_word(screen: Screen, backend: BackendInterface): - screen.home.action() - screen.choice_list.choose(1) - assert_current_equals(backend, SCREENSHOTS / "first_24.png") - tries = ["rand", "ok"] - for word in tries: - screen.keyboard.write(word[:4]) - screen.suggestions.choose(1) - # coming back N time, should bring back to the first word page - for _ in tries: - screen.navigation.tap() - assert_current_equals(backend, SCREENSHOTS / "first_24.png") - # one more 'back' tap will bring us to the passphrase length choice page - screen.navigation.tap() - assert_current_equals(backend, SCREENSHOTS / "passphrase_length.png") + +def test_check_info_then_leave(navigator: StaxNavigator, functional_test_directory: str): + instructions = format_instructions([ + NavInsID.HOME_TO_SETTINGS, + NavInsID.SETTINGS_TO_HOME, + NavInsID.HOME_TO_QUIT + ]) + navigator.navigate_and_compare(functional_test_directory, + "check_info_then_leave", + instructions, + screen_change_before_first_instruction=False, + screen_change_after_last_instruction=False) + + +def test_check_all_passphrase_lengths(navigator: StaxNavigator, functional_test_directory: str): + instructions = format_instructions([ + NavInsID.HOME_TO_CHECK, + NavInsID.LENGTH_CHOOSE_24, + NavInsID.LENGTH_TO_PREVIOUS, + NavInsID.LENGTH_CHOOSE_18, + NavInsID.LENGTH_TO_PREVIOUS, + NavInsID.LENGTH_CHOOSE_12, + NavInsID.LENGTH_TO_PREVIOUS + ]) + navigator.navigate_and_compare(functional_test_directory, + "check_all_passphrase_lengths", + instructions, + screen_change_before_first_instruction=False, + screen_change_after_last_instruction=True) + + +def test_check_previous_word(navigator: StaxNavigator, functional_test_directory: str): + instructions = format_instructions([ + NavInsID.HOME_TO_CHECK, + NavInsID.LENGTH_CHOOSE_24, + NavIns(NavInsID.KEYBOARD_WRITE, args=("rand", )), + NavIns(NavInsID.KEYBOARD_SELECT_SUGGESTION, args=(1, )), + NavIns(NavInsID.KEYBOARD_WRITE, args=("ok", )), + NavIns(NavInsID.KEYBOARD_SELECT_SUGGESTION, args=(1, )), + NavInsID.KEYBOARD_TO_PREVIOUS, + NavInsID.KEYBOARD_TO_PREVIOUS, + NavInsID.KEYBOARD_TO_PREVIOUS, + NavInsID.LENGTH_TO_PREVIOUS + ]) + navigator.navigate_and_compare(functional_test_directory, + "check_previous_word", + instructions, + screen_change_before_first_instruction=False, + screen_change_after_last_instruction=True) diff --git a/tests/functional/utils.py b/tests/functional/utils.py index dfc018d9..e27c81de 100644 --- a/tests/functional/utils.py +++ b/tests/functional/utils.py @@ -1,24 +1,9 @@ -from pathlib import Path -from time import time +from ragger.navigator import NavIns +from typing import Iterable, Union -from ragger.backend import BackendInterface +from .navigator import NavInsID -SCREENSHOTS = (Path(__file__).parent.parent / "screenshots").resolve() - -def assert_current_equals(backend: BackendInterface, existing: Path): - current = backend._client.get_screenshot() - assert_equal(current, existing) - - -def assert_equal(image: bytes, existing: Path): - error_file = existing.parent.parent / 'screenshots-tmp' / f'{existing.stem}_{time()}{existing.suffix}' - error_file.parent.mkdir(parents=True, exist_ok=True) - try: - with existing.open('rb') as filee: - assert image == filee.read(), \ - f"Given bytes does not match image '{existing}'. Check '{error_file}' for comparison" - except: - with error_file.open('wb') as filee: - filee.write(image) - raise +def format_instructions(instructions: Iterable[Union[NavIns, NavInsID]]) -> Iterable[NavIns]: + return [NavIns(instruction) if isinstance(instruction, NavInsID) else instruction + for instruction in instructions] diff --git a/tests/screenshots/first_12.png b/tests/screenshots/first_12.png deleted file mode 100644 index b38f39305af71364094a6526c9731b6ade979eb9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4893 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 diff --git a/tests/screenshots/first_18.png b/tests/screenshots/first_18.png deleted file mode 100644 index beecef6459c26b307d16debb8f322974bd1887d0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4901 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 BfoT8$ From a508b529f2d5cb2f0b85b9cb378a70ffe94d14f2 Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Fri, 10 Mar 2023 13:42:53 +0100 Subject: [PATCH 45/65] [fix] CodeQL clean and fix --- src/main.c | 1 - src/nano/nanos_enter_phrase.c | 14 -------------- src/nano/nanox_enter_phrase.c | 4 ---- src/nano/ux_nanos.c | 2 -- src/nano/ux_nanos_keyboard.c | 2 -- src/stax/passphrase_length_screen.c | 2 +- 6 files changed, 1 insertion(+), 24 deletions(-) diff --git a/src/main.c b/src/main.c index 271e5e3b..56535b8a 100644 --- a/src/main.c +++ b/src/main.c @@ -151,7 +151,6 @@ __attribute__((section(".boot"))) int main(void) { BEGIN_TRY { TRY { io_seproxyhal_init(); - // ui_idle(); ui_idle_init(); sample_main(); } diff --git a/src/nano/nanos_enter_phrase.c b/src/nano/nanos_enter_phrase.c index f9258f28..fb85be5d 100644 --- a/src/nano/nanos_enter_phrase.c +++ b/src/nano/nanos_enter_phrase.c @@ -203,19 +203,6 @@ const bagl_element_t* screen_onboarding_4_restore_word_keyboard_callback(unsigne } else { G_ux.string_buffer[0] = '_'; } - // will never occur on word stem, autocomplete always happen before that - // else { - // // first char is '...' to notify continuing - // if (value == 0) { - // G_ux.string_buffer[0] = '.'; - // G_ux.string_buffer[1] = '.'; - // G_ux.string_buffer[2] = '.'; - // G_ux.string_buffer[3] = 0; - // } - // else { - // G_ux.string_buffer[0] = (G_ux.string_buffer+16+l+1-8)[value]; - // } - // } break; } return NULL; @@ -428,7 +415,6 @@ unsigned int screen_onboarding_4_restore_word_select_button(unsigned int button_ G_bolos_ux_context.onboarding_step--; } } - // log_debug(G_bolos_ux_context.words_buffer); // clear previous word screen_onboarding_4_restore_word_init(RESTORE_WORD_ACTION_REENTER_WORD); diff --git a/src/nano/nanox_enter_phrase.c b/src/nano/nanox_enter_phrase.c index 139b6761..77d3f617 100644 --- a/src/nano/nanox_enter_phrase.c +++ b/src/nano/nanox_enter_phrase.c @@ -280,7 +280,6 @@ const bagl_element_t* screen_onboarding_4_restore_word_keyboard_callback(const u G_ux.tmp_element.component.x += 1 + G_ux.tmp_element.component.width / 2 - C_icon_backspace.width / 2; G_ux.tmp_element.component.y -= 7; - // G_ux.tmp_element.component.y = 5; G_ux.tmp_element.component.height = C_icon_backspace.height; G_ux.tmp_element.component.type = BAGL_ICON; G_ux.tmp_element.component.icon_id = 0; @@ -290,7 +289,6 @@ const bagl_element_t* screen_onboarding_4_restore_word_keyboard_callback(const u G_ux.tmp_element.text = (const char*) &C_icon_backspace; } } else { - // G_ux.string_buffer[0] = G_ux.string_buffer[32+value]-'a'+'A'; // render as // uppercase, always G_ux.string_buffer[0] = G_ux.string_buffer[32 + value]; // render as lowercase, always @@ -400,8 +398,6 @@ const bagl_element_t* screen_onboarding_4_restore_word_before_element_display_ca } uint8_t compare_recovery_phrase(void) { - // io_seproxyhal_general_status(); - // convert mnemonic to hex-seed uint8_t buffer[64]; diff --git a/src/nano/ux_nanos.c b/src/nano/ux_nanos.c index 3fbdb6b1..85767412 100644 --- a/src/nano/ux_nanos.c +++ b/src/nano/ux_nanos.c @@ -19,8 +19,6 @@ #if defined(TARGET_NANOS) -//#ifdef OS_IO_SEPROXYHAL - bolos_ux_context_t G_bolos_ux_context; unsigned short io_timeout(unsigned short last_timeout) { diff --git a/src/nano/ux_nanos_keyboard.c b/src/nano/ux_nanos_keyboard.c index 75c83859..2c3980a8 100644 --- a/src/nano/ux_nanos_keyboard.c +++ b/src/nano/ux_nanos_keyboard.c @@ -19,8 +19,6 @@ #ifdef TARGET_NANOS -//#ifdef OS_IO_SEPROXYHAL - const bagl_element_t screen_common_keyboard_elements[] = { // erase diff --git a/src/stax/passphrase_length_screen.c b/src/stax/passphrase_length_screen.c index 4ac46472..d4b636f0 100644 --- a/src/stax/passphrase_length_screen.c +++ b/src/stax/passphrase_length_screen.c @@ -40,7 +40,7 @@ void passphrase_length_configure_buttons(nbgl_button_t **buttons, const size_t size, nbgl_touchCallback_t callback) { nbgl_button_t *button; - for (uint8_t i = 0; i < size; i++) { + for (size_t i = 0; i < size; i++) { button = buttons[i]; button->innerColor = WHITE; button->borderColor = LIGHT_GRAY; From d99a1f7d646a6d805ca399f23f5da63585a4fe70 Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Mon, 27 Mar 2023 09:24:12 +0200 Subject: [PATCH 46/65] [clean] Renaming module for clarity --- src/stax/{ux_stax.c => mnemonic.c} | 2 +- src/stax/{ux_stax.h => mnemonic.h} | 0 src/stax/ui_stax.c | 2 +- tests/unit/CMakeLists.txt | 2 +- tests/unit/test_stax_mnemonic.c | 2 +- 5 files changed, 4 insertions(+), 4 deletions(-) rename src/stax/{ux_stax.c => mnemonic.c} (99%) rename src/stax/{ux_stax.h => mnemonic.h} (100%) diff --git a/src/stax/ux_stax.c b/src/stax/mnemonic.c similarity index 99% rename from src/stax/ux_stax.c rename to src/stax/mnemonic.c index 04053629..9257454d 100644 --- a/src/stax/ux_stax.c +++ b/src/stax/mnemonic.c @@ -2,7 +2,7 @@ #include #include "../ux_common/common_bip39.h" -#include "./ux_stax.h" +#include "./mnemonic.h" #if defined(TARGET_STAX) diff --git a/src/stax/ux_stax.h b/src/stax/mnemonic.h similarity index 100% rename from src/stax/ux_stax.h rename to src/stax/mnemonic.h diff --git a/src/stax/ui_stax.c b/src/stax/ui_stax.c index a84b2d8f..48870c43 100644 --- a/src/stax/ui_stax.c +++ b/src/stax/ui_stax.c @@ -15,7 +15,7 @@ #include "../ux_common/common_bip39.h" #include "../ui.h" -#include "./ux_stax.h" +#include "./mnemonic.h" #include "./passphrase_length_screen.h" #define HEADER_SIZE 50 diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 3c5f93d9..9c595ca9 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -43,7 +43,7 @@ include_directories(../../src ./mocks/) add_executable(test_stax_mnemonic test_stax_mnemonic.c) -add_library(stax SHARED ../../src/ux_common/onboarding_seed_rom_variables.c ../../src/stax/ux_stax.c) +add_library(stax SHARED ../../src/ux_common/onboarding_seed_rom_variables.c ../../src/stax/mnemonic.c) target_link_libraries(test_stax_mnemonic PUBLIC cmocka gcov stax) diff --git a/tests/unit/test_stax_mnemonic.c b/tests/unit/test_stax_mnemonic.c index c3df8c65..82d108aa 100644 --- a/tests/unit/test_stax_mnemonic.c +++ b/tests/unit/test_stax_mnemonic.c @@ -4,7 +4,7 @@ #include #include -#include "stax/ux_stax.h" +#include "stax/mnemonic.h" static int setup(void **state __attribute__((unused))) { From 7454388d2168e21ed78cc157e86f87fad27c76e2 Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Mon, 27 Mar 2023 09:56:26 +0200 Subject: [PATCH 47/65] [fix] Review corrections --- src/stax/passphrase_length_screen.c | 6 ++++-- src/stax/ui_stax.c | 6 ++++-- tests/functional/snapshots/stax/info.png | Bin 5132 -> 5157 bytes 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/stax/passphrase_length_screen.c b/src/stax/passphrase_length_screen.c index d4b636f0..0b0e0b9c 100644 --- a/src/stax/passphrase_length_screen.c +++ b/src/stax/passphrase_length_screen.c @@ -7,14 +7,16 @@ #define UPPER_MARGIN 4 #define BUTTON_DIAMETER 80 +#define ICON_X 0 +#define ICON_Y 148 nbgl_image_t *passphrase_length_set_icon() { nbgl_image_t *image = (nbgl_image_t *) nbgl_objPoolGet(IMAGE, 0); image->foregroundColor = BLACK; image->buffer = &C_stax_recovery_64px; image->bpp = NBGL_BPP_1; - image->alignmentMarginX = 0; - image->alignmentMarginY = 148; + image->alignmentMarginX = ICON_X; + image->alignmentMarginY = ICON_Y; image->alignment = TOP_MIDDLE; image->alignTo = NULL; return image; diff --git a/src/stax/ui_stax.c b/src/stax/ui_stax.c index 48870c43..39884ea2 100644 --- a/src/stax/ui_stax.c +++ b/src/stax/ui_stax.c @@ -55,7 +55,7 @@ static void on_quit(void) { * About menu */ static const char *const infoTypes[] = {"Version", "Recovery Check"}; -static const char *const infoContents[] = {APPVERSION, "(c) 2022 Ledger"}; +static const char *const infoContents[] = {APPVERSION, "(c) 2023 Ledger"}; static bool on_infos(uint8_t page, nbgl_pageContent_t *content) { if (page == 0) { @@ -138,6 +138,8 @@ static void passphrase_length_page(void) { /* * Word recover page */ +#define BUTTON_VMARGIN 32 + static char textToEnter[MAX_WORD_LENGTH + 1] = {0}; static int textIndex, suggestionIndex, keyboardIndex = 0; // the biggest word of BIP39 list is 8 char (9 with trailing '\0'), and @@ -240,7 +242,7 @@ static void display_keyboard_page() { get_current_word_number() + 1, // number to use textToEnter, // text to display false, // not grayed-out - 32, // vertical margin from the buttons + BUTTON_VMARGIN, // vertical margin from the buttons KBD_TEXT_TOKEN); nbgl_layoutDraw(layout); } diff --git a/tests/functional/snapshots/stax/info.png b/tests/functional/snapshots/stax/info.png index 8e15f931ca2f62acc42cee1e3370eb9e7ef949a5..f560fe63e275156be5e901cd28e6176d0cc8a889 100644 GIT binary patch delta 3441 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 z zhOA>c%<(`*-R!-*^y^`F=3K#xd-74MU0DqQs~YTM-N};?OH4r#C{R+)!;=}t1p7e! z*y0pe;*!Rq6?yw}?m>^v=I~WJ`?fa&O%sQ?2F20QCf`PAJY2q^3gi^6^I?h9V8_8e zX}EIzt9_&2OXvz#Up}KbPMN+9`=|0V-4$W7twbWB*ePb*a46`Xk#$TqO%66+sEWO+ z?DfwinQ>x6EcS!X%dRpW#>He_%qu)MOneduQ3XN&;6q%}(1LUc9{&42P);NEztaZ16{C-yjIq75ZV40Ce`XrcAc4*h-*g?D1 z2IgdKorvv@UoVL%i5v~Z0Cb?@Db%pqS5ZN;pkEZZR_(L8M^idAYwy)CWnc9MWRQ8W zFYVy)M>i7kaDN@J$0Aw0|kdpul9~T;)61R zomK4Z%yxArPF-$7UL<8`XYTA%B?#Cv=w(r=tn(XrmviyU_VT$*d%90qC0~QLgjOHqi{DSK&3Jm>%{|l!y#vSLs~r0EXl0fTF&wj z*hE`@+WLgRORIvffoPveH~jwEKM7H@rGxPkciImAlTfbf_fECw^gW+MBdxOQB>|k@ zWMh!LAOLJOU`p;h+Pghu4{HGq1Qp&uC;DY3I*$%tt3R`k;FlW(oy>@EB_WTaG3x(d zCC=$Om0)r&h~s;uc|7NCklHAyMTYK1o6pf976K9}12|bc`vEfiP0^_#c^^n3h#tOn z0+UX$lXHKHirHwm_KSw+knC68Ta+tf64z^?iY37&PsA*)N%N%!q|URPW0~})p-qRq zWc8w{*t`~gwP=c74Y_)b^z)k(%D)a;dA8A>=G@Ap|KM3*51A~RICgxGrUkm=m_pIH o#)_cP4DFkWj9@HR>9XbqFwWBvcnKWx Date: Wed, 12 Apr 2023 15:11:11 +0200 Subject: [PATCH 48/65] [update] rc9 -> Stax 1.0.0 --- src/stax/passphrase_length_screen.c | 8 ++------ src/stax/passphrase_length_screen.h | 6 ++---- src/stax/ui_stax.c | 14 ++++++-------- src/ux_common/common_bip39.h | 2 +- src/ux_common/onboarding_seed_bip39.c | 2 +- 5 files changed, 12 insertions(+), 20 deletions(-) diff --git a/src/stax/passphrase_length_screen.c b/src/stax/passphrase_length_screen.c index 0b0e0b9c..c8a1a51d 100644 --- a/src/stax/passphrase_length_screen.c +++ b/src/stax/passphrase_length_screen.c @@ -38,9 +38,7 @@ nbgl_text_area_t *passphrase_length_set_title(nbgl_obj_t *align_to) { return textArea; } -void passphrase_length_configure_buttons(nbgl_button_t **buttons, - const size_t size, - nbgl_touchCallback_t callback) { +void passphrase_length_configure_buttons(nbgl_button_t **buttons, const size_t size) { nbgl_button_t *button; for (size_t i = 0; i < size; i++) { button = buttons[i]; @@ -58,11 +56,10 @@ void passphrase_length_configure_buttons(nbgl_button_t **buttons, button->alignment = BOTTOM_MIDDLE; button->alignTo = NULL; button->touchMask = (1 << TOUCHED); - button->touchCallback = callback; } } -nbgl_button_t *passphrase_length_set_back_button(nbgl_touchCallback_t callback) { +nbgl_button_t *passphrase_length_set_back_button() { nbgl_button_t *button = (nbgl_button_t *) nbgl_objPoolGet(BUTTON, 0); button->innerColor = WHITE; button->borderColor = WHITE; @@ -77,7 +74,6 @@ nbgl_button_t *passphrase_length_set_back_button(nbgl_touchCallback_t callback) button->alignment = TOP_LEFT; button->alignTo = NULL; button->touchMask = (1 << TOUCHED); - button->touchCallback = callback; return button; } diff --git a/src/stax/passphrase_length_screen.h b/src/stax/passphrase_length_screen.h index 2c680a1d..69c3e897 100644 --- a/src/stax/passphrase_length_screen.h +++ b/src/stax/passphrase_length_screen.h @@ -4,9 +4,7 @@ nbgl_image_t *passphrase_length_set_icon(void); nbgl_text_area_t *passphrase_length_set_title(nbgl_obj_t *align_to); -void passphrase_length_configure_buttons(nbgl_button_t **buttons, - const size_t size, - nbgl_touchCallback_t callback); -nbgl_button_t *passphrase_length_set_back_button(nbgl_touchCallback_t callback); +void passphrase_length_configure_buttons(nbgl_button_t **buttons, const size_t size); +nbgl_button_t *passphrase_length_set_back_button(); #endif diff --git a/src/stax/ui_stax.c b/src/stax/ui_stax.c index 39884ea2..34317d2e 100644 --- a/src/stax/ui_stax.c +++ b/src/stax/ui_stax.c @@ -40,7 +40,7 @@ enum { /* * Utils */ -static char *buttonTexts[NB_MAX_SUGGESTION_BUTTONS] = {0}; +static const char *buttonTexts[NB_MAX_SUGGESTION_BUTTONS] = {0}; static void reset_globals() { reset_mnemonic(); @@ -84,7 +84,7 @@ enum { #define NB_BUTTONS 3 -static char *passphraseLength[] = {"12 words", "18 words", "24 words"}; +static const char *passphraseLength[] = {"12 words", "18 words", "24 words"}; static void passphrase_length_callback(nbgl_obj_t *obj, nbgl_touchType_t eventType) { nbgl_obj_t **screenChildren = nbgl_screenGetElements(0); if (eventType != TOUCHED) { @@ -110,7 +110,7 @@ static void passphrase_length_page(void) { nbgl_obj_t **screenChildren; // 3 buttons + icon + text + subText - nbgl_screenSet(&screenChildren, 6, NULL); + nbgl_screenSet(&screenChildren, 6, NULL, (nbgl_touchCallback_t) &passphrase_length_callback); screenChildren[ICON_INDEX] = (nbgl_obj_t *) passphrase_length_set_icon(); screenChildren[TEXT_INDEX] = @@ -119,8 +119,7 @@ static void passphrase_length_page(void) { // create nb words buttons nbgl_objPoolGetArray(BUTTON, NB_BUTTONS, 0, (nbgl_obj_t **) &screenChildren[BUTTON_12_INDEX]); passphrase_length_configure_buttons((nbgl_button_t **) &screenChildren[BUTTON_12_INDEX], - NB_BUTTONS, - (nbgl_touchCallback_t) &passphrase_length_callback); + NB_BUTTONS); ((nbgl_button_t *) screenChildren[BUTTON_12_INDEX])->text = passphraseLength[0]; ((nbgl_button_t *) screenChildren[BUTTON_18_INDEX])->text = passphraseLength[1]; ((nbgl_button_t *) screenChildren[BUTTON_24_INDEX])->text = passphraseLength[2]; @@ -129,8 +128,7 @@ static void passphrase_length_page(void) { ((nbgl_button_t *) screenChildren[BUTTON_24_INDEX])->foregroundColor = WHITE; // create back button - screenChildren[BACK_BUTTON_INDEX] = (nbgl_obj_t *) passphrase_length_set_back_button( - (nbgl_touchCallback_t) &passphrase_length_callback); + screenChildren[BACK_BUTTON_INDEX] = (nbgl_obj_t *) passphrase_length_set_back_button(); nbgl_screenRedraw(); } @@ -271,7 +269,7 @@ static void display_home_page() { /* * Result page */ -static char *possible_results[2][2] = { +static const char *possible_results[2][2] = { {"Incorrect Secret\nRecovery Phrase", "The Recovery Phrase you have\nentered doesn't match the one\npresent on this Ledger Stax."}, {"Correct Secret\nRecovery Phrase", diff --git a/src/ux_common/common_bip39.h b/src/ux_common/common_bip39.h index 127be91e..b3682794 100644 --- a/src/ux_common/common_bip39.h +++ b/src/ux_common/common_bip39.h @@ -25,7 +25,7 @@ unsigned int bolos_ux_bip39_get_word_next_letters_starting_with(const unsigned c size_t bolos_ux_bip39_fill_with_candidates(const unsigned char *startingChars, const size_t startingCharsLength, char wordCandidatesBuffer[], - char *wordIndexorBuffer[]); + const char *wordIndexorBuffer[]); uint32_t bolos_ux_bip39_get_keyboard_mask(const unsigned char *prefix, const unsigned int prefixLength); #endif diff --git a/src/ux_common/onboarding_seed_bip39.c b/src/ux_common/onboarding_seed_bip39.c index f45ffe25..a21f37ff 100644 --- a/src/ux_common/onboarding_seed_bip39.c +++ b/src/ux_common/onboarding_seed_bip39.c @@ -213,7 +213,7 @@ unsigned int bolos_ux_bip39_get_word_next_letters_starting_with( size_t bolos_ux_bip39_fill_with_candidates(const unsigned char* startingChars, const size_t startingCharsLength, char wordCandidatesBuffer[], - char* wordIndexorBuffer[]) { + const char* wordIndexorBuffer[]) { PRINTF("Calculating nb of words starting with '%s' (size is '%d')\n", startingChars, startingCharsLength); From ce2515e739885b70de4a1e087fb48b1a2c10497c Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Wed, 12 Apr 2023 15:34:33 +0200 Subject: [PATCH 49/65] [update] Adapting tests to newest Ragger --- tests/functional/navigator.py | 31 +++++++------ .../stax/check_previous_word/00003.png | Bin 4610 -> 7053 bytes .../stax/check_previous_word/00005.png | Bin 4594 -> 6986 bytes tests/functional/snapshots/stax/correct.png | Bin 6608 -> 13190 bytes tests/functional/snapshots/stax/first_12.png | Bin 4889 -> 7247 bytes tests/functional/snapshots/stax/first_18.png | Bin 4897 -> 7301 bytes tests/functional/snapshots/stax/first_24.png | Bin 4906 -> 7360 bytes tests/functional/snapshots/stax/incorrect.png | Bin 6532 -> 13128 bytes tests/functional/snapshots/stax/info.png | Bin 5157 -> 9496 bytes .../snapshots/stax/passphrase_length.png | Bin 5842 -> 11414 bytes tests/functional/snapshots/stax/second_24.png | Bin 4956 -> 7357 bytes tests/functional/snapshots/stax/third_24.png | Bin 5047 -> 7545 bytes tests/functional/snapshots/stax/welcome.png | Bin 7961 -> 15131 bytes tests/functional/test_fatstacks_full.py | 35 ++++++++------- tests/functional/test_fatstacks_options.py | 42 +++++++++--------- tests/functional/utils.py | 6 +-- 16 files changed, 59 insertions(+), 55 deletions(-) diff --git a/tests/functional/navigator.py b/tests/functional/navigator.py index c89579af..33f14f51 100644 --- a/tests/functional/navigator.py +++ b/tests/functional/navigator.py @@ -1,12 +1,13 @@ from enum import auto, Enum from functools import partial +from ragger.navigator import NavInsID from ragger.navigator.navigator import Navigator from time import sleep from .app import StaxScreen -class NavInsID(Enum): +class CustomNavInsID(Enum): # generic instructions WAIT = auto() TOUCH = auto() @@ -35,20 +36,22 @@ def __init__(self, backend, firmware): self.screen = StaxScreen(backend, firmware) callbacks = { + # has to be defined for Ragger Navigator internals NavInsID.WAIT: sleep, - NavInsID.TOUCH: backend.finger_touch, - NavInsID.HOME_TO_SETTINGS: self.screen.home.settings, - NavInsID.HOME_TO_QUIT: self.screen.home.quit, - NavInsID.HOME_TO_CHECK: self.screen.home.action, - NavInsID.SETTINGS_TO_HOME: self.screen.settings.single_page_exit, - NavInsID.LENGTH_CHOOSE_24: partial(self.screen.choice_list.choose, 1), - NavInsID.LENGTH_CHOOSE_18: partial(self.screen.choice_list.choose, 2), - NavInsID.LENGTH_CHOOSE_12: partial(self.screen.choice_list.choose, 3), - NavInsID.LENGTH_TO_PREVIOUS: self.screen.navigation.tap, - NavInsID.KEYBOARD_TO_PREVIOUS: self.screen.navigation.tap, - NavInsID.KEYBOARD_WRITE: self._write, - NavInsID.KEYBOARD_SELECT_SUGGESTION: self.screen.suggestions.choose, - NavInsID.RESULT_TO_HOME: self.screen.dismiss.tap + CustomNavInsID.WAIT: sleep, + CustomNavInsID.TOUCH: backend.finger_touch, + CustomNavInsID.HOME_TO_SETTINGS: self.screen.home.settings, + CustomNavInsID.HOME_TO_QUIT: self.screen.home.quit, + CustomNavInsID.HOME_TO_CHECK: self.screen.home.action, + CustomNavInsID.SETTINGS_TO_HOME: self.screen.settings.single_page_exit, + CustomNavInsID.LENGTH_CHOOSE_24: partial(self.screen.choice_list.choose, 1), + CustomNavInsID.LENGTH_CHOOSE_18: partial(self.screen.choice_list.choose, 2), + CustomNavInsID.LENGTH_CHOOSE_12: partial(self.screen.choice_list.choose, 3), + CustomNavInsID.LENGTH_TO_PREVIOUS: self.screen.navigation.tap, + CustomNavInsID.KEYBOARD_TO_PREVIOUS: self.screen.navigation.tap, + CustomNavInsID.KEYBOARD_WRITE: self._write, + CustomNavInsID.KEYBOARD_SELECT_SUGGESTION: self.screen.suggestions.choose, + CustomNavInsID.RESULT_TO_HOME: self.screen.dismiss.tap } super().__init__(backend, firmware, callbacks) #, golden_run=True) diff --git a/tests/functional/snapshots/stax/check_previous_word/00003.png b/tests/functional/snapshots/stax/check_previous_word/00003.png index 08ba93fe0066b0577b31099ba1795f2b6780572e..d9be1973ebf5beb36f434ffd8d3cebea86d613ba 100644 GIT binary patch literal 7053 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 UlLZc_W;p=Z0q^~ldn3~S36Ac}3IG5A diff --git a/tests/functional/snapshots/stax/correct.png b/tests/functional/snapshots/stax/correct.png index a44e0f746d22999983707991508fc3600d0b38c7..29980ad856bae378b6b2a83086b8deb585db5b60 100644 GIT binary patch literal 13190 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 JEAKhJ_;0f6N6G*I literal 6608 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 diff --git a/tests/functional/snapshots/stax/first_12.png b/tests/functional/snapshots/stax/first_12.png index 293399f34fa7218260584c138b716de568329d2b..10682359ac2c6882b3fe1c409a07c83afcfc6ccf 100644 GIT binary patch literal 7247 zcmeHMX*8Sb+EzLnvQJyI#yYCzD&1(6sEAsG;UDREku0$=<594KX%UP1Tqx zh?s(GRY?ssr701rF$F=8A~-}-R?^^dW-1mLm*K<9; zSXr6~AC)-D$Hymp%k+i~AK$)T`S|!>9_9z$=-p|?^6^QD-MV39`{2bQeKh;CspL7U zPkm?Fp7bLRU)(Rnf1F<=PNetw{7O^ z%4|(JoK1a!pC|UrUH3hz-!Jw@2u-a*%xC2k*svn*IqdwkS zXv^uFkxuvDY3l}Xr|Airw+{I=P`@C?A5lu9Iv8SDr!QWP-%@|$S znF5wCBmUGl5#Eudq6`MHI2>FT+k9r;vXpj62L@v8>=@Kl_IRZ!T%&i)eI1$$8oadx zEH%DwR9D49;q`-`mQbXErLo)uOAV2iB{;v_ns-t{OT~~yVv!L^EO1X)Lqh|(%iCOM zQ9I)brdG0KJ^eU%pLP6bz$7PJH2Y>EnQHCP6L zLZR`hEMN=q3$VJe4$N1)@78W(M#dI+^YLfiVqdS~@6vLn4L_Tt+%0h2~~%Tn@pmBV0cXMwR{6(Tn9xlkIw!%+Y@6 zs_3wa8deDq+BF5Ax8ZB`pz>+;=(_2)+g*097w+$l*3@1;y4^AX#AD2MRk#cWj{FSu zNh`smc6RyJ-S$D(hi%M=HOG1S54?Prewla=~2F9#DTXl(bqQ7k{r6Hmu#wVN|~t z3w&+o#3XxVN(64V`QqZM`hU>#5&Y3ydG>@unXz_cm6o^wwpqLK6Ewcl#t<9qdkH_@ zJNLo%Vwboj&sCMCT)hPA=*RSu$YR)qtTW6Vd+JeewCzUYV0LM>xN}}{TDqX4whitZ zk7sV3rJk{?%Sf@TQ;A&dL*}}UWJ&m<6R}eJZ(&FNvFIn3M^_TC1sid8w2J%vQVwY6 zsf`^wJkF`|9^rzW5;yTS5Gcz-oxx}GQ0u$9<6aS~&%nP9T0Fz!@yW@_9frjZmEtce zV-lUKs3_9p;-b2AtgNi;)6Ty>bBT_QUf^zryIB~|*y{|x36((HPSmNpP5CkUmRXwl zs^w@A>|nH+Psdg$pxrbhH_?|Ons#FMSk$Vu?m&F*PKz$WRbjL#Eq|Z6 zl!K3L=&a3vG&9smRIC92lBk#%3uvq!=6TmWJ$`WEtvGa!_2hf=_>qcT_qX@K*jjVX zdyQ#gBNJ0oQyVRN4CG!hyQ~>JR_L}t1yHIplmNh?GR7OJ82q&~(%h}PpfeW*j}M=1 zB@&Ut4aF6uyS9l%C!FV!GOygpFX|qP&!v??;KHvxNw=XkQJ%R-f8u_fZobzlE@8qQVK z@f3{v!Vh%Pf!TwJ2>aZA0NDKvfy>fxpCmQI+vqFEm>2_$C9|M0!KoO0KX=>&626(Q zV%RLpG-|Qj*xmukcuRBQqBs$7-1;UZ!GscDU*$0(t}3Mw8JqW16NFuLySXU?AsNua z@19sv@(yS@Uon~3Z9>xoHylb2m?29>p}F-nj3uwglw3b_{q;$+#$J*yA)d-w2X_7v z{Pf-qGh?JRkV)*(`8XVTK9_xLBw${WOMMo3grj|Du|og)s~CkoyW`NF%P+o9TUy9_(?3tn&8u zUaIf($}OgoeT4b3Ws>>Q$6N(iFDO5+mcQtTY3y{jzHea9&JEw6aXC_1JBPTOkssqJ z__eCSMp0=`zWpGkK6tBWFi7aX26K~w2I00V15CDgi#prcF_;Xr zf0k5Okc>`sgEP=sfy7;Z(TOc?*h7%L_3r40q$`H&VMbW+O&>^YN}yg91gmI{&6W|n zp_$)@+l%d88eWi_fwfO{-*g2cGFUQf4iY?k19<+05Bv-9i#7X1+=+!gc#p^_)bBm6 zEbD;y;I)w#&y##}0jNqQlewY05}f}boDJR5jA0s9P+6^R#_P5~CkDDJ#kp?zJ!(zr z=aBJR)`}0|ZMLEh{1FbK z41g99O#r3<9sU{TSfY-NoxvsVtH-bCFI#KtSMDY%MhsMctPyW0-ox-{S#Xe6vBp8V z33{C7yeU>fV?1}rW&033wl)0Az`}#MhaM`2LlF6Ps{o92x_?}Ll{v0!%SoMkwVnq} z6CEG=W%H2)!l6viDt7JR=8-XY-{l~}*G>~eX`f_cT)eoLi<<7kCp;?EUPml%>xwHy z1sOm|$YH}o14D*dmS0t=L!%rKE9#Qr3e{WG9~{scf29zoJa9A*c_SE#zHwAq)zvt!{KoOk{p#UWtpp%=P zN^{4Iqi#y_W0Asf=y8c(Wv5InnwK9FY8@s`EzKaU$n!)Grr%tRa~e$%4ohi056#ce z{?K@h$z7|}z}i4I)%LN5hUFeQUf(5EI=A+uzN=0+TfJp3?m)A<9`VJ(B$S!kFkock zP*ZcE#;L;1&28WzNK(v<{(@(1znNEijXC;#x@6t+^~zWy*tz$Ytox90=nB zReB;h(6m@V!*as%shwrn9oHnDG(gZeTU(6`_FxMI$e_{1ff@Uu^Uhu5!~py#J&@`$ zVhmJKC&2nla)vTgAX!w1BdP=CijUq_Z>uh`{BS7ULS&1F)qj$HVoT7IhBkFt{W1*Z zxX0^mc~QItuEPau%O*Q7S&38TRhpz7XPI^*a-K0Gr1?hXs9MwrNg^-G;kx6wObsMZ z14Bzl-nSI!*{AhY60~Z2z*V=tIz$)rnin-+UDVq<8EmT2+*Fw$?y82x0@USfy6y*( z@3&FMOmzlDY$Af+@64RdEYLdhC6qgBp!sOgjOGU(C7mjYC6LxS{q@TOt3g3QxB=dW z4ejPxcSkf(ZvcGLrG};-1@^-!+?oTk?L)53hioG`s&J)Ms^HvLm#8BgfF!OimOhO< zA}OZIP%y9YD<=EZZfN^+a9PteD#e!t;LzONx*kuT$<%6AMi9=UKA5R)jF9_#s#tJ& z9&|$;73! z>%f)vL$=rFYdt7s)h-ry*Q;O%Yd!x}?}?7z))pgjpDmpDQrPT~o4vHzIe*s8o2rc- z)N&rFW`dG)DR0t|tL?sZpZ#@M>w4RH(a*z=mOIx+(^F@N$6E*7Dqje-tM7;GN${(( zJhb<0g5Ej|?HntvVjP=Bmr|g-j!6+ey$3TdenfkQ`QDR{@# zaJ^HhY)qTG?OaGX%~&kl-AQ2TJMS2*DiWoBB)JO#r|j(@x@u(!s&k^oD1RNHUeORQ z101)%KY#j>`+o-Xe;)IbkY|Ub|3y^6-uGv zk>s-z4)I2+Q;;^%n=NP3!ENd{kE&T880*#l_jH1+>FlMR8kBd`JAq$IQGeGLrq4A! zQd;{1g@%8uI;kCMM`W;3=lC0rv(^#)Y$}PrV|m*9w2;BSH|alD7Wv=A=#Q3S{(sDW z=Kp`}-tN-aTrhRtOAMFG<(x>zx-eDns+muIpG5aqxo3v68GQw=4yb&#VW&e-yV;HW z%R_+bHe~nnck(xtBLmsJEr*dEP~6E=RUuwbZCvF{T0-DV5f!4%J;-Sy5;b1kW1NI@ zrZ5aUobK2_3i>^2#q92PQ$^_nl-)_{gKx0s9ag<(kji4a!;H3=qq^yGa5iuC*G zJ(<9;86x3V7y01d2C71yGvdgLKR84Vq)FiXz>Y8W%e{Kq#r}5a0RbFpJtLDrLXQux zmuT0I+XW6>9o?*)fuT)w2arX5gNlW8+5KlDsc~p~k>?>d3k5LI6O(kkihYO_qDr{I z#TVo$-F_pwT=iL>tLVsCiQOjdD^^7C%NQY?7^?f*p>LwzC4ME^NrsWNl|P5rOwp#v zmVT>I1P~Ef>cns(FX|+qpGAmIQw`^@RJt%0@=IQS{PeC_kD^2^x9dguDuXgu%llu!>#)LePx=> zX8(7-)dg;jIxE=|$T7X4uE+R)j~)z2%W2o022*g@3@MX-$e(EUv2mtiSCX`)+=_$_8kKOkzV`EBHB6)wwID@_zFgMkZ z7E89f{=>z0YRJcPd}HzhE-UtB zKyV)s4t+SIwC+RNH#huQ4?%h7E4+@6D@X;LR&Kh8$TJd_84oJ~d^9~_IzK7&gy_6Z z*$aO13TD-J-ZE3y?XJZ`%Ll!Gag_OVe`dQ$Yd`^}^DYE*D#dQ<2v;{*PJ2Sp|D9Lk z;7^MBp-W_r|E98lU#er_)a%QSWLsediwiwvVQ@9Ft%c-!ue%p5}m_@3@lz8~t zF-y<2W3psPGoxbOCKf)@McYzp1UN_U4z9WPT66#EzQNAXFdkJqvN2= zCu`F(3HCQ56vGPgQ8HpNp=(-7zrS;SU@#b^l>%2U16eks{5`XZe>a$Y*~{+xj%M>t zSJi<_ij}@0QNDMSBj#Js&%dGm-Kw_3v2(B)X^H3|dD-MlJlnsBZX zR(#W$K?}#~hZ6$G5@B?cVEbRf^00t$AIrXdjX-rIBm+==>lIS?XzX-%wI%%#hYSj>T%lLiNkPuT+v|M zsU;B-I?nErgu&xR=SyuD8s<`iQ)|hMRmm8~O?bG0=|jxtTMfBF)cqrAQij5BzX^fg z{)}b5&6dUI%;%J0d*ih2oJ7gexHRoycxSf9fpEU{b}fXu1WW zJErLXNAy>qwC%7Js*!JQeJVUf7>f?MCuT6cyXXX2D{AG)fb9_u%M-7>1M}J*cSV|c z(e1{gz9{L(H<2;eIoSX4J}PN*dwY8b&UfFl4gV|u8bv|wKZJY#Vcg>z@d)@Y##{TW S@_>s%zFWqYH!82)ef&2Aku&@N literal 4889 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 V*x{&C!0$ydtIKwm$}W1}{udiwCB6Uv literal 4897 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 literal 4906 zcmeHLX;c#E-bc&KeX=r5)4J7dCM`81l~SkeI$2qYA}*-3KtY1bxZ#4M<9171oODQ( zO3h?Z(lJw6A}UHnvP9gKRM1Qfi4YaM%suBl@3%Rpd*{9%-gCb`|K~jCJpbqS{FZ;5acm5ZZIHoPYKqJEG0E*2Ww&q0YIIlL!+XnsK zn4^1ct;IJ-=^2)pde6W4_e^beRH<|Cf2~N6Rxv!OX^#u-FH~?pU!W=2WKI4vNtqG z2Q*}u`>0Wc2%TK}g?7OmJsZkLQc0J?==U#HGDrv=?;6>zTR0nQTlI% zb);}1!x?I19AZ*dUyzu!^Q1;qGeKzx=eF*=GAxO}f09;2S$2l6o7vz( zb#+XfT68H1ge`z3l1F;uVfkCQofaSJT^x1~eDXv`3sfsatD8%1UB>8qP5$+oxSi^O zYTx%AcCZL<0w|qj>Z|~Jl^_Lw*>foQxAB`n9?v9)%^sy5aBVp6kovO8 zuDp6mPFf;8utIN&RdHuie_oZ%rw^}@0@v(1xL9Z4wae3GmycnY2O*+7>gs;ETYE`m zzp4U)NVwKyQkwYs-REZMy!WJ|{2<7y`a^c-s`4niJR+Rt(dR55%ZiOk>Xj|yH6vrW zFcwID^*3b0$H7X91JOoJWZSTAct;J_q(FLK<(kEiLp*$e+≤ulHYYVSej8rB>0s z^kEC{A(N9)XmNh!os;|GawXPY=eC)xbLY0)s%z~6exdP;iSKEQ{LP-H!QcfFSV6Fk zE4!2vh-@z|J*9|v?umT21{v!3Sb61B+`$e}8-mfiUXN0Ul6EaMxjnT233yUsQPa-_ zX=Spq4_V`=^d7PoY-V}{it9@sh8|ggiI=O1M%a${ipT#Z09Q}hivK(Awz4ds$ulxb)iYBBc_Fg>ucLL*hTu~+nXoU>)17c zrc5i>JFJbwDH8hPpu~c#sM5SRvG7;;;WaPtZd0bKowxbBJ6rzpIh|%n>B%@1S>H z6f1cS%UOf+#3fZ#DA=ohyn2mt(5c6qhpEcGexM=PcG%M5uTZ43+iAIrm0TH?)WqF{ zMSWg;X|x=7e(#{zU(dNxEl$$B>WXaJu@DMdwg)-BQl&JI&Tr29Q9~(OKz!WNLed(ZaMM zBz*(Tqr+lvxL+2(kr4(?z{I_K2qAKJQ-ncQ0=)aD(PE9NEOri^Pzdy8G)21ZfHsz? zjw!Bz_k^z$@jOKEJz+Iq+7jOY5v_n3&Bp~m-wrgZUvoPQsEBq&w9D^iTv!Kh}gU9@yE*WsynV6cO--^BqH*HXqO{`36<3E_`& zD>NsyW7#2>Xl)awomg zu(x8^SSxE~yYO{jiQLb_ij|E9h>kzz&z&a|=0f87l^=)!9KrLWo;3-ds1gR9rhv zX71>*nWha=lTlBU&fw&p)T1suZ1&IAw4l$nK3Q5Ha({q*NBV04dv= zq&NKKrBC;&%SZ1@Ep3fVix%}4a)KW0DFJq~6MjVHfrL*68tbEg?7hl)0hXfhxG)$m zrF$Y<^+loxg}bJOMOqP_%STaEvsCu>K-(@&*o>jLYz)&(z4vV3k=&9xCkov=WjeiZ zDRJXP`zWRKFzCRwRvGx&9l>-r6ba3lHo@iSU7Dx-K(iRdwDxq~Qzu(#(@cy-En>9# zS0nFcI;DJE5{-Zo^QJb+3kmQ(K`&Pm7p3pXMtlXs{%;QI@*oL%+5z+qxhgfPQHoSL z)y5R02x6E!SAT78l2t2o2Yp5H*LhRM*Joe%`1cbCHBVXmH4 z{?Z-ok&|^N{Vxp?Rv6`vr&F3}>KIap2U!a-%sXWPOeE}klXdUp^@9y<8}}23_6Lt} z8iJEWs7Xj8F2FMv*!#f5YfU=-;@Rx3Qfll`T8|PlpqjyGi>2@(WEKnPRpZw;)T1=U zKq=nbGX|8h9~K$zErJs!d*{lvyEMNK8dE$-0jkU*@lVGHI&zZ74fIGJS`b{ zgh^2#?2$?B{@|ISS<+0X+`O#@5tvjTn$~%CqVNVd;>;oP78q^oBl=+hy_&T%KC8`Hn5BlUM3u@4`5i^=A;adnUmwFYby}{P4sp~wr48~gNj*k@ZhG>pHUkyHZh3UjM;E^^ z-gyawGqtossB#Q?ue>|PBegK!=rV5GVh~ki+FZrZ?h@O?*HEB{nF-#A+~q>Gdnvs) z7mdI%+C9uj0$cD}RHB4E1mlZr@Mzc-H6kd4o;KA(E0rD!B+4VUEoJHJ$mdu*#u^9(@6~*)uG|zV;v@3;OIBZrw zNLKHdJ({zJ)ACy8QuN|r*x!%WX7dm$j6jO=6q;rVp@f*cq`ALFDLmQiLZItHk{)*6 zJbtGai7E^Jl82zRM*pbU;6D+;C$v1B|DJSpk`*2ibm@)6H+^cW5#Q_j+$!Yqxzu^& zNS$Qai8cPgbQ63i`nKa`O>ik_$nQ#wDIJp|MAemTZW12v6$aQgd4=)sp*`7Xo{yvA zHS!di0O!1@a&0>E`@`*@4q(bRd5bN4g=PHTh38)$$@~x4Do2}c=-RZ09{Ep>eEx5Y bhGrEXM9>_cQ(V=~zyK$X`yQ)35}ElAx{`X- diff --git a/tests/functional/snapshots/stax/incorrect.png b/tests/functional/snapshots/stax/incorrect.png index b04171a950544964213d2123dbb86ec01d2870ef..88fbe5ba0fa5ed0f44d877c51c0a9f734ddcc9c5 100644 GIT binary patch literal 13128 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
SWP}m2nxi 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 B68r!F diff --git a/tests/functional/snapshots/stax/info.png b/tests/functional/snapshots/stax/info.png index 26a58219af2fa06eda86c4c3358d12060d80610b..b2735e3d751f9d526dc32c037ea986424d948a1b 100644 GIT binary patch literal 9744 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 literal 9496 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 QoOl93Oe~Gd3|;U2A2FFVyZ`_I diff --git a/tests/functional/snapshots/stax/passphrase_length.png b/tests/functional/snapshots/stax/passphrase_length.png index 808a3431b8ed2f1fbd02feaadeb6dc540b8c5e7b..3a385fc0cb28079bf8e7a521d41c0b3687fe47a6 100644 GIT binary patch literal 12394 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 F{|~12@IL?m literal 11414 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 HxZnL>UJlmR diff --git a/tests/functional/snapshots/stax/second_24.png b/tests/functional/snapshots/stax/second_24.png index c986e3d11265031ea7daa7540dd1900ac2174b7f..471a2439a3ee77b184bbff14bc7484f76b05d696 100644 GIT binary patch literal 7538 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 Z)k<2wE@M*woTZAHU$ef7zwGw=e*v3azdrx~ delta 5682 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 diff --git a/tests/functional/snapshots/stax/welcome.png b/tests/functional/snapshots/stax/welcome.png index ab2b2db5a4752fc030bcb7505d48c1db030f0bbe..de43460dab2f66e91fbbfe467865880dc2905bfa 100644 GIT binary patch literal 15799 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 From ae7aea7611a6e7bac909b1cb7cc5fe8f1e1399df Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Wed, 26 Jul 2023 15:48:40 +0200 Subject: [PATCH 57/65] [clean] Cleaning deprecated SDK functions --- src/nano/nanos_enter_phrase.c | 10 +++++++--- src/nano/nanox_enter_phrase.c | 12 ++++++++---- tests/functional/test_fatstacks_options.py | 5 ++--- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/nano/nanos_enter_phrase.c b/src/nano/nanos_enter_phrase.c index fb85be5d..fa830c1a 100644 --- a/src/nano/nanos_enter_phrase.c +++ b/src/nano/nanos_enter_phrase.c @@ -229,14 +229,18 @@ void compare_recovery_phrase(void) { cx_hmac_sha512_t ctx; const char key[] = "Bitcoin seed"; - cx_hmac_sha512_init(&ctx, (const uint8_t*) key, strlen(key)); - cx_hmac((cx_hmac_t*) &ctx, CX_LAST, buffer, 64, buffer, 64); + cx_hmac_sha512_init_no_throw(&ctx, (const uint8_t*) key, strlen(key)); + cx_hmac_no_throw((cx_hmac_t*) &ctx, CX_LAST, buffer, 64, buffer, 64); PRINTF("Root key from input:\n%.*H\n", 64, buffer); // get rootkey from device's seed uint8_t buffer_device[64]; - os_perso_derive_node_bip32(CX_CURVE_256K1, NULL, 0, buffer_device, buffer_device + 32); + if (os_derive_bip32_no_throw(CX_CURVE_256K1, NULL, 0, buffer_device, buffer_device + 32) != + CX_OK) { + PRINTF("An error occurred while comparing the recovery phrase\n"); + return; + } PRINTF("Root key from device: \n%.*H\n", 64, buffer_device); // compare both rootkey diff --git a/src/nano/nanox_enter_phrase.c b/src/nano/nanox_enter_phrase.c index 77d3f617..39f9bbda 100644 --- a/src/nano/nanox_enter_phrase.c +++ b/src/nano/nanox_enter_phrase.c @@ -397,7 +397,7 @@ const bagl_element_t* screen_onboarding_4_restore_word_before_element_display_ca return element; } -uint8_t compare_recovery_phrase(void) { +static uint8_t compare_recovery_phrase(void) { // convert mnemonic to hex-seed uint8_t buffer[64]; @@ -410,14 +410,18 @@ uint8_t compare_recovery_phrase(void) { cx_hmac_sha512_t ctx; const char key[] = "Bitcoin seed"; - cx_hmac_sha512_init(&ctx, (const uint8_t*) key, strlen(key)); - cx_hmac((cx_hmac_t*) &ctx, CX_LAST, buffer, 64, buffer, 64); + cx_hmac_sha512_init_no_throw(&ctx, (const uint8_t*) key, strlen(key)); + cx_hmac_no_throw((cx_hmac_t*) &ctx, CX_LAST, buffer, 64, buffer, 64); PRINTF("Root key from input:\n%.*H\n", 64, buffer); // get rootkey from device's seed uint8_t buffer_device[64]; - os_perso_derive_node_bip32(CX_CURVE_256K1, NULL, 0, buffer_device, buffer_device + 32); + if (os_derive_bip32_no_throw(CX_CURVE_256K1, NULL, 0, buffer_device, buffer_device + 32) != + CX_OK) { + PRINTF("An error occurred while comparing the recovery phrase\n"); + return 0; + } PRINTF("Root key from device: \n%.*H\n", 64, buffer_device); // compare both rootkey diff --git a/tests/functional/test_fatstacks_options.py b/tests/functional/test_fatstacks_options.py index 1f2bd285..cc148dd2 100644 --- a/tests/functional/test_fatstacks_options.py +++ b/tests/functional/test_fatstacks_options.py @@ -12,14 +12,13 @@ def test_check_info_then_leave(navigator: StaxNavigator, functional_test_directory: str): instructions = format_instructions([ CustomNavInsID.HOME_TO_SETTINGS, - CustomNavInsID.SETTINGS_TO_HOME, - CustomNavInsID.HOME_TO_QUIT + CustomNavInsID.SETTINGS_TO_HOME ]) navigator.navigate_and_compare(functional_test_directory, "check_info_then_leave", instructions, screen_change_before_first_instruction=False, - screen_change_after_last_instruction=False) + screen_change_after_last_instruction=True) def test_check_all_passphrase_lengths(navigator: StaxNavigator, functional_test_directory: str): From bca99f9c4c570920bf093fb2dbdf2f7f144be6fc Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Wed, 4 Oct 2023 14:02:01 +0200 Subject: [PATCH 58/65] [bump] v1.2.1 --- Makefile | 2 +- tests/functional/snapshots/stax/info.png | Bin 9744 -> 9534 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index e7242896..1627c468 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,7 @@ all: default APPNAME = "Recovery Check" APPVERSION_M = 1 APPVERSION_N = 2 -APPVERSION_P = 0 +APPVERSION_P = 1 APPVERSION = "$(APPVERSION_M).$(APPVERSION_N).$(APPVERSION_P)" APP_LOAD_PARAMS = --appFlags 0x10 $(COMMON_LOAD_PARAMS) --curve secp256k1 --path "" diff --git a/tests/functional/snapshots/stax/info.png b/tests/functional/snapshots/stax/info.png index b2735e3d751f9d526dc32c037ea986424d948a1b..6502711f73b38c0d3010abad40e35dfc702d9ff7 100644 GIT binary patch literal 9534 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 From 7e77bb135306a31fddc18bd9515f998ec0358e63 Mon Sep 17 00:00:00 2001 From: Sarah GLINER Date: Tue, 31 Oct 2023 14:33:39 +0100 Subject: [PATCH 59/65] [Fix]: Add HAVE_WEBUSB flag --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 1627c468..84b70860 100644 --- a/Makefile +++ b/Makefile @@ -50,6 +50,7 @@ DEFINES += LEDGER_MINOR_VERSION=$(APPVERSION_N) DEFINES += LEDGER_PATCH_VERSION=$(APPVERSION_P) DEFINES += OS_IO_SEPROXYHAL DEFINES += UNUSED\(x\)=\(void\)x +DEFINES += HAVE_WEBUSB WEBUSB_URL_SIZE_B=0 WEBUSB_URL="" DEFINES += BOLOS_APP_ICON_SIZE_B=\(9+32\) #DEFINES += HAVE_ELECTRUM From 710371fe781bc6a8b33b4ba24f718d5d00505839 Mon Sep 17 00:00:00 2001 From: Sarah GLINER Date: Tue, 31 Oct 2023 14:55:06 +0100 Subject: [PATCH 60/65] Makefile: remove UNUSED macro defined in the sdk --- Makefile | 1 - 1 file changed, 1 deletion(-) diff --git a/Makefile b/Makefile index 84b70860..223284f6 100644 --- a/Makefile +++ b/Makefile @@ -49,7 +49,6 @@ DEFINES += LEDGER_MAJOR_VERSION=$(APPVERSION_M) DEFINES += LEDGER_MINOR_VERSION=$(APPVERSION_N) DEFINES += LEDGER_PATCH_VERSION=$(APPVERSION_P) DEFINES += OS_IO_SEPROXYHAL -DEFINES += UNUSED\(x\)=\(void\)x DEFINES += HAVE_WEBUSB WEBUSB_URL_SIZE_B=0 WEBUSB_URL="" DEFINES += BOLOS_APP_ICON_SIZE_B=\(9+32\) From 8cf1c88b0006a974716e5fb06ee1bbb419856eeb Mon Sep 17 00:00:00 2001 From: Sarah GLINER Date: Wed, 8 Nov 2023 17:52:25 +0100 Subject: [PATCH 61/65] Bump version --- Makefile | 2 +- tests/functional/snapshots/stax/info.png | Bin 9534 -> 9569 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 223284f6..2c279aa0 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,7 @@ all: default APPNAME = "Recovery Check" APPVERSION_M = 1 APPVERSION_N = 2 -APPVERSION_P = 1 +APPVERSION_P = 2 APPVERSION = "$(APPVERSION_M).$(APPVERSION_N).$(APPVERSION_P)" APP_LOAD_PARAMS = --appFlags 0x10 $(COMMON_LOAD_PARAMS) --curve secp256k1 --path "" diff --git a/tests/functional/snapshots/stax/info.png b/tests/functional/snapshots/stax/info.png index 6502711f73b38c0d3010abad40e35dfc702d9ff7..1e236bd3b9330cb4a80d70a51656d2310e1de0d1 100644 GIT binary patch literal 9569 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 literal 9534 zcmeI2c{r5q-~Y!F3Q*^Q_uWH$zrt-_rtGIm0?k+EhQW(;nM zGGv*UG-i~2Oh)#d=W;*aXZimA{r$f8^ZVnOIgUB5W6o=?>pDN@obS);{W;I)mgd*^ zkBA)sfk6C_>%ZRwf!JPwK2~GqDboO4m;ORrVf1 zRnGIkoA1vfhQ@7z>YRt!jG_c(I1m2`@826~(bqX`HGs9zT^|sM@3#smTZl3!AuB-1 zAW%f}{yOatD5{hP400E=1NBH(gE9 zM?~mB$ti9l>b=Wl4k@i;u&7g>)JuKKET*eSr@y$#z&2_HmFi|ssn3nJ67t4TS$6{p zxC{lm{KdO`mBu4^1x>8bGKla7FsZMvPnzdacJB|rkVep*J9lozO7oy)6ynF7a&HcmS!r{`actQ`sAWrnuUd|Hd^9HP{r3%ftvYm@q}TCIE? zR!K*#pLDZd`-)BTXo9xmD{ae$z(d=zill&g2aoVS19c^fO_y{Y;T%gd)@>w9v?LR^ z77FC!3;G4$3!!+*<>No^W3h97S`U$aMCVA~ksDsrV4^e5`!uJaNUVL@NfZAjr_tqV zFsZ7l%2#Rn(c~4pwsi&4Bl$9^Mh1eGIo#pb!oC;L+9W`lTC@A$|61mgXZ?{UZ#=Rv z9%0+Hz$f)md_i_x%58G)otvL`qqDA{3kzQNEAAE3Rjn{n-U^mS_ z$j1j0V1k@o6BkG|`Kxu_RCTCGtS0j0oki5B*W~Fy-VuV%Rw~_TXk{t!5ewm9BxU29 z_;jz0uIbn0G^h@5Ezn9pwidI{Rzea-%J!z6J z7e)A{%6F~dD@}riSH2}=vY>{ie7iM~HWSd)xXW@CG2h9}+cb&Ys$K~ZVNmBHhG{v= zn?Jp2yACyA-mON7(pw29hP=}-9&?hCt%Cs^W)>&cPy1QiPNYDbtcUu?)EEAcvR8u~ zM?hdbI{T}32#W5zx2((4drfJG)zo~YFxj6lOf~{59}gxKm1;RlPD#9~-T#dkUCeDP z-etT!xNWal9i|DhPF;tGs#jyA`u?^6FYd7GupACeC>e|WJX5O^HlI5b z_WpOPAnhD1HX!l(y5o@SgqCu=k@i3kWWAjHG-5WfmGJGk$?DZHvO3KNhqqFy)qGu| zD23IAAH&e;HYHAWbVB4{hmEkkny!LlL?QZZTkggs%JSt2S2&_7`UdQ zWp?jXZ5m;!HT3msb2-|r%V9L^6h~oREcik-7CT683-@~a?hSK*!y$cTxV(y)_dZ{i zR;sym*VnkGclbfP*h;a5Zn0qp9Sx_nH9Jb;G>YVYBoi+0o=SpyHr~iARh6`3 zu^OugULsqAEN%0;9=hqI#ej-_VHNM;@?$I(bL=f^2h6XvS+$q*b4^A3Xk5pSH)Rs` zH8{4nDxSou^`ac-+q)Z-Mvq$mi1x^0 zts@bln~g8Zr$Uvb1{BO$StX~2Xk%|IvM@_Yx_whO`jx_9A9Lm6it$nc9sQ@TAwQ^$ zS2|G^J_l_IBP8U27~Zk3u(1;Q_XlsZMfz zd3I_b@P-cU&K4@DL}OH3{xPg%n^^d45H`iJjOXQCr&c9OL zj1RTz_DWbi+5rX6y>DCc6ee@;jS9rNHjX^Hl>HKS2_dpl*_PFkO_7^+g_S@90SR}C zh7)JB{7=|iO{PKC1jZok&tkaJs2y|W;u4I|mlI$0lAJG)VWFDW=XRf7e)skS2Z=fk zt1Z4($$7M+KYIu6>(aZ2S`lue?dF@gh8gJGbAcL|E2opLa~C6Z_w{-C%b9+%Wk?e{ zRZD@abqAwF!eqbNt$6L-H49cTX~HbHDX=l-O9TE-gs%&96uQ_`|I9~)YMDG&{MVB) zSfwdHPHA#|$Rjg?0(Bo|V(xGoP-tS3mq@bPQc_Yko53VWNtI;(POXGH1B~tXJChH_ zefW)ez?^@!-V&S|vn>e*?Z~6Dhe3&>$ATX2asDb4v_oMm?hE>h8|*wf^qpVWA9bHE zDRgK_Bp*y_Szi+Re$LYll_qoe%pv2qu76v`?;MlnS<>BS-37IIM?RIlPF^9P*g&`e z8P30%H~txE-Zi#)QX49n3(AM0uI=P_nH*4v!an`cC1U_LM%1B#oK`}z5H-rM+L16oj&}HULM?<$ z1~wl@><6>;-2H66E(1}I4Wl|~>NI6xN|_E`uDK1~G~67XHLstZdfj5AH#Ovb&RO2k zdL__Bog8z@=0jr@=&gl{0{NiAbkc`#WF*?DVr}gZRRcy4VC!eM zcl9@Ll{2yF%}2jmPgl6Ya)L7*Q3F}FBC)J?WA8dFc5)Tymh97(&U7DjlG?)Rd~n(J zSK8~A<0eVlf6}o9jKegz>ogooQ60@ zy@ZJ*(UPu-fqC}68ZfCQNkK^a4WoVuEi({&7nhcfDW%4XXUg-sTva&?_Bh$#T~@MX ziL_k43lQB-`qflTE(&$5sLVUdz22e1rffBgIJV%MFvegl4?800d<{D7EuI z`V}z%>WIal6MlcLm@u)t=|q9sr@>)fG^M^}d}UncZ@^zl*E0*__z4fV1?PF$XdttS zcd2pIf=Nl61vMEFlbcYl8CyZl&|H$>eX=^B_gn>BHZY1aoB9$D_0XCvj6zTg4G`89B^+k1 zm^jh55@=9+T!L*QjW7lhSw~y7$Mvd7uJ@R_i(mAS4)Pq6F$|8xXCW>GP*2E*r?*!cYk0>Jdf~R+~c= zrEI91@a?KfK$=&slpRNuY{<}Vb=bko2^Hi7T;9yGz4IgL+Vb+PJsQ%Nt2Fp!Ia`}i zvqtrS4}0q&w$2Lkrp~oyV;B+L-e=Y-fySN7Ip)5(uSXi@lZm(cs@S4T#`+n1kB48` zw5jc;SQ&vDpZ`5>`2W+~cu)_RSa`wwXCwZCYIGnxn$WZRFAE+LXu+qvvM^whG|#wJ z!o)^}$&N2ufxIbta`#YMRe*L*<;7C0=Y3;;@w~p7riS|a=_I$=p0P)l={QlV>dxN=N7z<55tKL6?6Cqs&Hg+4H%G|FZTF^u8EnMnQRnibFUe@Vtm1J zD;5i+s&A4`Rg|i)<3@Gf1m`Mh1OF3gz78-czM#=b*~IG6rPKDnbN%J0&;2?dxaQh4 zDp{tLkSqiE0YgD`C|Moq8wtz}Z|YSmq1YcA-NjjJJLrIEZo~d?#AHRb$Ys9ld?}MQY7|7>&%0QqviA-~&X;^@<$O!xpf2i#p(QgH|?Q}PMXi0p* z$C?{i@e@;4bT-v(mBG_?VXE)_b?a2FP_o%c6%x0ti>sK?*%%LNYhYd_`Yx0Y2cC^D zATw@Tr{=cN?MuuK5_L>a!D=pCTfpTXt`~kMB?d>W@VQO;$v?yBj>DQ$nAXTrym9o| z(WPgujWi#Nl4KpWcGQcJ$Rr;ZkKz|~?L_)9J8Nr($M+VEFLwQ_vuqV+VP3CQJOxkRt(Ar&aMW4rlCl$a-$t|Bl@fI-CWEwlYqH6a$Lveo-F!8~ zMb=wF?b>Ot_loB~_YFoR?|V{i;JSdyi}+D)*!)K{k^f4#6*HVDH@{La*A<5qz&BTV zzWp}EShIj$xGA}7RhP#)+4{$J_*`zc*3t?Q^uFxsVM2j?cTLZH;gH87t?8&X%q&(2 zu8%-eHVo?oZRX1YPJdyYrMBcRYEN2r(l5C6dQ`(g1xsi(%u|O>oej-3gB#;!;V#JB z=G$+!7ghP-4{tV)pYpe@WFj8mL5L75ey?Q}J!L{G?E=C1teNM%_w${D z(-{qx&HHr6FWDyuaFoMT(tZMtyDC}nZf?@jL#gHn+o}zdW^+B?v1mebQLOAe>H%%a zFN?Icv|Q!F#!fF>r~fwxx{cH0>rjU->!u&R>yRjd{W%JrgSv{GgZyU8-r$W4M42Mw z`j+WJJWpI&$IQZ&0QENGmCIT@<^RDSyzNL?jcAR;JhOYN8H8tM>Z+?xC!AixkK8M$ z8l%R+;aHFlYKZBY>HI|@{WF8-N}AiZsI0+~!ZT{OKh2qEHr354DV1J$_k8m6>0fv{t*OR`xm4K} z#!wl|8E(6BhP8?bwKG0(Xmy2bZ(w58cq=;SlWkAOP3ORJgow{wD><$-&0~>UZF$>& zs`3>V_B&=f1~w^HV-}`YpNHDnVwxtbe2;)}26S(q7d_AZ_zxr4v|8oz27c)uMzFWi zi_)*^B*^J1a#SNkj%)a5>vdnYQQ88r@yP5dfBJMtYB@lc^S-PK?p~x)eitm@VWxsf zPQ#s@%_(mxpj%6@? z_yw|^za0BNEuVk(Gr)(pU5v1FQ8Fc~2ejr$^Hh@0U119uyMf?oI{VhVRgCe9{rj0} zx@q1=>2WQ|EcPRBP+gCSsXe^uT|92~m!<;zLGfb4judsrvHv%<-HDiJ>ohlN z10uYr(u;^t1%)`nIa*i4a%P(JZ-*9OJcgO89DN@iQrALs=V~1X)rU&rSI?6GoW^(@ zB!&0L`@_p#X>ubUmrh8@0I8eWKu(XmN^Jkx8h)UK^h!%a^7cKP%ntg* zQ6LvlOpEEF7`)^Av(@4<0U^(2xy+sRdW|Tc>3dKd-rroFnfU9K7r8gkRZl2txwt6v z4@6rChI#P>BeNz`@s-(_P?ugqK_L_Wd8Pz1slgkV`J=voRV6DmduB}oK-A)C5AgQU ziUtp{T^PXhdpnyz=m99~=5TwMbyfJ49IVQwtgp|4>&asE=C#AJ5Apf^4He>zK-G*V z>jftK@#EXpQ@L|ucFyw{O267oK16+hGQaiipn7WZt-|1~tZUaFC%!P!Z`(cR)_XG$ z$ggxw1-35@6)$W}Lt9!}!Vu61)Bzx*rG9}EovR2#orG>_84^ok=g{NjHCG6?jjuq8 z_{Bh7UB_T-)0F-_Yp0#y+isoT*piX zPP=#9JxpJDyS2DiC<@7|yb-%Z&kFeb0(`N?y>E5Q)FyX^xrzsqbo+{rEbB7feO_*+ z!jobn;`N^1lFf9>NtT>x%}Wiftx5Lo~Lv{Z4D9Fd$wIQ zrR8E(>Pk_^4?j^=*_#A|S3g4*#!~a;BWw8DIGh7;u`GX;XpP09@Hb4))&oYDkH=!g zx`lEc4Q;dfg(?)hjy{p@Th1pU!R2uei()%QWszn>yT^-O5k2(Pt$a*HqcVFVJQB)> zk&Bg9?fLngXO`A=fz^r$|G|8+#E}$J%k?9FKKB~tJ&f#Xm*EtM>j)Xim68R=8^z63 z7LGMND%1IOD#+bswh}5XyV9+y)_W+VZ6{h*&Ul?KPhY^Rn~$S7XXKW3E?zR57A*i574g;Yql_HvJd@7nS*&s}dkxPqiYj2(wC{N0Eg(;u%O#$i* ztWUb{OatJaH;ls`7|kiUjRpp!X&_WW8N*GnFA(rh*=8qYy3_gRMf9qVFF&jD3B*19 zN$XFq9eGrASN4p4kXM|7n{>5mm4U!0eHk<4Ii%jZ+8Wpb3*+dcD`1$bVFcQmbNi*U z5~3NnTe>_?8ob>`UDh-Z8c=cr0GJb0PpZw@dUWZ@fS36@9-$b%Ha=&64g(MW(Vy-t z*&(*d$qiv*uiqx=hsLF7-$k?se|GP=_=4St$OUdDJ}p~aRe3K0NL6`xd35zZ@RdNR zMs=NEE~=~96N9u8+DeII9lzf{dH$iv4Dz=c_8>`dtT`mM7CH&$%DF8=p zx@h7ChIhaG6r~wLu79R_27=Ddg@2d({;BU@wjc?V9)C%$@q!GO~-lseh+vnMg;TE)O?65S4LZlXIx9*^M$;a$6CO5Z| z){nvRcbUMgwDnx+hM|jVKdPYntk<~))kD!Sk_*qzpL!RgkP&w% zk1DY@{lyQr(RM{9LeJA~BTq^#prl?2>+s96qkVt#j7RZX&wNyyk>E~q-SPXvx}3s$ zlux^bgQgElKY}?qg>S5!NU|5?94^Sbu4w3fM!Gt!$I{`j8ZYOtQKtSp^ps?ubhU7= z<*I3hqUyAXi&9ck=NnY)5A-oR)IIGA&dhqdzxHsHyKNpCQZ$tH%KZ28Chw9L*7k|% zF#EB;wtl3Ir(0KXrj{3<`D+)5QhQao^WpRMD5L+}5VE>WEYRDS>B}>S+-qKbdiLQ- zW$x%0g2(viX+42RK|xMo7Qn2lW8}gv*dUBURU$g(+Gw_Y(Mfkhmmh}Yi>SJGC!X>< ztm2E+nES)X!T5;v80qR?DT_S?J~5#{?1wGglWa18pNR$?oU91slrFzOv6a z6}?I#uKY$&SgxQa+qZ6DR{emR^{nZ?@}OHQ zqsXb=4k=E~=!1@r?S>Kcg|!7D{W$elr2o?DdY;yew3#~YQ!#&)3G0PgYtSaEo*w@Z zW72v1+f~AU{Cgv~6?4$|j{!g}^ZwO{YIFFFoN3~qBpO}+a9WMz6n#O3-7EhF#q!2q zW1B{D>Uh;D%~Ni2F$IVwJ3H?=HB{XbAat!gThZ@|i+`&S6toi*<2E>7Tc=udk~2DH zf0+!NVni8RPzFT^R(Ws0AcnVHc{IQqquAkWFGrRzL@~&!!$<%z`%AB0NmKOsdA>8% zsOOoY5)I Date: Mon, 13 Nov 2023 16:12:01 +0100 Subject: [PATCH 62/65] [fix] os_derive_bip32 syscall can't take NULL path --- Makefile | 2 +- src/nano/nanos_enter_phrase.c | 9 +++++++-- src/nano/nanox_enter_phrase.c | 9 +++++++-- tests/functional/snapshots/stax/info.png | Bin 9569 -> 9749 bytes 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 2c279aa0..5707e99d 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,7 @@ all: default APPNAME = "Recovery Check" APPVERSION_M = 1 APPVERSION_N = 2 -APPVERSION_P = 2 +APPVERSION_P = 3 APPVERSION = "$(APPVERSION_M).$(APPVERSION_N).$(APPVERSION_P)" APP_LOAD_PARAMS = --appFlags 0x10 $(COMMON_LOAD_PARAMS) --curve secp256k1 --path "" diff --git a/src/nano/nanos_enter_phrase.c b/src/nano/nanos_enter_phrase.c index fa830c1a..2f790408 100644 --- a/src/nano/nanos_enter_phrase.c +++ b/src/nano/nanos_enter_phrase.c @@ -236,8 +236,13 @@ void compare_recovery_phrase(void) { // get rootkey from device's seed uint8_t buffer_device[64]; - if (os_derive_bip32_no_throw(CX_CURVE_256K1, NULL, 0, buffer_device, buffer_device + 32) != - CX_OK) { + // os_derive_bip32* do not accept NULL path, even with a size of 0, so we provide an empty path + const unsigned int empty_path = 0; + if (os_derive_bip32_no_throw(CX_CURVE_256K1, + &empty_path, + 0, + buffer_device, + buffer_device + 32) != CX_OK) { PRINTF("An error occurred while comparing the recovery phrase\n"); return; } diff --git a/src/nano/nanox_enter_phrase.c b/src/nano/nanox_enter_phrase.c index 39f9bbda..c8e28741 100644 --- a/src/nano/nanox_enter_phrase.c +++ b/src/nano/nanox_enter_phrase.c @@ -417,8 +417,13 @@ static uint8_t compare_recovery_phrase(void) { // get rootkey from device's seed uint8_t buffer_device[64]; - if (os_derive_bip32_no_throw(CX_CURVE_256K1, NULL, 0, buffer_device, buffer_device + 32) != - CX_OK) { + // os_derive_bip32* do not accept NULL path, even with a size of 0, so we provide an empty path + const unsigned int empty_path = 0; + if (os_derive_bip32_no_throw(CX_CURVE_256K1, + &empty_path, + 0, + buffer_device, + buffer_device + 32) != CX_OK) { PRINTF("An error occurred while comparing the recovery phrase\n"); return 0; } diff --git a/tests/functional/snapshots/stax/info.png b/tests/functional/snapshots/stax/info.png index 1e236bd3b9330cb4a80d70a51656d2310e1de0d1..c25c6e8dd57d23d4960dedb0614918b595491b5b 100644 GIT binary patch literal 9749 zcmeHtXH-*L`)4QuN>wp{QWX@XiXtr$QIQU!l+Zy0jPy=`5ReNfC@5$s5>Nzk=}0G( zP=tsGQ6iv*5TwNrI!1&LnB%?wnRjNb`8M<3cgEyiSgANAP~o65Qy^yA1CmMu2p*l2&6D!a@D{lDu0PW$t`joulBerg#k%%CSOXr zloT%qle@zwelJ;8?h>B}=N}-smJ9vkGZE+Tbz3X)(g|x(`SS+V^QwUd4LkBdasXQ`F7c;C@ zu9*5jO1QVSyu46#M(IwT_u_l1-vvw(6G9DW*1zGXa!Haf9v4yn<*iSmY>Pj`f)v(S zBySSnG#V34J?Y_qt{A=hWPzt zlSJM~1|EbNh}i6j8U|}iHb7fkYfw#orH%e=t~KN)CWN@jmN%(W_RZ^u3U5I6c6WC_ z-cr3LR9t^^6o)>m}*MT^(t-B-g-8_kLlZJ6qpC0dHss z!pvEd%-75Yf2Z-r6_!}c5t+3#XW!{#q5L{6dfU%jokw@pt^AfXJFdu#xo0Hqc3}cv z!1J`zDK&}$z1@Lj_}e1fDZL&+1D0x5D3fMF)y7(R`H!knc=efbvd>em+yc<@T_m;e z^DR_l7z1aH_DUT5?&@px$f=+dJ`op5-SmkgZtfiSM8cvW=0E(I`ix~uTU$(3o10mt z{G>?piSXgzqV1s(;Fi5J3LSy&^s?zVuH^?`Khot@Q*SK>I@0bJ!ZcEkN1C`)hVC+6 zFIOYHttp}J*`X>c3re0&i%)}sa^oh;lm|2ApOj9AA#7;z&O2ff0?JVF9(74_b~h21 zpxHt%Qq*;`PvIHcnVDUbJcF#kI0PPr1uR_>E~^in>JN4nHMF%|{@xg4$2fSHyPF<% zCbb88oDyi8Ezqkzs6oFR@Y&_Hs=4>UUv; zKB>0XH!~ZiOD}l$kzuD2D9%1VE5%izcmOIm(cFKOA14ic5k4@S#|&p@X*U(?F|WBv z`rU8+5^ZJO@$!d+-;o9Isd7u7Q?=Dcb0P<4nv#FWw})!c_Kw$C3TEq^J5tIuNRiqM zj}-KD`)W&gfNrfnv~}}GkI#=S3r^pAZLDpra(#U>F3a5t)F~F5Kc)9ukoNbxr_hpq zpoKKYm17V%P0UvRq8;+XT)3_0Xmxtr-Y3b!b01|4k7;K7GMresxoMNK=MC zEvK`lVeT&B`7Ls+fYAL^ucx(YeD~x{CR}e|z|=y<3sU24V{2Y4E3{FMYx1yJe{ef> zh2k|kqfv)0EsMR1)o%4IaI=X-b;t!v4jPS|e|FBG`7{0Ua!V+zd@q=~sW)@y_g$Yc zwkf%)`qSkzx9h)rSE?92!^hd`C!0Q#Eu$2h-4%k}Hz16s1Q0CWrq!EJ_^&U!g(%m~ zuHf@+Jjb6*=&y}ZijKiLSFGGxi(-EK!9NY&G2~f_XwzY?5n30tpHx>cqg86&e#$>@ z5!vP$>Y%-Navk2w zVR@4kV;Y6^%{bgzTd8ce^{t^irxfmhK76HO47hb`v91a)A)q;wcpe&Ck`8@+2-^Ox zZc({x+|?KpaLWYxR(If#;gfa+-omz~W2w4?h_+`i9RR0%cBa25)i;ov{K-FAtQhtB zpRA6C^?<>9Mmxus-=!6O(06a6SbB?6uMe{*LHSX89q|y0n{aK0%}9#awo&^PY>dxr zpVZ@{l3(bNFHCt8gjDqJJiY#17NciLb(l{svdoC^eLZ&xuid3Kf#AZp-i{P1*88e$ zEe$0!kk~Kr#wf?$zJwk^IL=MaRFPtfIc=Y4#7sjFx^y$FL_ZZe`>=02%vu#Q${h`x*s==Cclh14!u>{wlE>onKn`H)q4KlC{Ya0dmv=G85IWwaDujGYPXlF9#|~h zu-N#A)N-ch;QP}HJ7oRVY*j0@FSOxr;2Y!xUTR~4W~f#lbo>ym!_U*A9DtjxyqRJ= z_C~T;fIw;ni5?aV`3@bz3&oYSX-@b(G5e{nuU`^nk~sl!JvsBxL;PY9Y4afd?aCX< z(S8xwYCGzt$Q;!N8v)um6R1_iAr)fu;q}(xuA0m49h%7FiG01Q^FB%-t5tBbald%3 zy-LPG)67{PH7gF)qGaY+k>c@UcRtO3j`DxaAYAiG^<4x&EsGg6M6l#u*jdn&M&2L) zwBYz(HO-`I?bjxi!F8+|#}pWacC>aGM-&X=wVn@szJ(|l4_`vKif~(tsx&fl*CBh` z+v||P`Y)0~DW?LZx-ejVW$Gp#kDsF+_GtK<(N~Ptk6YZ_O7T_#T><+Z+U}Ggy#9>} zLaJG`*5YS01a9@5e>1?>vob`gZoHm~UGHC)G(K`oNki)BZ4dG7PW)7<&w+a{%FAhi zVqM44i(wf>#-4X-hr(ZHRa7(TlpDcgADgOJb`I!73a#!C{d8MPrjP^e4X3?7e^-ws zmW0sx`XOzmWy22bh5HDQZF62Jy+Nc`eNoh$)!sz*M|05g^$twr5KX7z%w?rZfLE{wBY>i6eT@&wIZnUx89kP36>Nyw6=XV-xG&l(NZCu z_a|RSpIM5i=zdaKHjIA!DahAfH&*9PhwGqEuloDCO2q>zmkMZYl{i449ILZ!6?Uvj@c;rU&^w$td40xAD^K2p#ssd*`b^F5Jj9*A z>h3Nta!M{Q=W)Z&S8xVl=6rCzK98MrM%KD5?VqRu>WURDp6nqWT`<04C)^u!tDn9W z71q|3C2x{>`__RZp*suoM7(yb z(cd$BRep{~>oc{uqKXmS=vZY3a6|`nMJ=65=giDdeWb&~k>EBhH-6}DtZ|kCi(I{!Qj#qfzhrC(Ax%FnCN*5gpL3 z{iT_fS05+|^p=;ut9!rEu54D-RswfK&wYjgVl2}r++|bA!Tt!S>lIYK_MR+X?{#nY zdBnxDZZ=?2#snU|w6hLTv$!d7ukyEM?7sA=>&#%?P0%9F^kdp_Bekk~}`^ zZ-l?z{Ue@8Br>C++7&MXo5@w2#EBY>UrsG z6E6I_>5W<&Y|@;ia|r!uMW?y|_EcF#Dg5No09_7bS?)4hh5~ zB$Vz&ZJ+fgOJ^7%11uy7T98A>iJ=JbatKU0-LnA2)t+tyF}5v4FS2&uXWVueLw&WN z>6DW4N+n`je#{hz@=pzb0}i3Rwl-I&>^PKdN2&5)O-&81dhS~p@UU89W%<@2|4LC;QYdlB0yMuf zauoeO;$XH$J1Eh35~8b z8xY~%)ux7!zTk=Tk7$UH*TaWOxK^{`?A-w+citjV_HCWt6JHQ8H#uVoRc`mMY6mLW zad_%VqO4--^w$PcQkZh)_h4Sv{y+%@b5d9{nPS&GBG*?6-!6P6d!i{`Y<2PISagSL zl^y1ZUdLO9YD8y*VCTx9a}_h1(p1&93OvsWH0m5rR}C{*7qeWb`@w#kdAD&L>4xi| zh(U?`0KlI?SO+x7?n#>~l|@)ocC+jUGW}AE6a}v2MxI##8wL}@5FmVCVt|+ed<5^5 zcr`P|lSeC(C&@#*gJ%@~=2U7?DO~z>BB)@+TC;M$KY3DzQ@bO%$-l(I7=L8-R@@5` zdw1&q$#)f5NIuS`Mi;a21;xYvA+Nt1Rghguw-Vz5sj>j>mn8hf{SQyd{il+Ke-S?l z>}*eNg6I8o+eGRM^vd#qC%^zQ4%SusZUu^P1HgiZ{ni071!4d_BwMLWqxeO*4MYuz zY9QBl01~Se0~vx~k_XIo^aeTF$Aa__v-Mkpu6_%8+}uQ7%6MwWFG5($5CzrDA}o69 zlf;wFFZ>0qm`IG-@ zBpRY~<&;ULmjmg%1KR(>F^7}H4ZvQ=sGM9?H2dk%Iw4)5F7Qebpqv8QXt>Kb(;a2! zGVV|2F_>cQ{&-iC7qOQaG9Obgj*6VFN;4e26g())Ho`Q|2U;&i=(zw?kGm~uXf3+W zpq9J@A?itIm}EYz!upeO)krG-bH%UwZrm+{C$@nhe#ci}jeOE9NN<7{!KZ-?;07~W z;J0QFQ2j!mqpQ(>wuE1FwSCq(dVa{(Fr4)eNI(vMcBxun9d=q=Ne^(z9;ovRlAti; zO}6LYdHqX~n@H6BW-L)nV=SeFz=d?mSwSR}U{l)+H?J;z4?84gc-HR3cgeNGX4kA| zRm;-ChxdS=`W86YZ0>|r99ysM)M)Cn+|Qd$r(xdJDVrREpRvp~3Kxn8jCFU{NTQrT z?`tH-c7Z5h=3gitK)MJ|Xs4$Y{aJ3Bu`=vXMY|@#jU|-*YH09BM!z_aY+`DmX@hV%#AZJ(iSoSk_;`hA@_Q2`lTD>v`WfEr4f7k1md z3 zH(^pjfbV=!fq{k8a{Q@wwO{-&OgTdgky2Pu#T0Uh^Bd4k-(+>PJ&bj} zHU`-2jPIhSB8N887M5$Y09MB7JZm z`>5-Jz8e|R*=+s3(9~0?6efN-hw|cqLgU%#2v2yZxsxJh&(-Nu+TphFG{?CodzQ_- z#CoM$Qb2ZaF^XL+y_OcH%hY8FInT*yDoc>jD47hST*5Cl8+!J-<^`0|y5(N+csLYk zFp;l7!7p{L5Q6hd%gjYc(Qja0G{9J|BlS~{w;rnXC#N~t;Pl{mLZ$IE1dHvu;A<@^ z=<29Vx`)57Byi;mwN81H#fB=pc^-Wk+g32(dBtTTIp{l}TW=!PM^IxtvJH2Q4>qOq5ZGt9Bix~O7SNGhI zRCDwV_g3~=K&a=flUK!)mmmDNpS%373WYdPRiA5!k$_=TTo-W>j|6)W1vJ(!EHTpC zU1c{-0Nh1kcc)dv8|VkYEwP!lwzhaYqV|99XtTW>e0WQmMuj_lxqRmXQe&zW!eSZ(ic~v6fSV%|v`# z7k?>lGViP^bji9m8i;cFPEUD%`ae=_5cOKXf{}lgXFT;P+Z5u7PdB1;1$MY%;pM`% z1!dI?W;C)l^z7FrC=i$|qP5eRWw#R7A;gXLYKAOdk>UbGgR!eQnB{kSD_1zvgw)_q z-WsI7@+Y6SfG#Tvhk|2OUuF_+PyW14mF25O-b0|h*tF{wkj0upZBMII{M{jTikvG` za4>lS`~G731T)O9Cc%v%m!Lpf^0y*9C18lCu(rwIs5Z*GSEE+gto)tC2dCi*R<9!q zdnff{Y8>mw6adA9v9uuB*p^rw%ipjSfpr-U^4i(jl8)rVL*|J!w@tJoIIH1#-5YKr zwWuOf?{K@2(L9Cc+cD6e(J~49oEu`XZIJ%ExWlHpwBKdnQobOYp@oYzy^W5{_-iD8W9AVH5`?auV`umP?jpSTis5_Vj_USJZB&3Dlg z<@$4u%gmD1o$|c-cjT>q@!~}w)zH`9+htc|_(qx_edbLHRz-#Pi-RxSFq?5nP_;y=tTpXYp zi{XyrjqM>dh3Y?3Py4xvxhV4Ve!^ z=KY+?KbJ|%L{5#7V?^K{acoEk*#Ieqb(9(P%L~KVR9ZmOfJo-vt^(jPZVNu|dz* z-?KH=7WzFnR5Si8zt;W|uW|h?UZ+?wm+;-i{~_jA6G`<7C;N9*s}c8BSR;{Ra0LoI zNc;SW`Kx#Lcr&GBcG@(+bCGC{mhLOdV;}UF6>7Z;8nnV(mH_yVli@b77WK?6%Btd_ zNMl(zqy6>Dq;IVovyrOg z1>qe;hlDuJ_ngswJ+GXcdCm#B^)qRzRTu%{GgS(qS3!>!TV zq(m`QH@76KK(+eYFal<4J8*QurR}aXT0qKFAJcx^K3UjR@?yOJHyGU*>u#b_>h|)3 ziMfG@Uo#nqMO4y;9lqucp6l+Q+Nl@2@dx=_#%VP#xXs3n=22n>uUa=y0w`m1`QjsL zK95=cE!0`>`rzhKD-k$`9;qtxRHDFBETL_-3Csq1GNG%d4lP{y;JF779Z7frt7#|= zVr6DN<|rwrJNI$4nZ#zk^X(zFnd^3V^;wdO3+B)T3sSC`!jtpAj$CA6`12FH!l9J5FPUlq&#&{Yc&f@=nI%PxFI!Vnl5wp zvp_ap64QTj^@OxWl}G&-Zi8CtN)6lH0UeUX)8ALPvazwjU{stFEK(k%1khBFz5Ayw zWaQBcymBwjdF)~raB-JCMG~jA(X4@%=Ikf^Ebsbn!|J~TrAJcsy;_G*mm-$UttHGb zp`IF3`wPNU3=X@xd}3I%%tNH+!}xmK*HdijVuWb-()hu7RJ25GTJ_i*=)W<@fU2>^I+KAbeG6DiPB3nV+=cxrbkhjIHma06et(gi zw6|e4Vro5l8IY7j780)TRyj-r_RH8;HOHhpsyA*Wi~< zU=9nEJd>@z!5wL;#w`&{c(i(bh@KuU!evp8LYA|%*hIrO<+1j9N`un1QBw*kz<1p_hb}-3= zqD&)4O_HN`2HHJHWsR%xUT9CzSjxJK{j4SSK9n%nc4WkBU%aDk~>-^{7;1 z1}(ik4*niL`I**j@UoHnx4TE7+6sv@@@3k*u;mK^8~H!gBuT7u zPG_mR`SRaI6;pE z)4(_skLsPMoy==}uVL@5oYob6v(#6yls_Xz1O~*Uy^s54`6`>e73+Ony%@GegJj6ScDEh2m_z;U30He4PAyv}P|bJgHM(-vzXD9Mkm8N@1UM(%Db7 zALi^D3!KRz&Bl%|Gme6JTCSuJ%lumhjBPvw5|XTm4^)Z{Yykho3mK=$LjxmlzeBb; ziCsO)n5`16H$$*UaqFZlK4oVQzlB~w)6QzT)Qv?ie{Xm4QIYsy1gWu0*|5edC^J)_1~A$R8h0U2BZ%Ds_1#5JKIzVJIyekh2H0 zu*Bt*sDnV4#auvNEj)O1?zZxJ?1|sqx}R6@&JjKd&`Ev`kgw=J`+;u)LA1e$ zn7GsSlxIFedR^OA#7uK@X#Czl+NthkHp5f$Q<$_>?{eA7!%QDnYF&OjSR6}bv3H1dv(ja^EX=g&}5OPQ!I80{So zHQc!bfjn+nG)zq+F&wFj28MYKmVVs8x}{Tt%eRTJo%2C;ya73|&EbJS<;uqev-QN6 z4k{eQ&g$<#2b9goeJngoL!w5HGv?}we%OA7CYZk+t8Je#=1GLvwKd@u4clW|sBwp_ zz~YHX^2JdPTT=>Vf23hBk%YXpy;~b%X#d9LQv7mDklU7|y7%*1pT_+k7Jj2N*&JoFV!v23^Jz*7X-MKGH84 zEPstN9i(TdI1QMRM$D;8)UmZN4`X@MpqhFPRAeZMQ=uq zkmv`J%xcS04`OSa?{BI7Z1-E)u;;yTZ*y%m2M^09*m=SS=WD` z2hH6bX0uEKN}M6r%=&Nl%-6kr?VD9%Y^L9SK4!(P*P%eLq2M{ zlM&ANt@~xEPX_|uqyc+HHD$6oUe7*m+kKqChbCy8Z~n20GKHUkx!cdd?u754E#sBr zcBYD?@TrsB#7ed-y;YzsbgT1FjJ0EfeDJ4`BC2mqcAsqA3&2T7@NQaJOZPRpI+kzO z3>5|7Y4fp*!xcDwjqIc7IL>;sE9uAfwy7>yem*F%cnRE?NN&|nkohxxVS{gqL@HO(qLM8TA=B-7 zPv-K5n8L3*LGccn=Kko5vr2i!j+~%dmJpI3p>8<*)wLJj4zuw*A>u>OUCghiQv0pD zPsqUIW)1qFgP~ys3t@NJ)UO9RLo;!cnDw_@A7GJxZ4=zRs!oNNTY($w;^AcL^0rGb zL^(n#*bAbv2TiShZniknth2vuAh6C2+tfD_&{M{H<0s8z?42;nBq%$Yb7MxJd|99L zAh$|4_q$eg%SM*cg(ymExU~yCu~1R5GEADF(x>QzSLf`_K<#)ql+S%^U0RD82xO+K z$G7RMNW?*py41ue5)XxraeLnA5G(UeR29pqlu!b^@AR4G$GtQVDqjv&EQ_{s(diM+ zS$8{=Mwse3bs1x{J@azJE{lZw9!?Dv3-y_BkvAfM_KhD7p zwhZdrYds^nRl^77#zea9ZM8!Ze1sN0YbOX}UxHu)@vDO?sr!$mGIBXskjeY2TW58X zKH~!$A74cKm`5W8Ld7)8acHG|3cD@+J^y8a|I0Wpv&AnUqP7FWI$7ps0*J(3ln*SfY@;ndNRMVp0X=wzxEm)7M7y$EU~z-EU{LY%(^Vr^mhz=KtJBSfQN+Qnk@7IYQ+)h> zht<9>b3)}9OcE?t>A_WqK@|#Di3zN5Jn$#=3#;lJD$#QL{ZzQx&Us2L2E$l<+gL>p zCP2w1-L!gp3Y-YL5ju&iC{tF_A92ccxVHA|R2?=^@&Z1yJ5_w3ZJ zjx1!5cuYE*G|Ou!+PAKr7+w-+>!C-zO>=@|n#hvg;e2|i8-01cuG6N?u*y}_NJL56 zS@2|Y6|Qu45I{X^X0ct?yT2JNSHh|9<#Ay}%XddFrUyUu$=-~T20-Lqiyj%LG)9zn zJ2SQgk=$CNL9V{(y)V(hA4P0ZvZ;OfSI`vT&$VOL5}mxydo6kGP>Dgbxl*%!x&9^> zt*nx)cfWHNlAX;m~6P_>rcsg<#3|E zWRm6MLOcUw+S!xo>q^l>=7*d*YEzN!BOrB7@%kE{#KN&@cSDR*S>}FoyEfa+yNb|~ zwsD=>XK*E3_b_R>G!njoTwTs`qXi@LsS&1nhmTmLPlADnd2SGL2(;iYvaehzzhTRc3bpUd>;}wLWXXG}f_qbvqrNIAfX001`QVofPtE_Jlw*>54 zefnDMvSVb>ODGri2jwiS#<>(m)a`V6yx=F$%ATNjpOZqBUO=Zi~03Z33Lfe2H>} z2#0Uwhq0ptdXLv;%rc@%hMy41$)Q z>$KTh#7pYYF7sMGP-Vzh2`7qPIQfSLxgyXr(?{*ZjG1K8>yZq%{22H?5NzzlU3Nf@ zuWMwRV2uWg2wh(3C$#*2uN9B9{d(hwTEG!|wKeVmP*XD%nhc>vxT3-09Y6Zho&okZ z`Fqp_sn3|Ah{#jw7!UnX3Uu-+OR#(@ z+!gI3O)~K7+ExZd*;gKN$?f{1ds#IlrJy0m>cg*_uG-=*w0QtYznKy{H55SZXa6w~^kn_Gw$ z1^lE8fbNiI5Agn*X#0PQpMIaJhwHnRn>M2)lftB}!Fr5l0B1m{_LR?Yg{nZXp?BpJ ztC4m26AV)shOvQ%cCN;&rer7~V|OocOWgozz1Z z^k5Z7fa}YZVx8peDRQNt*rziQfAjAMsCiIY%cb88d{_gzxy@ywLqOz| zgFxmZjps$QvdGro7wv0<2<}?6dZ7gQ{cRfLlmdYCTY<}+)Q5^S$N=hPysGdDV&z=(n?42OmIk0%rc+_~`1wx$N>E*lfvu4Ii{q2~*R}B$Q z78N^H^ z>J7%R=zT}0n8D1{2BfyRY}(I$CLB2B*w4XWaTG71J;~Fsn#88{5?QCLz$?Lp93yh| z|HP;FeyZ1@FZ0?t9zvwCAIvT8c~FLt3KWa7-ujHT$!NGb=R3Ob(#I7y`2Z5p?TBks z=$=!wj9v-+i(qfSxdIkRu}ktNgCk-54#U1%y$B$Pyudsn z(?s+3TPED>WyCX#@gc(xSVzeu*6Pnt!v;b5rq9o}k23e5Pj#MGcV``mhd?s(_pmQp z=Iz$B7V)R|sk_QilRA3PR^ZvjB9XPok0UpUB^cs=r@H}&5CNPSyE+4indHCuZcZk# zE7W6TdC+qo0`W7eo?vFT%Yj>Fd6F(x2Ar|9`P#qIHKUF1{CriviE57z-7}wDe}Mug(DAg-WFKG6?pw$QT4CXK`7p@(_MVtNz09mhq*j*@z0ly! zeB<(^cs99Br@z!@@jq!S$T{kw;Eh!gQ!PSVO34*Rt}rW(ewqMNyc+2DQRY)0`}x|% zrXASTfro;Gh_vC!^qti8z1>fbdgZ78oRw^fbpj|ulZ7dVxXT%NUeIp3erjxU8#Z_% zHZCsh!WALxSb7u1oM}&abWPO2$8J3&S~4jhXBw#`__0>zME_rnE&~rg=v>G+Tfuje zwl*P~*_>z69er7zKVd#neZEe~2Bod}!v9mq$3vudO=I{9z>lW-A9GP3UJ>#;EH7VY zH+fb*RQ%)TkGktFekJ?9t+^1z&`d~PIP=9*JCPU)&w}bLIF3AzT*~Y=YRyv76XMg%^d*3jXevm1qPS2!q1@@r) z4wf06sA@8WN#i4h?Avx*h$f9+Oh$Nlba5ZAIJL%X5)V7`5M#gfNmr&vn8*?sUBvlU z$V%mvhO5FsC*J^y2)Ba>N8Tn^6sR^CsA93f3jonPI9A*+<8}t|F;Cz1`798_++Vyz z32y}5QENG-L#@;wxru^U-;Ry7U9Hw2Ae%2t9|4ZjkKIZp6GwU2i)Z3jopOrP$7uT! zzL^B^h~JsP!jqp|;~SWAR@`+7WWqOinJIJRtTf5e?oS?gSZ-xWM#Omd(bpQ}Vs|a! zKYKr*FiJN3YTtZo|DW_=6P>EgYG9>cvic*iEi(H-`PSQa#=;xFEW3wbOaOdJo?1%U z^a2j#G0y6!WgQ~yJ#PZ-Y=dtLDyw*Go~C)Rd0Vh+A)b7t)E|+=k(SG<I!P{GE&>`ay^;S$z_d zp`457pk^1fcm*5$xfY1V#{)m)pY5+$7w$;*e6D0e(K~28=7Si`d)lV+OrT-CUm)Sn zJ;6%#?o0PgqOW$KMEokfk&U>;N9W9JvPR@l6V}7@sHUw45&Yan@=)F76L)gVt0rqX z#6~2o9&jUr!C3x%amHFugvhK~#2@*Su4ozIWHy8T`i9VZ_W7qKm#7gGJ%eY`d=_Bw zndaVidjn`|o13(4rIzOAkfad1r$wIW|`sVgUfV#}CFKujWEH0+cASrrFpM@g) zbIzT5Vhq)mcvxAf{YOGr&4%ATWqxxiS()Sgq9 zgm5mWvyyUR0T|d(3N|KAiX9kJta)M(YHp+E>ZbLu{llh`@}1%O%njSp?)7y9=jv0= zv0wP$%@ceDpp4^&q98K&yoc(Zw%iFBJ)v==bxtNe@wMcXF_ zLx`~T=Cgs}VO1@-Nd=Vi4011BS>CA(g1822FS@|dyy1qt?y|HS*&gVIJnC3}S6YOg zwF{Shy2{(a67dK*X_A#`KdPo_y%7}A6Bfq7KDO?Qdhs~xd)SVwl8tUyJ>1KqLuJS~ zMO)!m9%XsfZ!}LbQ@g^z34heF^6s8NCR{4+TC4cUG*25`nQ^D0Y2m2s`|;4Xm_Yw8 ztRy3U&h<6n&q2!F#uH{sHi0Fa2{e7R;P68tR`^&-fl7H&yKz(>c8)|!^HsWzua@kL z4(vRAvbyVhNB*+;<8qU3z!$D)WWL?O=~!K_ycr~7*3HaYW(_TbGGAs(KD{|&tV5N# zlMCDH0do{8w|)u-L~W(ZNp3+5Y@?bo;!Oa`D38nCE<4P+5I5god^O0j@ixSGHtdG} z9fgyKeQ*4zrW?`Gw+}-jxHnJbnsgVc9#1J)Ph^xSpPS}tp-qnMw>)CJjIu!t&qtQ< zTs~H}xx3>iXPclaK7|{ zn{N$a6Q5Kev@Vbw|I3Cx0U$zWUwmS}-G(Ko8 z6yD`S9;+&qRQT*$b6we>XGj0*N0bi5aGxkn`}jfDpB|pD<-O=yFT9Wd^dlh2pop+a z>Tu1%*GnOq-Tdq8^#x{hr#plLecNGWY^$%%nculUIzoxHIy?_`c%ZNyifVkBv}se} zq>i{o=Sv^Wj-6gQ<2rsCF4kZdJ6J$i{AeWV@|?OAufKHN1;_&B`lK-$qIDf_X1vIg zpgbAPeDd?DF8lOpyAHY?3?EdXz_5hEgd9tr;?TCoO4k*`3s3HE18_1Yc(%(p13 zAu9JXAXzO!zDlPu?P%=iazVRSuU^+i%Z+B}&t3UMy9+=H3PofS?**MRI;q2X6(!0S zMC)vN|6LiOZx7^xB$F7Mfi$O4j*y-Dq?F|bCEQWw&JmM|y<0`3u(0QiS$caLG8on? zk!@4j$P!BNt~jx3;!^{#-&^^hXeZY*S-gC~reo zp%ebF)oTRuITuqLkl%CUY$c%MuX~TY%u4^d8CW-w<+&grtDCgDu(NvP<|(!y$J@a2==iiqn z{XZN2Whv?ZbM?R3)sFy)V-m^zEpc%9ES?lI9i4Z9vzw7di_R|yN6Aq=-VO2^rYNe|9% zp=zrZDi3EjQU)dLHKY7J$Kzm1SaEwzxR?un0@Y64!uP$U$M~29JIhx6b=|QVQBfD2 z6Aov6Eks>Hy3V@o(-q{2U;VoOO{RR!pB_XtN(qtK$&lq6yn3>~>A>{yM@TUj&S!VO z9xKv9o|sDr2@br%3i`VOWR$G!58Ef@%6ag+2xRqKto^cW&u5h7@0C!FSS05-C!8B3 zDE#II#KdbuO$YRf67#Q8+Vwm^Y#I!@$>> z8g4WSpg?$K5Jk+Tp}j8K%FR=PPa;^)wC23}m&DeeQ?hv88@9ftDp*NG+(AAGwr;*_ zEmlXHS8!dbQ=?CyMzKogZm@5_4^4m9t32=ep`j3J z{;ORIcdSN7kyr5KZ1JJ&L~FpH%GdS4zae0B@(+PMx`NBcs$v_HVcrHl#DQD7tiwD0 zrtZHCiiuF2*8nW~pjiPWJNW*xvLcd}Y2Yovw)Pkglrp8lkhhivc`Q7(W78xN<}bT( ziC??4rQhG*1it$p^)x8KmLDqJOp)e8ze{Qc2|qF)Yved{lK$ajSH*|Z14obj?bNJm zl+Ojo)%?FdBV^{VUbR1rsL9Kc$^NR*H}`AKy4-!U%XFr#eoe2YI3<&0%3r?jQ*r&g zpXjf7e&$+qh?iIJ58a2g3G!77A-+V9n*rGK>U?|>_ddP1^Fcc${cbR?L<3Laxm)nv z3ul^|#~qb-gA)~MtG?aJb#n|3YK&IG_E-rDnvRsV&D7S9cq?ImeB*~#Gan_uiA;VC z_nyaCk5unPqujHHiUFMQ0N1>d)Nn*fSP`Qm6Q8KQUo)n`^Je~w4?_GJBOe~-s>NHK s8IR?Y@cmPA{IpB*KefV$vBT4+dc|dT_wI3^bP8l+<#4v*j7P$M06Qm9vj6}9 From 3051c6f8184b3409b70a78666c079381e2425cba Mon Sep 17 00:00:00 2001 From: Sarah Gliner <105934250+sgliner-ledger@users.noreply.github.com> Date: Wed, 29 Nov 2023 11:45:34 +0100 Subject: [PATCH 63/65] [auto]: add PR template --- .github/PULL_REQUEST_TEMPLATE.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..98b720fe --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,8 @@ +# Checklist + +- [ ] App update process has been followed +- [ ] Target branch is `develop` +- [ ] Application version has been bumped + + From 18c4e74f297687520ea87e092cc921e5dc504765 Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Fri, 15 Dec 2023 10:44:32 +0100 Subject: [PATCH 64/65] [add] manifest --- .github/workflows/build_tests_and_analysis.yml | 1 - Makefile | 1 - ledger_app.toml | 8 ++++++++ 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 ledger_app.toml diff --git a/.github/workflows/build_tests_and_analysis.yml b/.github/workflows/build_tests_and_analysis.yml index bd909ab9..31ce0931 100644 --- a/.github/workflows/build_tests_and_analysis.yml +++ b/.github/workflows/build_tests_and_analysis.yml @@ -22,7 +22,6 @@ jobs: uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_ragger_tests.yml@v1 with: download_app_binaries_artifact: compiled_app_binaries - test_dir: tests/functional run_for_devices: '["stax"]' unittesting: diff --git a/Makefile b/Makefile index 5707e99d..28f598ff 100644 --- a/Makefile +++ b/Makefile @@ -43,7 +43,6 @@ endif # Build configuration -DEFINES += APPNAME=\"$(APPNAME)\" DEFINES += APPVERSION=\"$(APPVERSION)\" DEFINES += LEDGER_MAJOR_VERSION=$(APPVERSION_M) DEFINES += LEDGER_MINOR_VERSION=$(APPVERSION_N) diff --git a/ledger_app.toml b/ledger_app.toml new file mode 100644 index 00000000..2b7b03a5 --- /dev/null +++ b/ledger_app.toml @@ -0,0 +1,8 @@ +[app] +build_directory = "./" +sdk = "C" +devices = ["nanos", "nanox", "nanos+", "stax"] + +[tests] +unit_directory = "./tests/unit" +pytest_directory = "./tests/functional" From 646ccfc555fa63e5a9416784e3cefd7c6793b8c3 Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Fri, 15 Dec 2023 11:03:40 +0100 Subject: [PATCH 65/65] [fix] Checking HMAC return values --- src/nano/nanos_enter_phrase.c | 7 +++++-- src/nano/nanox_enter_phrase.c | 7 +++++-- .../stax/check_previous_word/00003.png | Bin 7273 -> 7132 bytes .../stax/check_previous_word/00005.png | Bin 7183 -> 7040 bytes 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/nano/nanos_enter_phrase.c b/src/nano/nanos_enter_phrase.c index 2f790408..c19a925a 100644 --- a/src/nano/nanos_enter_phrase.c +++ b/src/nano/nanos_enter_phrase.c @@ -16,6 +16,7 @@ #include #include +#include #include "constants.h" #include "glyphs.h" @@ -229,8 +230,10 @@ void compare_recovery_phrase(void) { cx_hmac_sha512_t ctx; const char key[] = "Bitcoin seed"; - cx_hmac_sha512_init_no_throw(&ctx, (const uint8_t*) key, strlen(key)); - cx_hmac_no_throw((cx_hmac_t*) &ctx, CX_LAST, buffer, 64, buffer, 64); + LEDGER_ASSERT(cx_hmac_sha512_init_no_throw(&ctx, (const uint8_t*) key, strlen(key)) == CX_OK, + "HMAC init failed"); + LEDGER_ASSERT(cx_hmac_no_throw((cx_hmac_t*) &ctx, CX_LAST, buffer, 64, buffer, 64) == CX_OK, + "HMAC failed"); PRINTF("Root key from input:\n%.*H\n", 64, buffer); // get rootkey from device's seed diff --git a/src/nano/nanox_enter_phrase.c b/src/nano/nanox_enter_phrase.c index c8e28741..6615eac0 100644 --- a/src/nano/nanox_enter_phrase.c +++ b/src/nano/nanox_enter_phrase.c @@ -16,6 +16,7 @@ #include #include +#include #include "../constants.h" #include "../ui.h" @@ -410,8 +411,10 @@ static uint8_t compare_recovery_phrase(void) { cx_hmac_sha512_t ctx; const char key[] = "Bitcoin seed"; - cx_hmac_sha512_init_no_throw(&ctx, (const uint8_t*) key, strlen(key)); - cx_hmac_no_throw((cx_hmac_t*) &ctx, CX_LAST, buffer, 64, buffer, 64); + LEDGER_ASSERT(cx_hmac_sha512_init_no_throw(&ctx, (const uint8_t*) key, strlen(key)) == CX_OK, + "HMAC init failed"); + LEDGER_ASSERT(cx_hmac_no_throw((cx_hmac_t*) &ctx, CX_LAST, buffer, 64, buffer, 64) == CX_OK, + "HMAC failed"); PRINTF("Root key from input:\n%.*H\n", 64, buffer); // get rootkey from device's seed diff --git a/tests/functional/snapshots/stax/check_previous_word/00003.png b/tests/functional/snapshots/stax/check_previous_word/00003.png index bbe94337197d436638600c73a9ada2b0c684bd82..0720b025cbf017fbef59c1880a56e236f126ffbb 100644 GIT binary patch delta 5220 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 delta 5438 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 E13EPzxBvhE diff --git a/tests/functional/snapshots/stax/check_previous_word/00005.png b/tests/functional/snapshots/stax/check_previous_word/00005.png index 00328c26775f0fe4bf96fc3220a9cea42d981ad3..24cdb16d94615f5aafd3bcae2295a07890bb63e4 100644 GIT binary patch delta 5270 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 OVSCyBQuRge2mc4!e=Au4 delta 5498 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 Du2Z_q
SWP}m2nxi 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 B68r!F literal 6532 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 diff --git a/tests/functional/snapshots/stax/info.png b/tests/functional/snapshots/stax/info.png index f560fe63e275156be5e901cd28e6176d0cc8a889..26a58219af2fa06eda86c4c3358d12060d80610b 100644 GIT binary patch literal 9496 zcmeHtc{r4B-}hJ(p_N1=YtbfK41QVC5K;z{wL-*<-7sUDB1IXg2q8;ktl5Sc!<5S2 z$ePBCeH~1OY3#jM{qEzqkM}*^Kc3@w?)!fJd9J_CV~%Ui>$<+@`Tc&D@A>GerKzys z5kU|LBn-K1d<_KRN&G}|!h5Z%0 zs$AyX`(OPY+*G{?3}5q};4*tT)2xHeeD&j&O_0aj_GY!#0i*CS-~GqK7dV?!0TNu~ za!!;14FnP}apgLDpUmybkKrTl;TDRP;1vU%7f=Ox?6n7V9;*Poy!h|DfqdwDZ7Bnl zjKbm4^Kp3AOrhkDzu?rR5aQBLb&Fp0ClhPy7O0!SPRtP93)~ahUn+g-9e;}HN{Lt> zbcdxB%vCvrx$syP_TGB;FxQjh92F5k8;<^TE64hU`_OV|Ln#hfJ;H2qv4N$?S%EEj zSJtU>BA^j?Y#0pA0L@`$Q;<(}4v$*+CUX;wGf6StsqFDzQpK<%-r) zEf&28O`gg_q*}bvmpGfb%9G_p8X_M))=M$i(TPhcJYUTyrspMn@YM?YB@$>)IxhlA zUTx&I=nWmoh9pa2TM)vuWPd4+B_!rKIGeIDxYUgPb+E?<`Q@#u`L=!&9Bbr`a>@3c zF&R)|)V5@7#ZPGe2yUD?Dv{pYQ;^sR50)u*V1b$XnU$*ScVStk5yOxAepvP4&*v0W zsy-7L6_TsinK|T#y=j#-v(!wPS-E&9e5?*b~UY1H6d3y-jIQ$=J{eeyxS(3mwjEmx*vl_=q-eGDiPV0mw-96b74J1 zpCmmup9tn-(6uU7^+K?Fw>#ZoioNGHw;MTc2cEFtXG{wQ<`ffCuJl%`nx`4}CNEhR zW_z7st@_QEhBOqRjJ>){tYx*lp(atF5(;f!X14WzM4?_U)ZIYhFM`I0)Y z6!sD-f0_1sZ4xJzdcT%DEz+Y62Gpx|RN}YA4U`Di7e8uPObkfvguGly#6VNtL|&|q z>Y-pbu?6heUEjAq)+rD*X77AqJg77G4ayU#Y`9BQ@b$dgm#RO8Ug(jV#2r{cpohO+ zWeTw#B0mZpeL{O0c?(}lCkJFF5^1Fuku$-fGq3L%R(jI!O;0AKAC7H2q4PHE zqwcuA?xG-HV*Vv=@5#A3$vAEXj`B!T7m|#0Fkfp|p|yPS-(s_;zAVhvSM}eWIy6vq z7Eb-_U!p)P#FL5yBV6%r=>@Do9Bwt#p?ol-OEEsTdF+_XEvFLZfE3+^rmxP(K@Ts6 zu=GZVEvHMjYM=Sjetz^{fU+MVjoVPjF_H7W=T}+$g!#bane`Cj3sWHFE}T*^I8|3A z(KJHjO^K|U`>HH-b65>k{rD9-wb{trdFwg$`GVf)3V4v5yr!LLdF`7X&mnr>@^mco zojLYNkr*UMZ~W+7&5)1b#soI7f5y0=Oe`VR$BX(iNGH>>u**Yk#f5U0EgG6YPX6+I zOSeO9oj^z6m-0O(!<%KWEjbjD)SDZuTgs20OKm(Z@H(mD?e^%2&qCIsh|k@JNqQlt zQq9-1QllBbBPPq2)>MY&==UF#I25Xp zO>wzUMZYQ1k@2u!E^M~%CDMcB?e=->%Y_o(J2&!`1ucYqamO2qWDi`s;n6fAy7(?t z+R#WzkbPS-Ai2yT!%_+OtYzEVbyX8u4KL-aOpAlHEY;#P=$ah9a6RT>`=vB-PU%rt zxO^Zl%Kk2-d>SEaq=2UEDC^}88+nJD8d(Uxx|phl+Lw@Y-DBhzIw{ojH8H-DW8-t# z6_{EAHsH6c)P)r)Db`nFuys1rwll9HvAih|IYz|;F+oCcWP7Dg`gADxr&x&HHUKD< zsx(OJU9L=xP`X;XSIp*fFJ@RnOGHuzJV#vB`nypN?gMB|i$+k8UNd zYW^joyK_?SbVXzN`m5QD7xItFntPsb93kTzbT2tz*ZR`8J8m`a?Ak83W!Mo^nA{MO z%gf)>mL%zkERVOHS04q&Z6icmqUZldd}vbCl+!|8|0k4 z((gVmu`QNa?+;p=F}8gY>~diJ92x5)b3Ey{6RosAEVOubo8T<$7GZ@ z%9Q`9d|Q|JFTSi%3niND_u3cUG3~MTrj|BbO>k3gjgJR$izBpWD zun%V5bvsx}Sd@-5mLSKp0)RKr`8V*I;yWXoQVmK68I`K;ZZpst=W^1W=kBmxb(&d9 z$5bfKxc2Gz58j3PV9H8?*Kp)5sTOw_{bw#$-Fse)MzWa^EJQy$j$54)cxu|?iNhr! zc}R}~zys1@B#cH6sJsZ_dG{9K%gjpVnPbs^9VlsTue^eF^R>}dmwrW=+4#1yKCIpa zyn$IQ6^`syVgpgiRj$SXmHYNzi*xE0az=f%a03Ga)9)q1mjT%9SXp;G7^}~x$DT`W zs{>Q8EqR+kpOL(-W*kmMW2S!^H9=xN%q#m7qvN^z32Lu8+w79{dG!3G-;Ld(L{YoN}wJaM9-O)j&LDN+#hQ0Z6y)oy`6 zFD)fV@>749Rz;9BPWSd3;qGuLFCGjZcu3u#3({Myds6C%==?+9nj2leQnRL4zPx9D zfqK;hJ5fBUnxcN47E4$RK`%Szh|QJ-^{X;O%;TmzK;SO7CWSt(^iK9HmrTn$JhyrC z+xgiP3EAs*N1nr}?W?^$n!P7*&Nsr4`)Q?%w|GF{A(D`3KZ*nZ#Mh?87hAgXj87l^ zRsH?I(r60NK;-N1)aY6`iT|;iM6`?+%vAxBEdS_n zEwEWzQD5aD`RDr=!)b~Z*Gp&ow1Hxy953I13N)4U)0TShpxB`1v<&&-i8E*N@(dq6 zo^^>2`&3QochWnXO%YEp88yUED3njP&Si8Bk5RitE6|d!X@B|aWCo!a>jpjieqW8a z%@5>j>8}p928@Z%2?B_>9_|c%k#S^V(mL3wgU3C=!(FPys96X87qzx(=*r0<1Re!_ zB(wfhGde+0a{r}Y4?>rQ*Snv(q)ho}CgffYXfOWgB&spaU@(Go+CHrlsum_*c=F5> z243xlrOX+g0t(zmJE>rz4J7$-XAv^0UEJg=iX_rhd2ef{z~NCzmP~fcJUn_UageGKM1%(R# zYo*Ccmg2!+CmE4Ac}N*Sm@?*V5dpV1rUKe5GBBw%Kn@qa#z!RP zD*uorAwiv$3%KnNp3@e+j0{n7^zH|LBtjJpf$Yy)P3iat%TH{%oh%>lYlCbysmT75 z?xi_6+|rZRTn+bh(e|6$&hxPH!I>4<&QaE1a}tM4#^9+`p)AS_*+~g94o}_OW7!|b z`vW~pZB7=kgr&H+xY*I!t8qBxcovvb+tGc^qvkVQGebGPv9YmXvC?EmaP-Bn?p&jD zc|va87{y4@dX6QIsEjIX$CGcA+5$y3#gtdyxS*dw zSlTZ3X&STB2Srw8i{8XT9xr^#%xL;>5~&c(ZzaXBX10xNPDKgI) zCp&rPK1pMCZx1aJS_AS43w8A>3AKy(`tk8*op_oAZftYYA?nbilX>xG#$C`E*LI;b zwpp?~B$-e~lNqmjHbpYuJIqbecnGHSsymh10wFXS*QPT$k9T2GSKo~=%L#wYGW%*v zxBAi)^2kEyP*KK(hbKpmZf)tPf&`5Jed>AZQSPjt_Eyk&&}bz9)+BOfHdh1BgP8w- zZ2#vYCMcO$-`}ttj4$*`FYuCTK@N!8ISmqNWn~QI`0@XrVqX_J68$Hsm{(J#E>C%- z1DhT_ALp6wG27}=KIlBQ9ci#yIMdA!0FsOs$hrUaKScQ4zEKY-bbEUfkHgWM0Fz)n zd}mUT%ivZeIeubo0Z@u9_Y*$d!jT$sYT-0+`^r|hzLv%HlsAdHig6oTTU$B&+?4a~ zqALH7sP3;_{M0VL}R8nEW3m%?aKme0`lN`7ShdA z#5|*YSPZekgPD?U&H__tae!Trv+|g(6q)};wPQH>Mh;T_uHC0oGT&@CYe5~MXw?qZ zxU}Alz+`zN_x4IvfDZ|ljsU8T?{vAsvnDwb=1u(tRyf8VD_laCGd5U;rL+YBoP(){ z3(gCtc04T#COpbh=v6<}aZT|n3QGV}h%G(%Ky9|IF5N~?zh*jDIjE3e%6#R-l-6Ug z37mHm4L>KtBEix%60DQ;Q%X^$1k4L}7_$sh3_e)_7#qwGj@5?s&qj|L>FvL~|7Z_X zXDBUcV)<=LGNSKdiX9w3DQvN5k^Mt2WX_+iD=0l)+u~%~vw05;s2Xnb+sOQEftb(* z9o4ZVLo9h|LseM2+>K-ZVqcPNvC|6#q_nqdAMKm&142&G#*dt(`zeDsZo88-347Yl z*$lu+sj)i0b<{sQ^XI~3{>8N_5Mjw_PXBb`M0xocq=zXbv(#3lydtX^U~GxEPYpSB zE+!&|cZHY(@-C_rXNJYIQKTWCTxFol><=N>kCa|$C?b%MA6jV>2>i1hSEe+vnnktz z985jX^3JXwP<#puTC8p-FGBsMsP{G&v0UKOlM>KR@w`D=|Q6{Hq^8MqW*s2(2^ER)LiE}i>cZ%$v_clglCmc zSA+paDX!7?jmv8VlJga2S)AEj-w)KRf}#@hWInUXcj#LEiXvIVpWkX6PvfcZgC>Q& zUT-@+s-89xCd&PQxgB9CkQE&6yN@P)hO6$&eMK7kvb2uIbS8{>BrS~)D8iu5aUV3v zDH^jFkI~Y|k9*_(b91{}8ttXNCsVl(A22EJgx6eD_=oH~T9$z3x&$ou@PTVf0=L|j zto{^c@g8QMsAHT)_0?`C_VpC^UiFrVclZk8{GFzeRQEZNFqHU}wvE?MgYDZT z6WJ-deeSxthN1TuEEk#*8@_Z*1(2MHl6k)t>m3i-k6Eqgt{t6&N9(7!FVzk4@N{Hi zkuCai3>?Yqk{P|B$Ekk1+|%{=g^D5!=1^setTQ<50e=Gqqp_~rLj`eG{ALP8d>QXl zXmJBt$SACsaOc-6n+}H96sZfCLp^;zIt*eL{2x*o{ti9Djov&eKfFXaSW zBi3n7M7FpmXb7*X#=SR@jM65_-M^e{Z1;`3P7uG2s;h-h9mxrh9e8>G4*LAH8Kqqx z+--&D4&__@Z&|ZfqyUhgqmA+O2bbazyQ249cP9J3vwvk-=-IIj6( znLc2|1=KIl`8tAiGDYHysI>uSdX{y!ouGkGD3NBFtYdMVL(dhbx(o(~uIs|_4)tj( zfg!uD_~IOHvr=nlfuN z%*XS+P6Cd~9g>{oq)O^xR@M$wqvL9s zq?-|yD4;revX?hSDmN?(`}{&SMb5N9-efdK#pz{^N2tHt`@W-jZQT^1%Hq3UDpeCF z5E%-MM*Q`7zJWEB3pT7w3!0$b5*4VVR+ z2t4CUX2(K~CAz}^RaLY3MD1FgE|)l+|Kg3YebR^R_wO2v^eaErbfeU=@OrYm8hN=D zGl)KU>WH45-@9p9Sc-?3_Zf}S-Q*bGmaJQbha%{6zOvma~sTOf7lQt;} zj?HMdsOJaolshzsD$jhdcZkrOoqBdg_g??Rnxh0x-STac|I9h5P-WUHfBcY>9vYpYPezttxXQ zeN;hz#NGW@NZum^m)&YXPbR)O9R}JROl&g`^)J^k|AO7B7mSHe4GX#KY3;;8MsU`Q z&JHWmMR8ZuDE3P=`kSuiTu6EltrqEDyp;Ua+k0TWT-}27MpTw!;_}KFXnxpT<4n0f z;}1%fX8_>u%%C)&H!EmWF{rHfDpKG0xrMOjEnM4X(6bx7^!5kz;1{gIqzI>1d`vx? z#Fji=%J|O{OC=X5fxi5A>K*yZ(xkFC=4!jq3KQB(P6Z?CPjt^IS%pl-A8DDe4ghzk zN$6r#50L(zcZgjv=^dU=ZY}@mek{cQ)B5&gG8Ai%Ii+E+Xn4*Y_5hrakrCu?cCJB~ z9HS_tD&ZgF5<^j-VIb)kZ+2ut>#^^WKHLU~%8aW3S(b;?QWtAPpJ!YJFA}ExeiLQ% zVk)6*Dq>-~U*yOAf(Gv|%b||tgM(!UUsN9%!u6z z-yhnDr6TO^r&>e%XJ!hCwVIIRT;(;JQE}9$@9vw1=>=Ojep|npJO9cy;j&Wvapq|& z&=1}1N4XC-^FU`Yva0a=b`fvy~PP==A0R zldl2wTXP;DviqLhT>q}a20L3pJU;{t5%4zJq@Mw|5Wa3~nn*WlJu@6l{&46)z)AUt zhL-s#$mtllioKINJ5M=#ORw%+^pWMo#0m|+O+RZ}eX=O~o1p-=(0%EHQ0bzyKV!YD z1JKjAgk?F77e&EN)db<`uirdEhB*FYkcE`KPw`It`6oN#~8c6k1jH+WWSz z-t;9b*X_cv2lfP)$JF!RzQH@7tEP20=-k=xJG=wbbD6+jP!50W(Gm`eo2GJ6iJ`ae z&jAj4Ye!HyZ)SyEx3 zZ{7#aMc1*r>Dq&rC|#nwKtzfoqM{g0&3P@S<_17O#QYMx&nHxWkQY!LRt8sHR=E;O zM)`|6VGbq3C=iut8bJGEiI4AyMW|d!Ab%h|Py=qB6i~hLIs=w}NE$=_p#ES37%3XN z-c+Oc94|e4>hE(XisvI!_SVT@V!ifsIXs2E+>43b9n>4&`9W^4ecj;I5nu#NGwdGD z$CDQ8wa@;{QjS5T5X_hTsuMMqFwkt`I(BKS*l6y~&6B)h2>-8P$R>P zJjNn<#mWl41}>iQMMNGuCQvfSc^JlFGMQAvndx1LuAjFf{&@t36f531d12x`-tdo~ z2SjnGEf>v3{GCF+lb>{8LFM$lf5(*6a78I$sULm(k3=J7iqE=zB>~F=tXU>3zh76b zf}URAsW#{egadWG0IgggiAC?VXLbyelimX_g7;7cqWF7HxQTi30l{xdV+-9jcQCmj zd%WV9x=19{!|V2C6<{;|TBZWQG#t#%Mt~ys`TDF~hg`I|DWIpiHe5FF4DGNa4N(dD z+H5Gw=^G0~IUv;bLk}Ce-s}#%mGIV3n_Eac=(bAwaahB9MeEqTykhr48RotZGjit6 z<_eAps5;*CoNE>iT$wzG0Em{D^KcBW*zVZPefBRwH)Cd8kN!&!3av$19;>+F5$oj$ QoOl93Oe~Gd3|;U2A2FFVyZ`_I literal 5157 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 HxZnL>UJlmR literal 5842 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 diff --git a/tests/functional/snapshots/stax/second_24.png b/tests/functional/snapshots/stax/second_24.png index e3643f43684b49bc834988b88a0613bca98eba9f..c986e3d11265031ea7daa7540dd1900ac2174b7f 100644 GIT binary patch literal 7357 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 literal 4956 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 QQWY?}YIOzsr`!Gi0FLO~ivR!s literal 5047 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 Hu-JbAaVqI9 diff --git a/tests/functional/snapshots/stax/welcome.png b/tests/functional/snapshots/stax/welcome.png index 34a689979779946a088ed382543a75ff67841fec..ab2b2db5a4752fc030bcb7505d48c1db030f0bbe 100644 GIT binary patch literal 15131 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 literal 7961 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 diff --git a/tests/functional/test_fatstacks_full.py b/tests/functional/test_fatstacks_full.py index dffe1a9c..f4827f81 100644 --- a/tests/functional/test_fatstacks_full.py +++ b/tests/functional/test_fatstacks_full.py @@ -1,6 +1,6 @@ from ragger.navigator import NavIns -from .navigator import NavInsID, StaxNavigator +from .navigator import CustomNavInsID, StaxNavigator from .utils import format_instructions @@ -12,26 +12,26 @@ def test_nominal_full_passphrase_check_ok(navigator: StaxNavigator, functional_test_directory: str): # instructions to go the the keyboard instructions = [ - NavInsID.HOME_TO_CHECK, - NavInsID.LENGTH_CHOOSE_24, - NavIns(NavInsID.WAIT, args=(0.5, )) + CustomNavInsID.HOME_TO_CHECK, + CustomNavInsID.LENGTH_CHOOSE_24, ] # instruction to write the words for word in SPECULOS_MNEMONIC.split(): instructions += [ - NavIns(NavInsID.KEYBOARD_WRITE, args=(word[:4], )), - NavIns(NavInsID.KEYBOARD_SELECT_SUGGESTION, args=(1, )), + NavIns(CustomNavInsID.KEYBOARD_WRITE, args=(word[:4], )), + NavIns(CustomNavInsID.KEYBOARD_SELECT_SUGGESTION, args=(1, )), ] - instructions.append(NavIns(NavInsID.WAIT, args=(1, ))) instructions = format_instructions(instructions) # running the instruction to go to result screen - navigator.navigate(instructions) + navigator.navigate(instructions, + screen_change_before_first_instruction=False, + screen_change_after_last_instruction=False) # now that the 24 words have been written, we check the resulting screen # should be correct instructions = format_instructions([ - NavInsID.RESULT_TO_HOME, + CustomNavInsID.RESULT_TO_HOME, ]) navigator.navigate_and_compare(functional_test_directory, @@ -44,26 +44,27 @@ def test_nominal_full_passphrase_check_ok(navigator: StaxNavigator, functional_t def test_nominal_full_passphrase_check_error_wrong_passphrase(navigator: StaxNavigator, functional_test_directory: str): # instructions to go the the keyboard instructions = [ - NavInsID.HOME_TO_CHECK, - NavInsID.LENGTH_CHOOSE_12, - NavIns(NavInsID.WAIT, args=(0.5, )) + CustomNavInsID.HOME_TO_CHECK, + CustomNavInsID.LENGTH_CHOOSE_12, ] # instruction to write the words for word in SPECULOS_MNEMONIC.split()[:12]: instructions += [ - NavIns(NavInsID.KEYBOARD_WRITE, args=(word[:4], )), - NavIns(NavInsID.KEYBOARD_SELECT_SUGGESTION, args=(1, )), + # test is designed to fail, so the first character is enough to select a word + NavIns(CustomNavInsID.KEYBOARD_WRITE, args=(word[0], )), + NavIns(CustomNavInsID.KEYBOARD_SELECT_SUGGESTION, args=(1, )), ] - instructions.append(NavIns(NavInsID.WAIT, args=(1, ))) instructions = format_instructions(instructions) # running the instruction to go to result screen - navigator.navigate(instructions) + navigator.navigate(instructions, + screen_change_before_first_instruction=False, + screen_change_after_last_instruction=False) # now that the 12 words have been written, we check the resulting screen # should be incorrect instructions = format_instructions([ - NavInsID.RESULT_TO_HOME, + CustomNavInsID.RESULT_TO_HOME, ]) navigator.navigate_and_compare(functional_test_directory, diff --git a/tests/functional/test_fatstacks_options.py b/tests/functional/test_fatstacks_options.py index f70b4896..1f2bd285 100644 --- a/tests/functional/test_fatstacks_options.py +++ b/tests/functional/test_fatstacks_options.py @@ -1,6 +1,6 @@ from ragger.navigator import NavIns -from .navigator import NavInsID, StaxNavigator +from .navigator import CustomNavInsID, StaxNavigator from .utils import format_instructions @@ -11,9 +11,9 @@ def test_check_info_then_leave(navigator: StaxNavigator, functional_test_directory: str): instructions = format_instructions([ - NavInsID.HOME_TO_SETTINGS, - NavInsID.SETTINGS_TO_HOME, - NavInsID.HOME_TO_QUIT + CustomNavInsID.HOME_TO_SETTINGS, + CustomNavInsID.SETTINGS_TO_HOME, + CustomNavInsID.HOME_TO_QUIT ]) navigator.navigate_and_compare(functional_test_directory, "check_info_then_leave", @@ -24,13 +24,13 @@ def test_check_info_then_leave(navigator: StaxNavigator, functional_test_directo def test_check_all_passphrase_lengths(navigator: StaxNavigator, functional_test_directory: str): instructions = format_instructions([ - NavInsID.HOME_TO_CHECK, - NavInsID.LENGTH_CHOOSE_24, - NavInsID.LENGTH_TO_PREVIOUS, - NavInsID.LENGTH_CHOOSE_18, - NavInsID.LENGTH_TO_PREVIOUS, - NavInsID.LENGTH_CHOOSE_12, - NavInsID.LENGTH_TO_PREVIOUS + CustomNavInsID.HOME_TO_CHECK, + CustomNavInsID.LENGTH_CHOOSE_24, + CustomNavInsID.LENGTH_TO_PREVIOUS, + CustomNavInsID.LENGTH_CHOOSE_18, + CustomNavInsID.LENGTH_TO_PREVIOUS, + CustomNavInsID.LENGTH_CHOOSE_12, + CustomNavInsID.LENGTH_TO_PREVIOUS ]) navigator.navigate_and_compare(functional_test_directory, "check_all_passphrase_lengths", @@ -41,16 +41,16 @@ def test_check_all_passphrase_lengths(navigator: StaxNavigator, functional_test_ def test_check_previous_word(navigator: StaxNavigator, functional_test_directory: str): instructions = format_instructions([ - NavInsID.HOME_TO_CHECK, - NavInsID.LENGTH_CHOOSE_24, - NavIns(NavInsID.KEYBOARD_WRITE, args=("rand", )), - NavIns(NavInsID.KEYBOARD_SELECT_SUGGESTION, args=(1, )), - NavIns(NavInsID.KEYBOARD_WRITE, args=("ok", )), - NavIns(NavInsID.KEYBOARD_SELECT_SUGGESTION, args=(1, )), - NavInsID.KEYBOARD_TO_PREVIOUS, - NavInsID.KEYBOARD_TO_PREVIOUS, - NavInsID.KEYBOARD_TO_PREVIOUS, - NavInsID.LENGTH_TO_PREVIOUS + CustomNavInsID.HOME_TO_CHECK, + CustomNavInsID.LENGTH_CHOOSE_24, + NavIns(CustomNavInsID.KEYBOARD_WRITE, args=("rand", )), + NavIns(CustomNavInsID.KEYBOARD_SELECT_SUGGESTION, args=(1, )), + NavIns(CustomNavInsID.KEYBOARD_WRITE, args=("ok", )), + NavIns(CustomNavInsID.KEYBOARD_SELECT_SUGGESTION, args=(1, )), + CustomNavInsID.KEYBOARD_TO_PREVIOUS, + CustomNavInsID.KEYBOARD_TO_PREVIOUS, + CustomNavInsID.KEYBOARD_TO_PREVIOUS, + CustomNavInsID.LENGTH_TO_PREVIOUS ]) navigator.navigate_and_compare(functional_test_directory, "check_previous_word", diff --git a/tests/functional/utils.py b/tests/functional/utils.py index e27c81de..5daa858c 100644 --- a/tests/functional/utils.py +++ b/tests/functional/utils.py @@ -1,9 +1,9 @@ from ragger.navigator import NavIns from typing import Iterable, Union -from .navigator import NavInsID +from .navigator import CustomNavInsID -def format_instructions(instructions: Iterable[Union[NavIns, NavInsID]]) -> Iterable[NavIns]: - return [NavIns(instruction) if isinstance(instruction, NavInsID) else instruction +def format_instructions(instructions: Iterable[Union[NavIns, CustomNavInsID]]) -> Iterable[NavIns]: + return [NavIns(instruction) if isinstance(instruction, CustomNavInsID) else instruction for instruction in instructions] From 63d08162cce31ffb7e390e713ce26d1fe724571d Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Fri, 21 Apr 2023 17:24:16 +0200 Subject: [PATCH 50/65] [fix] Review corrections --- .github/workflows/codeql-workflow.yml | 2 +- Makefile | 18 +++++++----------- src/stax/ui_stax.c | 9 ++++----- src/ui.h | 4 ++-- 4 files changed, 14 insertions(+), 19 deletions(-) diff --git a/.github/workflows/codeql-workflow.yml b/.github/workflows/codeql-workflow.yml index 54c50f27..fb757ec6 100644 --- a/.github/workflows/codeql-workflow.yml +++ b/.github/workflows/codeql-workflow.yml @@ -45,7 +45,7 @@ jobs: # CodeQL will create the database during the compilation - name: Build run: | - make BOLOS_SDK=${{ matrix.SDK }} TARGET=${{ matrix.name }} + make BOLOS_SDK=${{ matrix.SDK }} - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 diff --git a/Makefile b/Makefile index 1669cfe3..b9d1ba0a 100755 --- a/Makefile +++ b/Makefile @@ -31,7 +31,7 @@ APPVERSION_N = 2 APPVERSION_P = 0 APPVERSION = "$(APPVERSION_M).$(APPVERSION_N).$(APPVERSION_P)" -APP_LOAD_PARAMS = --appFlags 0x10 $(COMMON_LOAD_PARAMS) --apdu --curve secp256k1 --path "" +APP_LOAD_PARAMS = --appFlags 0x10 $(COMMON_LOAD_PARAMS) --curve secp256k1 --path "" ifeq ($(TARGET_NAME), TARGET_NANOS) ICONNAME=icons/nanos_app_recovery_check.gif @@ -51,8 +51,6 @@ DEFINES += LEDGER_PATCH_VERSION=$(APPVERSION_P) DEFINES += OS_IO_SEPROXYHAL DEFINES += UNUSED\(x\)=\(void\)x -DEFINES += HAVE_UX_FLOW - DEFINES += BOLOS_APP_ICON_SIZE_B=\(9+32\) #DEFINES += HAVE_ELECTRUM DEFINES += IO_USB_MAX_ENDPOINTS=4 IO_HID_EP_LENGTH=64 @@ -60,11 +58,10 @@ DEFINES += HAVE_SPRINTF ifneq ($(TARGET_NAME), TARGET_STAX) $(info Using BAGL) - DEFINES += HAVE_BAGL + DEFINES += HAVE_BAGL HAVE_UX_FLOW else - $(info Usgin NBGL) + $(info Using NBGL) DEFINES += NBGL_KEYBOARD - APP_LOAD_PARAMS += --appFlags 0x200 endif ifeq ($(TARGET_NAME), TARGET_NANOS) @@ -126,11 +123,10 @@ include $(BOLOS_SDK)/Makefile.glyphs APP_SOURCE_PATH += src -ifeq ($(TARGET_NAME), TARGET_STAX) - SDK_SOURCE_PATH += lib_nbgl/src - SDK_SOURCE_PATH += lib_ux_stax -else ifneq ($(TARGET_NAME), TARGET_NANOS) - SDK_SOURCE_PATH += lib_ux +ifneq ($(TARGET_NAME), TARGET_NANOS) + ifneq ($(TARGET_NAME), TARGET_STAX) + SDK_SOURCE_PATH += lib_ux + endif endif # Main rules diff --git a/src/stax/ui_stax.c b/src/stax/ui_stax.c index 34317d2e..d2444cfd 100644 --- a/src/stax/ui_stax.c +++ b/src/stax/ui_stax.c @@ -61,8 +61,8 @@ static bool on_infos(uint8_t page, nbgl_pageContent_t *content) { if (page == 0) { content->type = INFOS_LIST; content->infosList.nbInfos = 2; - content->infosList.infoTypes = (const char **) infoTypes; - content->infosList.infoContents = (const char **) infoContents; + content->infosList.infoTypes = infoTypes; + content->infosList.infoContents = infoContents; return true; } return false; @@ -218,10 +218,9 @@ static void display_keyboard_page() { .icon = NULL, .offsetY = 0, .onTop = true}; - strlcpy(textToEnter, "", 1); - memset(buttonTexts, 0, NB_MAX_SUGGESTION_BUTTONS); + textToEnter[0] = '\0'; + memset(buttonTexts, 0, sizeof(buttonTexts[0]) * NB_MAX_SUGGESTION_BUTTONS); layout = nbgl_layoutGet(&layoutDescription); - memset(headerText, 0, HEADER_SIZE); snprintf(headerText, HEADER_SIZE, "Enter word n. %d/%d from your\nRecovery Sheet", diff --git a/src/ui.h b/src/ui.h index 425d8b3e..1d0d02fb 100644 --- a/src/ui.h +++ b/src/ui.h @@ -20,11 +20,11 @@ #if defined(TARGET_NANOS) -#include "./ux_nanos.h" +#include "nano/ux_nanos.h" #elif defined(TARGET_NANOX) || defined(TARGET_NANOS2) -#include "./ux_nanox.h" +#include "nano/ux_nanox.h" #endif From 3feec27245de17281dda65ef1d44853d893edcbd Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Fri, 21 Apr 2023 17:54:08 +0200 Subject: [PATCH 51/65] [fix] Review refactorization --- src/nano/{ux_nanos.h => ux_nano.h} | 39 ++++++----- src/nano/ux_nanos.c | 2 +- src/nano/ux_nanox.c | 2 +- src/nano/ux_nanox.h | 101 ----------------------------- src/ui.h | 8 +-- 5 files changed, 23 insertions(+), 129 deletions(-) rename src/nano/{ux_nanos.h => ux_nano.h} (75%) delete mode 100644 src/nano/ux_nanox.h diff --git a/src/nano/ux_nanos.h b/src/nano/ux_nano.h similarity index 75% rename from src/nano/ux_nanos.h rename to src/nano/ux_nano.h index d94e60a8..aa70844b 100644 --- a/src/nano/ux_nanos.h +++ b/src/nano/ux_nano.h @@ -16,11 +16,9 @@ #pragma once -#include "../ux_common/common.h" +#include "ux_common/common.h" -#if defined(TARGET_NANOS) - -typedef unsigned int (*callback_t)(unsigned int); +#if (defined(TARGET_NANOS) || defined(TARGET_NANOX) || defined(TARGET_NANOS2)) // bolos ux context (not mandatory if redesigning a bolos ux) typedef struct bolos_ux_context { @@ -35,31 +33,28 @@ typedef struct bolos_ux_context { unsigned int onboarding_words_checked; unsigned int words_buffer_length; + + // 128 of words (215 => hashed to 64, or 128) + HMAC_LENGTH*2 = 256 +#define WORDS_BUFFER_MAX_SIZE_B 257 + char words_buffer[WORDS_BUFFER_MAX_SIZE_B]; + +#if defined(TARGET_NANOS) // after an int to make sure it's aligned char string_buffer[MAX( 64, sizeof(bagl_icon_details_t) + BOLOS_APP_ICON_SIZE_B - 1)]; // to store the seed wholly +#else + // label line for common PIN and common keyboard screen (displayed over the entry) + const char* common_label; +#endif // defined(TARGET_NANOS) - char words_buffer[257]; // 128 of words (215 => hashed to 64, or 128) + - // HMAC_LENGTH*2 = 256 - -#define MAX_PIN_LENGTH 8 -#define MIN_PIN_LENGTH 4 - - // slider management + // slider management / menu list management unsigned int hslider3_before; unsigned int hslider3_current; unsigned int hslider3_after; unsigned int hslider3_total; keyboard_callback_t keyboard_callback; - -// detect stack/global variable overlap -// have a zero byte to avoid buffer overflow from strings in the ux (we never -// know) -#define CANARY_MAGIC 0x7600E9AB - unsigned int canary; - // for CheckSeed app only uint8_t processing; @@ -78,8 +73,12 @@ void screen_common_keyboard_init(unsigned int stack_slot, #include "ux_common/common_bip39.h" +#if defined(TARGET_NANOS) extern const bagl_element_t screen_onboarding_word_list_elements[9]; - void compare_recovery_phrase(void); +#else +// to be included into all flow that needs to go back to the dashboard +extern const ux_flow_step_t ux_ob_goto_dashboard_step; +#endif // defined(TARGET_NANOS) -#endif // TARGET_NANOS +#endif // (TARGET_NANOS || TARGET_NANOX || TARGET_NANOS2) diff --git a/src/nano/ux_nanos.c b/src/nano/ux_nanos.c index 85767412..7b5e2a2a 100644 --- a/src/nano/ux_nanos.c +++ b/src/nano/ux_nanos.c @@ -15,7 +15,7 @@ * limitations under the License. ********************************************************************************/ -#include "ux_nanos.h" +#include "ux_nano.h" #if defined(TARGET_NANOS) diff --git a/src/nano/ux_nanox.c b/src/nano/ux_nanox.c index 5b94dc59..a4f54ec6 100644 --- a/src/nano/ux_nanox.c +++ b/src/nano/ux_nanox.c @@ -14,7 +14,7 @@ * limitations under the License. ********************************************************************************/ -#include "ux_nanox.h" +#include "ux_nano.h" #if defined(TARGET_NANOX) || defined(TARGET_NANOS2) diff --git a/src/nano/ux_nanox.h b/src/nano/ux_nanox.h deleted file mode 100644 index d10259d9..00000000 --- a/src/nano/ux_nanox.h +++ /dev/null @@ -1,101 +0,0 @@ -/******************************************************************************* - * (c) 2016-2022 Ledger SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - ********************************************************************************/ - -#pragma once - -#include "ux_common/common.h" - -#if (defined(TARGET_NANOX) || defined(TARGET_NANOS2)) - -// bolos ux context (not mandatory if redesigning a bolos ux) -typedef struct bolos_ux_context { - unsigned int onboarding_kind; - -#ifdef HAVE_ELECTRUM - unsigned int onboarding_algorithm; -#endif - - unsigned int onboarding_step; - unsigned int onboarding_index; - unsigned int onboarding_words_checked; - - unsigned int words_buffer_length; - // 128 of words (215 => hashed to 64, or 128) + HMAC_LENGTH*2 = 256 -#define WORDS_BUFFER_MAX_SIZE_B 257 - char words_buffer[WORDS_BUFFER_MAX_SIZE_B]; -#define MAX_PIN_LENGTH 8 -#define MIN_PIN_LENGTH 4 - char pin_digit_buffer; // digit to be displayed - - appmain_t flow_end_callback; - - // label line for common PIN and common keyboard screen (displayed over the entry) - const char* common_label; - - // slider management / menu list management - unsigned int hslider3_before; - unsigned int hslider3_current; - unsigned int hslider3_after; - unsigned int hslider3_total; - - keyboard_callback_t keyboard_callback; - - unsigned int overlay_refresh; - unsigned int battery_percentage; - unsigned int status_batt_level; - unsigned int status_flags; - unsigned int batt_low_displayed; - unsigned int batt_critical_displayed; - -#define BATTERY_FULL_CHARGE_MV 4200 // 100% -#define BATTERY_SUFFICIENT_CHARGE_MV 3840 // 40% -#define BATTERY_LOW_LEVEL_MV 3750 // 25% -#define BATTERY_CRITICAL_LEVEL_MV 3460 // 10% -#define BATTERY_AUTO_POWER_OFF_LEVEL_MV 3200 // 0% - -#define BATTERY_FULL_CHARGE_PERCENT 95 -#define BATTERY_SUFFICIENT_CHARGE_PERCENT 40 -#define BATTERY_LOW_LEVEL_PERCENT 25 -#define BATTERY_CRITICAL_LEVEL_PERCENT 10 -#define BATTERY_AUTO_POWER_OFF_LEVEL_PERCENT 2 - - // detect stack/global variable overlap - // have a zero byte to avoid buffer overflow from strings in the ux (we never know) -#define CANARY_MAGIC 0x7600E9AB - unsigned int canary; - - // for CheckSeed app only - uint8_t processing; - -} bolos_ux_context_t; - -extern bolos_ux_context_t G_bolos_ux_context; - -// update before, current, after index for horizontal slider with 3 positions -// slider distinguish handling from the data, to be more generic :) -#define BOLOS_UX_HSLIDER3_NONE (-1UL) - -void screen_common_keyboard_init(unsigned int stack_slot, - unsigned int current_element, - unsigned int nb_elements, - keyboard_callback_t callback); - -#include "ux_common/common_bip39.h" - -// to be included into all flow that needs to go back to the dashboard -extern const ux_flow_step_t ux_ob_goto_dashboard_step; - -#endif // (TARGET_NANOX || TARGET_NANOS2) diff --git a/src/ui.h b/src/ui.h index 1d0d02fb..c9d7dbcc 100644 --- a/src/ui.h +++ b/src/ui.h @@ -18,13 +18,9 @@ #include -#if defined(TARGET_NANOS) +#if defined(TARGET_NANOS) || defined(TARGET_NANOX) || defined(TARGET_NANOS2) -#include "nano/ux_nanos.h" - -#elif defined(TARGET_NANOX) || defined(TARGET_NANOS2) - -#include "nano/ux_nanox.h" +#include "nano/ux_nano.h" #endif From 4298085ca5f00e1cf711303dd0f7c6d7d851de20 Mon Sep 17 00:00:00 2001 From: abonnaudet-ledger Date: Thu, 13 Apr 2023 10:37:10 +0200 Subject: [PATCH 52/65] Fat: Adapt font name change --- src/stax/passphrase_length_screen.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stax/passphrase_length_screen.c b/src/stax/passphrase_length_screen.c index c8a1a51d..9db7b53f 100644 --- a/src/stax/passphrase_length_screen.c +++ b/src/stax/passphrase_length_screen.c @@ -27,7 +27,7 @@ nbgl_text_area_t *passphrase_length_set_title(nbgl_obj_t *align_to) { textArea->textColor = BLACK; textArea->text = "How long is your\nRecovery Phrase?"; textArea->textAlignment = CENTER; - textArea->fontId = BAGL_FONT_INTER_REGULAR_32px; + textArea->fontId = BAGL_FONT_INTER_MEDIUM_32px; textArea->width = SCREEN_WIDTH - 2 * BORDER_MARGIN; textArea->height = nbgl_getTextHeight(textArea->fontId, textArea->text); textArea->style = NO_STYLE; From aaa987dc017f971031ad2b237308232d76bd2818 Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Wed, 10 May 2023 11:13:24 +0200 Subject: [PATCH 53/65] [clean] Special refresh to improve keyboard reactivity --- src/stax/ui_stax.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stax/ui_stax.c b/src/stax/ui_stax.c index d2444cfd..9b6f2fad 100644 --- a/src/stax/ui_stax.c +++ b/src/stax/ui_stax.c @@ -201,7 +201,7 @@ static void key_press_callback(const char touchedKey) { } nbgl_layoutUpdateKeyboard(layout, keyboardIndex, mask, false, LOWER_CASE); nbgl_layoutUpdateEnteredText(layout, textIndex, false, 0, &(textToEnter[0]), false); - nbgl_refresh(); + nbgl_refreshSpecialWithPostRefresh(BLACK_AND_WHITE_REFRESH, POST_REFRESH_FORCE_POWER_ON); } static void display_keyboard_page() { From f29d98389257f1ca74d110eb84dd7dbf33f06fbe Mon Sep 17 00:00:00 2001 From: Alexandre Paillier Date: Tue, 25 Jul 2023 17:25:38 +0200 Subject: [PATCH 54/65] Removed hardcoded optimization/debug levels from Makefile --- Makefile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) mode change 100755 => 100644 Makefile diff --git a/Makefile b/Makefile old mode 100755 new mode 100644 index b9d1ba0a..e7242896 --- a/Makefile +++ b/Makefile @@ -113,10 +113,9 @@ ifeq ($(GCCPATH),) endif CC := $(CLANGPATH)clang -CFLAGS += -O3 -Os -Wshadow -Wformat +CFLAGS += -Wshadow -Wformat AS := $(GCCPATH)arm-none-eabi-gcc LD := $(GCCPATH)arm-none-eabi-gcc -LDFLAGS += -O3 -Os LDLIBS += -lm -lgcc -lc include $(BOLOS_SDK)/Makefile.glyphs From 81a88128d1e3d128385769d27f1c84ae5d01ee92 Mon Sep 17 00:00:00 2001 From: Alexandre Paillier Date: Tue, 25 Jul 2023 17:26:10 +0200 Subject: [PATCH 55/65] Fixed compilation with API LEVEL 12 --- src/stax/passphrase_length_screen.c | 50 ++++++++++++++--------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/src/stax/passphrase_length_screen.c b/src/stax/passphrase_length_screen.c index 9db7b53f..2fc41e40 100644 --- a/src/stax/passphrase_length_screen.c +++ b/src/stax/passphrase_length_screen.c @@ -14,11 +14,11 @@ nbgl_image_t *passphrase_length_set_icon() { nbgl_image_t *image = (nbgl_image_t *) nbgl_objPoolGet(IMAGE, 0); image->foregroundColor = BLACK; image->buffer = &C_stax_recovery_64px; - image->bpp = NBGL_BPP_1; - image->alignmentMarginX = ICON_X; - image->alignmentMarginY = ICON_Y; - image->alignment = TOP_MIDDLE; - image->alignTo = NULL; + image->obj.area.bpp = NBGL_BPP_1; + image->obj.alignmentMarginX = ICON_X; + image->obj.alignmentMarginY = ICON_Y; + image->obj.alignment = TOP_MIDDLE; + image->obj.alignTo = NULL; return image; } @@ -28,13 +28,13 @@ nbgl_text_area_t *passphrase_length_set_title(nbgl_obj_t *align_to) { textArea->text = "How long is your\nRecovery Phrase?"; textArea->textAlignment = CENTER; textArea->fontId = BAGL_FONT_INTER_MEDIUM_32px; - textArea->width = SCREEN_WIDTH - 2 * BORDER_MARGIN; - textArea->height = nbgl_getTextHeight(textArea->fontId, textArea->text); + textArea->obj.area.width = SCREEN_WIDTH - 2 * BORDER_MARGIN; + textArea->obj.area.height = nbgl_getTextHeight(textArea->fontId, textArea->text); textArea->style = NO_STYLE; - textArea->alignment = BOTTOM_MIDDLE; - textArea->alignTo = align_to; - textArea->alignmentMarginX = 0; - textArea->alignmentMarginY = BORDER_MARGIN; + textArea->obj.alignment = BOTTOM_MIDDLE; + textArea->obj.alignTo = align_to; + textArea->obj.alignmentMarginX = 0; + textArea->obj.alignmentMarginY = BORDER_MARGIN; return textArea; } @@ -45,17 +45,17 @@ void passphrase_length_configure_buttons(nbgl_button_t **buttons, const size_t s button->innerColor = WHITE; button->borderColor = LIGHT_GRAY; button->foregroundColor = BLACK; - button->width = SCREEN_WIDTH - 2 * BORDER_MARGIN; - button->height = BUTTON_DIAMETER; + button->obj.area.width = SCREEN_WIDTH - 2 * BORDER_MARGIN; + button->obj.area.height = BUTTON_DIAMETER; button->radius = BUTTON_RADIUS; button->fontId = BAGL_FONT_INTER_SEMIBOLD_24px; button->icon = NULL; button->localized = true; - button->alignmentMarginX = 0; - button->alignmentMarginY = (button->height + 8) * i + BORDER_MARGIN; - button->alignment = BOTTOM_MIDDLE; - button->alignTo = NULL; - button->touchMask = (1 << TOUCHED); + button->obj.alignmentMarginX = 0; + button->obj.alignmentMarginY = (button->obj.area.height + 8) * i + BORDER_MARGIN; + button->obj.alignment = BOTTOM_MIDDLE; + button->obj.alignTo = NULL; + button->obj.touchMask = (1 << TOUCHED); } } @@ -64,16 +64,16 @@ nbgl_button_t *passphrase_length_set_back_button() { button->innerColor = WHITE; button->borderColor = WHITE; button->foregroundColor = BLACK; - button->width = BUTTON_DIAMETER; - button->height = BUTTON_DIAMETER; + button->obj.area.width = BUTTON_DIAMETER; + button->obj.area.height = BUTTON_DIAMETER; button->radius = BUTTON_RADIUS; button->text = NULL; button->icon = &C_leftArrow32px; - button->alignmentMarginX = 0; - button->alignmentMarginY = UPPER_MARGIN; - button->alignment = TOP_LEFT; - button->alignTo = NULL; - button->touchMask = (1 << TOUCHED); + button->obj.alignmentMarginX = 0; + button->obj.alignmentMarginY = UPPER_MARGIN; + button->obj.alignment = TOP_LEFT; + button->obj.alignTo = NULL; + button->obj.touchMask = (1 << TOUCHED); return button; } From 1652c11135280f07e280fb88f312d26dce0587a6 Mon Sep 17 00:00:00 2001 From: Lucas PASCAL Date: Fri, 21 Jul 2023 15:43:17 +0200 Subject: [PATCH 56/65] [update] New icons --- glyphs/stax_recovery_64px.gif | Bin 218 -> 0 bytes glyphs/stax_recovery_check_64px.gif | Bin 258 -> 525 bytes icons/stax_recovery_check_32px.gif | Bin 139 -> 274 bytes src/stax/passphrase_length_screen.c | 2 +- tests/functional/navigator.py | 2 +- .../stax/check_previous_word/00003.png | Bin 7053 -> 7273 bytes .../stax/check_previous_word/00005.png | Bin 6986 -> 7183 bytes tests/functional/snapshots/stax/correct.png | Bin 13190 -> 13403 bytes tests/functional/snapshots/stax/first_12.png | Bin 7247 -> 7322 bytes tests/functional/snapshots/stax/first_18.png | Bin 7301 -> 7332 bytes tests/functional/snapshots/stax/first_24.png | Bin 7360 -> 7560 bytes tests/functional/snapshots/stax/incorrect.png | Bin 13128 -> 13341 bytes tests/functional/snapshots/stax/info.png | Bin 9496 -> 9744 bytes .../snapshots/stax/passphrase_length.png | Bin 11414 -> 12394 bytes tests/functional/snapshots/stax/second_24.png | Bin 7357 -> 7538 bytes tests/functional/snapshots/stax/third_24.png | Bin 7545 -> 7685 bytes tests/functional/snapshots/stax/welcome.png | Bin 15131 -> 15799 bytes 17 files changed, 2 insertions(+), 2 deletions(-) delete mode 100644 glyphs/stax_recovery_64px.gif diff --git a/glyphs/stax_recovery_64px.gif b/glyphs/stax_recovery_64px.gif deleted file mode 100644 index 7854f092e1335538e49f46b3a67843792648e039..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 218 zcmZ?wbhEHbbYO5`_`t{j1poj4SNzEWVlgQG=l0A^Oi%SqOwUZt=1Wh^%}um5&@(Xw zK?WU=5|BOyrpZ11D^I`WUp!~at?teD_Wb5=dvt@I zzRtb(dit+gwuxdU}hCi`LfG3=9my!osVot7&Oz78Vv+Sy@U- zN-i!gA|fI@JUsvZ{{R30A^8LV00000EC2ui06+jh000F4(8)=wy*TU5yZ>M)j$;ud z4TB)Wka^COwx3GO~B`4 zUq31U10D?f(1pl=%Vm+S}zL_ zy)H3Hp@G;bWGCP}`$LojO>C7o92i##HwsznLdC#TZ%qsU{KhzDVQpNXfb)tKr~$5C zo*4{JbE_eh*t8rDUQWcuE9DPWRS}pwVt`6Z2{;=u-SrKD1FD}4JV9fH76L06G_Y}3 Pw(sA-g9{%HA_4$AgU8Tf literal 258 zcmV+d0sa0*Nk%w1VL$*t0Pq0-00030|NkNR1ONa4001li0000$06+i$0@;j@smtvT zqnxzbi?iOm`wxcV$N{FAoa)N9YpJI0OrzV2)Orh_@(u*_Y=>ZwgiI=z%%<~+!4-|W zp)g0)S%2Bt7TfjSV&E_jicYK7?6y*BpmNC?c;GnIM(Oct-*{o?ch_YhSd&-yWaiVv z$cOZ3HzElsQaQPBc*aPWsj$iNmz7B55K3k?cXbNsG74DM*g6Z_s90JHLgVTBO8MK% zYHR5c{I~jP<(jNKcHHdzd=_2&fha3Rq9@Foq=om5IUHH(Bnj1t7Pdy(2ISTpKTlt8 IUr_)6J7?B@SpWb4 diff --git a/icons/stax_recovery_check_32px.gif b/icons/stax_recovery_check_32px.gif index 2008fae95a1994e2a5c68767f390758c3f7dcce6..36815d0a92f17fbf6a32d3ca618c97bd2a399e79 100644 GIT binary patch literal 274 zcmV+t0qy=rNk%w1VITk?0J8u9!otF7X=##@l1WKP78VwJdwV=QJR%|@*4EZ)YHI50 z>RDM?E-o(D*Vq65{{R30A^8LV00000EC2ui03ZM$000EP(8)=wy;y=|2zg*A5(kqY znWHRX>$U@GA>V7eggnuA-x3PSz())%cE#iaAW0~T0SV(tG)N9qA3;*f+O`hw2!Rku z#35!mbVdstJa(A47`CE{E~xF3*g;VaSS@@#2`YmFelLYR1r7})O*V}^ZwNt7lye|D z6apXwOb(zog$)-4Ap`;eeGYnPt6Z`!2NDLgEew;oA%OtCAv!F+iW7OdITdkY1gJBA YR0}~F2muTSu|EU`0i!A5;u8@7J9#-=F#rGn literal 139 zcmZ?wbhEHbRA5kG_`t{j1poj4SNzEWVln7|2#^c|Q&La=%GnPR=FPdHDY^RC+oyYD z;~1LGbS`^!YMVxQ+g#UUQ$22f=ecV2oQtdd$aRlzk{>5I2s3T#w(S>D+q9H>T8-_r o%hR_Q9ZXzzL&nnQRA}CJo?h+p8>V~pM4z?aPuhJWh=IWx05=FZ+5i9m diff --git a/src/stax/passphrase_length_screen.c b/src/stax/passphrase_length_screen.c index 2fc41e40..e2f23706 100644 --- a/src/stax/passphrase_length_screen.c +++ b/src/stax/passphrase_length_screen.c @@ -13,7 +13,7 @@ nbgl_image_t *passphrase_length_set_icon() { nbgl_image_t *image = (nbgl_image_t *) nbgl_objPoolGet(IMAGE, 0); image->foregroundColor = BLACK; - image->buffer = &C_stax_recovery_64px; + image->buffer = &C_stax_recovery_check_64px; image->obj.area.bpp = NBGL_BPP_1; image->obj.alignmentMarginX = ICON_X; image->obj.alignmentMarginY = ICON_Y; diff --git a/tests/functional/navigator.py b/tests/functional/navigator.py index 33f14f51..7eae2509 100644 --- a/tests/functional/navigator.py +++ b/tests/functional/navigator.py @@ -62,4 +62,4 @@ def _write(self, characters: str): # the screen can be compared, when it has not reached its last state yet. # Adding extra time after writing to have a better chance to get the expected screen self.screen.keyboard.write(characters) - sleep(0.3) + sleep(1) diff --git a/tests/functional/snapshots/stax/check_previous_word/00003.png b/tests/functional/snapshots/stax/check_previous_word/00003.png index d9be1973ebf5beb36f434ffd8d3cebea86d613ba..bbe94337197d436638600c73a9ada2b0c684bd82 100644 GIT binary patch literal 7273 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 literal 7053 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

TNGGS`D`J()XddCEg;e-3HjHiH`QO}G8sDa|w8 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 gS_l5GctK#|($;jO9Se2@KJNU+7%_oF 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 literal 13190 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 JEAKhJ_;0f6N6G*I diff --git a/tests/functional/snapshots/stax/first_12.png b/tests/functional/snapshots/stax/first_12.png index 10682359ac2c6882b3fe1c409a07c83afcfc6ccf..f8c75cb5ceb4d1ab7433021f9d7019392c6ba084 100644 GIT binary patch delta 5593 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 literal 7247 zcmeHMX*8Sb+EzLnvQJyI#yYCzD&1(6sEAsG;UDREku0$=<594KX%UP1Tqx zh?s(GRY?ssr701rF$F=8A~-}-R?^^dW-1mLm*K<9; zSXr6~AC)-D$Hymp%k+i~AK$)T`S|!>9_9z$=-p|?^6^QD-MV39`{2bQeKh;CspL7U zPkm?Fp7bLRU)(Rnf1F<=PNetw{7O^ z%4|(JoK1a!pC|UrUH3hz-!Jw@2u-a*%xC2k*svn*IqdwkS zXv^uFkxuvDY3l}Xr|Airw+{I=P`@C?A5lu9Iv8SDr!QWP-%@|$S znF5wCBmUGl5#Eudq6`MHI2>FT+k9r;vXpj62L@v8>=@Kl_IRZ!T%&i)eI1$$8oadx zEH%DwR9D49;q`-`mQbXErLo)uOAV2iB{;v_ns-t{OT~~yVv!L^EO1X)Lqh|(%iCOM zQ9I)brdG0KJ^eU%pLP6bz$7PJH2Y>EnQHCP6L zLZR`hEMN=q3$VJe4$N1)@78W(M#dI+^YLfiVqdS~@6vLn4L_Tt+%0h2~~%Tn@pmBV0cXMwR{6(Tn9xlkIw!%+Y@6 zs_3wa8deDq+BF5Ax8ZB`pz>+;=(_2)+g*097w+$l*3@1;y4^AX#AD2MRk#cWj{FSu zNh`smc6RyJ-S$D(hi%M=HOG1S54?Prewla=~2F9#DTXl(bqQ7k{r6Hmu#wVN|~t z3w&+o#3XxVN(64V`QqZM`hU>#5&Y3ydG>@unXz_cm6o^wwpqLK6Ewcl#t<9qdkH_@ zJNLo%Vwboj&sCMCT)hPA=*RSu$YR)qtTW6Vd+JeewCzUYV0LM>xN}}{TDqX4whitZ zk7sV3rJk{?%Sf@TQ;A&dL*}}UWJ&m<6R}eJZ(&FNvFIn3M^_TC1sid8w2J%vQVwY6 zsf`^wJkF`|9^rzW5;yTS5Gcz-oxx}GQ0u$9<6aS~&%nP9T0Fz!@yW@_9frjZmEtce zV-lUKs3_9p;-b2AtgNi;)6Ty>bBT_QUf^zryIB~|*y{|x36((HPSmNpP5CkUmRXwl zs^w@A>|nH+Psdg$pxrbhH_?|Ons#FMSk$Vu?m&F*PKz$WRbjL#Eq|Z6 zl!K3L=&a3vG&9smRIC92lBk#%3uvq!=6TmWJ$`WEtvGa!_2hf=_>qcT_qX@K*jjVX zdyQ#gBNJ0oQyVRN4CG!hyQ~>JR_L}t1yHIplmNh?GR7OJ82q&~(%h}PpfeW*j}M=1 zB@&Ut4aF6uyS9l%C!FV!GOygpFX|qP&!v??;KHvxNw=XkQJ%R-f8u_fZobzlE@8qQVK z@f3{v!Vh%Pf!TwJ2>aZA0NDKvfy>fxpCmQI+vqFEm>2_$C9|M0!KoO0KX=>&626(Q zV%RLpG-|Qj*xmukcuRBQqBs$7-1;UZ!GscDU*$0(t}3Mw8JqW16NFuLySXU?AsNua z@19sv@(yS@Uon~3Z9>xoHylb2m?29>p}F-nj3uwglw3b_{q;$+#$J*yA)d-w2X_7v z{Pf-qGh?JRkV)*(`8XVTK9_xLBw${WOMMo3grj|Du|og)s~CkoyW`NF%P+o9TUy9_(?3tn&8u zUaIf($}OgoeT4b3Ws>>Q$6N(iFDO5+mcQtTY3y{jzHea9&JEw6aXC_1JBPTOkssqJ z__eCSMp0=`zWpGkK6tBWFi7aX26K~w2I00V15CDgi#prcF_;Xr zf0k5Okc>`sgEP=sfy7;Z(TOc?*h7%L_3r40q$`H&VMbW+O&>^YN}yg91gmI{&6W|n zp_$)@+l%d88eWi_fwfO{-*g2cGFUQf4iY?k19<+05Bv-9i#7X1+=+!gc#p^_)bBm6 zEbD;y;I)w#&y##}0jNqQlewY05}f}boDJR5jA0s9P+6^R#_P5~CkDDJ#kp?zJ!(zr z=aBJR)`}0|ZMLEh{1FbK z41g99O#r3<9sU{TSfY-NoxvsVtH-bCFI#KtSMDY%MhsMctPyW0-ox-{S#Xe6vBp8V z33{C7yeU>fV?1}rW&033wl)0Az`}#MhaM`2LlF6Ps{o92x_?}Ll{v0!%SoMkwVnq} z6CEG=W%H2)!l6viDt7JR=8-XY-{l~}*G>~eX`f_cT)eoLi<<7kCp;?EUPml%>xwHy z1sOm|$YH}o14D*dmS0t=L!%rKE9#Qr3e{WG9~{scf29zoJa9A*c_SE#zHwAq)zvt!{KoOk{p#UWtpp%=P zN^{4Iqi#y_W0Asf=y8c(Wv5InnwK9FY8@s`EzKaU$n!)Grr%tRa~e$%4ohi056#ce z{?K@h$z7|}z}i4I)%LN5hUFeQUf(5EI=A+uzN=0+TfJp3?m)A<9`VJ(B$S!kFkock zP*ZcE#;L;1&28WzNK(v<{(@(1znNEijXC;#x@6t+^~zWy*tz$Ytox90=nB zReB;h(6m@V!*as%shwrn9oHnDG(gZeTU(6`_FxMI$e_{1ff@Uu^Uhu5!~py#J&@`$ zVhmJKC&2nla)vTgAX!w1BdP=CijUq_Z>uh`{BS7ULS&1F)qj$HVoT7IhBkFt{W1*Z zxX0^mc~QItuEPau%O*Q7S&38TRhpz7XPI^*a-K0Gr1?hXs9MwrNg^-G;kx6wObsMZ z14Bzl-nSI!*{AhY60~Z2z*V=tIz$)rnin-+UDVq<8EmT2+*Fw$?y82x0@USfy6y*( z@3&FMOmzlDY$Af+@64RdEYLdhC6qgBp!sOgjOGU(C7mjYC6LxS{q@TOt3g3QxB=dW z4ejPxcSkf(ZvcGLrG};-1@^-!+?oTk?L)53hioG`s&J)Ms^HvLm#8BgfF!OimOhO< zA}OZIP%y9YD<=EZZfN^+a9PteD#e!t;LzONx*kuT$<%6AMi9=UKA5R)jF9_#s#tJ& z9&|$;73! z>%f)vL$=rFYdt7s)h-ry*Q;O%Yd!x}?}?7z))pgjpDmpDQrPT~o4vHzIe*s8o2rc- z)N&rFW`dG)DR0t|tL?sZpZ#@M>w4RH(a*z=mOIx+(^F@N$6E*7Dqje-tM7;GN${(( zJhb<0g5Ej|?HntvVjP=Bmr|g-j!6+ey$3TdenfkQ`QDR{@# zaJ^HhY)qTG?OaGX%~&kl-AQ2TJMS2*DiWoBB)JO#r|j(@x@u(!s&k^oD1RNHUeORQ z101)%KY#j>`+o-Xe;)IbkY|Ub|3y^6-uGv zk>s-z4)I2+Q;;^%n=NP3!ENd{kE&T880*#l_jH1+>FlMR8kBd`JAq$IQGeGLrq4A! zQd;{1g@%8uI;kCMM`W;3=lC0rv(^#)Y$}PrV|m*9w2;BSH|alD7Wv=A=#Q3S{(sDW z=Kp`}-tN-aTrhRtOAMFG<(x>zx-eDns+muIpG5aqxo3v68GQw=4yb&#VW&e-yV;HW z%R_+bHe~nnck(xtBLmsJEr*dEP~6E=RUuwbZCvF{T0-DV5f!4%J;-Sy5;b1kW1NI@ zrZ5aUobK2_3i>^2#q92PQ$^_nl-)_{gKx0s9ag<(kji4a!;H3=qq^yGa5iuC*G zJ(<9;86x3V7y01d2C71yGvdgLKR84Vq)FiXz>Y8W%e{Kq#r}5a0RbFpJtLDrLXQux zmuT0I+XW6>9o?*)fuT)w2arX5gNlW8+5KlDsc~p~k>?>d3k5LI6O(kkihYO_qDr{I z#TVo$-F_pwT=iL>tLVsCiQOjdD^^7C%NQY?7^?f*p>LwzC4ME^NrsWNl|P5rOwp#v zmVT>I1P~Ef>cns(FX|+qpGAmIQw`^@RJt%0@=IQS{PeC_kD^2^x9dguDuXgu%llu!>#)LePx=> zX8(7-)dg;jIxE=|$T7X4uE+R)j~)z2%W2o022*g@3@MX-$e(EUv2mtiSCX`)+=_$_8kKOkzV`EBHB6)wwID@_zFgMkZ z7E89f{=>z0YRJcPd}HzhE-UtB zKyV)s4t+SIwC+RNH#huQ4?%h7E4+@6D@X;LR&Kh8$TJd_84oJ~d^9~_IzK7&gy_6Z z*$aO13TD-J-ZE3y?XJZ`%Ll!Gag_OVe`dQ$Yd`^}^DYE*D#dQ<2v;{*PJ2Sp|D9Lk z;7^MBp-W_r|E98lU#er_)a%QSWLsediwiwvVQ@9Ft%c-!ue%p5}m_@3@lz8~t zF-y<2W3psPGoxbOCKf)@McYzp1UN_U4z9WPT66#EzQNAXFdkJqvN2= zCu`F(3HCQ56vGPgQ8HpNp=(-7zrS;SU@#b^l>%2U16eks{5`XZe>a$Y*~{+xj%M>t zSJi<_ij}@0QNDMSBj#Js&%dGm-Kw_3v2(B)X^H3|dD-MlJlnsBZX zR(#W$K?}#~hZ6$G5@B?cVEbRf^00t$AIrXdjX-rIBm+==>lIS?XzX-%wI%%#hYSj>T%lLiNkPuT+v|M zsU;B-I?nErgu&xR=SyuD8s<`iQ)|hMRmm8~O?bG0=|jxtTMfBF)cqrAQij5BzX^fg z{)}b5&6dUI%;%J0d*ih2oJ7gexHRoycxSf9fpEU{b}fXu1WW zJErLXNAy>qwC%7Js*!JQeJVUf7>f?MCuT6cyXXX2D{AG)fb9_u%M-7>1M}J*cSV|c z(e1{gz9{L(H<2;eIoSX4J}PN*dwY8b&UfFl4gV|u8bv|wKZJY#Vcg>z@d)@Y##{TW S@_>s%zFWqYH!82)ef&2Aku&@N diff --git a/tests/functional/snapshots/stax/first_18.png b/tests/functional/snapshots/stax/first_18.png index 44a02522c4cdf3a3fad88d2ebc934f3ba3f0eb63..bd8ca5873cca5d0ce3a8031fda0b298f72836caa 100644 GIT binary patch literal 7332 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 SA>j5=K>v=(?UGv#PyP$9?3RN7 delta 5586 zcmY*ddpy(o|1Tv<<#baNx~UU$i@A?ex(G)iHn*|d!dS^%X7))PoI-V4Bh6?<(&kRH zF>^SQ&HdU4Npl;PxwU3|Kj-}Z`h7l+$LH~QeJ-!h>;1YtpRaeGd6M~)lh+-uTsU_< zHhXS(2>X@G+U9KLuT{SzQT?8}+<)gi_4Ag0L@S=!YPogaz3qShQBQc=WiiM)mZ-O8 zFzITla{Y>Sf;MS~v5m%Kqq~ahn3)}$Dz%8mEhY5N>g25Y`&bXxLcRDu&)Iao9vXS zPdw@cAL3gxABgIcE+sq(VqNA{B;K;bLPCRal!&3j~6(F;tc3?}!u`fioPD$rIq% z#a=^Ai&7#VI z#P@Vgw`}f>iVk<}e}4aOcJDh`it)`)4RbaqrZ>2pb;;u7xB5;3r{giKDh|MS$_fe! z8iZ#)z8{)vQG1Wk&cw{HZp)C-?iI%#KL5VE1s%N%ql-i${%FK`l-=^GXq10LurTP+ zS89ivT^SprIw#Vr!RNE(m1Ai>%w537x{m~>_AJDe*3r@7RP}RxUC-MC^gm-3RrR@n z@{JMtbB&de@zYguZxE<84gVu@{7P=W)lt&&7jZeGGGr5F1y0}usI%6-d-LY9Gw4=G z7ue|NXvf=ckxU5SXJMiPFK9bw_|_nC`TO@L^IOAB$YOWt`uYgkWIEOhqC##9s^IUB zMrj*L!}_0!Ezb_67XH~6T7@i8inq`wAwI>Ca=n>cKBX`cC=!z}#ct8VmEL+iDS1sU z3V8{BP|R2bRKeY%c;>~q;dtRIPGrn}d_n?=Y+rW_ik@+j>V$V!x|aVy6uB3QLo9W=A7$GmNxhnDSps15DKkYXpn z8JJ~}AQf=P&DIzGFdf{1>0oWr4I{m{hUzQlU@BVtxG%x_S$S!Z{1{T$)uhSw=Z~Ool z{J``=s(WEI_uP}pP~9ywfr_=Q?Nr_`m}<4qSPbDGSeQd<(<_7B&IRVB+KdkZDJ>$n zY9~E1_?`2-DhaKLpzMA)=4hoXoSQ3ZOIuMYI9fnfD=8__)YMF}$&ddv$?^>4p;yU3 z&G7v4GE|)a-cM~hp6-pueKtI z)O!1Z*Gq0`_6G@sP-D=K(+KWhXK0nZ!c~iXDueFpCq14#VwkWWS2_kxmQgRq?dpDb z@O*80vXg*r__uy9Z=K~#2ttRVllT=rIv%R1f$T8A^bkVOMv1;N__sV5A@LSVb!rL{=W%nq_RgymPvG^s@VOkeUEb0;3uJvBlm(of?^TxU& zsdGA6%S}6)d8FLQ8?BA+YBR6ZrS#ppYFhQBL0tf*#P#%s%4DNcnt`X+8elKcmhh2} z04@70G2xD7P3mWFTBZ69&2FrQJ5RS3Su!;GfCmtm`Bf!e5_U+~Ktg6|TNHJo<_|3S zXM+qo=F@<06TbgVYOg^v8IVfj`=3@E?6Ced+2FQcpU>+9b=qCxTU?}L4fVT#w6-(~ zHrd(h#L=o?5gBcl@0?42#x;isJOM1_8J?ayJrd$+OV-Uk&>JF$3Y-e4=xdJ^EdZPc zq*KV?q3|+9WtLtP($A-wgWFE`pPxmT57!22J>IBM?O8q2?Nmsf9;r)iWOm0P_{X}S zrH`0tzjoL`(t7n&QqK#`<45yOlzwJ96n3p8X5!~`yp95Uo>`oe=x;XxrYe(qbSQQH z2vrL{y2@yTEGx8{9~tz>E57{Nz%nj@5d!ULl#}afByQe&1Q6|A!PE?^5MG(KUDKF6cm{*` znbkIHt|U2qJs62ZX7B{6SHB+HYBVg(taJ306fEN?u=|-SAXY!twBdSHeJ3hU#~jVz z1&$?J_;WSX4cz1-$Qu35V$M?asAyFx6AFbOx#$4ajmF0IcSSJb@%ZevAmOsb^1IzB zjX-mAGmpmuujAw6TFS5)_`7$$?W&!B*l5Jk)Ck4vX}+_0sMmqYw;VPoYYamn;p4Nq zLs6P;xFC$c^?49rc_ZAqK=5?VrZnFPN{=sXQ%!2O@2Ic`3%&vYQPWuJmL`WH&-zp% z7yZi{$QFheXIo_*>}IZw47n~p53oipG0bl*xl2wZrcqmzlp$sw5i9$|Aa3#zu`|*r zUD{fi?%~k76u0W8YpeCCb?@~GoQM-QkTQ1QnEvF^V{zHEBjeGib{{6ht zfWY8@Do@=^-jV@Om)zuHdK&0;vp!wqELnlJHoOv--3{l4IzLw`_bd`CQ(BFCsbbaC zxqmf2mJ5rOqXE-q_}Ey4j0DRs?_9m&lg9PeSAXSEm#200(F0>h#_rlR^~&DMA1@=~ zvR0t5j)~*}AOe`_7-|OA4sFlWtA31VfNGU_YYKNIC3RL3}`J(^n7*BxOUD ztwLd##2U^zKpK8x?~s#SPf?*|yX3MCNi4m|Z4(knT+Q&C_)L&_3Itkg!lhjmSUUYv zPfrhz=K%f(27~2;81Tc6IOx)}V{uSh=p0)n%)oHu5B2vxh`V3ja?TeZr963h9-sn{ zn!Q&Xa%yp*Nc#5+qi6}*?`=e)3DnA5VJ5)CCy;SMiCxRJ$fI}Q5dZn~G4B5iR#sN# zar%YW(0>^zSD$!sC?*~KNk=zM9FU_zBP(1dz_19H;9$m3zQPCv-bi%H=w61Dog_Ln06q2s$UX?wnTmwsp zZfsMLu3Y&L=W}UN5>8Mx~c?4E@?8(bUQPw`d0dBX}|=?%ujM?DE6vrd&+MhG4@X< zfm!Y27ic=XNPN(}z4plokm66S~6Tw=44P;D3q^wre1J7l$Jp+p~&Z}kr zrM+^>{bQ1m?T||(!Xs$S$9qnWVSZL1OAq-;s{Cq&L!_asbAvbf*5;K%@&0&cdc|)1Yoe$k8+= zPw(gs7}0Q_%VGcTm-{e`A!_QT;(u^#jitrq(z^y$5>-W8<%+UBadO0?dvy+`0pPsS zi`{f{6685%@&au*lRL-NBA(lC3i#PCO7PaXSA9|qwyN|UvGb+^Z;Cg4t^#ew9ys3B zDA!ffzaywcd7UL7DiEcj3``nV==LbHDMyaOgJA1}w`^A%wOZ$n> z+ z^yq+gRJwkB9;ns0{6hH9p}F-Vd&08!*4m@a)*TnK;q&JHh$Ojfo*j`2<&x3H@cFgV z2cL#*eT4W4ZC|i-W~$2x?Qt&t(c#*u#G@CY#6Q$GHtEOMJI#DO0JgC`Lu7HRfyV#WbSYstG*Wli0yy zOJtUPKUZ8L7>0CoWsL!6-F@}8;Aar<;?*Vp!;3SJ(I=PmuElGh!;`-!335OUfAGqL z*ASyzctkghK8|L2Jaf%@4?Fm3JT0=L*1>FoP^2SNo4FOAgtV1ECDHD_exRE128-$< zAh#*n`rX!T4f>L2uB>EMkYxwaW7P(3KkQ0>I>V z{r1Vl+a^)eDJm}MB~jXjP35zK^ei?Q%8OCMkEvCj=4oi^_K zIWiROG_P9}^m@5;KKTAAqH<{3>`4Er+Gmt!+bPh2gTJ{%-;T1mulBiC>ImwH+0J7q zUgpu!&L=j$Dqd`SIgiTL!_?LTFIKW|<(?@2T-w#X3J=V4Xh5fptDQYg`=CCLiLec= zkNK3=7FD1S6Z_4ZB;|`;xAmW_xYT?tX45FXku}68yE2m z@$aNN$7wYcw`sz7Sh)&8dGwN6VG60E5Mur2h1;j^H#kWnL+B6jUZY_MqX{c&VcJ0B zi<8g~`cnZ+|Nelrs=A!L7^q$W?9r(+dL1t;5{!IivG-m%;@_L0Kr<{`Fcy6K2rnIryy2XXqS;nc%&c=H~F)gHZxAeVH^C)O9q57m#Qics@ zemPP5$xa{dM*H;lbrsX@aZ`~)&!7{m$6wy1^p!G;TT`#B1yfY zZc$t6!0Qx^elrCZsQ(>3a|m*EU{VSh|(q>#(XPe?dh6a*El_OCs_uABSFJ2h~Ex@(YEy??%WIAv&djD3Wb|Gxp%J~ klb_0TI8UVR?NnLt6}E4O;s;|+Yyyuf7o9E?pZB}>Ke%<5)Bpeg diff --git a/tests/functional/snapshots/stax/first_24.png b/tests/functional/snapshots/stax/first_24.png index b04d6fd4ec0dbeb98a771a4f32a7227ca2f59d1b..f681254f2ece42efdb1176b8f6b7af13d2e02eb8 100644 GIT binary patch literal 7560 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 delta 5638 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