Skip to content

fix: name the whole share set on the label the user copies down - #155

Merged
aido merged 1 commit into
aido:bip85from
buzzromain:feat/numbered-sskr-words-bip85
Aug 5, 2026
Merged

aido merged 1 commit into
aido:bip85from
buzzromain:feat/numbered-sskr-words-bip85

Conversation

@buzzromain

Copy link
Copy Markdown

Why

Each generated SSKR share is headed by a label the user copies onto the sheet along with the words. It read:

SSKR Share #2

That says which sheet this is. It never says how many the set has.

On a threshold scheme that count is not decoration. Whoever opens the box later — the owner years on, or an heir — cannot tell a complete backup from a partial one, cannot know how many more sheets to look for, and cannot tell whether enough of them survive to rebuild the secret at all. A 2-of-3 set missing one sheet is still recoverable; a 2-of-3 set missing two is not, and the sheets themselves never said which situation you are in. The application knows the number at the moment it prints the label, and was dropping it.

What changed

Before After
Touch (NBGL) SSKR Share #2 SSKR share 2 of 3
Nano (BAGL) SSKR Share #2 SSKR 2/3

Two forms rather than one shared macro, because they are not free to be the same length: BAGL's bnnn_paging appends its own (page/total) counter to this title, and the long form plus that suffix overruns the Nano title line. That is not a guess — it was seen truncated as SSKR Share... of 3 (4/5) under Speculos while trying exactly that.

SSKR stays in the label. A sheet carrying dozens of four-letter words and no name for its own format cannot be recovered by anyone who no longer has this application: there is nothing to search for, so neither this app nor Gordian SeedTool nor seedtool-cli can be found from the paper alone. That this application can itself rebuild a BIP39 phrase from SSKR shares does not cover that case — the case is precisely not having it.

Nothing else about the screen changes: same layout, same paging, same words, same navigation.

A hand-written termination goes with it

The touch caller followed its SPRINTF with:

headerText[sskr_sharecount_get() > 9 ? sizeof(headerText) - 1 : sizeof(headerText) - 2] = '\0';

It wrote a NUL at a fixed offset near the end of a 50-byte buffer, on the theory that the index might need one digit or two. SPRINTF is snprintf bounded by sizeof (os_print.h) and had already terminated the string, so this never truncated anything — and its condition tested the digit count of a total that was not in the string. Keeping it alongside the new format would have kept a line making a false claim.

The new unit check, and why it is not in the existing table

A bnnn_paging title does not wrap. It clips, silently. That is how the overlong label above reached a screenshot instead of a failing test.

tests/unit/tests/ui_strings.c already measures strings against the pixel budget of the fixed Nano layouts, but it excludes bnnn_paging — correctly for the body, which wraps and so cannot clip, and by oversight for the title, which can. This adds a check for the share label:

  • it builds the widest title the device can actually produce, from SSS_MAX_SHARE_COUNT and the page count a 46-ByteWord share can reach on the one-line 128×32 screen;
  • it measures it in the bold font a paging title is drawn in, not the regular one;
  • 108px against the 114px a Nano line holds.

It is deliberately not an entry in the bounded table above it. That table measures a macro on its own, and what reaches the screen here is the macro plus a counter the widget appends and the string knows nothing about — measuring the macro alone reports 68px and calls it comfortable while the drawn title is 108px. Expanding both fields to 99 as that table's helper does gives 116px and would fail a layout that works, since 99 shares cannot be generated.

The check was run in both states: it passes on this label, and rejects the overlong one with the right diagnostic (157px, over the 114px a Nano line holds).

Verification

  • Six unit-test-matrix configurations (ASan, UBSan, -O2, -Os, -fsigned-char, -funsigned-char): 80/80 each.
  • Six targets built, nanos included by hand: text/data/bss byte-identical to before on all six. The only difference in any binary is the two label strings themselves, confirmed by diffing sorted strings output.
  • Functional tests under Speculos, both stacks: stax 12 passed / 6 skipped, nanox 9 passed / 9 skipped — unchanged from before this change. test_sskr_generate_two_button reads the labels back off an emulated device, so the new Nano format is exercised end to end, not only asserted.
  • clang-format --dry-run -Werror clean on the touched src/ files.

What is not covered

  • The Nano S is not run. Speculos no longer emulates it (--model offers nanox, nanosp, stax, flex, apex_p), so the 128×32 device is covered only by compiling and by the pixel check above — which is written to bound that device in particular, since it is the one whose single text line per page pushes the page counter highest.
  • Not covered by this repository's CI on a fork PR: functional-test workflows do not run on pull requests from forks (read-only workflow permissions there), so the Speculos runs above are local and are the only evidence for this PR.

Each generated SSKR share is headed by a label the user writes onto the
sheet along with the words. It read "SSKR Share aido#2", which says which sheet
this is but never how many the set has.

That count is not decoration on a threshold scheme. Whoever opens the box
later -- the owner years on, or an heir -- cannot tell a complete backup
from a partial one, nor know how many more sheets to look for, nor whether
enough of them survive to rebuild the secret at all. The application knows
the number at the moment it prints the label and was dropping it.

The label now reads "SSKR share 2 of 3" on the touch devices and
"SSKR 2/3" on the Nano ones. Two forms rather than one because BAGL's
bnnn_paging appends its own "(page/total)" counter to this title, and the
long form plus that suffix overruns the Nano title line -- seen truncated
as "SSKR Share... of 3 (4/5)" under Speculos while trying exactly that.

"SSKR" stays in the label. A sheet carrying dozens of four-letter words and
no name for its own format cannot be recovered by anyone who no longer has
this application: there is nothing to search for, so neither this app nor
Gordian SeedTool nor seedtool-cli can be found from the paper alone. That
this application can itself rebuild a BIP39 phrase from SSKR shares does not
cover that case -- the case is precisely not having it.

A hand-written termination in the touch caller goes with it: it wrote a NUL
at a fixed offset near the end of a 50-byte buffer, on the theory that the
index might need one digit or two, when SPRINTF is snprintf bounded by
sizeof and had already terminated the string.

The new unit check is the one this change turned out to need. A
bnnn_paging title does not wrap, it clips, silently -- which is how the
overlong form above reached a screenshot rather than a test. The check
builds the widest title the device can actually produce, from
SSS_MAX_SHARE_COUNT and the page count a 46-ByteWord share can reach, and
measures it in the bold font the title is drawn in: 108px against the 114px
a Nano line holds. It is deliberately not an entry in the existing bounded
table, which measures a macro alone: what reaches the screen here is the
macro plus a counter the widget appends and the string knows nothing about,
and expanding both fields to "99" as that table does would fail a layout
that works, since 99 shares cannot be generated.

Verified: six unit-test-matrix configurations 80/80 each; six targets
built with text/data/bss byte-identical to before, the only binary
difference being the two label strings themselves; functional tests under
Speculos unchanged on both stacks, stax 12 passed/6 skipped and nanox
9 passed/9 skipped; clang-format clean. The new check was run in both
states -- it passes on this label and rejects the overlong one with the
right diagnostic.
@aido
aido merged commit d8c1b60 into aido:bip85 Aug 5, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants