diff --git a/components/libwally-core/upstream b/components/libwally-core/upstream index 8fc41b9d..12ab3fa2 160000 --- a/components/libwally-core/upstream +++ b/components/libwally-core/upstream @@ -1 +1 @@ -Subproject commit 8fc41b9d2b152868953176176cff9890aa268c8d +Subproject commit 12ab3fa22cbdc26040a9900d02cdb03da5164c85 diff --git a/main/button_events.h b/main/button_events.h index 7a24058d..c477d8ad 100644 --- a/main/button_events.h +++ b/main/button_events.h @@ -232,6 +232,9 @@ typedef enum { BTN_OTP_ISSUER, BTN_OTP_TYPE, BTN_OTP_DETAILS, + BTN_OTP_DETAILS_VIEW, + BTN_OTP_DETAILS_EXPORT, + BTN_OTP_DETAILS_SECRET, BTN_OTP_RETAIN_CONFIRM, BTN_OTP_DISCARD_DELETE, diff --git a/main/process/dashboard.c b/main/process/dashboard.c index 9f47d917..83a0d70d 100644 --- a/main/process/dashboard.c +++ b/main/process/dashboard.c @@ -202,6 +202,8 @@ gui_activity_t* make_bip39_passphrase_prefs_activity( gui_activity_t* make_otp_activity(void); gui_activity_t* make_new_otp_activity(void); +gui_activity_t* make_view_export_otp_activity(const char* name); + bool show_otp_details_activity( const otpauth_ctx_t* ctx, bool initial_confirmation, bool is_valid, bool show_delete_btn); gui_activity_t* make_show_hotp_code_activity(const char* name, const char* codestr, bool confirm_only); @@ -1465,6 +1467,34 @@ static bool delete_otp_record(const char* otpname) return true; } + +static bool show_otp_detail_options_activity( + const otpauth_ctx_t* otp_ctx, + const bool initial_confirmation, + const bool is_valid, + const bool show_delete_btn) +{ + JADE_ASSERT(otp_ctx); + JADE_ASSERT(otp_ctx->name); + + gui_activity_t* const act = make_view_export_otp_activity(otp_ctx->name); + int32_t ev_id; + + while (true) { + gui_set_current_activity(act); + + if (gui_activity_wait_event(act, GUI_BUTTON_EVENT, ESP_EVENT_ANY_ID, NULL, &ev_id, NULL, 0)) { + if (ev_id == BTN_BACK) { + return true; + } else if (ev_id == BTN_OTP_DETAILS_VIEW) { + show_otp_details_activity(otp_ctx, initial_confirmation, is_valid, show_delete_btn); + } else if (ev_id == BTN_OTP_DETAILS_EXPORT) { + show_otp_uri_qr_activity(otp_ctx); + } + } + } + return true; +} // HOTP token-code fixed static bool display_hotp_screen(const otpauth_ctx_t* otp_ctx, const char* token, const bool confirm_only) { @@ -1492,7 +1522,7 @@ static bool display_hotp_screen(const otpauth_ctx_t* otp_ctx, const char* token, const bool is_valid = true; // asserted above const bool initial_confirmation = false; const bool show_delete_btn = false; - const bool retain = show_otp_details_activity(otp_ctx, initial_confirmation, is_valid, show_delete_btn); + const bool retain = show_otp_detail_options_activity(otp_ctx, initial_confirmation, is_valid, show_delete_btn); JADE_ASSERT(retain); // should be no 'discard' option } else if (ev_id == BTN_OTP_DISCARD_DELETE) { if (confirm_only || delete_otp_record(otp_ctx->name)) @@ -1504,6 +1534,7 @@ static bool display_hotp_screen(const otpauth_ctx_t* otp_ctx, const char* token, } } + // TOTP token-code display updates with passage of time (unless flagged not to) static bool display_totp_screen(otpauth_ctx_t* otp_ctx, uint64_t epoch_value, char* token, const size_t token_len, const bool confirm_only, const bool auto_update) @@ -1582,7 +1613,7 @@ static bool display_totp_screen(otpauth_ctx_t* otp_ctx, uint64_t epoch_value, ch const bool is_valid = true; // asserted above const bool initial_confirmation = false; const bool show_delete_btn = false; - const bool retain = show_otp_details_activity(otp_ctx, initial_confirmation, is_valid, show_delete_btn); + const bool retain = show_otp_detail_options_activity(otp_ctx, initial_confirmation, is_valid, show_delete_btn); JADE_ASSERT(retain); // should be no 'discard' option } else if (ev_id == BTN_OTP_DISCARD_DELETE) { if (confirm_only || delete_otp_record(otp_ctx->name)) @@ -1703,7 +1734,7 @@ static void handle_view_otps(void) JADE_LOGE("Error loading or executing otp record: %s", names[selected]); const bool initial_confirmation = false; const bool show_delete_btn = true; - if (!show_otp_details_activity(&otp_ctx, initial_confirmation, is_valid, show_delete_btn)) { + if (!show_otp_detail_options_activity(&otp_ctx, initial_confirmation, is_valid, show_delete_btn)) { // Delete invalid record delete_otp_record(otp_ctx.name); } diff --git a/main/qrmode.c b/main/qrmode.c index 917d97ca..f67c3b22 100644 --- a/main/qrmode.c +++ b/main/qrmode.c @@ -36,6 +36,12 @@ #define ACCOUNT_INDEX_MAX 65536 #define ACCOUNT_INDEX_FLAGS_SHIFT 16 +#define MAX_OTP_SCREENS 1 +#define OTP_TEXTSPLITLEN 4 +#define OTP_GRID_TOPPAD 4 +#define OTP_GRID_X 4 +#define OTP_GRID_Y 6 +#define OTP_GRID_SIZE (OTP_GRID_X * OTP_GRID_Y) // When we are displaying a BCUR QR code we ensure the timeout is at least this value // as we don't want the unit to shut down because of apparent inactivity. #define BCUR_QR_DISPLAY_MIN_TIMEOUT_SECS 300 @@ -61,6 +67,8 @@ gui_activity_t* make_show_xpub_qr_activity( gui_activity_t* make_xpub_qr_options_activity( gui_view_node_t** script_textbox, gui_view_node_t** wallet_textbox, gui_view_node_t** density_textbox); +gui_activity_t* make_show_otp_qr_actvity(const char* otp_name, Icon* qr_icon); + gui_activity_t* make_search_verify_address_activity( const char* root_label, gui_view_node_t** label_text, progress_bar_t* progress_bar, gui_view_node_t** index_text); gui_activity_t* make_search_address_options_activity( @@ -80,6 +88,8 @@ int sign_message_file( int get_bip85_bip39_entropy_cbor(const CborValue* params, CborEncoder* output, const char** errmsg); bool show_confirm_address_activity(const char* address, bool default_selection); +static void split_text(const char* src, const size_t len, const size_t wordlen, char* output, const size_t output_len, + size_t* num_words, size_t* written); bool handle_mnemonic_qr(const char* mnemonic); @@ -843,6 +853,7 @@ static bool verify_address(const address_data_t* const addr_data) return verified; } + // Handle QR Options dialog - ie. QR size and frame-rate static bool handle_qr_options(uint32_t* qr_flags) { @@ -1489,6 +1500,85 @@ static void add_cr_after_last_slash(const char* url, char* output, const size_t strcpy(output + index + 2, url + index + 1); } +bool show_otp_secret_text_activity(const otpauth_ctx_t* otp_ctx) +{ + JADE_ASSERT(otp_ctx); + + size_t num_words = 0; + size_t words_len = 0; + char secret_display[256]; + + split_text(otp_ctx->secret, otp_ctx->secret_len, OTP_TEXTSPLITLEN, secret_display, sizeof(secret_display), &num_words, &words_len); + JADE_ASSERT(num_words <= MAX_OTP_SCREENS * OTP_GRID_SIZE); + JADE_ASSERT(words_len <= sizeof(secret_display)); + + btn_data_t hdrbtns[] = { + { .txt = "=", .font = JADE_SYMBOLS_16x16_FONT, .ev_id = BTN_BACK }, + { .txt = NULL, .font = GUI_DEFAULT_FONT, .ev_id = GUI_BUTTON_EVENT_NONE } }; + + const char* remaining_words = NULL; + gui_activity_t* const act = make_text_grid_activity("Secret Key", hdrbtns, 2, OTP_GRID_TOPPAD, OTP_GRID_X, OTP_GRID_Y, secret_display, num_words, GUI_DEFAULT_FONT, &remaining_words); + JADE_ASSERT(remaining_words == secret_display + words_len); + gui_set_current_activity(act); + + int32_t ev_id; + while (gui_activity_wait_event(act, GUI_BUTTON_EVENT, ESP_EVENT_ANY_ID, NULL, &ev_id, NULL, 0)) { + if (ev_id == BTN_BACK) { + break; + } + } + + return true; +} + +bool show_otp_uri_qr_activity(const otpauth_ctx_t* otp_ctx) +{ + JADE_ASSERT(otp_ctx); + JADE_ASSERT(otp_ctx->name); + + bool ok = false; + char uri[OTP_MAX_URI_LEN]; + size_t written = 0; + + SENSITIVE_PUSH(uri, sizeof(uri)); + + if (!otp_load_uri(otp_ctx->name, uri, sizeof(uri), &written) || !written) { + const char* msg[] = { "Failed to load", "OTP URI" }; + await_error_activity(msg, 2); + goto cleanup; + } + + if (written >= MAX_QR_V6_DATA_LEN) { + const char* msg[] = { "URI too long", "for QR" }; + await_error_activity(msg, 2); + goto cleanup; + } + + Icon* const qr_icon = JADE_MALLOC(sizeof(Icon)); + bytes_to_qr_icon((const uint8_t*)uri, written, qr_icon); + + gui_activity_t* const act = make_show_otp_qr_actvity(otp_ctx->name, qr_icon); + int32_t ev_id; + + while(true){ + gui_set_current_activity(act); + if (gui_activity_wait_event(act, GUI_BUTTON_EVENT, ESP_EVENT_ANY_ID, NULL, &ev_id, NULL, 0)) { + if (ev_id == BTN_BACK) { + ok = true; + goto cleanup; + } else if (ev_id == BTN_OTP_DETAILS_SECRET) { + show_otp_secret_text_activity(otp_ctx); + } else if (ev_id == BTN_HELP) { + await_qr_help_activity("blkstrm.com/otp"); + } + } + } +cleanup: + SENSITIVE_POP(uri); + return ok; +} + + // Display screen with help url and qr code // Handles up to v4 codes - ie. text up to 78 bytes void await_qr_help_activity(const char* url) diff --git a/main/qrmode.h b/main/qrmode.h index 936993b5..26633edb 100644 --- a/main/qrmode.h +++ b/main/qrmode.h @@ -28,6 +28,7 @@ void await_qr_help_activity(const char* url); bool await_qr_back_continue_activity( const char* message[], size_t message_size, const char* url, bool default_selection); +bool show_otp_uri_qr_activity(const otpauth_ctx_t* otp_ctx); // Start pinserver authentication via qr codes void handle_qr_auth(bool suppress_pin_change_confirmation); diff --git a/main/ui/otpauth.c b/main/ui/otpauth.c index 104c912b..c7d0fe85 100644 --- a/main/ui/otpauth.c +++ b/main/ui/otpauth.c @@ -164,6 +164,18 @@ static gui_activity_t* make_otp_details_activities(const otpauth_ctx_t* ctx, con return act; } +gui_activity_t* make_view_export_otp_activity(const char* name) { + + //TODO change BTN EVENTS + btn_data_t hdrbtns[] = { { .txt = "=", .font = JADE_SYMBOLS_16x16_FONT, .ev_id = BTN_BACK}, + { .txt = NULL, .font = GUI_DEFAULT_FONT, .ev_id = GUI_BUTTON_EVENT_NONE }}; + + btn_data_t menubtns[] = { { .txt = "View", .font = GUI_DEFAULT_FONT, .ev_id = BTN_OTP_DETAILS_VIEW}, + { .txt = "Export", .font = GUI_DEFAULT_FONT, .ev_id = BTN_OTP_DETAILS_EXPORT}}; + + return make_menu_activity(name, hdrbtns, 2, menubtns, 2); +} + // otp details screen for viewing or confirmation // returns true if we are to store/retain this record, false if we are to discard/delete the record bool show_otp_details_activity( diff --git a/main/ui/qrmode.c b/main/ui/qrmode.c index 04add7b6..236aa251 100644 --- a/main/ui/qrmode.c +++ b/main/ui/qrmode.c @@ -201,6 +201,59 @@ gui_activity_t* make_qr_options_activity(gui_view_node_t** density_textbox, gui_ return make_menu_activity("QR Settings", hdrbtns, 2, menubtns, 2); } +// NOTE: 'icons' passed in here must be heap-allocated as the gui element takes ownership +gui_activity_t* make_show_otp_qr_actvity(const char* otp_name, Icon* qr_icon) { + + JADE_ASSERT(otp_name); + JADE_ASSERT(qr_icon); + + gui_activity_t* const act = gui_make_activity(); + gui_view_node_t* node; + + gui_view_node_t* hsplit; + gui_make_hsplit(&hsplit, GUI_SPLIT_RELATIVE, 2, 44, 56); + gui_set_parent(hsplit, act->root_node); + + // LHS + gui_view_node_t* vsplit; + gui_make_vsplit(&vsplit, GUI_SPLIT_RELATIVE, 4, 20, 30, 25, 25); + gui_set_parent(vsplit, hsplit); + + // back button + btn_data_t hdrbtns[] + = { { .txt = "=", .font = JADE_SYMBOLS_16x16_FONT, .ev_id = BTN_BACK, .borders = GUI_BORDER_ALL }, + { .txt = NULL, .font = GUI_DEFAULT_FONT, .ev_id = GUI_BUTTON_EVENT_NONE }, + { .txt = "?", .font = GUI_TITLE_FONT, .ev_id = BTN_HELP, .borders = GUI_BORDER_ALL } }; + add_buttons(vsplit, UI_ROW, hdrbtns, 3); // 44 (hsplit) / 3 == 14 - almost 15 so ok + + // second row, type label + gui_make_text(&node, otp_name, TFT_WHITE); + gui_set_parent(node, vsplit); + gui_set_align(node, GUI_ALIGN_LEFT, GUI_ALIGN_MIDDLE); + + // third row, path + gui_make_text_font(&node, "Scan Secret Key", TFT_WHITE, DEFAULT_FONT); // fits path + gui_set_parent(node, vsplit); + gui_set_align(node, GUI_ALIGN_LEFT, GUI_ALIGN_TOP); + + // button + btn_data_t ftrbtn + = { .txt = "View Secret", .font = GUI_DEFAULT_FONT, .ev_id = BTN_OTP_DETAILS_SECRET, .borders = GUI_BORDER_TOP }; + add_buttons(vsplit, UI_COLUMN, &ftrbtn, 1); + + // RHS - QR icons + gui_view_node_t* fill; + gui_make_fill(&fill, GUI_BLOCKSTREAM_QR_PALE); + gui_set_parent(fill, hsplit); + + gui_make_icon(&node, qr_icon, TFT_BLACK, &GUI_BLOCKSTREAM_QR_PALE); + gui_set_align(node, GUI_ALIGN_CENTER, GUI_ALIGN_MIDDLE); + gui_set_parent(node, fill); + gui_set_icon_animation(node, qr_icon, 1, 0); + + return act; +} + // NOTE: 'icons' passed in here must be heap-allocated as the gui element takes ownership gui_activity_t* make_show_qr_activity(const char* message[], const size_t message_size, Icon* icons, const size_t num_icons, const size_t frames_per_qr_icon, const bool show_options_button, const bool show_help_btn)