diff --git a/docs/security/evidence/rom-printf-integer-percent/01-thorchain-withdraw-25.05pct.png b/docs/security/evidence/rom-printf-integer-percent/01-thorchain-withdraw-25.05pct.png new file mode 100644 index 000000000..22d9f0735 Binary files /dev/null and b/docs/security/evidence/rom-printf-integer-percent/01-thorchain-withdraw-25.05pct.png differ diff --git a/docs/security/evidence/rom-printf-integer-percent/02-thorchain-sending-eth.png b/docs/security/evidence/rom-printf-integer-percent/02-thorchain-sending-eth.png new file mode 100644 index 000000000..61cac6355 Binary files /dev/null and b/docs/security/evidence/rom-printf-integer-percent/02-thorchain-sending-eth.png differ diff --git a/docs/security/evidence/rom-printf-integer-percent/README.md b/docs/security/evidence/rom-printf-integer-percent/README.md new file mode 100644 index 000000000..b54b7d4fa --- /dev/null +++ b/docs/security/evidence/rom-printf-integer-percent/README.md @@ -0,0 +1,17 @@ +# Integer percent rendering on the THOR/Maya withdraw confirm + +Emulator captures for the change that routes all device `snprintf` calls to +newlib's integer-only `sniprintf` and rewrites the last two float format +users (`%3.2f` in `thorchain.c` / `mayachain.c`) as integer basis-point math. + +- `01-thorchain-withdraw-25.05pct.png` — ETH router `deposit()` carrying memo + `WITHDRAW:ETH.USDT-0xdac17f958d2ee523a2206206994597c13d831ec7:2505`. + 2505 bps renders as `25.05%`: the integer path preserves the `%02d` + zero-padding of the fractional digits. +- `02-thorchain-sending-eth.png` — the amount screen from the same flow, + showing `%llu`-family rendering is unaffected. + +Reproduce with `scripts/emulator/capture-thor-percent.py` against kkemu +(`KEEPKEY_SCREENSHOT=1`, abandon test seed, no PIN). Emulator captures do not +satisfy Gate-3 on their own — an on-device pass of the THOR withdraw screen +and one Osmosis `%llu` amount screen is still owed before release. diff --git a/include/keepkey/board/bsd_compat.h b/include/keepkey/board/bsd_compat.h new file mode 100644 index 000000000..0e2b46495 --- /dev/null +++ b/include/keepkey/board/bsd_compat.h @@ -0,0 +1,28 @@ +#ifndef KEEPKEY_BOARD_BSD_COMPAT_H +#define KEEPKEY_BOARD_BSD_COMPAT_H + +/* + * Declarations for BSD libc extensions that macOS/BSD expose via + * but glibc (Linux) and MinGW (Windows) do not. The emulator build compiles + * lib/board/strlcpy.c + strlcat.c when the libc lacks the definitions + * (KK_HAVE_STRLCPY / KK_HAVE_STRLCAT), so only the prototypes are missing. + * + * Force-included for non-Apple emulator builds (see CMakeLists.txt) so every + * translation unit sees the prototypes without us having to chase down ~20 + * call sites — and without touching the real hardware (ARM) build at all. + */ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +size_t strlcpy(char *dst, const char *src, size_t siz); +size_t strlcat(char *dst, const char *src, size_t siz); + +#ifdef __cplusplus +} +#endif + +#endif /* KEEPKEY_BOARD_BSD_COMPAT_H */ diff --git a/include/keepkey/board/confirm_sm.h b/include/keepkey/board/confirm_sm.h index fa74c829f..765a90125 100644 --- a/include/keepkey/board/confirm_sm.h +++ b/include/keepkey/board/confirm_sm.h @@ -95,6 +95,10 @@ bool confirm(ButtonRequestType type, const char* request_title, const char* request_body, ...) __attribute__((format(printf, 3, 4))); +bool confirm_with_icon(ButtonRequestType type, IconType iconNum, + const char* request_title, const char* request_body, ...) + __attribute__((format(printf, 4, 5))); + bool confirm_constant_power(ButtonRequestType type, const char* request_title, const char* request_body, ...) __attribute__((format(printf, 3, 4))); diff --git a/include/keepkey/board/draw.h b/include/keepkey/board/draw.h index f1e6f0627..a1a1c90ca 100644 --- a/include/keepkey/board/draw.h +++ b/include/keepkey/board/draw.h @@ -51,6 +51,31 @@ void draw_char_simple(Canvas* canvas, const Font* font, char c, uint8_t color, void draw_box(Canvas* canvas, BoxDrawableParams* p); void draw_box_simple(Canvas* canvas, uint8_t color, uint16_t x, uint16_t y, uint16_t width, uint16_t height); +/* + * draw_bitmap_mono_rle_valid() - Validate an RLE stream against a geometry. + * + * Pure and side-effect-free: decodes nothing, writes nothing, touches no + * canvas. Returns true iff the stream is EXACTLY well-formed for a w*h image: + * - every packet count is valid (never 0, never 0x80/-128 — the decoder's + * counter is int8_t and cannot represent a 128 literal), + * - no run straddles the end of the image, + * - exactly w*h pixels are produced, and + * - the whole input is consumed (no trailing packets). + * + * The drawing path is lenient by construction (it stops once the canvas is + * full), so callers that accept host-supplied streams MUST validate here at + * the trust boundary rather than infer validity from a successful draw. + * + * INPUT + * - data: RLE stream + * - length: stream length in bytes + * - w, h: target image geometry + * OUTPUT + * true iff the stream decodes exactly to w*h pixels + */ +bool draw_bitmap_mono_rle_valid(const uint8_t* data, uint32_t length, + uint16_t w, uint16_t h); + bool draw_bitmap_mono_rle(Canvas* canvas, const AnimationFrame* frame, bool erase); diff --git a/include/keepkey/board/font.h b/include/keepkey/board/font.h index 95933f2a5..1fe6eedf4 100644 --- a/include/keepkey/board/font.h +++ b/include/keepkey/board/font.h @@ -20,6 +20,7 @@ #ifndef FONT_H #define FONT_H +#include #include /* Data pertaining to the image of a character */ @@ -53,5 +54,9 @@ uint32_t font_width(const Font* font); uint32_t calc_str_width(const Font* font, const char* str); uint32_t calc_str_line(const Font* font, const char* str, uint16_t line_width); +uint32_t calc_str_line_n(const Font* font, const char* str, size_t str_len, + uint16_t line_width); +size_t calc_str_page(const Font* font, const char* str, size_t str_len, + uint16_t line_width, uint32_t max_lines); #endif diff --git a/include/keepkey/board/layout.h b/include/keepkey/board/layout.h index ed5ab8f33..08e7ab368 100644 --- a/include/keepkey/board/layout.h +++ b/include/keepkey/board/layout.h @@ -79,6 +79,11 @@ typedef enum { typedef enum { NO_ICON = 0, ETHEREUM_ICON, + VERIFIED_ICON, + /* A runtime-supplied 1bpp mono RLE bitmap (e.g. a loaded clear-sign identity + * logo). The frame is set via layout_set_runtime_icon() before the confirm; + * drawn by layout_add_icon(). */ + RUNTIME_ICON, } IconType; typedef void (*AnimateCallback)(void* data, uint32_t duration, @@ -111,6 +116,12 @@ void layout_constant_power_notification(const char* str1, const char* str2, NotificationType type); void layout_notification_icon(NotificationType type, DrawableParams* sp); void layout_add_icon(IconType type); + +/// \brief Set the frame drawn for RUNTIME_ICON on the next confirm. Pass NULL +/// to clear. The AnimationFrame + its Image must outlive the confirm +/// (typically file-static in the caller). +struct AnimationFrame_; +void layout_set_runtime_icon(const struct AnimationFrame_* frame); void layout_warning(const char* str); void layout_warning_static(const char* str); void layout_simple_message(const char* str); @@ -124,6 +135,10 @@ void animating_progress_handler(const char* desc, int permil); void layoutProgress(const char* desc, int permil); void layoutProgressForAuth(const char* otp, const char* desc, int permil); void layoutProgressSwipe(const char* desc, int permil); +void layoutProgressTrickle(const char* desc, int base_permil, + int target_permil); +void layoutProgressTrickleStop(void); +void layout_animate_poll(void); void layout_add_animation(AnimateCallback callback, void* data, uint32_t duration); void layout_animate_images(void* data, uint32_t duration, uint32_t elapsed); diff --git a/include/keepkey/board/util.h b/include/keepkey/board/util.h index 5272a41ea..f6fab624a 100644 --- a/include/keepkey/board/util.h +++ b/include/keepkey/board/util.h @@ -54,8 +54,7 @@ void dec64_to_str(uint64_t dec64_val, char* str); bool is_valid_ascii(const uint8_t* data, uint32_t size); -int base_to_precision(uint8_t* dest, const uint8_t* value, - const uint8_t dest_len, const uint8_t value_len, - const uint8_t precision); +int base_to_precision(uint8_t* dest, const uint8_t* value, size_t dest_len, + size_t value_len, uint8_t precision); #endif diff --git a/include/keepkey/firmware/app_confirm.h b/include/keepkey/firmware/app_confirm.h index 15fcd7c64..cf1630288 100644 --- a/include/keepkey/firmware/app_confirm.h +++ b/include/keepkey/firmware/app_confirm.h @@ -24,6 +24,7 @@ #include #include +#include #define CONFIRM_SIGN_IDENTITY_TITLE 32 #define CONFIRM_SIGN_IDENTITY_BODY 416 @@ -46,10 +47,23 @@ bool confirm_load_device(bool is_node); bool confirm_address(const char* desc, const char* address); bool confirm_xpub(const char* node_str, const char* xpub); bool confirm_sign_identity(const IdentityType* identity, const char* challenge); +/** + * Review every byte of a length-delimited payload. Printable ASCII and LF line + * breaks are paged as text; any payload containing another control/non-ASCII + * byte is paged as complete hexadecimal. Page boundaries use the OLED + * renderer's actual font and word-wrap budget, so no accepted byte can be + * clipped below the third row. + */ +bool confirm_bytes_is_text(const uint8_t* data, size_t size); +bool confirm_bytes(ButtonRequestType button_request, const char* title, + const uint8_t* data, size_t size); bool confirm_cosmos_address(const char* desc, const char* address); bool confirm_osmosis_address(const char* desc, const char* address); bool confirm_ethereum_address(const char* desc, const char* address); bool confirm_nano_address(const char* desc, const char* address); +#if ZCASH_PRIVACY +bool confirm_zcash_address(const char* desc, const char* address); +#endif bool confirm_omni(ButtonRequestType button_request, const char* title, const uint8_t* data, uint32_t size); bool confirm_data(ButtonRequestType button_request, const char* title, diff --git a/include/keepkey/firmware/app_layout.h b/include/keepkey/firmware/app_layout.h index fc6d9ca96..10e35b891 100644 --- a/include/keepkey/firmware/app_layout.h +++ b/include/keepkey/firmware/app_layout.h @@ -118,8 +118,16 @@ void layout_ethereum_address_notification(const char* desc, const char* address, NotificationType type); void layout_nano_address_notification(const char* desc, const char* address, NotificationType type); +#if ZCASH_PRIVACY +void layout_zcash_address_notification(const char* desc, const char* address, + NotificationType type); +void layout_zcash_address_text_notification(const char* desc, + const char* address, + NotificationType type); +#endif void layout_pin(const char* str, char* pin); -void layout_cipher(const char* current_word, const char* cipher); +void layout_cipher(const char* current_word, const char* cipher, + const char* prev_word_info); void layout_address(const char* address, QRSize qr_size); void set_leaving_handler(leaving_handler_t leaving_func); diff --git a/lib/board/confirm_sm.c b/lib/board/confirm_sm.c index 0669536ab..8804f5f24 100644 --- a/lib/board/confirm_sm.c +++ b/lib/board/confirm_sm.c @@ -315,6 +315,29 @@ bool confirm(ButtonRequestType type, const char* request_title, return ret; } +bool confirm_with_icon(ButtonRequestType type, IconType iconNum, + const char* request_title, const char* request_body, + ...) { + button_request_acked = false; + + va_list vl; + va_start(vl, request_body); + vsnprintf(strbuf, sizeof(strbuf), request_body, vl); + va_end(vl); + + ButtonRequest resp; + memset(&resp, 0, sizeof(ButtonRequest)); + resp.has_code = true; + resp.code = type; + msg_write(MessageType_MessageType_ButtonRequest, &resp); + + bool ret = + confirm_helper(request_title, strbuf, &layout_standard_notification, + false, iconNum, false); + memzero(strbuf, sizeof(strbuf)); + return ret; +} + bool confirm_constant_power(ButtonRequestType type, const char* request_title, const char* request_body, ...) { button_request_acked = false; @@ -414,10 +437,11 @@ bool review(ButtonRequestType type, const char* request_title, resp.code = type; msg_write(MessageType_MessageType_ButtonRequest, &resp); - (void)confirm_helper(request_title, strbuf, &layout_standard_notification, - false, NO_ICON, false); + bool ret = + confirm_helper(request_title, strbuf, &layout_standard_notification, + false, NO_ICON, false); memzero(strbuf, sizeof(strbuf)); - return true; + return ret; } bool review_without_button_request(const char* request_title, @@ -429,10 +453,11 @@ bool review_without_button_request(const char* request_title, vsnprintf(strbuf, sizeof(strbuf), request_body, vl); va_end(vl); - (void)confirm_helper(request_title, strbuf, &layout_standard_notification, - false, NO_ICON, false); + bool ret = + confirm_helper(request_title, strbuf, &layout_standard_notification, + false, NO_ICON, false); memzero(strbuf, sizeof(strbuf)); - return true; + return ret; } bool review_with_icon(ButtonRequestType type, IconType iconNum, @@ -452,10 +477,11 @@ bool review_with_icon(ButtonRequestType type, IconType iconNum, resp.code = type; msg_write(MessageType_MessageType_ButtonRequest, &resp); - (void)confirm_helper(request_title, strbuf, &layout_standard_notification, - false, iconNum, false); + bool ret = + confirm_helper(request_title, strbuf, &layout_standard_notification, + false, iconNum, false); memzero(strbuf, sizeof(strbuf)); - return true; + return ret; } bool review_immediate(ButtonRequestType type, const char* request_title, @@ -474,8 +500,9 @@ bool review_immediate(ButtonRequestType type, const char* request_title, resp.code = type; msg_write(MessageType_MessageType_ButtonRequest, &resp); - (void)confirm_helper(request_title, strbuf, &layout_standard_notification, - false, NO_ICON, true); + bool ret = + confirm_helper(request_title, strbuf, &layout_standard_notification, + false, NO_ICON, true); memzero(strbuf, sizeof(strbuf)); - return true; + return ret; } diff --git a/lib/board/draw.c b/lib/board/draw.c index 3a51e145f..14c2a070f 100644 --- a/lib/board/draw.c +++ b/lib/board/draw.c @@ -287,6 +287,58 @@ void draw_box_simple(Canvas* canvas, uint8_t color, uint16_t x, uint16_t y, * OUTPUT * true/false whether image was drawn */ +/* + * draw_bitmap_mono_rle_valid() - see draw.h. Pure walk of the RLE grammar; + * writes nothing. The drawing path below stops as soon as the canvas is full, + * so it cannot tell a well-formed stream from one whose last run straddles the + * image or that carries trailing packets. Host-supplied icons must be checked + * here, at the trust boundary, before they are shown or cached for a session. + */ +bool draw_bitmap_mono_rle_valid(const uint8_t* data, uint32_t length, + uint16_t w, uint16_t h) { + if (!data || w == 0 || h == 0) { + return false; + } + + const uint32_t pixels = (uint32_t)w * (uint32_t)h; + uint32_t emitted = 0; + uint32_t i = 0; + + while (emitted < pixels) { + if (i >= length) { + return false; /* ran out of input mid-image */ + } + const uint8_t raw = data[i]; + if (raw == 0x80u || raw == 0u) { + return false; /* undecodable (int8_t counter) / not a packet */ + } + i++; + + uint32_t run; + if (raw > 127u) { + run = (uint32_t)(256u - raw); /* LITERAL: 1..127 distinct values */ + if (i + run > length) { + return false; /* literal body truncated */ + } + i += run; + } else { + run = raw; /* RUN: 1..127 copies of one value */ + if (i >= length) { + return false; /* missing the run's value byte */ + } + i++; + } + + if (emitted + run > pixels) { + return false; /* run straddles the end of the image */ + } + emitted += run; + } + + /* Exactly filled, and nothing left over. */ + return emitted == pixels && i == length; +} + bool draw_bitmap_mono_rle(Canvas* canvas, const AnimationFrame* frame, bool erase) { if (!frame || !canvas) { @@ -302,6 +354,16 @@ bool draw_bitmap_mono_rle(Canvas* canvas, const AnimationFrame* frame, return false; } + /* Validate the whole stream up front. The loop below fills the canvas and + * stops, so on its own it cannot reject a final run that straddles the image + * or trailing packets past the last pixel — it would draw and report success. + * Checking first makes the return value mean "this stream is well-formed AND + * was drawn", which is what callers gating on host-supplied icons need. + * (Verified: every bundled image stream terminates exactly.) */ + if (!draw_bitmap_mono_rle_valid(img->data, img->length, img->w, img->h)) { + return false; + } + int8_t sequence = 0; int8_t nonsequence = 0; uint32_t pixel_index = 0; @@ -315,12 +377,29 @@ bool draw_bitmap_mono_rle(Canvas* canvas, const AnimationFrame* frame, // sequence > 0 implies the next x pixels are the same // sequence < 0 implies the next -x pixels are all different if ((sequence == 0) && (nonsequence == 0)) { - sequence = img->data[pixel_index]; + /* Read the packet count. 0x80 (-128) is rejected: `nonsequence` below + * is int8_t, so -(-128) = 128 does not fit and wraps back to -128, + * breaking the `nonsequence > 0` invariant. Under NDEBUG the assert is + * compiled out and we would decode with a negative counter + * (signed-overflow UB). 0 is likewise not a valid packet: it leaves + * both counters at zero and breaks the same invariant. A host-supplied + * icon reaches here, so fail closed rather than trust the encoder. */ + const uint8_t raw = img->data[pixel_index]; + if (raw == 0x80u || raw == 0u) { + return false; + } pixel_index++; - if (sequence < 0) { - nonsequence = -sequence; + /* Explicit two's-complement read. Narrowing a uint8_t > 127 straight + * into an int8_t is implementation-defined, so spell the conversion + * out: 1..127 stay positive (RUN), 129..255 become -127..-1 (LITERAL). + */ + if (raw > 127u) { + nonsequence = (int8_t)((int)raw - 256); /* -127..-1 */ + nonsequence = (int8_t)(-nonsequence); /* 1..127, fits int8_t */ sequence = 0; + } else { + sequence = (int8_t)raw; /* 1..127 */ } } diff --git a/lib/board/font.c b/lib/board/font.c index 5fccfcb33..d00c215e7 100644 --- a/lib/board/font.c +++ b/lib/board/font.c @@ -21,6 +21,7 @@ #include "keepkey/board/font.h" #include +#include /* --- Image Font ------------------------------------------------------------ */ @@ -2598,29 +2599,31 @@ uint32_t calc_str_width(const Font* font, const char* str) { * OUTPUT * line count */ -uint32_t calc_str_line(const Font* font, const char* str, uint16_t line_width) { +uint32_t calc_str_line_n(const Font* font, const char* str, size_t str_len, + uint16_t line_width) { uint8_t line_count = 1; uint16_t x_offset = 0; + size_t offset = 0; - while (*str) { - uint8_t character_width = font_get_char(font, str[0])->width; + while (offset < str_len && str[offset]) { + uint8_t character_width = font_get_char(font, str[offset])->width; uint16_t word_width = character_width; - const char* next_character = str + 1; + size_t next_offset = offset + 1; /* Allow line breaks */ - if (*str == '\n') { + if (str[offset] == '\n') { line_count++; x_offset = 0; - str++; + offset++; continue; } /* Calculate next work width */ - if (*str == ' ') { - while (*next_character && *next_character != ' ' && - *next_character != '\n') { - word_width += font_get_char(font, *next_character)->width; - next_character++; + if (str[offset] == ' ') { + while (next_offset < str_len && str[next_offset] && + str[next_offset] != ' ' && str[next_offset] != '\n') { + word_width += font_get_char(font, str[next_offset])->width; + next_offset++; } } @@ -2631,14 +2634,27 @@ uint32_t calc_str_line(const Font* font, const char* str, uint16_t line_width) { } /* Remove leading spaces */ - if (x_offset == 0 && *str == ' ') { - str++; + if (x_offset == 0 && str[offset] == ' ') { + offset++; continue; } x_offset += character_width; - str++; + offset++; } return line_count; } + +uint32_t calc_str_line(const Font* font, const char* str, uint16_t line_width) { + return calc_str_line_n(font, str, strlen(str), line_width); +} + +size_t calc_str_page(const Font* font, const char* str, size_t str_len, + uint16_t line_width, uint32_t max_lines) { + size_t best = 0; + for (size_t take = 1; take <= str_len; take++) { + if (calc_str_line_n(font, str, take, line_width) <= max_lines) best = take; + } + return best; +} diff --git a/lib/board/layout.c b/lib/board/layout.c index 59e9dd9e2..7c4d44d66 100644 --- a/lib/board/layout.c +++ b/lib/board/layout.c @@ -324,12 +324,30 @@ void layout_standard_notification(const char* str1, const char* str2, * OUTPUT * none */ +/* Frame drawn for RUNTIME_ICON — a loaded clear-sign identity logo. Set by + * layout_set_runtime_icon() before the confirm; the caller owns the storage. */ +static const AnimationFrame* runtime_icon_frame = NULL; + +void layout_set_runtime_icon(const struct AnimationFrame_* frame) { + runtime_icon_frame = frame; +} + void layout_add_icon(IconType type) { switch (type) { case ETHEREUM_ICON: + /* ponytail: reuse the ETH glyph as the "verified" mark — it's an ETH tx. + * Swap in a dedicated checkmark bitmap if the trust mark needs to differ. + */ + case VERIFIED_ICON: draw_bitmap_mono_rle(canvas, get_ethereum_icon_frame(), false); break; + case RUNTIME_ICON: + if (runtime_icon_frame) { + draw_bitmap_mono_rle(canvas, runtime_icon_frame, false); + } + break; + default: /* no action requires */ break; @@ -706,11 +724,20 @@ static const char* _otpStr = ""; * OTP in large font desc - text to display permil - progress in units of 1 to * 1000 OUTPUT none */ -void animating_progress_handler(const char* desc, int permil) { +/* Render the progress bar into the framebuffer WITHOUT clearing the animation + * queue, so an animation callback (trickle_progress_callback) can redraw itself + * every frame without removing itself from the queue. + * + * marker_phase: 0..999 breathes a glint on the fill's leading segment (a + * perpetual "working" cue that keeps the display visibly moving even after + * the eased fill has pixel-saturated); pass -1 for no glint. */ +static void progress_render_ex(const char* desc, int permil, int marker_phase) { if (!canvas) return; - call_leaving_handler(); - layout_clear(); + layout_clear_static(); +#if DEBUG_LINK + layout_debuglink_watermark(); +#endif permil = permil >= 1000 ? 1000 : permil; permil = permil <= 0 ? 0 : permil; @@ -775,9 +802,43 @@ void animating_progress_handler(const char* desc, int permil) { draw_box(canvas, &bp); } + // Front glint: the fill's leading segment breathes (dim <-> bright) while + // the trickle is active. Activity always shows exactly at the progress + // front — unlike a marker sweeping the track, it cannot detach from the + // fill and open a gap, and it cannot run out of travel as the unfilled + // span shrinks near 100% (long final-action proofs). + if (marker_phase >= 0 && finished_width > 4) { + const uint32_t glint_max = 10; + uint32_t glint_w = + finished_width - 2 < glint_max ? finished_width - 2 : glint_max; + /* Triangle wave 0..500..0 over one breath period. */ + uint32_t tri = (uint32_t)marker_phase < 500 ? (uint32_t)marker_phase + : 1000 - (uint32_t)marker_phase; + bp.width = glint_w; + bp.height = height - 2; + bp.base.x = x + finished_width - glint_w; + bp.base.y = y + 1; + bp.base.color = (uint8_t)(0x44 + (tri * 0x66) / 500); + draw_box(canvas, &bp); + } + display_refresh(); } +static void progress_render(const char* desc, int permil) { + progress_render_ex(desc, permil, -1); +} + +/* One-shot progress draw: clears any queued animation (historical behaviour, so + * a stray animation cannot redraw over a static progress screen) then renders. + */ +void animating_progress_handler(const char* desc, int permil) { + if (!canvas) return; + call_leaving_handler(); + layout_clear_animations(); + progress_render(desc, permil); +} + void layoutProgress(const char* desc, int permil) { animating_progress_handler(desc, permil); } @@ -818,6 +879,87 @@ void layout_add_animation(AnimateCallback callback, void* data, animation_queue_push(&active_queue, animation); } +/* --- Trickle progress for long host-driven operations ----------------------- + * Shielded Zcash signing blocks on the host generating zk-proofs, so the device + * would otherwise sit on a frozen progress bar and look like it has failed. + * This ramps a "trickle" smoothly through most of the gap to the next real + * milestone over the expected host-proof duration, holding short of it (so it + * never falsely shows work done). It is driven off the animation timer, which + * layout_animate_poll() pumps from usbPoll() while the device blocks on host + * I/O. A dedicated flag gates that pump so no other flow is affected. */ +static volatile bool trickle_active = false; +static struct { + const char* desc; + int base; /* permil committed by the last real milestone */ + int target; /* permil to ease toward (the next milestone) */ +} trickle; + +static void trickle_progress_callback(void* data, uint32_t duration, + uint32_t elapsed) { + (void)data; + (void)duration; + /* Host proof windows between milestones run ~40-50s. Ramp linearly through + * 90% of the milestone span over that guessed duration, then hold — the + * last 10% is only crossed by the next REAL milestone, so the bar never + * claims work that hasn't happened. The breathing glint keeps signalling + * activity while the ramp holds. + * ponytail: EXPECTED_MS is a guess, not a measurement — retune if host + * proof times change materially. */ + const uint32_t EXPECTED_MS = 45000; + int span = trickle.target - trickle.base; + int cap = (span * 9) / 10; + int add = 0; + if (cap > 0) { + add = elapsed >= EXPECTED_MS + ? cap + : (int)(((uint64_t)cap * elapsed) / EXPECTED_MS); + } + /* Glint phase loops forever, so the display keeps changing even after the + * eased fill has stopped producing new pixels (long zk-proof waits). */ + const uint32_t BREATH_PERIOD = 1600; /* ms per dim<->bright breath cycle */ + int phase = (int)(((elapsed % BREATH_PERIOD) * 1000) / BREATH_PERIOD); + /* Draw via progress_render_ex (not animating_progress_handler) so redrawing + * the frame does not clear the animation queue and remove this callback. */ + progress_render_ex(trickle.desc, trickle.base + add, phase); +} + +/* (Re-)arm the trickle to ease from base_permil toward target_permil. Re-adding + * the callback resets its elapsed to 0 so the ease restarts from base_permil. + */ +void layoutProgressTrickle(const char* desc, int base_permil, + int target_permil) { + trickle.desc = desc; + trickle.base = base_permil; + trickle.target = target_permil; + trickle_active = true; + layout_add_animation(&trickle_progress_callback, NULL, 0 /* loop forever */); + force_animation_start(); + /* Draw the first frame now (at base, glint at its dimmest) so the bar + * appears immediately, before the animation timer next fires. + * progress_render_ex keeps the animation queue intact. */ + progress_render_ex(desc, base_permil, 0); +} + +void layoutProgressTrickleStop(void) { + trickle_active = false; + Animation* animation = + animation_queue_get(&active_queue, &trickle_progress_callback); + if (animation != NULL) { + animation_queue_push(&free_queue, animation); + } +} + +/* Advance a queued progress animation one step if the timer has ticked. Called + * from usbPoll() so the trickle keeps moving while we block on host I/O. Gated + * on trickle_active so it is a no-op for every other flow (confirm dialogs, + * PIN entry, etc. are untouched). */ +void layout_animate_poll(void) { + if (trickle_active && is_animating()) { + animate(); + display_refresh(); + } +} + /* * layout_clear_animations() - Clear all animation from queue * @@ -827,6 +969,7 @@ void layout_add_animation(AnimateCallback callback, void* data, * none */ void layout_clear_animations(void) { + trickle_active = false; Animation* animation = animation_queue_pop(&active_queue); while (animation != NULL) { diff --git a/lib/board/timer.c b/lib/board/timer.c index 3868b6d16..c66c62e4d 100644 --- a/lib/board/timer.c +++ b/lib/board/timer.c @@ -25,6 +25,11 @@ #else #include #include +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN /* exclude winsock.h — it declares \ + shutdown(SOCKET,int) */ +#include /* Sleep() */ +#endif #endif #include "keepkey/board/keepkey_board.h" @@ -229,11 +234,13 @@ void timer_init(void) { nvic_set_priority(NVIC_TIM4_IRQ, 16 * 2); timer_enable_counter(TIM4); -#else +#elif !defined(_WIN32) void tim4_sighandler(int sig); signal(SIGALRM, tim4_sighandler); ualarm(1000, 1000); #endif + /* _WIN32: no SIGALRM/ualarm — libkkemu's kkemu_poll() drives timerisr_usr(). + */ } uint32_t fi_defense_delay(volatile uint32_t value) { @@ -287,8 +294,21 @@ void delay_us(uint32_t us) { void delay_ms(uint32_t ms) { remaining_delay = ms; +#ifdef _WIN32 + /* No async SIGALRM timer on Windows, and kkemu_poll() drives timerisr_usr() + * only once per poll — so a plain spin here would never make progress when + * delay_ms() is reached from inside usbPoll() (e.g. PIN/U2F/authenticator + * flows). Advance the tick ourselves from wall-clock Sleep instead. Keeps + * timeSinceWakeup + the runnable queue moving exactly like the SIGALRM path, + * and stays single-threaded (no data races). */ while (remaining_delay > 0) { + Sleep(1); + timerisr_usr(); } +#else + while (remaining_delay > 0) { + } +#endif } /* @@ -310,6 +330,12 @@ void delay_ms_with_callback(uint32_t ms, callback_func_t callback_func, if (remaining_delay % frequency_ms == 0) { (*callback_func)(); } +#ifdef _WIN32 + /* See delay_ms(): drive the tick from wall-clock Sleep on Windows so this + * loop terminates when reached from inside usbPoll(). */ + Sleep(1); + timerisr_usr(); +#endif } } @@ -348,7 +374,7 @@ void timerisr_usr(void) { #endif } -#ifdef EMULATOR +#if defined(EMULATOR) && !defined(_WIN32) void tim4_sighandler(int sig) { timerisr_usr(); } #endif diff --git a/lib/board/usb.c b/lib/board/usb.c index 0db5e32c6..53fb07a89 100644 --- a/lib/board/usb.c +++ b/lib/board/usb.c @@ -414,6 +414,11 @@ void usbInit(const char* origin_url) { void usbPoll(void) { // poll read buffer usbd_poll(usbd_dev); + // Keep a queued progress animation moving while we block on host I/O (e.g. + // Zcash proof generation on the host), so the screen never looks frozen. + // No-op unless a trickle animation is active, so all other flows are + // unaffected. + layout_animate_poll(); } void usbReconnect(void) { @@ -435,28 +440,31 @@ bool msg_write(MessageType msg_id, const void* msg) { if (!fields) return false; - TrezorFrameBuffer framebuf; - memset(&framebuf, 0, sizeof(framebuf)); - framebuf.frame.usb_header.hid_type = '?'; - framebuf.frame.header.pre1 = '#'; - framebuf.frame.header.pre2 = '#'; - framebuf.frame.header.id = __builtin_bswap16(msg_id); + /* Encode into the shared frame arena instead of a 12 KB automatic — that + * stack frame overflowed the zcash-privacy variant's SRAM gap. Safe on the + * single-threaded transport; see the FrameArena contract in messages.c. */ + TrezorFrameBuffer* framebuf = frame_arena_tx(); + memset(framebuf, 0, sizeof(*framebuf)); + framebuf->frame.usb_header.hid_type = '?'; + framebuf->frame.header.pre1 = '#'; + framebuf->frame.header.pre2 = '#'; + framebuf->frame.header.id = __builtin_bswap16(msg_id); pb_ostream_t os = - pb_ostream_from_buffer(framebuf.buffer, sizeof(framebuf.buffer)); + pb_ostream_from_buffer(framebuf->buffer, sizeof(framebuf->buffer)); if (!pb_encode(&os, fields, msg)) return false; - framebuf.frame.header.len = __builtin_bswap32(os.bytes_written); + framebuf->frame.header.len = __builtin_bswap32(os.bytes_written); // Chunk out data - for (uint32_t pos = 1; pos < sizeof(framebuf.frame) + os.bytes_written; + for (uint32_t pos = 1; pos < sizeof(framebuf->frame) + os.bytes_written; pos += 64 - 1) { uint8_t tmp_buffer[64] = {0}; tmp_buffer[0] = '?'; - memcpy(tmp_buffer + 1, ((const uint8_t*)&framebuf) + pos, 64 - 1); + memcpy(tmp_buffer + 1, ((const uint8_t*)framebuf) + pos, 64 - 1); #ifndef EMULATOR while (usbd_ep_write_packet(usbd_dev, ENDPOINT_ADDRESS_IN, tmp_buffer, @@ -476,28 +484,29 @@ bool msg_debug_write(MessageType msg_id, const void* msg) { if (!fields) return false; - TrezorFrameBuffer framebuf; - memset(&framebuf, 0, sizeof(framebuf)); - framebuf.frame.usb_header.hid_type = '?'; - framebuf.frame.header.pre1 = '#'; - framebuf.frame.header.pre2 = '#'; - framebuf.frame.header.id = __builtin_bswap16(msg_id); + /* Same shared-arena encode as msg_write — see the FrameArena contract. */ + TrezorFrameBuffer* framebuf = frame_arena_tx(); + memset(framebuf, 0, sizeof(*framebuf)); + framebuf->frame.usb_header.hid_type = '?'; + framebuf->frame.header.pre1 = '#'; + framebuf->frame.header.pre2 = '#'; + framebuf->frame.header.id = __builtin_bswap16(msg_id); pb_ostream_t os = - pb_ostream_from_buffer(framebuf.buffer, sizeof(framebuf.buffer)); + pb_ostream_from_buffer(framebuf->buffer, sizeof(framebuf->buffer)); if (!pb_encode(&os, fields, msg)) return false; - framebuf.frame.header.len = __builtin_bswap32(os.bytes_written); + framebuf->frame.header.len = __builtin_bswap32(os.bytes_written); // Chunk out data - for (uint32_t pos = 1; pos < sizeof(framebuf.frame) + os.bytes_written; + for (uint32_t pos = 1; pos < sizeof(framebuf->frame) + os.bytes_written; pos += 64 - 1) { uint8_t tmp_buffer[64] = {0}; tmp_buffer[0] = '?'; - memcpy(tmp_buffer + 1, ((const uint8_t*)&framebuf) + pos, 64 - 1); + memcpy(tmp_buffer + 1, ((const uint8_t*)framebuf) + pos, 64 - 1); #ifndef EMULATOR while (usbd_ep_write_packet(usbd_dev, ENDPOINT_ADDRESS_DEBUG_IN, tmp_buffer, diff --git a/lib/board/util.c b/lib/board/util.c index 027f53522..f9948b967 100644 --- a/lib/board/util.c +++ b/lib/board/util.c @@ -103,33 +103,48 @@ bool is_valid_ascii(const uint8_t* data, uint32_t size) { } /* convert number in base units to specified decimal precision */ -int base_to_precision(uint8_t* dest, const uint8_t* value, - const uint8_t dest_len, const uint8_t value_len, - const uint8_t precision) { - if (!(dest && value)) { - // invalid pointer - return -1; +int base_to_precision(uint8_t* dest, const uint8_t* value, size_t dest_len, + size_t value_len, uint8_t precision) { + if (!dest || !value || dest_len == 0 || value_len == 0) return -1; + + // Decimal inputs are signed as strings. Accept only their unique canonical + // representation so the value shown on the OLED is byte-for-byte bound to + // the value placed in the transaction. + if ((value_len > 1 && value[0] == '0')) return -1; + for (size_t i = 0; i < value_len; i++) { + if (value[i] < '0' || value[i] > '9') return -1; } - if (value_len + 1 > dest_len) { - // value too large for output buffer - return -1; + + size_t rendered_len; + if (precision == 0) { + rendered_len = value_len; + } else if (value_len <= precision) { + rendered_len = (size_t)precision + 2; // "0." + precision digits + } else { + rendered_len = value_len + 1; // digits plus decimal point } - memset(dest, '0', dest_len); - uint8_t leading_digits = - ((value_len - precision) > 0) ? (value_len - precision) : 0; - - if (!leading_digits) { - memcpy(dest, "0.", 2); - uint8_t offset = - 2 + (((precision - value_len) > 0) ? (precision - value_len) : 0); - strlcpy((char*)&dest[offset], (char*)value, value_len); + if (rendered_len + 1 > dest_len) return -1; + + size_t offset = 0; + if (precision == 0) { + memcpy(dest, value, value_len); + offset = value_len; + } else if (value_len <= precision) { + dest[offset++] = '0'; + dest[offset++] = '.'; + const size_t zeroes = (size_t)precision - value_len; + memset(dest + offset, '0', zeroes); + offset += zeroes; + memcpy(dest + offset, value, value_len); + offset += value_len; } else { - uint8_t copy_len = MIN((value_len - leading_digits), precision); + const size_t leading_digits = value_len - precision; memcpy(dest, value, leading_digits); - dest[leading_digits] = '.'; - strlcpy((char*)&dest[leading_digits + 1], (char*)&value[leading_digits], - copy_len); + offset = leading_digits; + dest[offset++] = '.'; + memcpy(dest + offset, value + leading_digits, precision); + offset += precision; } - dest[dest_len] = '\0'; + dest[offset] = '\0'; return 0; } diff --git a/lib/firmware/app_confirm.c b/lib/firmware/app_confirm.c index d0b17e9bc..fdd9378a8 100644 --- a/lib/firmware/app_confirm.c +++ b/lib/firmware/app_confirm.c @@ -30,6 +30,7 @@ #include "keepkey/board/layout.h" #include "keepkey/board/messages.h" #include "keepkey/board/confirm_sm.h" +#include "keepkey/board/font.h" #include "keepkey/board/usb.h" #include "keepkey/board/util.h" @@ -321,6 +322,31 @@ bool confirm_nano_address(const char* desc, const char* address) { desc, "%s", address); } +/* + * confirm_zcash_address() - Show zcash address confirmation + * + * INPUT + * - desc: description (title) shown on both screens + * - address: zcash unified address — full text on the first screen, + * QR on the second + * OUTPUT + * true/false of confirmation + * + */ +#if ZCASH_PRIVACY +bool confirm_zcash_address(const char* desc, const char* address) { + if (!confirm_with_custom_layout(&layout_zcash_address_text_notification, + ButtonRequestType_ButtonRequest_Address, desc, + "%s", address)) { + return false; + } + + return confirm_with_custom_layout(&layout_zcash_address_notification, + ButtonRequestType_ButtonRequest_Address, + desc, "%s", address); +} +#endif + /* * confirm_address() - Show address confirmation * @@ -391,6 +417,142 @@ bool confirm_sign_identity(const IdentityType* identity, body); } +bool confirm_bytes_is_text(const uint8_t* data, size_t size) { + if (!data && size != 0) return false; + bool has_visible_character = false; + for (size_t i = 0; i < size; i++) { + if (data[i] == '\n') continue; + if (data[i] < 0x20 || data[i] > 0x7e) return false; + if (data[i] != ' ') has_visible_character = true; + } + return has_visible_character; +} + +static size_t confirm_bytes_render_page(const uint8_t* data, size_t size, + bool text, + char rendered[BODY_CHAR_MAX]) { + if (size == 0) return 0; + + const Font* font = get_body_font(); + size_t consumed = 0; + size_t written = 0; + uint32_t row = 1; + uint16_t x = 0; + + while (consumed < size) { + if (text && data[consumed] == '\n') { + // A page boundary already advances past the current third row. Consume + // its terminating LF without adding a blank row to the next page. + consumed++; + if (row == BODY_ROWS) break; + if (written + 1 >= BODY_CHAR_MAX) break; + rendered[written++] = '\n'; + row++; + x = 0; + continue; + } + + char chars[2]; + size_t char_count; + if (text) { + chars[0] = (char)data[consumed]; + char_count = 1; + } else { + static const char hex[] = "0123456789abcdef"; + chars[0] = hex[data[consumed] >> 4]; + chars[1] = hex[data[consumed] & 0x0f]; + char_count = 2; + } + + uint16_t width = 0; + for (size_t i = 0; i < char_count; i++) { + width += font_get_char(font, chars[i])->width; + } + + // draw_string() wraps only at spaces and otherwise clips overlong words. + // Pre-insert hard line breaks so long addresses, hashes and IBC denoms are + // actually visible rather than merely counted as one renderer line. + if (text && chars[0] == ' ') { + uint32_t word_width = width; + for (size_t i = consumed + 1; + i < size && data[i] != ' ' && data[i] != '\n'; i++) { + word_width += font_get_char(font, (char)data[i])->width; + } + if (x == 0) { + // The renderer discards a leading separator. Consume it here only + // after the preceding word has been disclosed on this or the prior + // page; the visual line/page boundary remains the separator. + consumed++; + continue; + } + if ((uint32_t)x + word_width > BODY_WIDTH) { + if (row == BODY_ROWS) break; + if (written + 1 >= BODY_CHAR_MAX) break; + rendered[written++] = '\n'; + row++; + x = 0; + consumed++; + continue; + } + } + + if ((uint32_t)x + width > BODY_WIDTH) { + if (row == BODY_ROWS) break; + if (written + 1 >= BODY_CHAR_MAX) break; + rendered[written++] = '\n'; + row++; + x = 0; + } + if (written + char_count >= BODY_CHAR_MAX) break; + memcpy(rendered + written, chars, char_count); + written += char_count; + x += width; + consumed++; + } + + rendered[written] = '\0'; + return consumed; +} + +bool confirm_bytes(ButtonRequestType button_request, const char* title, + const uint8_t* data, size_t size) { + if (!title || (!data && size != 0)) return false; + if (size == 0) return confirm(button_request, title, "(empty)"); + + const bool text = confirm_bytes_is_text(data, size); + size_t pages = 0; + size_t offset = 0; + while (offset < size) { + char rendered[BODY_CHAR_MAX]; + const size_t take = + confirm_bytes_render_page(data + offset, size - offset, text, rendered); + if (take == 0) return false; + offset += take; + pages++; + } + + offset = 0; + for (size_t page = 0; page < pages; page++) { + char rendered[BODY_CHAR_MAX]; + const size_t take = + confirm_bytes_render_page(data + offset, size - offset, text, rendered); + if (take == 0) return false; + + char page_title[TITLE_CHAR_MAX]; + if (pages > 1 || !text) { + snprintf(page_title, sizeof(page_title), + text ? "%s %u/%u" : "%s Hex %u/%u", title, (unsigned)(page + 1), + (unsigned)pages); + } else { + strlcpy(page_title, title, sizeof(page_title)); + } + + if (!confirm(button_request, page_title, "%s", rendered)) return false; + offset += take; + } + return true; +} + bool confirm_omni(ButtonRequestType button_request, const char* title, const uint8_t* data, uint32_t size) { uint32_t tx_type; @@ -428,17 +590,5 @@ bool confirm_omni(ButtonRequestType button_request, const char* title, bool confirm_data(ButtonRequestType button_request, const char* title, const uint8_t* data, uint32_t size) { - const char* str = (const char*)data; - char hex[50 * 2 + 1]; - if (!is_valid_ascii(data, size)) { - if (size > 50) size = 50; - memset(hex, 0, sizeof(hex)); - data2hex(data, size, hex); - if (size > 50) { - hex[50 * 2 - 1] = '.'; - hex[50 * 2 - 2] = '.'; - } - str = hex; - } - return confirm(button_request, title, "%s", str); + return confirm_bytes(button_request, title, data, size); } diff --git a/lib/firmware/app_layout.c b/lib/firmware/app_layout.c index 25b0db18b..82bff62b3 100644 --- a/lib/firmware/app_layout.c +++ b/lib/firmware/app_layout.c @@ -597,6 +597,81 @@ void layout_nano_address_notification(const char* desc, const char* address, layout_notification_icon(type, &sp); } +#if ZCASH_PRIVACY +/* + * layout_zcash_address_notification() - Display zcash unified address QR + * with title; the second confirm step in the view-on-device flow. + * + * INPUT + * - desc: title text (e.g. "Zcash #0 Orchard") + * - address: zcash unified address (rendered as QR only — full text is + * shown on the preceding confirm step) + * - type: notification type + * OUTPUT + * none + */ +void layout_zcash_address_notification(const char* desc, const char* address, + NotificationType type) { + DrawableParams sp; + Canvas* canvas = layout_get_canvas(); + + call_leaving_handler(); + layout_clear(); + + if (strcmp(desc, "") != 0) { + const Font* title_font = get_title_font(); + sp.y = TOP_MARGIN_FOR_TWO_LINES; + sp.x = LEFT_MARGIN + 65; + sp.color = BODY_COLOR; + draw_string(canvas, title_font, desc, &sp, TRANSACTION_WIDTH - 2, + font_height(title_font) + BODY_FONT_LINE_PADDING); + } + + layout_address(address, QR_LARGE); + layout_notification_icon(type, &sp); +} + +/* + * layout_zcash_address_text_notification() - Display full zcash unified + * address text with title; the first confirm step in the view-on-device flow. + * + * INPUT + * - desc: title text (e.g. "Zcash #0 Orchard") + * - address: zcash unified address to display as text (3 lines) + * - type: notification type + * OUTPUT + * none + */ +void layout_zcash_address_text_notification(const char* desc, + const char* address, + NotificationType type) { + DrawableParams sp; + Canvas* canvas = layout_get_canvas(); + const Font* address_font = get_body_font(); + + call_leaving_handler(); + layout_clear(); + + if (strcmp(desc, "") != 0) { + const Font* title_font = get_title_font(); + sp.y = TOP_MARGIN_FOR_THREE_LINES; + sp.x = LEFT_MARGIN; + sp.color = BODY_COLOR; + draw_string(canvas, title_font, desc, &sp, TRANSACTION_WIDTH - 2, + font_height(title_font) + BODY_FONT_LINE_PADDING); + } + + /* Full UA below the title; -25 leaves the right column for confirm icons. */ + sp.y = TOP_MARGIN_FOR_THREE_LINES + ADDRESS_XPUB_TOP_MARGIN; + sp.x = LEFT_MARGIN; + sp.color = BODY_COLOR; + draw_string(canvas, address_font, address, &sp, TRANSACTION_WIDTH - 25, + font_height(address_font) + BODY_FONT_LINE_PADDING); + + layout_notification_icon(type, &sp); +} +#endif // ZCASH_PRIVACY + /* * layout_address_notification() - Display address notification * @@ -628,8 +703,35 @@ void layout_address_notification(const char* desc, const char* address, sp.y += font_height(address_font) + ADDRESS_TOP_MARGIN; sp.x = LEFT_MARGIN; sp.color = BODY_COLOR; + + /* Bech32 addresses longer than one line (p2wsh and p2tr are both 62 chars) + did not fit: draw_string() stops at the bottom of the canvas and drops the + remainder SILENTLY, so the user verified a prefix while the QR beside it + encoded the whole address. + Close the padding between lines rather than moving the block up -- the QR + is drawn last and would overwrite the start of a raised first line. */ + uint16_t address_line_height = + font_height(address_font) + BODY_FONT_LINE_PADDING; + { + const uint32_t lines = + calc_str_line(address_font, address, TRANSACTION_WIDTH); + if (lines > ONE_LINE) { + /* Close the inter-line padding first: raising the block is what collides + with the QR, which is drawn afterwards and would overwrite the start of + the first line. */ + address_line_height = font_height(address_font); + const uint16_t bottom = + sp.y + (lines - 1) * address_line_height + font_height(address_font); + if (bottom > KEEPKEY_DISPLAY_HEIGHT) { + /* Still short: raise by the minimum that fits, no more. */ + const uint16_t overflow = bottom - KEEPKEY_DISPLAY_HEIGHT; + sp.y = (sp.y > overflow) ? sp.y - overflow : 0; + } + } + } + draw_string(canvas, address_font, address, &sp, TRANSACTION_WIDTH, - font_height(address_font) + BODY_FONT_LINE_PADDING); + address_line_height); /* Draw description */ if (strcmp(desc, "") != 0) { @@ -693,7 +795,8 @@ void layout_pin(const char* str, char pin[]) { * OUTPUT * none */ -void layout_cipher(const char* current_word, const char* cipher) { +void layout_cipher(const char* current_word, const char* cipher, + const char* prev_word_info) { DrawableParams sp; const Font* title_font = get_body_font(); Canvas* canvas = layout_get_canvas(); @@ -701,8 +804,18 @@ void layout_cipher(const char* current_word, const char* cipher) { call_leaving_handler(); layout_clear(); - /* Draw prompt */ - sp.y = 11; + /* Draw previous word info at top-left -- must be x < 76 to avoid + * being wiped by cipher animation which clears x >= CIPHER_START_X */ + if (prev_word_info && prev_word_info[0]) { + sp.y = 2; + sp.x = 4; + sp.color = CIPHER_FONT_COLOR; /* gray -- less prominent than current word */ + draw_string(canvas, title_font, prev_word_info, &sp, 68, + font_height(title_font)); + } + + /* Draw prompt -- push down when prev word is shown */ + sp.y = (prev_word_info && prev_word_info[0]) ? 14 : 11; sp.x = 4; sp.color = BODY_COLOR; draw_string(canvas, title_font, "Recovery Cipher:", &sp, 58, diff --git a/scripts/emulator/capture-thor-percent.py b/scripts/emulator/capture-thor-percent.py new file mode 100644 index 000000000..bac5d6b56 --- /dev/null +++ b/scripts/emulator/capture-thor-percent.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +"""Capture the THOR/Maya LP-withdraw percent confirm screens from kkemu. + +Evidence tool for the integer-percent rendering change (no float formats). +""" + +import os +import sys +import time +from pathlib import Path + +os.environ.setdefault("PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION", "python") +os.environ.setdefault("TEMPORARILY_DISABLE_PROTOBUF_VERSION_CHECK", "true") + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT / "deps" / "python-keepkey")) +sys.path.insert(0, str(ROOT / "scripts" / "zoo")) + +from keepkeylib.client import KeepKeyDebuglinkClient +from keepkeylib.transport_udp import UDPTransport +from keepkeylib import messages_pb2 as proto +from keepkeylib.tools import parse_path + + +def dump_layout(debug_client, filename): + """Save the raw 2048-byte 1bpp OLED layout; converted to PNG on the host.""" + state = debug_client._call(proto.DebugLinkGetState()) + if not state.layout: + return False + with open(filename, "wb") as f: + f.write(state.layout) + return True + +OUT = Path(sys.argv[1]).resolve() +OUT.mkdir(parents=True, exist_ok=True) + +main_ep = os.environ.get("KK_TRANSPORT_MAIN", "127.0.0.1:11044") +debug_ep = os.environ.get("KK_TRANSPORT_DEBUG", "127.0.0.1:11045") + +client = KeepKeyDebuglinkClient(UDPTransport(main_ep)) +client.set_debuglink(UDPTransport(debug_ep)) + +client.auto_button = True +client.wipe_device() +client.load_device_by_mnemonic( + mnemonic=("all " * 11 + "all").strip(), + pin="", + passphrase_protection=False, + label="percent evidence", + language="english", +) + +counter = {"n": 0} +real_press_yes = client.debug.press_yes + + +def capturing_press_yes(): + counter["n"] += 1 + time.sleep(0.2) + path = OUT / ("thor-withdraw-%02d.layout" % counter["n"]) + dump_layout(client.debug, str(path)) + print(path) + real_press_yes() + + +client.debug.press_yes = capturing_press_yes + + +THOR_ROUTER = "d37bbe5744d730a1d98d8dc97c42f0ca46ad7146" + + +def _build_deposit_calldata(memo): + selector = bytes.fromhex("1fece7b4") + vault = bytes(12) + bytes.fromhex(THOR_ROUTER) + asset = bytes(32) + amount = (500000000000000000).to_bytes(32, "big") + memo_offset = (4 * 32).to_bytes(32, "big") + memo_bytes = memo.encode("ascii") + memo_len = len(memo_bytes).to_bytes(32, "big") + pad = ((len(memo_bytes) + 31) // 32) * 32 + return selector + vault + asset + amount + memo_offset + memo_len + \ + memo_bytes + bytes(pad - len(memo_bytes)) + + +from binascii import unhexlify + +client.ethereum_sign_tx( + n=parse_path("m/44'/60'/0'/0/0"), + nonce=1, + gas_price=50000000000, + gas_limit=300000, + to=unhexlify(THOR_ROUTER), + value=500000000000000000, + chain_id=1, + data=_build_deposit_calldata( + "WITHDRAW:ETH.USDT-0xdac17f958d2ee523a2206206994597c13d831ec7:2505" + ), +) +print("captured %d screens" % counter["n"]) diff --git a/tools/firmware/keepkey.ld b/tools/firmware/keepkey.ld index 964b43676..9538bc4cd 100644 --- a/tools/firmware/keepkey.ld +++ b/tools/firmware/keepkey.ld @@ -71,3 +71,13 @@ _buttonusr_isr = _comram_end - 4; _data_size = SIZEOF(.data); _codelen = SIZEOF(.text) + SIZEOF(.data) + SIZEOF(.ARM.exidx) + SIZEOF(.version); + +/* Runtime SRAM gate: everything between the end of static allocation (.bss) + * and the top-of-RAM stack is the ONLY memory the running firmware has for + * call frames. RC7's privacy-enabled build shipped with an 11.2 KB gap while + * msg_write() put a 12.4 KB frame on the stack — a guaranteed boot-path + * overwrite of static memory. Never again: require a 16 KiB reserve at link + * time, for every variant. (Initial limit — replace with measured worst-case + * high-water + margin once the -fstack-usage CI reporting has data.) */ +ASSERT((_stack - _ebss) >= 0x4000, + "Insufficient runtime SRAM: require 16 KiB stack/heap reserve between _ebss and _stack"); diff --git a/unittests/firmware/CMakeLists.txt b/unittests/firmware/CMakeLists.txt index 5ef9e8489..6c7262aeb 100644 --- a/unittests/firmware/CMakeLists.txt +++ b/unittests/firmware/CMakeLists.txt @@ -1,4 +1,5 @@ set(sources + app_confirm.cpp coins.cpp cosmos.cpp eos.cpp diff --git a/unittests/firmware/app_confirm.cpp b/unittests/firmware/app_confirm.cpp new file mode 100644 index 000000000..fdec4316d --- /dev/null +++ b/unittests/firmware/app_confirm.cpp @@ -0,0 +1,42 @@ +extern "C" { +#include "keepkey/firmware/app_confirm.h" +} + +#include "gtest/gtest.h" + +TEST(AppConfirm, MultilineAsciiMessageUsesTextMode) { + static const char message[] = + "Welcome to DegenQuest!\n" + "\n" + "Sign this message to authenticate your wallet.\n" + "\n" + "This request will not trigger a blockchain transaction or cost any gas " + "fees.\n" + "\n" + "Nonce: d2d8d32b-a7fc-4129-a60b-e0664f0b2169"; + + ASSERT_EQ(193U, sizeof(message) - 1); + EXPECT_TRUE(confirm_bytes_is_text(reinterpret_cast(message), + sizeof(message) - 1)); +} + +TEST(AppConfirm, UnsafeControlsAndBinaryBytesUseHexMode) { + static const uint8_t spaces[] = {' ', ' ', ' '}; + static const uint8_t blank_lines[] = {'\n', '\n'}; + static const uint8_t nul[] = {'a', 0x00, 'b'}; + static const uint8_t tab[] = {'a', '\t', 'b'}; + static const uint8_t carriage_return[] = {'a', '\r', 'b'}; + static const uint8_t escape[] = {'a', 0x1b, 'b'}; + static const uint8_t del[] = {'a', 0x7f, 'b'}; + static const uint8_t utf8[] = {0xc3, 0xa9}; + + EXPECT_FALSE(confirm_bytes_is_text(spaces, sizeof(spaces))); + EXPECT_FALSE(confirm_bytes_is_text(blank_lines, sizeof(blank_lines))); + EXPECT_FALSE(confirm_bytes_is_text(nul, sizeof(nul))); + EXPECT_FALSE(confirm_bytes_is_text(tab, sizeof(tab))); + EXPECT_FALSE(confirm_bytes_is_text(carriage_return, sizeof(carriage_return))); + EXPECT_FALSE(confirm_bytes_is_text(escape, sizeof(escape))); + EXPECT_FALSE(confirm_bytes_is_text(del, sizeof(del))); + EXPECT_FALSE(confirm_bytes_is_text(utf8, sizeof(utf8))); + EXPECT_FALSE(confirm_bytes_is_text(nullptr, 1)); +}