feat: give the touch stack three distinct verdict screens - #153
Merged
Merged
Conversation
NBGL's check-result screen shared one title, "Valid Secret\nRecovery
Phrase", between "the phrase is well formed but doesn't match this
device's seed" and "the phrase matches" -- only the paragraph
underneath said which. The two-button screens already had this right:
ux_bip39_nomatch_flow and ux_bip39_match_flow (src/bagl/ux_nano.c)
carry two different titles, "doesn't match" and "is correct", on two
distinct flows. NBGL's invalid-phrase screen also gave no advice,
where the two-button invalid flows share a line telling the user to
check length, order and spelling.
Both are fixed the same way: two new macros in ui_strings.h
(UI_STR_NBGL_RESULT_NOMATCH_TITLE, UI_STR_NBGL_RESULT_INVALID_ADVICE),
and display_check_result_page() (src/nbgl/ui.c) now indexes a
three-title array by the same 0/1/2 outcome it already computed for
the verdict icon, instead of pulling a title from the two-row body-text
table. The invalid case's advice goes into a third text field
(nbgl_pageInfoDescription_t.centeredInfo.text3), which the SDK only
renders as its own line under LARGE_CASE_GRAY_INFO -- the style this
screen used before, LARGE_CASE_INFO, maps text2 and text3 onto the
same underlying field, so text3 would have silently replaced the
verdict's own body text rather than appending to it
(nbgl_layoutAddCenteredInfo() in the SDK). Confirmed under Speculos on
stax before adopting the fix: the body text disappeared, replaced by
the advice line, the moment text3 was set under the old style.
The new nomatch title, "Mismatched Secret\nRecovery Phrase", keeps the
exact "<Adjective> Secret\nRecovery Phrase" template the other two
outcomes already use ("Invalid Secret", "Valid Secret") -- including
the same line break, after "Secret" rather than after "Recovery". The
three screens read as one answer set with a single word changing,
rather than three differently-shaped messages, which matters most on
the screen the project's own planning doc calls the one where a fault
injection can flip the result. Confirmed by rendering all three
screens under Speculos on stax and reading them side by side, not by
inspecting the string alone.
No two-button file changes: those flows already worked this way.
Confirmed on all six targets, built before and after with strings
extracted and diffed: `nanos`, `nanox` and `nanos+` are byte-for-byte
identical; the touch targets' diffs contain exactly the two new
strings and the new title array's symbol, nothing else.
tests/functional/test_bip39_seed_match.py extends from two verdicts to
three (its own docstring predicted the need), and its touch-side
helper now asserts the title too, not just the body line -- the
touch/two-button split this file already draws is what made checking
"the two stacks answer the same question the same way" for the new
invalid case a small addition rather than a new file. The invalid
case's body-text assertion had to be a full-line prefix rather than a
free substring: ragger's wait_for_text_on_screen() matches per
rendered line with re.match, which anchors at that line's start, and
"you have entered is not valid" (this string's second line) has no
internal break before "is not valid" the way the match/nomatch
strings do before their own verdict word.
Verified: all six unit-test-matrix configurations (80/80, the two new
ui_strings.h macros added to tests/unit/tests/ui_strings.c's own
enumeration); functional suite on both stacks under Speculos (nanox
9 passed/9 skipped, stax 12 passed/6 skipped, all skips pre-existing
device gating); clang-format clean on touched src/ files.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
NBGL's check-result screen shared one title,
"Valid Secret\nRecovery Phrase", between two different outcomes: a phrase that is well formed but does not match this device's seed, and a phrase that matches. Only the paragraph underneath said which. The two-button screens already made this distinction:ux_bip39_nomatch_flowandux_bip39_match_flow(src/bagl/ux_nano.c) carry two different titles,"doesn't match"and"is correct", on two separate flows. NBGL's invalid-phrase screen also gave no advice, where the two-button invalid flows share a line telling the user to check length, order and spelling.This is the single most security-sensitive screen in the application — the one screen where a fault injection has been shown to be able to flip the displayed result. An ambiguous verdict on it is the most expensive kind of interface bug this project has.
What changed
Two additions to
src/common/ui_strings.h:UI_STR_NBGL_RESULT_NOMATCH_TITLEandUI_STR_NBGL_RESULT_INVALID_ADVICE.display_check_result_page()(src/nbgl/ui.c) now indexes a three-title array by the same 0/1/2 outcome it already computed for the verdict icon (previouslyicons[result + seed_match]; that expression is unchanged, just factored into a namedoutcomevariable also used for the title), instead of pulling a shared title from the two-row body-text table. The invalid case's advice line goes intonbgl_pageInfoDescription_t.centeredInfo.text3.No two-button file changed — those flows already worked this way.
An SDK pitfall found before it shipped, not after
text3only renders as its own line under theLARGE_CASE_GRAY_INFOstyle. UnderLARGE_CASE_INFO— what this screen used before —nbgl_layoutAddCenteredInfo()(SDK) maps bothtext2andtext3onto the same underlyingcenteredInfo.descriptionfield, so settingtext3would have silently replaced the verdict's own body text rather than adding a line under it. Confirmed by reading the SDK source, then reproduced under Speculos onstaxbefore applying the fix: the body text disappeared, replaced by the advice line, the momenttext3was set under the old style. The style isLARGE_CASE_GRAY_INFOin this PR.Wording
The new nomatch title is
"Mismatched Secret\nRecovery Phrase"— chosen to keep the exact"<Adjective> Secret\nRecovery Phrase"template the other two outcomes already use ("Invalid Secret","Valid Secret"), including the same line break, after"Secret"rather than after"Recovery". An earlier draft,"Recovery Phrase\nDoesn't Match", was correct in meaning but broke that symmetry — caught by rendering all three screens under Speculos and comparing them side by side, not by re-reading the string in isolation:Invalid Secret/Recovery PhraseMismatched Secret/Recovery PhraseValid Secret/Recovery PhraseTwo limits worth stating plainly rather than glossing over:
"Invalid"and"Mismatched"currently lead to the exact same next screen (check_result_callback(), unchanged by this PR) — the title tells the user why the phrase was rejected, but the app doesn't yet act differently on the two reasons. Out of scope here; noted for a later step.Proof that BAGL is untouched
All six targets were built before and after this change (base = the tip of the
ui_strings.hextraction this PR builds on, notbip85, to isolate this PR's own diff), with strings extracted from every.elfand diffed:nanos,nanox,nanos+: byte-for-byte identical —text/data/bssand sortedstringsoutput, no difference at all.stax,flex,apex_p: diffs contain exactly the two new strings and the newtitlesarray's debug symbol, nothing else.Tests
tests/functional/test_bip39_seed_match.pyextends from two verdict tests to three (its own docstring predicted this need), and its touch-side helper now asserts the title as well as the body line. One assertion needed a fix along the way:ragger'swait_for_text_on_screen()matches withre.matchagainst each rendered line individually, anchored at that line's start — not a substring search over the whole screen."not valid"failed because it isn't a line-start in"you have entered is not valid"; fixed to assert the full line. Found by querying Speculos's screen-content API directly rather than guessing.tests/unit/tests/ui_strings.c's macro enumeration gets the two new macros (it doesn't track the header automatically — a renamed/removed macro is a compile error there, but a newly added one needs a matching line by hand, which this PR includes).Verification
-O2,-Os,-fsigned-char,-funsigned-char): 80/80 each.nanox9 passed / 9 skipped (was 8/9 before this PR's new test);stax12 passed / 6 skipped (was 11/6). All skips are pre-existing device gating, unrelated to this change.clang-format --dry-run -Werrorclean on touchedsrc/files.