From c1b8437d0851f74d5924c0e761f45aa3076365f1 Mon Sep 17 00:00:00 2001 From: ftheirs Date: Thu, 8 Jun 2023 09:57:08 -0300 Subject: [PATCH 1/3] restrict EVM path --- app/src/addr.c | 5 +++-- app/src/apdu_handler.c | 12 +++++----- app/src/chain_config.c | 51 ++++++++++++++++++++++++++++++++++++++++++ app/src/chain_config.h | 30 +++++++++++++++++++++++++ app/src/coin.h | 7 +++++- app/src/crypto.c | 51 +++++++++++++++++++++++++++++------------- app/src/crypto.h | 4 +++- 7 files changed, 135 insertions(+), 25 deletions(-) create mode 100644 app/src/chain_config.c create mode 100644 app/src/chain_config.h diff --git a/app/src/addr.c b/app/src/addr.c index 3debc5fa..6e43055e 100644 --- a/app/src/addr.c +++ b/app/src/addr.c @@ -25,7 +25,8 @@ zxerr_t addr_getNumItems(uint8_t *num_items) { zemu_log_stack("addr_getNumItems"); *num_items = 1; - if (app_mode_expert() || isEthPath) { + + if (app_mode_expert() || encoding != BECH32_COSMOS ) { zemu_log("num_items 2\n"); *num_items = 2; } else { @@ -47,7 +48,7 @@ zxerr_t addr_getItem(int8_t displayIdx, ZEMU_LOGF(200, "[addr_getItem] pageCount %d\n", *pageCount) return zxerr_ok; case 1: { - if (!app_mode_expert() && !isEthPath) { + if (!app_mode_expert() && encoding == BECH32_COSMOS) { return zxerr_no_data; } diff --git a/app/src/apdu_handler.c b/app/src/apdu_handler.c index 1671f0dd..150abcfb 100644 --- a/app/src/apdu_handler.c +++ b/app/src/apdu_handler.c @@ -35,7 +35,7 @@ #include "parser_impl.h" #include "view_internal.h" -bool isEthPath = false; +#include "chain_config.h" static const char *msg_error1 = "Expert Mode"; static const char *msg_error2 = "Required"; @@ -74,8 +74,11 @@ static void extractHDPath(uint32_t rx, uint32_t offset) { THROW(APDU_CODE_DATA_INVALID); } - // Set EthPath flag - isEthPath = (hdPath[1] == HDPATH_ETH_1_DEFAULT) ? true : false; + encoding = checkChainConfig(hdPath[1], bech32_hrp, bech32_hrp_len); + if (encoding == UNSUPPORTED) { + ZEMU_LOGF(50, "Chain config not supported for: %s\n", bech32_hrp) + THROW(APDU_CODE_COMMAND_NOT_ALLOWED); + } // Limit values unless the app is running in expert mode if (!app_mode_expert()) { @@ -124,7 +127,6 @@ __Z_INLINE void handleGetAddrSecp256K1(volatile uint32_t *flags, volatile uint32 extractHDPath(rx, OFFSET_DATA + 1 + len); uint8_t requireConfirmation = G_io_apdu_buffer[OFFSET_P1]; - zxerr_t zxerr = app_fill_address(); if (zxerr != zxerr_ok) { *tx = 0; @@ -158,7 +160,7 @@ __Z_INLINE void handleSign(volatile uint32_t *flags, volatile uint32_t *tx, uint } parser_tx_obj.tx_json.own_addr = (const char *) (G_io_apdu_buffer + VIEW_ADDRESS_OFFSET_SECP256K1); - if (isEthPath && !app_mode_expert()) { + if ((encoding != BECH32_COSMOS) && !app_mode_expert()) { *flags |= IO_ASYNCH_REPLY; view_custom_error_show(PIC(msg_error1),PIC(msg_error2)); THROW(APDU_CODE_DATA_INVALID); diff --git a/app/src/chain_config.c b/app/src/chain_config.c new file mode 100644 index 00000000..1fd848a7 --- /dev/null +++ b/app/src/chain_config.c @@ -0,0 +1,51 @@ +/******************************************************************************* +* (c) 2018 - 2023 Zondax AG +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +********************************************************************************/ +#include "chain_config.h" +#include + +typedef struct { + const uint32_t path; + const char *hrp; + const address_encoding_e encoding; +} chain_config_t; + +// To enable custom config for a new chain, just add a new entry in this array with path, hrp and encoding +static const chain_config_t chainConfig[] = { + // {118, cosmos, BECH32_COSMOS}, + {60, "inj", BECH32_ETH}, +}; + +static const uint32_t chainConfigLen = sizeof(chainConfig) / sizeof(chainConfig[0]); + +address_encoding_e checkChainConfig(uint32_t path, const char* hrp, uint8_t hrpLen) { + // Always allowed for 118 (default Cosmos) + if (path == HDPATH_1_DEFAULT) { + return BECH32_COSMOS; + } + + // Check special cases + for (uint32_t i = 0; i < chainConfigLen; i++) { + if (path == (0x80000000u | chainConfig[i].path)) { + const char* hrpPtr = (const char *) PIC(chainConfig[i].hrp); + const uint16_t hrpPtrLen = strlen(hrpPtr); + if (hrpPtrLen == hrpLen && memcmp(hrpPtr, hrp, hrpLen) == 0) { + return chainConfig[i].encoding; + } + } + } + + return UNSUPPORTED; +} diff --git a/app/src/chain_config.h b/app/src/chain_config.h new file mode 100644 index 00000000..3c0409b7 --- /dev/null +++ b/app/src/chain_config.h @@ -0,0 +1,30 @@ +/******************************************************************************* +* (c) 2018 - 2023 Zondax AG +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +********************************************************************************/ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include "coin.h" + +address_encoding_e checkChainConfig(uint32_t path, const char* hrp, uint8_t hrpLen); + +#ifdef __cplusplus +} +#endif diff --git a/app/src/coin.h b/app/src/coin.h index a58e1750..ece4d77f 100644 --- a/app/src/coin.h +++ b/app/src/coin.h @@ -32,7 +32,6 @@ extern "C" { #define PK_LEN_SECP256K1 33u #define PK_LEN_SECP256K1_UNCOMPRESSED 65u -extern bool isEthPath; typedef enum { addr_secp256k1 = 0, } address_kind_e; @@ -42,6 +41,12 @@ typedef enum { tx_textual } tx_type_e; +typedef enum { + BECH32_COSMOS = 0, + BECH32_ETH, + UNSUPPORTED = 0xFF, +} address_encoding_e; + #define VIEW_ADDRESS_OFFSET_SECP256K1 PK_LEN_SECP256K1 #define VIEW_ADDRESS_LAST_PAGE_DEFAULT 0 diff --git a/app/src/crypto.c b/app/src/crypto.c index 6544d8e1..d6de7652 100644 --- a/app/src/crypto.c +++ b/app/src/crypto.c @@ -21,11 +21,13 @@ #include "tx.h" #include +#include "chain_config.h" uint32_t hdPath[HDPATH_LEN_DEFAULT]; uint8_t bech32_hrp_len; char bech32_hrp[MAX_BECH32_HRP_LEN + 1]; +address_encoding_e encoding; #include "cx.h" @@ -94,11 +96,13 @@ zxerr_t crypto_sign(uint8_t *signature, const uint8_t *message = tx_get_buffer(); const uint16_t messageLen = tx_get_buffer_length(); - switch(hdPath[1]) { - case HDPATH_1_DEFAULT: + switch (encoding) { + case BECH32_COSMOS: { cx_hash_sha256(message, messageLen, messageDigest, CX_SHA256_SIZE); break; - case HDPATH_ETH_1_DEFAULT: { + } + + case BECH32_ETH: { cx_sha3_t sha3 = {0}; cx_err_t status = cx_keccak_init_no_throw(&sha3, 256); if (status != CX_OK) { @@ -108,8 +112,11 @@ zxerr_t crypto_sign(uint8_t *signature, if (status != CX_OK) { return zxerr_ledger_api_error; } + break; } - break; + + default: + return zxerr_unknown; } cx_ecfp_private_key_t cx_privateKey = {0}; @@ -198,20 +205,32 @@ zxerr_t crypto_fillAddress(uint8_t *buffer, uint16_t buffer_len, uint16_t *addrR char *addr = (char *) (buffer + PK_LEN_SECP256K1); uint8_t hashed1_pk[CX_SHA256_SIZE] = {0}; - if (isEthPath) { - cx_sha3_t ctx; - if (cx_keccak_init_no_throw(&ctx, 256) != CX_OK) { - return zxerr_unknown; + + switch (encoding) { + case BECH32_COSMOS: { + // Hash it + cx_hash_sha256(buffer, PK_LEN_SECP256K1, hashed1_pk, CX_SHA256_SIZE); + uint8_t hashed2_pk[CX_RIPEMD160_SIZE]; + ripemd160_32(hashed2_pk, hashed1_pk); + CHECK_ZXERR(bech32EncodeFromBytes(addr, buffer_len - PK_LEN_SECP256K1, bech32_hrp, hashed2_pk, CX_RIPEMD160_SIZE, 1, BECH32_ENCODING_BECH32)) + break; } - cx_hash((cx_hash_t *)&ctx, CX_LAST, uncompressedPubkey+1, sizeof(uncompressedPubkey)-1, hashed1_pk, sizeof(hashed1_pk)); - CHECK_ZXERR(bech32EncodeFromBytes(addr, buffer_len - PK_LEN_SECP256K1, bech32_hrp, hashed1_pk + 12, sizeof(hashed1_pk) - 12, 1, BECH32_ENCODING_BECH32)) - } else { - // Hash it - cx_hash_sha256(buffer, PK_LEN_SECP256K1, hashed1_pk, CX_SHA256_SIZE); - uint8_t hashed2_pk[CX_RIPEMD160_SIZE]; - ripemd160_32(hashed2_pk, hashed1_pk); - CHECK_ZXERR(bech32EncodeFromBytes(addr, buffer_len - PK_LEN_SECP256K1, bech32_hrp, hashed2_pk, CX_RIPEMD160_SIZE, 1, BECH32_ENCODING_BECH32)) + + case BECH32_ETH: { + cx_sha3_t ctx; + if (cx_keccak_init_no_throw(&ctx, 256) != CX_OK) { + return zxerr_unknown; + } + cx_hash((cx_hash_t *)&ctx, CX_LAST, uncompressedPubkey+1, sizeof(uncompressedPubkey)-1, hashed1_pk, sizeof(hashed1_pk)); + CHECK_ZXERR(bech32EncodeFromBytes(addr, buffer_len - PK_LEN_SECP256K1, bech32_hrp, hashed1_pk + 12, sizeof(hashed1_pk) - 12, 1, BECH32_ENCODING_BECH32)) + break; + } + + default: + *addrResponseLen = 0; + return zxerr_encoding_failed; } + *addrResponseLen = PK_LEN_SECP256K1 + strnlen(addr, (buffer_len - PK_LEN_SECP256K1)); return zxerr_ok; diff --git a/app/src/crypto.h b/app/src/crypto.h index 8cb3182c..6a7d319f 100644 --- a/app/src/crypto.h +++ b/app/src/crypto.h @@ -29,7 +29,9 @@ extern "C" { #define MAX_BECH32_HRP_LEN 83u extern uint32_t hdPath[HDPATH_LEN_DEFAULT]; -extern char *hrp; +extern char bech32_hrp[MAX_BECH32_HRP_LEN + 1]; +extern uint8_t bech32_hrp_len; +extern address_encoding_e encoding; uint8_t extractHRP(uint32_t rx, uint32_t offset); From 6f22df286de565aeae1a510d69b080c1affe9e10 Mon Sep 17 00:00:00 2001 From: ftheirs Date: Thu, 8 Jun 2023 11:04:57 -0300 Subject: [PATCH 2/3] update zemu tests --- tests_zemu/package.json | 2 +- .../snapshots/s-show_eth_address/00000.png | Bin 637 -> 633 bytes .../snapshots/s-show_eth_address/00001.png | Bin 430 -> 410 bytes .../snapshots/sp-show_eth_address/00001.png | Bin 778 -> 782 bytes .../snapshots/x-show_eth_address/00001.png | Bin 778 -> 782 bytes tests_zemu/tests/json.test.ts | 10 ++----- tests_zemu/tests/standard.test.ts | 26 ++++++++++++------ tests_zemu/tests/textual.test.ts | 10 +++---- 8 files changed, 25 insertions(+), 23 deletions(-) diff --git a/tests_zemu/package.json b/tests_zemu/package.json index f4d35d5d..88d928c9 100644 --- a/tests_zemu/package.json +++ b/tests_zemu/package.json @@ -14,7 +14,7 @@ "Ledger" ], "scripts": { - "test": "ts-node tests/pullImageKillOld.ts && jest" + "test": "ts-node tests/pullImageKillOld.ts && jest --maxConcurrency 2" }, "dependencies": { "@zondax/ledger-cosmos-js": "^3.0.2", diff --git a/tests_zemu/snapshots/s-show_eth_address/00000.png b/tests_zemu/snapshots/s-show_eth_address/00000.png index 83311eeffa2e8b4da7052260d013a7e827442145..7e3a8f230decff071acec130cd81aac033c8aecb 100644 GIT binary patch delta 608 zcmV-m0-yc;1o;GzB!56jL_t(|ob6iMZo@DLCDZ!r;7kzi^$MG40;9oTG096XRV}WwSQCOJU2Od1Vr?XAFZUL z^adoxQ1-m{K89)Y)mk5x-v9tOUjLtU{83N}yS&``u=WNZVJ+~nHc}1O6NHnntYM6a z3wOyBEWTpxwXs z1^Q;q^3={hSAUZbqB%hAOHp~Qa$}hYlqy@c(4tTK^nkWlimToZr4hx{ zG>$%DPo%QRShJ$)%V-@$kLDlK0jCf*E0!}R+aaujBun!yPAHw*-vVEjdV{Y6oS34F~qZbKX!NNX(9e8=cdV(I(m=xor zfxd_UU$ uCqUz{2+`1rL#;vXgxifYoOnhNoAC!)j2}`_89krB&*H;e{c`oLspT?%LB$vDrMA5{QwNM zF%zN%c)wy@&iI`6IP?zwMbO0HXRfBxYk!BzacTbTkx{%f`UxPSZU6txlg}=tbjX)`pXQzbrtG} zkPyBlOI2d<4xIdQzvvmd7>5(1yYGUc>A`YrR~rC;?O&%3kh4guB)alLS;RNv>mkX5 z=*`g9dPpxWzklM63g&?B!^0Xt+A{``fNU=COsMJ*nQ{t?mRu~NA`GYOw6>2w29cnS zl(eN~B(AQzgv3Yy6AG~s%E3XXvb%j@T>T6k=L1R@g#!yJTd9#^m(Lt3Vw`##moV;q zwUYP6#z~jR31C?iOscp*P(mV+`YuzqEqi_8H(qG`gMTt)iShYt=z|A$MwW^QrF)jR zqH_a5*Ba+ip#0ZDc0DcAe`TgB|F=uZS!YLy*OiE6(2$#qEju^}t3-M497vm3oRt@X^hMUCz0000EMy4Y z5Rgd3c|FMkW!P+&?KED`{Slv2kD|9^d~z~feWzWP1|Jguy|EMzkI5thu1D*#8GCA; zM6ofpgt>k7IxWk_)n19NP(Qlk`JOfseKZQ8Dbadz=_4!#kALIcgQ%K`i*!fUU#qjC z+~OOs9AyTO%nM^@R%F*Zk1;jKryGn_pY{w0IOV~z?T-^;+WNPcOno{sAYkiWIEzV^ z)u zMVD-ZIw}*BZ9$K^i8ogF&%q@{Bd!jBpK3RW#{`G4exml(2E@NWX&D0m0000000000 d008i7`~iKJ2IakUxLp7M002ovPDHLkV1kFBu!aBt delta 404 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 ksfYs^000000DLe102B~_$}Lbb Date: Thu, 8 Jun 2023 11:12:14 -0300 Subject: [PATCH 3/3] bump version --- app/Makefile.version | 2 +- tests_zemu/snapshots/s-mainmenu/00004.png | Bin 455 -> 461 bytes tests_zemu/snapshots/s-mainmenu/00010.png | Bin 455 -> 461 bytes tests_zemu/snapshots/sp-mainmenu/00004.png | Bin 396 -> 400 bytes tests_zemu/snapshots/sp-mainmenu/00010.png | Bin 396 -> 400 bytes tests_zemu/snapshots/x-mainmenu/00004.png | Bin 396 -> 400 bytes tests_zemu/snapshots/x-mainmenu/00010.png | Bin 396 -> 400 bytes 7 files changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Makefile.version b/app/Makefile.version index 230bef3c..163597f4 100644 --- a/app/Makefile.version +++ b/app/Makefile.version @@ -3,4 +3,4 @@ APPVERSION_M=2 # This is the `spec_version` field of `Runtime` APPVERSION_N=35 # This is the patch version of this release -APPVERSION_P=9 +APPVERSION_P=10 diff --git a/tests_zemu/snapshots/s-mainmenu/00004.png b/tests_zemu/snapshots/s-mainmenu/00004.png index 1a35f133997571b0567ecc3f0be40c59c9f9dae7..98030e9a626cfad6fd5f2b8b0592ea0ee3b0cc9c 100644 GIT binary patch delta 435 zcmV;k0ZjhK1I+`FB!84iL_t(|ob8!Ol7lb|MNL<@|AASMg}PmernU^Roz#CT$`j_x zgCt4;0002q$a$3WFrF_C^%Xp3Paw&*&z`}}Of>*s%cSgilx_bluMeU@VP>cecnR-l z_F9RbX>YEgaE(eaPMF%p8$t9U8+-^ub^t9&zA6K`i(0DY`hV3O1Kk(4N1Fk%_g?!4 z$1R1a7S^$wNk>r1LS|!K7>v2F?=;E)RUPp_(s=6dT2JwXFW45grdH;~$ZN$-__1<~ zr)viYeIfS+ECa;HCHJ_6^9M{!hwTq#01?OtLT55!uY;!kNw%+ANNsH=V8GKns4k^!Zx)M-Tu2 d00013gD2HN-RaCu%3lBg002ovPDHLkV1m%0%f|o! delta 429 zcmV;e0aE_W1IGi9B!7-cL_t(|ob8!OlEWYjMdeiI{s;ELSni}E6K=R+p#o*yf zfFu(E0001Qx3Xfr_e+-v{f zc%(4Z#5#5}XbVbN$Q-O2gL_UKJ6&afs*boQ(s=6dUQh9f57-vArcvg>$a}>}xU3x4 z)3pMGzL5I_mI31Jl1JPk_yZV XIK*s%cSgilx_bluMeU@VP>cecnR-l z_F9RbX>YEgaE(eaPMF%p8$t9U8+-^ub^t9&zA6K`i(0DY`hV3O1Kk(4N1Fk%_g?!4 z$1R1a7S^$wNk>r1LS|!K7>v2F?=;E)RUPp_(s=6dT2JwXFW45grdH;~$ZN$-__1<~ zr)viYeIfS+ECa;HCHJ_6^9M{!hwTq#01?OtLT55!uY;!kNw%+ANNsH=V8GKns4k^!Zx)M-Tu2 d00013gD2HN-RaCu%3lBg002ovPDHLkV1m%0%f|o! delta 429 zcmV;e0aE_W1IGi9B!7-cL_t(|ob8!OlEWYjMdeiI{s;ELSni}E6K=R+p#o*yf zfFu(E0001Qx3Xfr_e+-v{f zc%(4Z#5#5}XbVbN$Q-O2gL_UKJ6&afs*boQ(s=6dUQh9f57-vArcvg>$a}>}xU3x4 z)3pMGzL5I_mI31Jl1JPk_yZV XIKN1CRrdB!5>)L_t(|obB0Nj>8}fMp35fPT2pFyq7+pB9F~am3A<6?slM?TJIM1!vi%Gol*K-m7<>0yMk#|YVbF`zJDnA4k$xXkIF&Z$qm)! z`1K73xrf(d(w8nvF&pyKY^9I(5)XgC zdXsohe2+d(H)PUZrV%yiFGdX_F$+cFIULEy`xDZSDZi34IONxb{P}wShOejl8r+fN zMGJk%EC)?nt!@~FkIl>aVgmpG0000000000fUoobE3xp8 Ty4gh-00000NkvXXu0mjf{ywst delta 369 zcmbQh+`~LUr9Rlx#WAE}&fA-Tc}Em@Tmz#@)!)>6-t)G}|AL zZ0%QV``-E$zjc*{)#6XvwS4n8?>c;8_0Z_9=C@1X;hpXF32U1x z%4_=Xoij?gRDVV#>r%ahX~z*Qk6?d4V4cRN2{{!*@g zZae417xoVoXJzGh-c`967jXOD#o6cEW8bWLq!VeWs@0Gj+P^UBa(m;xlXH{zWHdAF zN!@&Tz3+k;)(iLd+?u&|`>gXPLK`%H3l{#kQu;%(sEQL2G7L4_;FVdQ&MBb@0H1QOGynhq diff --git a/tests_zemu/snapshots/sp-mainmenu/00010.png b/tests_zemu/snapshots/sp-mainmenu/00010.png index 6b0b34a7e0c81801b70907cc07fd684944b8a0ae..d8d85a04136e6c8d9f8ae3c2d99784df195a393e 100644 GIT binary patch delta 373 zcmV-*0gC>N1CRrdB!5>)L_t(|obB0Nj>8}fMp35fPT2pFyq7+pB9F~am3A<6?slM?TJIM1!vi%Gol*K-m7<>0yMk#|YVbF`zJDnA4k$xXkIF&Z$qm)! z`1K73xrf(d(w8nvF&pyKY^9I(5)XgC zdXsohe2+d(H)PUZrV%yiFGdX_F$+cFIULEy`xDZSDZi34IONxb{P}wShOejl8r+fN zMGJk%EC)?nt!@~FkIl>aVgmpG0000000000fUoobE3xp8 Ty4gh-00000NkvXXu0mjf{ywst delta 369 zcmbQh+`~LUr9Rlx#WAE}&fA-Tc}Em@Tmz#@)!)>6-t)G}|AL zZ0%QV``-E$zjc*{)#6XvwS4n8?>c;8_0Z_9=C@1X;hpXF32U1x z%4_=Xoij?gRDVV#>r%ahX~z*Qk6?d4V4cRN2{{!*@g zZae417xoVoXJzGh-c`967jXOD#o6cEW8bWLq!VeWs@0Gj+P^UBa(m;xlXH{zWHdAF zN!@&Tz3+k;)(iLd+?u&|`>gXPLK`%H3l{#kQu;%(sEQL2G7L4_;FVdQ&MBb@0H1QOGynhq diff --git a/tests_zemu/snapshots/x-mainmenu/00004.png b/tests_zemu/snapshots/x-mainmenu/00004.png index 6b0b34a7e0c81801b70907cc07fd684944b8a0ae..d8d85a04136e6c8d9f8ae3c2d99784df195a393e 100644 GIT binary patch delta 373 zcmV-*0gC>N1CRrdB!5>)L_t(|obB0Nj>8}fMp35fPT2pFyq7+pB9F~am3A<6?slM?TJIM1!vi%Gol*K-m7<>0yMk#|YVbF`zJDnA4k$xXkIF&Z$qm)! z`1K73xrf(d(w8nvF&pyKY^9I(5)XgC zdXsohe2+d(H)PUZrV%yiFGdX_F$+cFIULEy`xDZSDZi34IONxb{P}wShOejl8r+fN zMGJk%EC)?nt!@~FkIl>aVgmpG0000000000fUoobE3xp8 Ty4gh-00000NkvXXu0mjf{ywst delta 369 zcmbQh+`~LUr9Rlx#WAE}&fA-Tc}Em@Tmz#@)!)>6-t)G}|AL zZ0%QV``-E$zjc*{)#6XvwS4n8?>c;8_0Z_9=C@1X;hpXF32U1x z%4_=Xoij?gRDVV#>r%ahX~z*Qk6?d4V4cRN2{{!*@g zZae417xoVoXJzGh-c`967jXOD#o6cEW8bWLq!VeWs@0Gj+P^UBa(m;xlXH{zWHdAF zN!@&Tz3+k;)(iLd+?u&|`>gXPLK`%H3l{#kQu;%(sEQL2G7L4_;FVdQ&MBb@0H1QOGynhq diff --git a/tests_zemu/snapshots/x-mainmenu/00010.png b/tests_zemu/snapshots/x-mainmenu/00010.png index 6b0b34a7e0c81801b70907cc07fd684944b8a0ae..d8d85a04136e6c8d9f8ae3c2d99784df195a393e 100644 GIT binary patch delta 373 zcmV-*0gC>N1CRrdB!5>)L_t(|obB0Nj>8}fMp35fPT2pFyq7+pB9F~am3A<6?slM?TJIM1!vi%Gol*K-m7<>0yMk#|YVbF`zJDnA4k$xXkIF&Z$qm)! z`1K73xrf(d(w8nvF&pyKY^9I(5)XgC zdXsohe2+d(H)PUZrV%yiFGdX_F$+cFIULEy`xDZSDZi34IONxb{P}wShOejl8r+fN zMGJk%EC)?nt!@~FkIl>aVgmpG0000000000fUoobE3xp8 Ty4gh-00000NkvXXu0mjf{ywst delta 369 zcmbQh+`~LUr9Rlx#WAE}&fA-Tc}Em@Tmz#@)!)>6-t)G}|AL zZ0%QV``-E$zjc*{)#6XvwS4n8?>c;8_0Z_9=C@1X;hpXF32U1x z%4_=Xoij?gRDVV#>r%ahX~z*Qk6?d4V4cRN2{{!*@g zZae417xoVoXJzGh-c`967jXOD#o6cEW8bWLq!VeWs@0Gj+P^UBa(m;xlXH{zWHdAF zN!@&Tz3+k;)(iLd+?u&|`>gXPLK`%H3l{#kQu;%(sEQL2G7L4_;FVdQ&MBb@0H1QOGynhq