@@ -38,6 +38,80 @@ static const char mnemonic24[]
3838static const char bad_checksum_mnemonic []
3939 = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon" ;
4040
41+ /* Eleven flips name one word, most significant bit first, and the words they
42+ * name are handed to seedtool_complete_checksum - the same call the typed-word
43+ * flow makes. The screen that shows the reader "these eleven flips are word
44+ * 0819" is only telling the truth if that index survives into the finished
45+ * mnemonic unchanged, so that is what is checked, for every one of the 2048
46+ * words rather than for a chosen few.
47+ *
48+ * The all-zero case is pinned to the published vectors the rest of this file
49+ * already uses: eleven words of index 0 plus seven zero bits are 128 zero
50+ * entropy bits, and twenty-three plus three zero bits are 256 - `mnemonic` and
51+ * `mnemonic24`. That is the one case where the whole chain, checksum included,
52+ * is fixed by something outside this project. */
53+ static bool coin_words_build_the_entropy (const size_t words , const uint16_t index , char * out , const size_t out_len )
54+ {
55+ const size_t word_count = words == 12 ? 11 : 23 ;
56+ const size_t tail_count = words == 12 ? 7 : 3 ;
57+ char prefix [SEEDTOOL_MAX_MNEMONIC_LEN + 1 ] = { 0 };
58+ uint8_t tail [7 ] = { 0 };
59+ size_t used = 0 ;
60+ for (size_t i = 0 ; i < word_count ; ++ i ) {
61+ const char * const word = seedtool_word (index );
62+ const size_t len = strlen (word );
63+ if (used + len + (i ? 1 : 0 ) >= sizeof (prefix )) {
64+ return false;
65+ }
66+ if (i ) {
67+ prefix [used ++ ] = ' ' ;
68+ }
69+ memcpy (prefix + used , word , len );
70+ used += len ;
71+ prefix [used ] = '\0' ;
72+ }
73+ return seedtool_complete_checksum (prefix , tail , tail_count , out , out_len ) == SEEDTOOL_OK ;
74+ }
75+
76+ static bool coin_word_encoding_is_sound (void )
77+ {
78+ char completed [SEEDTOOL_MAX_MNEMONIC_LEN + 1 ];
79+
80+ /* Zero flips, both lengths, against the published vectors. */
81+ if (!coin_words_build_the_entropy (12 , 0 , completed , sizeof (completed )) || strcmp (completed , mnemonic ) != 0 ) {
82+ return false;
83+ }
84+ if (!coin_words_build_the_entropy (24 , 0 , completed , sizeof (completed )) || strcmp (completed , mnemonic24 ) != 0 ) {
85+ return false;
86+ }
87+
88+ /* Every word survives the trip, at both lengths: whatever the reader was
89+ * shown on the confirmation screen is what the finished mnemonic spells. */
90+ for (uint16_t index = 0 ; index < SEEDTOOL_WORDLIST_LEN ; ++ index ) {
91+ const size_t lengths [] = { 12 , 24 };
92+ for (size_t l = 0 ; l < 2 ; ++ l ) {
93+ const size_t word_count = lengths [l ] == 12 ? 11 : 23 ;
94+ uint16_t numbers [24 ];
95+ size_t count = 0 ;
96+ if (!coin_words_build_the_entropy (lengths [l ], index , completed , sizeof (completed ))
97+ || seedtool_mnemonic_word_numbers (completed , numbers , 24 , & count ) != SEEDTOOL_OK
98+ || count != lengths [l ]) {
99+ return false;
100+ }
101+ for (size_t i = 0 ; i < word_count ; ++ i ) {
102+ if (numbers [i ] != index + 1 ) {
103+ return false;
104+ }
105+ }
106+ }
107+ }
108+
109+ /* An eleven-bit index is the whole wordlist and nothing past it: the top
110+ * index must be reachable, and the encoding must not overflow into a
111+ * twelfth bit. */
112+ return SEEDTOOL_WORDLIST_LEN == (1u << 11 );
113+ }
114+
41115/* `count` words, all "abandon" when `spread` is zero and a spread of the
42116 * wordlist otherwise.
43117 *
@@ -433,6 +507,7 @@ static bool word_numbers_round_trip_is_sound(void)
433507static const char * const menu_labels [] = { "Master fingerprint" , "Native SegWit (BIP84)" , "Taproot (BIP86)" ,
434508 "Account key" , "Addresses" , "Account key format" , "xpub" , "zpub" , "Erase and restart" , "New Seed" , "From entropy" ,
435509 "Restore Seed" , "Complete checksum" , "About" , "Settings" , "11 words + 7 coins" , "23 words + 3 coins" ,
510+ "Coin method" , "Flip each word" , "Flip and hash" ,
436511 "No passphrase" , "Enter passphrase" , "D6 dice" , "D20 dice" , "Coin flips" , "Cards" , "Back" , "12 words" ,
437512 "24 words" , "[delete]" , "[back]" , "Type the letters" , "Enter word numbers" , "Plain text" , "Backup" ,
438513 "Stackbit 1248" , "Compact SeedQR" , "Simple grid" , "Physical layout" };
@@ -448,6 +523,37 @@ static bool labels_fit_a_row(void)
448523 return true;
449524}
450525
526+ /* The coin-word screens are not choice lists, but they are stacked text at a
527+ * fixed y: draw_centered wraps a line too wide for the display rather than
528+ * cutting it, and the wrapped remainder would land on top of the line below.
529+ * The word's number and the word itself are on one of those lines, so the
530+ * check runs over all 2048 rather than over a chosen few.
531+ *
532+ * seedtool_render_fit_row measures against the choice list's text column,
533+ * which is narrower than the full width these screens centre in - a
534+ * conservative bound, and the one measurement this file can reach. */
535+ static bool coin_word_screens_fit (void )
536+ {
537+ static const char * const fixed [] = { "Word 11 of 23" , "Final bits" , "Flip word 11 again?" ,
538+ "These flips are lost" , "11111111111" , "Heads (up) Tails (down)" };
539+ for (size_t i = 0 ; i < sizeof (fixed ) / sizeof (fixed [0 ]); ++ i ) {
540+ if (seedtool_render_fit_row (fixed [i ]) != strlen (fixed [i ])) {
541+ return false;
542+ }
543+ }
544+ for (uint16_t index = 0 ; index < SEEDTOOL_WORDLIST_LEN ; ++ index ) {
545+ char subject [32 ];
546+ (void )snprintf (subject , sizeof (subject ), "%04u %s" , (unsigned )index + 1u , seedtool_word (index ));
547+ if (seedtool_render_fit_row (subject ) != strlen (subject )) {
548+ return false;
549+ }
550+ if (seedtool_render_fit_row (seedtool_word (index )) != strlen (seedtool_word (index ))) {
551+ return false;
552+ }
553+ }
554+ return true;
555+ }
556+
451557/* Scrolling is only navigation if the selection is always on screen and the end
452558 * of a list looks like the end of it. Both hold for every list the firmware can
453559 * build, from any previous scroll position, so both are checked exhaustively. */
@@ -1577,6 +1683,14 @@ static int self_test(void)
15771683 fputs ("Origo word number round-trip self-test failed\n" , stderr );
15781684 return 1 ;
15791685 }
1686+ if (!coin_word_encoding_is_sound ()) {
1687+ fputs ("Origo coin word encoding self-test failed\n" , stderr );
1688+ return 1 ;
1689+ }
1690+ if (!coin_word_screens_fit ()) {
1691+ fputs ("Origo coin word screen fit self-test failed\n" , stderr );
1692+ return 1 ;
1693+ }
15801694 if (!final_word_filter_is_exact ()) {
15811695 fputs ("Origo final word filter self-test failed\n" , stderr );
15821696 return 1 ;
0 commit comments