Skip to content

Commit 71a7204

Browse files
authored
Merge pull request #22 from oroderico/qr-shade
Let the reader dim a QR, and give every QR the same two controls
2 parents dda8aef + fe58c7d commit 71a7204

7 files changed

Lines changed: 419 additions & 144 deletions

File tree

‎host/origo_sdl.c‎

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -160,45 +160,46 @@ void seedtool_display_keyboard(const char* title, const char* text, const char*
160160
present();
161161
}
162162

163-
bool seedtool_display_qr(const char* title, const char* text)
163+
bool seedtool_display_qr(const char* title, const char* text, const size_t selected)
164164
{
165-
const bool ok = seedtool_render_qr(title, text);
165+
const bool ok = seedtool_render_qr(title, text, selected);
166166
if (ok) {
167167
present();
168168
}
169169
return ok;
170170
}
171171

172-
bool seedtool_display_qr_address(const char* title, const char* text)
172+
bool seedtool_display_qr_address(const char* title, const char* text, const size_t selected)
173173
{
174-
const bool ok = seedtool_render_qr_address(title, text);
174+
const bool ok = seedtool_render_qr_address(title, text, selected);
175175
if (ok) {
176176
present();
177177
}
178178
return ok;
179179
}
180180

181-
bool seedtool_display_qr_bytes(const char* title, const uint8_t* data, const size_t len)
181+
bool seedtool_display_qr_bytes(const char* title, const uint8_t* data, const size_t len, const size_t selected)
182182
{
183-
const bool ok = seedtool_render_qr_bytes(title, data, len);
183+
const bool ok = seedtool_render_qr_bytes(title, data, len, selected);
184184
if (ok) {
185185
present();
186186
}
187187
return ok;
188188
}
189189

190-
bool seedtool_display_qr_bytes_region(const char* title, const uint8_t* data, const size_t len, const size_t region_index)
190+
bool seedtool_display_qr_bytes_region(
191+
const char* title, const uint8_t* data, const size_t len, const size_t region_index, const size_t selected)
191192
{
192-
const bool ok = seedtool_render_qr_bytes_region(title, data, len, region_index);
193+
const bool ok = seedtool_render_qr_bytes_region(title, data, len, region_index, selected);
193194
if (ok) {
194195
present();
195196
}
196197
return ok;
197198
}
198199

199-
bool seedtool_display_qr_bytes_map(const char* title, const uint8_t* data, const size_t len)
200+
bool seedtool_display_qr_bytes_map(const char* title, const uint8_t* data, const size_t len, const size_t selected)
200201
{
201-
const bool ok = seedtool_render_qr_bytes_map(title, data, len);
202+
const bool ok = seedtool_render_qr_bytes_map(title, data, len, selected);
202203
if (ok) {
203204
present();
204205
}

‎host/origo_simulator.c‎

Lines changed: 155 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1235,7 +1235,7 @@ static bool compact_seedqr_is_sound(void)
12351235
if (qrcode_versionForBytes(ECC_LOW, (uint16_t)len, 6) != 1) {
12361236
return false;
12371237
}
1238-
if (!seedtool_render_qr_bytes("Compact SeedQR", entropy, len)) {
1238+
if (!seedtool_render_qr_bytes("Compact SeedQR", entropy, len, SEEDTOOL_NAV_BACK)) {
12391239
return false;
12401240
}
12411241
if (seedtool_mnemonic_entropy(mnemonic24, entropy, sizeof(entropy), &len) != SEEDTOOL_OK || len != 32) {
@@ -1249,7 +1249,7 @@ static bool compact_seedqr_is_sound(void)
12491249
if (qrcode_versionForBytes(ECC_LOW, (uint16_t)len, 6) != 2) {
12501250
return false;
12511251
}
1252-
if (!seedtool_render_qr_bytes("Compact SeedQR", entropy, len)) {
1252+
if (!seedtool_render_qr_bytes("Compact SeedQR", entropy, len, SEEDTOOL_NAV_BACK)) {
12531253
return false;
12541254
}
12551255
return seedtool_mnemonic_entropy(bad_checksum_mnemonic, entropy, sizeof(entropy), &len) != SEEDTOOL_OK;
@@ -1275,11 +1275,11 @@ static bool zoomed_qr_regions_are_sound(void)
12751275
return false;
12761276
}
12771277
for (size_t i = 0; i < 9; ++i) {
1278-
if (!seedtool_render_qr_bytes_region("Compact SeedQR", entropy, len, i)) {
1278+
if (!seedtool_render_qr_bytes_region("Compact SeedQR", entropy, len, i, SEEDTOOL_NAV_BACK)) {
12791279
return false;
12801280
}
12811281
}
1282-
if (seedtool_render_qr_bytes_region("Compact SeedQR", entropy, len, 9)) {
1282+
if (seedtool_render_qr_bytes_region("Compact SeedQR", entropy, len, 9, SEEDTOOL_NAV_BACK)) {
12831283
return false;
12841284
}
12851285

@@ -1290,11 +1290,11 @@ static bool zoomed_qr_regions_are_sound(void)
12901290
return false;
12911291
}
12921292
for (size_t i = 0; i < 25; ++i) {
1293-
if (!seedtool_render_qr_bytes_region("Compact SeedQR", entropy, len, i)) {
1293+
if (!seedtool_render_qr_bytes_region("Compact SeedQR", entropy, len, i, SEEDTOOL_NAV_BACK)) {
12941294
return false;
12951295
}
12961296
}
1297-
return !seedtool_render_qr_bytes_region("Compact SeedQR", entropy, len, 25);
1297+
return !seedtool_render_qr_bytes_region("Compact SeedQR", entropy, len, 25, SEEDTOOL_NAV_BACK);
12981298
}
12991299

13001300
/* The bar's warn (red) and complete (green) colors appear nowhere else on a
@@ -1756,7 +1756,7 @@ static bool qr_address_draws_every_group(void)
17561756

17571757
for (size_t v = 0; v < sizeof(fits) / sizeof(fits[0]); ++v) {
17581758
const char* const value = fits[v];
1759-
if (!seedtool_render_qr_address("m/84'/0'/0'/0/0", value)) {
1759+
if (!seedtool_render_qr_address("m/84'/0'/0'/0/0", value, SEEDTOOL_NAV_BACK)) {
17601760
return false; /* these do fit; refusing them is its own failure */
17611761
}
17621762
const int edge = qr_code_left_edge();
@@ -1776,7 +1776,7 @@ static bool qr_address_draws_every_group(void)
17761776
for (size_t i = total - SEEDTOOL_GROUP_LEN; i < total; ++i) {
17771777
altered[i] = altered[i] == 'q' ? 'p' : 'q';
17781778
}
1779-
if (!seedtool_render_qr_address("m/84'/0'/0'/0/0", altered)) {
1779+
if (!seedtool_render_qr_address("m/84'/0'/0'/0/0", altered, SEEDTOOL_NAV_BACK)) {
17801780
return false;
17811781
}
17821782
const uint16_t* const after = seedtool_render_pixels();
@@ -1796,8 +1796,143 @@ static bool qr_address_draws_every_group(void)
17961796
}
17971797

17981798
/* And the one the margin cannot hold, which must say so rather than clip. */
1799-
return !seedtool_render_qr_address(
1800-
"m/86'/0'/0'/0/0", "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr");
1799+
return !seedtool_render_qr_address("m/86'/0'/0'/0/0",
1800+
"bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr", SEEDTOOL_NAV_BACK);
1801+
}
1802+
1803+
/* How far right the theme's orange reaches in the strip the two QR controls
1804+
* share - NAV_BACK_Y + NAV_BACK_HEIGHT tall, above the first line a margin
1805+
* draws. Which control the cursor is on is read off this rather than off a
1806+
* box's coordinates, so the proofs below survive the chrome being moved. */
1807+
static int qr_highlight_reach(void)
1808+
{
1809+
const uint16_t highlight = 0xfd20;
1810+
const uint16_t* const pixels = seedtool_render_pixels();
1811+
int reach = -1;
1812+
for (int y = 0; y < 20; ++y) {
1813+
for (int x = 0; x < SEEDTOOL_DISPLAY_WIDTH; ++x) {
1814+
if (pixels[y * SEEDTOOL_DISPLAY_WIDTH + x] == highlight && x > reach) {
1815+
reach = x;
1816+
}
1817+
}
1818+
}
1819+
return reach;
1820+
}
1821+
1822+
/* Every QR screen wears the same two controls, and the cursor reaches the sun
1823+
* on all of them: the account key animates and the Compact SeedQR carousels,
1824+
* so a shade that only the address screen could reach would be missing from
1825+
* exactly the codes hardest to scan. */
1826+
static bool qr_chrome_is_the_same_everywhere(void)
1827+
{
1828+
static const char* const text = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4";
1829+
static const uint8_t entropy[16] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 };
1830+
for (int screen = 0; screen < 5; ++screen) {
1831+
int reach[2];
1832+
for (int i = 0; i < 2; ++i) {
1833+
const size_t selected = i ? SEEDTOOL_NAV_SHADE : SEEDTOOL_NAV_BACK;
1834+
const bool ok = screen == 0 ? seedtool_render_qr("Account key", text, selected)
1835+
: screen == 1 ? seedtool_render_qr_address("m/84'/0'/0'/0/0", text, selected)
1836+
: screen == 2 ? seedtool_render_qr_bytes("Compact SeedQR", entropy, sizeof(entropy), selected)
1837+
: screen == 3 ? seedtool_render_qr_bytes_map("Compact SeedQR", entropy, sizeof(entropy), selected)
1838+
: seedtool_render_qr_bytes_region("Compact SeedQR", entropy, sizeof(entropy), 0, selected);
1839+
if (!ok) {
1840+
return false;
1841+
}
1842+
reach[i] = qr_highlight_reach();
1843+
}
1844+
if (reach[0] < 0 || reach[1] <= reach[0]) {
1845+
return false; /* no cursor, or one that cannot reach the sun */
1846+
}
1847+
}
1848+
return true;
1849+
}
1850+
1851+
/* Cycling the QR's shade has to move the light half of the code and nothing
1852+
* else: a step that also touched the dark modules would be lowering contrast
1853+
* from both sides, which is the opposite of what a camera that cannot lock on
1854+
* needs. Measured off the panel rather than read off the ramp, so the test
1855+
* says nothing about which greys were chosen - only that one press lands the
1856+
* light field on one new colour, that the ring closes on itself, and that the
1857+
* cursor can be seen to be on the control that does it. */
1858+
static bool qr_shade_moves_only_the_light_field(void)
1859+
{
1860+
static const char* const value = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4";
1861+
static const char* const path = "m/84'/0'/0'/0/0";
1862+
static uint16_t white[SEEDTOOL_DISPLAY_WIDTH * SEEDTOOL_DISPLAY_HEIGHT];
1863+
static uint16_t cycled[SEEDTOOL_DISPLAY_WIDTH * SEEDTOOL_DISPLAY_HEIGHT];
1864+
const size_t count = sizeof(white) / sizeof(white[0]);
1865+
1866+
/* Nothing has touched the shade yet, so this is also the proof that a QR
1867+
* left alone is drawn exactly as it was before the ramp existed. */
1868+
if (!seedtool_render_qr_address(path, value, SEEDTOOL_NAV_BACK)) {
1869+
return false;
1870+
}
1871+
memcpy(white, seedtool_render_pixels(), sizeof(white));
1872+
1873+
/* The cursor has to be visible ON the control, not merely somewhere on the
1874+
* screen: moving to the second control has to carry the highlight to it.
1875+
* A check that "something changed" passes even when the second control
1876+
* ignores the cursor entirely, because the first one still gives its
1877+
* highlight up. */
1878+
const int back_reach = qr_highlight_reach();
1879+
if (back_reach < 0) {
1880+
return false; /* the cursor was never on the way out either */
1881+
}
1882+
if (!seedtool_render_qr_address(path, value, SEEDTOOL_NAV_SHADE)) {
1883+
return false;
1884+
}
1885+
if (qr_highlight_reach() <= back_reach) {
1886+
return false; /* the highlight did not move to the second control */
1887+
}
1888+
1889+
seedtool_render_qr_cycle_shade();
1890+
if (!seedtool_render_qr_address(path, value, SEEDTOOL_NAV_BACK)) {
1891+
return false;
1892+
}
1893+
memcpy(cycled, seedtool_render_pixels(), sizeof(cycled));
1894+
1895+
/* Every pixel that moved was white and is now one single other colour -
1896+
* the code's light field and the swatch on the button that reports it. */
1897+
uint16_t shade = 0xffff;
1898+
size_t moved = 0;
1899+
for (size_t i = 0; i < count; ++i) {
1900+
if (white[i] == cycled[i]) {
1901+
continue;
1902+
}
1903+
if (white[i] != 0xffff) {
1904+
return false; /* something other than the light field moved */
1905+
}
1906+
if (!moved) {
1907+
shade = cycled[i];
1908+
} else if (cycled[i] != shade) {
1909+
return false; /* the light field is no longer one colour */
1910+
}
1911+
++moved;
1912+
}
1913+
if (!moved || shade == 0xffff) {
1914+
return false;
1915+
}
1916+
1917+
/* The ring closes: pressing on round the ramp lands back on white, in the
1918+
* same number of presses every time. Each one has to move something, or a
1919+
* ramp with a repeated entry would still come home and look correct. */
1920+
for (int i = 0; i < 32; ++i) {
1921+
seedtool_render_qr_cycle_shade();
1922+
if (!seedtool_render_qr_address(path, value, SEEDTOOL_NAV_BACK)) {
1923+
return false;
1924+
}
1925+
if (memcmp(cycled, seedtool_render_pixels(), sizeof(cycled)) == 0) {
1926+
return false; /* a press that changed nothing */
1927+
}
1928+
memcpy(cycled, seedtool_render_pixels(), sizeof(cycled));
1929+
if (memcmp(white, cycled, sizeof(white)) == 0) {
1930+
/* Home again, and the ramp is left where every other test expects
1931+
* to find it. */
1932+
return true;
1933+
}
1934+
}
1935+
return false; /* the ring never came back to where it started */
18011936
}
18021937

18031938
/* The grouped line is centred with its gaps counted in, so it can run off an
@@ -2437,6 +2572,14 @@ static int self_test(void)
24372572
fputs("Origo address-beside-QR self-test failed\n", stderr);
24382573
return 1;
24392574
}
2575+
if (!qr_chrome_is_the_same_everywhere()) {
2576+
fputs("Origo QR chrome self-test failed\n", stderr);
2577+
return 1;
2578+
}
2579+
if (!qr_shade_moves_only_the_light_field()) {
2580+
fputs("Origo QR shade self-test failed\n", stderr);
2581+
return 1;
2582+
}
24402583
if (!nav_chrome_bands_do_not_collide()) {
24412584
fputs("Origo nav chrome geometry self-test failed\n", stderr);
24422585
return 1;
@@ -2452,7 +2595,7 @@ static int self_test(void)
24522595
const unsigned purpose = i ? 86 : 84;
24532596
(void)snprintf(payload, sizeof(payload), "[73c5da0a/%u'/0'/%u']%s", purpose, SEEDTOOL_MAX_ACCOUNT_INDEX,
24542597
i ? expected_xpub86 : expected_xpub84);
2455-
if (strlen(payload) > 134 || !seedtool_render_qr("BIP84 account key", payload)) {
2598+
if (strlen(payload) > 134 || !seedtool_render_qr("BIP84 account key", payload, SEEDTOOL_NAV_BACK)) {
24562599
fputs("Origo account key QR self-test failed\n", stderr);
24572600
return 1;
24582601
}
@@ -2465,7 +2608,7 @@ static int self_test(void)
24652608
}
24662609
seedtool_render_screen("ORIGO", "HOST SELF-TEST", address, "OK");
24672610
seedtool_render_splash();
2468-
if (!seedtool_render_qr("BIP84 address 0", address)) {
2611+
if (!seedtool_render_qr("BIP84 address 0", address, SEEDTOOL_NAV_BACK)) {
24692612
fputs("Origo QR self-test failed\n", stderr);
24702613
return 1;
24712614
}

0 commit comments

Comments
 (0)