From 261199fb9c348d03ebd00bdf64d98bb2826269a3 Mon Sep 17 00:00:00 2001 From: highlander Date: Thu, 6 Aug 2026 12:02:11 -0300 Subject: [PATCH] fix(board): show whole addresses, and drop the float printf engine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Display: - Long bech32/bech32m addresses were cut at the first 42 characters on the verify screen, so the QR code and the text disagreed about where funds were going. Addresses now wrap across the full screen, and Zcash unified addresses get a dedicated text-first confirmation layout. - Font, draw and layout helpers gained the measurement and wrapping support those screens need. ROM: - Device snprintf/vsnprintf are redirected to newlib's integer-only engine (see cmake/caches/device.cmake), which removes the float formatting path, soft-double libgcc and its malloc dependency from the image. Percent and amount rendering that used %f is reworked to integer formatting, so THORChain withdraw percentages and send amounts still display exactly as before — Gate-3 frames under docs/security/evidence/rom-printf-integer-percent. - keepkey.ld asserts the 16 KiB stack reserve the SRAM budget gate measures. --- .../01-thorchain-withdraw-25.05pct.png | Bin 0 -> 932 bytes .../02-thorchain-sending-eth.png | Bin 0 -> 470 bytes .../rom-printf-integer-percent/README.md | 17 ++ include/keepkey/board/bsd_compat.h | 28 +++ include/keepkey/board/confirm_sm.h | 4 + include/keepkey/board/draw.h | 25 +++ include/keepkey/board/font.h | 5 + include/keepkey/board/layout.h | 15 ++ include/keepkey/board/util.h | 5 +- include/keepkey/firmware/app_confirm.h | 14 ++ include/keepkey/firmware/app_layout.h | 10 +- lib/board/confirm_sm.c | 51 +++-- lib/board/draw.c | 85 ++++++++- lib/board/font.c | 44 +++-- lib/board/layout.c | 149 ++++++++++++++- lib/board/timer.c | 30 ++- lib/board/usb.c | 49 +++-- lib/board/util.c | 61 +++--- lib/firmware/app_confirm.c | 176 ++++++++++++++++-- lib/firmware/app_layout.c | 121 +++++++++++- scripts/emulator/capture-thor-percent.py | 99 ++++++++++ tools/firmware/keepkey.ld | 10 + unittests/firmware/CMakeLists.txt | 1 + unittests/firmware/app_confirm.cpp | 42 +++++ 24 files changed, 943 insertions(+), 98 deletions(-) create mode 100644 docs/security/evidence/rom-printf-integer-percent/01-thorchain-withdraw-25.05pct.png create mode 100644 docs/security/evidence/rom-printf-integer-percent/02-thorchain-sending-eth.png create mode 100644 docs/security/evidence/rom-printf-integer-percent/README.md create mode 100644 include/keepkey/board/bsd_compat.h create mode 100644 scripts/emulator/capture-thor-percent.py create mode 100644 unittests/firmware/app_confirm.cpp 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 0000000000000000000000000000000000000000..22d9f0735002a0fa0d1078df922ea3078088f408 GIT binary patch literal 932 zcmV;V16%xwP)X3m} z@msfi?W)pCvCXR%3;t=BE}#Q@O`@rbka1K-3(?ot$Isf$HFIq1hrUEWtJz~O6M$x` zp0e$m2uN)f4Btoq-X#J!a+v@OK zaB%)#BB1c6c4_Yt_y7X(eI`I?sw7UF%_b4xCYxaI`zybpkUMOt8NI+TmKwROR zCY;~LM`IH3bOI6s@Al_@JbnDkz`K2U51u~$iHQ-4Me&<~t|D{vslYudr`CI z*6O@w@$y)9cC?d#rBPANifTL_==M4S1SZG!AmXQ7zKv=hIIEH_t!6`bXiPwlEfm7- zjumNl6a+3Hpihnmxb~9EC`=uy^sOBez>XWOs?f7rq6t<(nL^+^j0q6gPrxFu1OsVc zFRJy3;`m_J^JC)@0+g<=65zKb0qW-2R3?DLy7{XJAf~$~1p$7fAp*m(X_r28aLCgi z7GWtnW#9sMR34CAa#S!eF)=YQ@nrPZIiz6pFG7@tlK*ePuXzPM$bCUCz3IIk-~B>< z$>{3&NbunZ0h9@b6jpo` zKs+rIpOx*wll%1&h_t|LN5UmpqL^@c0*T40$!81bBlLxF#=h<+AgrlK4@3aEbcg^! zxkx}Qxwh`<8kWx?0*a?Cs)kUGS9YU%VuwGt!}C8JECqSA$u;#`px2x4dr6p>n3$ND zcqaO*SRijzJIE!;_4lfd?(rQT+CBjxuf&?{>Rtk>`lA`zUEo|Q5|FkSOIz5<7uz5T zEB`&4fSgnItThZ!W5N&ZS<^B~%KRzkuCwwUNI4@lCQl^*{EHqV@!}B;duM9Tnm&&j zO|28qG-B&T5g=m%c!1I#Y8qO%2uMSSU5QBPz^w%E{!H4r0ekI(PX~1AjKQN;CE0E| zcurud!wBfsZjFF)322Uhuf`eAfUibXw*V6-BH5K;vwPmkmofZ7TGwG{wrE71F2 zP5}y_Zr2u|yDS6+a8B11AT1Pus{jQkEorVLk;$ z0bU*f-J=h#)_DgUy!ZR)$X0>(3P1=Ugb+fClAjRV-o>V-d!LpPx`gu6V4huaGMcXj zmlIh919>41+Zs-DS)+!>dhJfxF>*BUq*n2(EI|3&R@9@AkO!_0<|MKRfTihc@bwhHni0@lS9%8wtAHKwz|~{bRe=18A31LCJM5YkMF0Q* M07*qoM6N<$f^@LY;s5{u literal 0 HcmV?d00001 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)); +}