diff --git a/.gitignore b/.gitignore index a5dda430..3a42a935 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ /dep /obj *.swp +/src/glyphs.* \ No newline at end of file diff --git a/Makefile b/Makefile index 875e298e..84262b02 100755 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ include $(BOLOS_SDK)/Makefile.glyphs # Main app configuration APPNAME = "Algorand" -APPVERSION = 1.0.8 +APPVERSION = 1.2.8 APP_LOAD_PARAMS = --appFlags 0x250 $(COMMON_LOAD_PARAMS) APP_LOAD_PARAMS += --path "44'/283'" @@ -22,16 +22,16 @@ endif # Build configuration APP_SOURCE_PATH += src -SDK_SOURCE_PATH += lib_stusb lib_stusb_impl lib_u2f +SDK_SOURCE_PATH += lib_stusb lib_stusb_impl lib_u2f lib_ux DEFINES += APPVERSION=\"$(APPVERSION)\" DEFINES += OS_IO_SEPROXYHAL DEFINES += HAVE_BAGL HAVE_SPRINTF DEFINES += HAVE_BOLOS_APP_STACK_CANARY +DEFINES += HAVE_UX_FLOW ifeq ($(TARGET_NAME),TARGET_NANOS) -DEFINES += HAVE_UX_LEGACY DEFINES += IO_SEPROXYHAL_BUFFER_SIZE_B=128 else ifeq ($(TARGET_NAME),TARGET_NANOX) DEFINES += IO_SEPROXYHAL_BUFFER_SIZE_B=300 @@ -45,10 +45,7 @@ DEFINES += HAVE_BAGL_FONT_OPEN_SANS_REGULAR_11PX DEFINES += HAVE_BAGL_FONT_OPEN_SANS_EXTRABOLD_11PX DEFINES += HAVE_BAGL_FONT_OPEN_SANS_LIGHT_16PX -DEFINES += HAVE_UX_FLOW - SDK_SOURCE_PATH += lib_blewbxx lib_blewbxx_impl -SDK_SOURCE_PATH += lib_ux else $(error unknown device TARGET_NAME) endif @@ -94,13 +91,13 @@ $(info GCCPATH is not set: arm-none-eabi-* will be used from PATH) endif CC := $(CLANGPATH)clang -CFLAGS += -O3 -Oz +CFLAGS += -O3 -Os AS := $(GCCPATH)arm-none-eabi-gcc AFLAGS += LD := $(GCCPATH)arm-none-eabi-gcc -LDFLAGS += -O3 -Oz +LDFLAGS += -O3 -Os LDLIBS += -lm -lgcc -lc # Main rules diff --git a/cli/.gitignore b/cli/.gitignore index a74b07ae..ed948393 100644 --- a/cli/.gitignore +++ b/cli/.gitignore @@ -1 +1 @@ -/*.pyc +/*.pyc \ No newline at end of file diff --git a/glyphs/icon_back.gif b/glyphs/icon_back.gif new file mode 100644 index 00000000..a2a7e6d4 Binary files /dev/null and b/glyphs/icon_back.gif differ diff --git a/glyphs/icon_back_x.gif b/glyphs/icon_back_x.gif new file mode 100644 index 00000000..ff043615 Binary files /dev/null and b/glyphs/icon_back_x.gif differ diff --git a/glyphs/icon_certificate.gif b/glyphs/icon_certificate.gif new file mode 100644 index 00000000..89b529f7 Binary files /dev/null and b/glyphs/icon_certificate.gif differ diff --git a/glyphs/icon_eye.gif b/glyphs/icon_eye.gif new file mode 100644 index 00000000..df4bb829 Binary files /dev/null and b/glyphs/icon_eye.gif differ diff --git a/glyphs/icon_toggle_reset.gif b/glyphs/icon_toggle_reset.gif new file mode 100644 index 00000000..450bc869 Binary files /dev/null and b/glyphs/icon_toggle_reset.gif differ diff --git a/glyphs/icon_toggle_set.gif b/glyphs/icon_toggle_set.gif new file mode 100644 index 00000000..571264c7 Binary files /dev/null and b/glyphs/icon_toggle_set.gif differ diff --git a/glyphs/icon_warning.gif b/glyphs/icon_warning.gif new file mode 100644 index 00000000..1393e053 Binary files /dev/null and b/glyphs/icon_warning.gif differ diff --git a/src/algo_asa.c b/src/algo_asa.c new file mode 100644 index 00000000..b50c4426 --- /dev/null +++ b/src/algo_asa.c @@ -0,0 +1,43 @@ +#include "os.h" +#include "algo_asa.h" + + +#define ARRAY_SIZE(__arr) (sizeof(__arr) / sizeof(__arr[0])) + +#define ALGO_ASA(__id, __name, __unit, __decimals) { \ + .assetId = __id, \ + .decimals = __decimals, \ + .unit = __unit, \ + .name = __name, \ + } + + +static const algo_asset_info_t algo_assets[] = { + ALGO_ASA(438840, "Micro-Tesla", "M-TSLA", 0), + ALGO_ASA(438839, "Micro-Apple", "M-AAPL", 0), + ALGO_ASA(438838, "Micro-Google", "M-GOOGL", 0), + ALGO_ASA(438837, "Micro-Netflix", "M-NFLX", 0), + ALGO_ASA(438836, "Micro-Twitter", "M-TWTR", 0), + ALGO_ASA(438833, "Micro-Amazon", "M-AMZN", 0), + ALGO_ASA(438832, "Micro-Microsoft", "M-MSFT", 0), + ALGO_ASA(438831, "MESE Index Fund", "MESX", 6), + ALGO_ASA(438828, "MESE USD Exchange Token", "USD-MESE", 6), + ALGO_ASA(312769, "Tether USDt", "USDt", 6), + ALGO_ASA(163650, "Asia Reserve Currency Coin", "ARCC", 6), +}; + + +const algo_asset_info_t * +algo_asa_get(uint64_t id) +{ + const algo_asset_info_t *p; + const algo_asset_info_t *endp = algo_assets + ARRAY_SIZE(algo_assets); + + for (p = algo_assets; p && p < endp; p++) { + if (p->assetId == id) { + return p; + } + } + return NULL; +} + diff --git a/src/algo_asa.h b/src/algo_asa.h new file mode 100644 index 00000000..7dd8ada1 --- /dev/null +++ b/src/algo_asa.h @@ -0,0 +1,17 @@ +#ifndef __ALGO_ASA_H__ +#define __ALGO_ASA_H__ + +#define __packed __attribute__((packed)) + + +typedef struct { + uint64_t assetId; + uint8_t decimals; + const char unit[15]; + const char name[32]; +} __packed algo_asset_info_t; + + +const algo_asset_info_t *algo_asa_get(uint64_t id); + +#endif diff --git a/src/algo_keys.c b/src/algo_keys.c index 1aa13527..c1788343 100644 --- a/src/algo_keys.c +++ b/src/algo_keys.c @@ -8,8 +8,8 @@ void algorand_key_derive(uint32_t accountId, cx_ecfp_private_key_t *privateKey) { - static uint8_t privateKeyData[64]; - static uint32_t bip32Path[5]; + uint8_t privateKeyData[64]; + uint32_t bip32Path[5]; bip32Path[0] = 44 | 0x80000000; bip32Path[1] = 283 | 0x80000000; @@ -51,3 +51,17 @@ algorand_public_key(const cx_ecfp_private_key_t *privateKey, uint8_t *buf) } return 32; } + +size_t fetch_public_key(uint32_t accountId, uint8_t* pubkey){ + if(!current_pubkey.initialized || + current_pubkey.accountID != accountId){ + cx_ecfp_private_key_t privateKey; + algorand_key_derive(accountId, &privateKey); + algorand_public_key(&privateKey, current_pubkey.pubkey); + memset(&privateKey, 0, sizeof(privateKey)); + current_pubkey.accountID = accountId; + current_pubkey.initialized = true; + } + memcpy(pubkey, current_pubkey.pubkey, sizeof(current_pubkey.pubkey)); + return sizeof(current_pubkey.pubkey); +} \ No newline at end of file diff --git a/src/algo_keys.h b/src/algo_keys.h index 289c82f5..8b258c97 100644 --- a/src/algo_keys.h +++ b/src/algo_keys.h @@ -1,4 +1,13 @@ #include "cx.h" +#include "stdbool.h" + +typedef struct{ + bool initialized; + uint32_t accountID; + uint8_t pubkey[32]; +} already_computed_key_t; + +extern already_computed_key_t current_pubkey; void algorand_key_derive(uint32_t accountId, cx_ecfp_private_key_t *privateKey); -size_t algorand_public_key(const cx_ecfp_private_key_t *privateKey, uint8_t *buf); +size_t fetch_public_key(uint32_t accountId, uint8_t* pubkey); diff --git a/src/algo_tx.c b/src/algo_tx.c index 90062f0d..7202ab49 100644 --- a/src/algo_tx.c +++ b/src/algo_tx.c @@ -203,7 +203,7 @@ map_kv_params(uint8_t **p, uint8_t *e, char *key, struct asset_params *params) } unsigned int -tx_encode(struct txn *t, uint8_t *buf, int buflen) +tx_encode(txn_t *t, uint8_t *buf, int buflen) { char *typestr; switch (t->type) { diff --git a/src/algo_tx.h b/src/algo_tx.h index 5c0b8c60..2d4d6114 100644 --- a/src/algo_tx.h +++ b/src/algo_tx.h @@ -58,7 +58,7 @@ struct txn_asset_config { struct asset_params params; }; -struct txn { +typedef struct{ enum TXTYPE type; // Account Id asscociated with this transaction. uint32_t accountId; @@ -88,23 +88,24 @@ struct txn { struct txn_asset_freeze asset_freeze; struct txn_asset_config asset_config; }; -}; +} txn_t; // tx_encode produces a canonical msgpack encoding of a transaction. // buflen is the size of the buffer. The return value is the length // of the resulting encoding. -unsigned int tx_encode(struct txn *t, uint8_t *buf, int buflen); +unsigned int tx_encode(txn_t *t, uint8_t *buf, int buflen); // tx_decode takes a canonical msgpack encoding of a transaction, and -// unpacks it into a struct txn. The return value is NULL for success, +// unpacks it into a txn_t. The return value is NULL for success, // or a string describing the error on failure. Decoding may or may // not succeed for a non-canonical encoding. -char* tx_decode(uint8_t *buf, int buflen, struct txn *t); +char* tx_decode(uint8_t *buf, int buflen, txn_t *t); // We have a global transaction that is the subject of the current // operation, if any. -extern struct txn current_txn; +extern txn_t current_txn; // Two callbacks into the main code: approve and deny signing. void txn_approve(); -void txn_deny(); +void address_approve(); +void user_approval_denied(); diff --git a/src/algo_tx_dec.c b/src/algo_tx_dec.c index 56c6ae0e..15cdef14 100644 --- a/src/algo_tx_dec.c +++ b/src/algo_tx_dec.c @@ -212,7 +212,7 @@ decode_asset_params(uint8_t **bufp, uint8_t *buf_end, struct asset_params *res) } char* -tx_decode(uint8_t *buf, int buflen, struct txn *t) +tx_decode(uint8_t *buf, int buflen, txn_t *t) { char* ret = NULL; uint8_t* buf_end = buf + buflen; diff --git a/src/algo_ui.h b/src/algo_ui.h index 83898431..f055818b 100644 --- a/src/algo_ui.h +++ b/src/algo_ui.h @@ -1,62 +1,16 @@ -#if defined(TARGET_NANOX) #include "ux.h" -#endif // TARGET_NANOX extern char lineBuffer[]; +extern char caption[20]; extern char text[128]; -void ui_loading(); void ui_idle(); -void ui_address(); +void ui_address_approval(); void ui_txn(); +void ux_approve_txn(); void ui_text_put(const char *msg); void ui_text_putn(const char *msg, size_t maxlen); -int ui_text_more(); -void step_address(); - -// Override some of the Ledger X UI macros to enable step skipping -#if defined(TARGET_NANOX) - -// If going backwards, skip backwards. Otherwise, skip forwards. -#define SKIPEMPTY(stepnum) \ - if (stepnum < ux_last_step) { \ - ux_flow_prev(); \ - } else { \ - ux_flow_next(); \ - } \ - return - -#define ALGO_UX_STEP_NOCB_INIT(txtype, stepnum, layoutkind, preinit, ...) \ - void txn_flow_ ## stepnum ##_init (unsigned int stack_slot) { \ - if (txtype != ALL_TYPES && txtype != current_txn.type) { SKIPEMPTY(stepnum); }; \ - if (preinit == 0) { SKIPEMPTY(stepnum); }; \ - ux_last_step = stepnum; \ - ux_layout_ ## layoutkind ## _init(stack_slot); \ - } \ - const ux_layout_ ## layoutkind ## _params_t txn_flow_ ## stepnum ##_val = __VA_ARGS__; \ - const ux_flow_step_t txn_flow_ ## stepnum = { \ - txn_flow_ ## stepnum ## _init, \ - & txn_flow_ ## stepnum ## _val, \ - NULL, \ - NULL, \ - } - -#define ALGO_UX_STEP(stepnum, layoutkind, preinit, timeout_ms, validate_cb, error_flow, ...) \ - UX_FLOW_CALL(txn_flow_ ## stepnum ## _validate, { validate_cb; }) \ - void txn_flow_ ## stepnum ##_init (unsigned int stack_slot) { \ - preinit; \ - ux_last_step = stepnum; \ - ux_layout_ ## layoutkind ## _init(stack_slot); \ - ux_layout_set_timeout(stack_slot, timeout_ms);\ - } \ - const ux_layout_ ## layoutkind ## _params_t txn_flow_ ## stepnum ##_val = __VA_ARGS__; \ - const ux_flow_step_t txn_flow_ ## stepnum = { \ - txn_flow_ ## stepnum ## _init, \ - & txn_flow_ ## stepnum ## _val, \ - txn_flow_ ## stepnum ## _validate, \ - error_flow, \ - } - -#endif // TARGET_NANOX +#define ALGORAND_PUBLIC_KEY_SIZE 32 +#define ALGORAND_DECIMALS 6 \ No newline at end of file diff --git a/src/glyphs.c b/src/glyphs.c deleted file mode 100644 index 554a56c8..00000000 --- a/src/glyphs.c +++ /dev/null @@ -1,108 +0,0 @@ -#include "glyphs.h" -unsigned int const C_icon_crossmark_colors[] = { - 0x00000000, - 0x00ffffff, -}; - -unsigned char const C_icon_crossmark_bitmap[] = { - 0x00, 0x80, 0x01, 0xe6, 0xc0, 0x71, 0x38, 0x38, 0x07, 0xfc, 0x00, 0x1e, 0x80, 0x07, 0xf0, 0x03, - 0xce, 0xc1, 0xe1, 0x38, 0x70, 0x06, 0x18, 0x00, 0x00, -}; -#ifdef OS_IO_SEPROXYHAL - #include "os_io_seproxyhal.h" - const bagl_icon_details_t C_icon_crossmark = { GLYPH_icon_crossmark_WIDTH, GLYPH_icon_crossmark_HEIGHT, 1, C_icon_crossmark_colors, C_icon_crossmark_bitmap }; - #endif // OS_IO_SEPROXYHAL -#include "glyphs.h" -unsigned int const C_icon_dashboard_colors[] = { - 0x00000000, - 0x00ffffff, -}; - -unsigned char const C_icon_dashboard_bitmap[] = { - 0xe0, 0x01, 0xfe, 0xc1, 0xff, 0x38, 0x70, 0x06, 0xd8, 0x79, 0x7e, 0x9e, 0x9f, 0xe7, 0xe7, 0xb9, - 0x01, 0xe6, 0xc0, 0xf1, 0x3f, 0xf8, 0x07, 0x78, 0x00, -}; -#ifdef OS_IO_SEPROXYHAL - #include "os_io_seproxyhal.h" - const bagl_icon_details_t C_icon_dashboard = { GLYPH_icon_dashboard_WIDTH, GLYPH_icon_dashboard_HEIGHT, 1, C_icon_dashboard_colors, C_icon_dashboard_bitmap }; - #endif // OS_IO_SEPROXYHAL -#include "glyphs.h" -unsigned int const C_icon_dashboard_x_colors[] = { - 0x00000000, - 0x00ffffff, -}; - -unsigned char const C_icon_dashboard_x_bitmap[] = { - 0x00, 0x00, 0x00, 0x00, 0x0c, 0x80, 0x07, 0xf0, 0x03, 0xfe, 0xc1, 0xff, 0xf0, 0x3f, 0xf0, 0x03, - 0xcc, 0x00, 0x33, 0xc0, 0x0c, 0x00, 0x00, 0x00, 0x00, -}; -#ifdef OS_IO_SEPROXYHAL - #include "os_io_seproxyhal.h" - const bagl_icon_details_t C_icon_dashboard_x = { GLYPH_icon_dashboard_x_WIDTH, GLYPH_icon_dashboard_x_HEIGHT, 1, C_icon_dashboard_x_colors, C_icon_dashboard_x_bitmap }; - #endif // OS_IO_SEPROXYHAL -#include "glyphs.h" -unsigned int const C_icon_down_colors[] = { - 0x00000000, - 0x00ffffff, -}; - -unsigned char const C_icon_down_bitmap[] = { - 0x41, 0x11, 0x05, 0x01, -}; -#ifdef OS_IO_SEPROXYHAL - #include "os_io_seproxyhal.h" - const bagl_icon_details_t C_icon_down = { GLYPH_icon_down_WIDTH, GLYPH_icon_down_HEIGHT, 1, C_icon_down_colors, C_icon_down_bitmap }; - #endif // OS_IO_SEPROXYHAL -#include "glyphs.h" -unsigned int const C_icon_left_colors[] = { - 0x00000000, - 0x00ffffff, -}; - -unsigned char const C_icon_left_bitmap[] = { - 0x48, 0x12, 0x42, 0x08, -}; -#ifdef OS_IO_SEPROXYHAL - #include "os_io_seproxyhal.h" - const bagl_icon_details_t C_icon_left = { GLYPH_icon_left_WIDTH, GLYPH_icon_left_HEIGHT, 1, C_icon_left_colors, C_icon_left_bitmap }; - #endif // OS_IO_SEPROXYHAL -#include "glyphs.h" -unsigned int const C_icon_right_colors[] = { - 0x00000000, - 0x00ffffff, -}; - -unsigned char const C_icon_right_bitmap[] = { - 0x21, 0x84, 0x24, 0x01, -}; -#ifdef OS_IO_SEPROXYHAL - #include "os_io_seproxyhal.h" - const bagl_icon_details_t C_icon_right = { GLYPH_icon_right_WIDTH, GLYPH_icon_right_HEIGHT, 1, C_icon_right_colors, C_icon_right_bitmap }; - #endif // OS_IO_SEPROXYHAL -#include "glyphs.h" -unsigned int const C_icon_up_colors[] = { - 0x00000000, - 0x00ffffff, -}; - -unsigned char const C_icon_up_bitmap[] = { - 0x08, 0x8a, 0x28, 0x08, -}; -#ifdef OS_IO_SEPROXYHAL - #include "os_io_seproxyhal.h" - const bagl_icon_details_t C_icon_up = { GLYPH_icon_up_WIDTH, GLYPH_icon_up_HEIGHT, 1, C_icon_up_colors, C_icon_up_bitmap }; - #endif // OS_IO_SEPROXYHAL -#include "glyphs.h" -unsigned int const C_icon_validate_14_colors[] = { - 0x00000000, - 0x00ffffff, -}; - -unsigned char const C_icon_validate_14_bitmap[] = { - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x38, 0x00, 0x67, 0xe0, 0x38, 0x1c, 0x9c, 0x03, - 0x7e, 0x00, 0x0f, 0x80, 0x01, 0x00, 0x00, 0x00, 0x00, -}; -#ifdef OS_IO_SEPROXYHAL - #include "os_io_seproxyhal.h" - const bagl_icon_details_t C_icon_validate_14 = { GLYPH_icon_validate_14_WIDTH, GLYPH_icon_validate_14_HEIGHT, 1, C_icon_validate_14_colors, C_icon_validate_14_bitmap }; - #endif // OS_IO_SEPROXYHAL diff --git a/src/glyphs.h b/src/glyphs.h deleted file mode 100644 index 8d57a5cc..00000000 --- a/src/glyphs.h +++ /dev/null @@ -1,88 +0,0 @@ -#ifndef GLYPH_icon_crossmark_BPP - #define GLYPH_icon_crossmark_WIDTH 14 - #define GLYPH_icon_crossmark_HEIGHT 14 - #define GLYPH_icon_crossmark_BPP 1 -extern unsigned int const C_icon_crossmark_colors[]; -extern unsigned char const C_icon_crossmark_bitmap[]; -#ifdef OS_IO_SEPROXYHAL - #include "os_io_seproxyhal.h" - extern const bagl_icon_details_t C_icon_crossmark; - #endif // GLYPH_icon_crossmark_BPP - #endif // OS_IO_SEPROXYHAL -#ifndef GLYPH_icon_dashboard_BPP - #define GLYPH_icon_dashboard_WIDTH 14 - #define GLYPH_icon_dashboard_HEIGHT 14 - #define GLYPH_icon_dashboard_BPP 1 -extern unsigned int const C_icon_dashboard_colors[]; -extern unsigned char const C_icon_dashboard_bitmap[]; -#ifdef OS_IO_SEPROXYHAL - #include "os_io_seproxyhal.h" - extern const bagl_icon_details_t C_icon_dashboard; - #endif // GLYPH_icon_dashboard_BPP - #endif // OS_IO_SEPROXYHAL -#ifndef GLYPH_icon_dashboard_x_BPP - #define GLYPH_icon_dashboard_x_WIDTH 14 - #define GLYPH_icon_dashboard_x_HEIGHT 14 - #define GLYPH_icon_dashboard_x_BPP 1 -extern unsigned int const C_icon_dashboard_x_colors[]; -extern unsigned char const C_icon_dashboard_x_bitmap[]; -#ifdef OS_IO_SEPROXYHAL - #include "os_io_seproxyhal.h" - extern const bagl_icon_details_t C_icon_dashboard_x; - #endif // GLYPH_icon_dashboard_x_BPP - #endif // OS_IO_SEPROXYHAL -#ifndef GLYPH_icon_down_BPP - #define GLYPH_icon_down_WIDTH 7 - #define GLYPH_icon_down_HEIGHT 4 - #define GLYPH_icon_down_BPP 1 -extern unsigned int const C_icon_down_colors[]; -extern unsigned char const C_icon_down_bitmap[]; -#ifdef OS_IO_SEPROXYHAL - #include "os_io_seproxyhal.h" - extern const bagl_icon_details_t C_icon_down; - #endif // GLYPH_icon_down_BPP - #endif // OS_IO_SEPROXYHAL -#ifndef GLYPH_icon_left_BPP - #define GLYPH_icon_left_WIDTH 4 - #define GLYPH_icon_left_HEIGHT 7 - #define GLYPH_icon_left_BPP 1 -extern unsigned int const C_icon_left_colors[]; -extern unsigned char const C_icon_left_bitmap[]; -#ifdef OS_IO_SEPROXYHAL - #include "os_io_seproxyhal.h" - extern const bagl_icon_details_t C_icon_left; - #endif // GLYPH_icon_left_BPP - #endif // OS_IO_SEPROXYHAL -#ifndef GLYPH_icon_right_BPP - #define GLYPH_icon_right_WIDTH 4 - #define GLYPH_icon_right_HEIGHT 7 - #define GLYPH_icon_right_BPP 1 -extern unsigned int const C_icon_right_colors[]; -extern unsigned char const C_icon_right_bitmap[]; -#ifdef OS_IO_SEPROXYHAL - #include "os_io_seproxyhal.h" - extern const bagl_icon_details_t C_icon_right; - #endif // GLYPH_icon_right_BPP - #endif // OS_IO_SEPROXYHAL -#ifndef GLYPH_icon_up_BPP - #define GLYPH_icon_up_WIDTH 7 - #define GLYPH_icon_up_HEIGHT 4 - #define GLYPH_icon_up_BPP 1 -extern unsigned int const C_icon_up_colors[]; -extern unsigned char const C_icon_up_bitmap[]; -#ifdef OS_IO_SEPROXYHAL - #include "os_io_seproxyhal.h" - extern const bagl_icon_details_t C_icon_up; - #endif // GLYPH_icon_up_BPP - #endif // OS_IO_SEPROXYHAL -#ifndef GLYPH_icon_validate_14_BPP - #define GLYPH_icon_validate_14_WIDTH 14 - #define GLYPH_icon_validate_14_HEIGHT 14 - #define GLYPH_icon_validate_14_BPP 1 -extern unsigned int const C_icon_validate_14_colors[]; -extern unsigned char const C_icon_validate_14_bitmap[]; -#ifdef OS_IO_SEPROXYHAL - #include "os_io_seproxyhal.h" - extern const bagl_icon_details_t C_icon_validate_14; - #endif // GLYPH_icon_validate_14_BPP - #endif // OS_IO_SEPROXYHAL diff --git a/src/main.c b/src/main.c index baeca83a..b6f0be49 100644 --- a/src/main.c +++ b/src/main.c @@ -4,6 +4,7 @@ #include "algo_keys.h" #include "algo_ui.h" +#include "algo_addr.h" #include "algo_tx.h" unsigned char G_io_seproxyhal_spi_buffer[IO_SEPROXYHAL_BUFFER_SIZE_B]; @@ -20,6 +21,8 @@ unsigned char G_io_seproxyhal_spi_buffer[IO_SEPROXYHAL_BUFFER_SIZE_B]; #define P1_FIRST 0x00 #define P1_MORE 0x80 #define P1_WITH_ACCOUNT_ID 0x01 +#define P1_WITH_REQUEST_USER_APPROVAL 0x80 + #define P2_LAST 0x00 #define P2_MORE 0x80 @@ -33,7 +36,8 @@ unsigned char G_io_seproxyhal_spi_buffer[IO_SEPROXYHAL_BUFFER_SIZE_B]; #define INS_SIGN_MSGPACK 0x08 /* The transaction that we might ask the user to approve. */ -struct txn current_txn; +txn_t current_txn; +already_computed_key_t current_pubkey; /* A buffer for collecting msgpack-encoded transaction via APDUs, * as well as for msgpack-encoding transaction prior to signing. @@ -41,7 +45,7 @@ struct txn current_txn; #if defined(TARGET_NANOX) static uint8_t msgpack_buf[2048]; #else -static uint8_t msgpack_buf[1024]; +static uint8_t msgpack_buf[900]; #endif static unsigned int msgpack_next_off; @@ -84,8 +88,22 @@ txn_approve() ui_idle(); } +void address_approve() +{ + unsigned int tx = ALGORAND_PUBLIC_KEY_SIZE; + + G_io_apdu_buffer[tx++] = 0x90; + G_io_apdu_buffer[tx++] = 0x00; + + // Send back the response, do not restart the event loop + io_exchange(CHANNEL_APDU | IO_RETURN_AFTER_TX, tx); + + // Display back the original UX + ui_idle(); +} + void -txn_deny() +user_approval_denied() { G_io_apdu_buffer[0] = 0x69; G_io_apdu_buffer[1] = 0x85; @@ -104,6 +122,12 @@ copy_and_advance(void *dst, uint8_t **p, size_t len) *p += len; } +void init_globals(){ + memset(¤t_txn, 0, sizeof(current_txn)); + memset(¤t_pubkey, 0, sizeof(current_pubkey)); + fetch_public_key(0, text); +} + static void algorand_main(void) { @@ -113,11 +137,6 @@ algorand_main(void) msgpack_next_off = 0; -#if defined(TARGET_NANOS) - // next timer callback in 500 ms - UX_CALLBACK_SET_INTERVAL(500); -#endif - // DESIGN NOTE: the bootloader ignores the way APDU are fetched. The only // goal is to retrieve APDU. // When APDU are to be fetched from multiple IOs, like NFC+USB+BLE, make @@ -135,6 +154,8 @@ algorand_main(void) rx = io_exchange(CHANNEL_APDU | flags, rx); flags = 0; + PRINTF("New APDU received:\n%.*H\n", rx, G_io_apdu_buffer); + // no apdu received, well, reset the session, and reset the // bootloader configuration if (rx == 0) { @@ -256,8 +277,9 @@ algorand_main(void) } break; case INS_GET_PUBLIC_KEY: { - cx_ecfp_private_key_t privateKey; - uint32_t accountId = 0; + uint32_t accountId = 0; + char checksummed[65]; + uint8_t user_approval_required = G_io_apdu_buffer[OFFSET_P1] == P1_WITH_REQUEST_USER_APPROVAL; if (rx > OFFSET_LC) { uint8_t lc = G_io_apdu_buffer[OFFSET_LC]; @@ -272,9 +294,19 @@ algorand_main(void) * Push derived key to `G_io_apdu_buffer` * and return pushed buffer length. */ - algorand_key_derive(accountId, &privateKey); - tx = algorand_public_key(&privateKey, G_io_apdu_buffer); - THROW(0x9000); + fetch_public_key(accountId, G_io_apdu_buffer); + + if(user_approval_required){ + checksummed_addr(G_io_apdu_buffer, checksummed); + ui_text_put(checksummed); + ui_address_approval(); + flags |= IO_ASYNCH_REPLY; + } + else{ + tx = ALGORAND_PUBLIC_KEY_SIZE; + THROW(0x9000); + } + } break; case 0xFF: // return to dashboard @@ -350,12 +382,6 @@ unsigned char io_event(unsigned char channel) { case SEPROXYHAL_TAG_TICKER_EVENT: UX_TICKER_EVENT(G_io_seproxyhal_spi_buffer, { -#if defined(TARGET_NANOS) - // defaulty retrig very soon (will be overriden during - // stepper_prepro) - UX_CALLBACK_SET_INTERVAL(500); - UX_REDISPLAY(); -#endif }); break; } @@ -422,10 +448,6 @@ main(void) for (;;) { UX_INIT(); -#if defined(TARGET_NANOS) - UX_MENU_INIT(); -#endif - BEGIN_TRY { TRY { io_seproxyhal_init(); @@ -434,20 +456,17 @@ main(void) G_io_app.plane_mode = os_setting_get(OS_SETTING_PLANEMODE, NULL, 0); #endif + init_globals(); + USB_power(0); USB_power(1); - // Display a loading screen before (slowly) deriving keys. BLE_power - // also seems to be a bit slow, so show the loading screen here. - ui_loading(); - #if defined(TARGET_NANOX) BLE_power(0, NULL); BLE_power(1, "Nano X"); #endif ui_idle(); - algorand_main(); } CATCH(EXCEPTION_IO_RESET) { diff --git a/src/ui_address.c b/src/ui_address.c index 4c1166cb..cef9291a 100644 --- a/src/ui_address.c +++ b/src/ui_address.c @@ -2,74 +2,55 @@ #include "os_io_seproxyhal.h" #include "algo_ui.h" +#include "algo_tx.h" #include "algo_keys.h" #include "algo_addr.h" -#if defined(TARGET_NANOS) -static const bagl_element_t -bagl_ui_address_nanos[] = { - { {BAGL_RECTANGLE, 0x00, 0, 0, 128, 32, 0, 0, BAGL_FILL, 0x000000, 0xFFFFFF, - 0, 0}, - NULL, - 0, 0, 0, NULL, NULL, NULL, }, - { {BAGL_LABELINE, 0x02, 0, 12, 128, 11, 0, 0, 0, 0xFFFFFF, 0x000000, - BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, 0}, - "Public address", - 0, 0, 0, NULL, NULL, NULL, }, - { {BAGL_LABELINE, 0x02, 23, 26, 82, 11, 0x80 | 10, 0, 0, 0xFFFFFF, 0x000000, - BAGL_FONT_OPEN_SANS_REGULAR_11px | BAGL_FONT_ALIGNMENT_CENTER, 26}, - lineBuffer, - 0, 0, 0, NULL, NULL, NULL, }, - { {BAGL_ICON, 0x00, 3, 12, 7, 7, 0, 0, 0, 0xFFFFFF, 0x000000, 0, - BAGL_GLYPH_ICON_CROSS}, - NULL, - 0, 0, 0, NULL, NULL, NULL, }, - { {BAGL_ICON, 0x00, 117, 13, 8, 6, 0, 0, 0, 0xFFFFFF, 0x000000, 0, - BAGL_GLYPH_ICON_RIGHT}, - NULL, - 0, 0, 0, NULL, NULL, NULL, }, -}; -static unsigned int -bagl_ui_address_nanos_button(unsigned int button_mask, unsigned int button_mask_counter) -{ - switch (button_mask) { - case BUTTON_EVT_RELEASED | BUTTON_RIGHT: - if (ui_text_more()) { - UX_REDISPLAY(); - } else { - ui_idle(); - } - break; +UX_FLOW_DEF_NOCB( + ux_display_public_flow_1_step, + pnn, + { + &C_icon_eye, + "Verify", + "address", + }); +UX_FLOW_DEF_NOCB( + ux_display_public_flow_2_step, + bnnn_paging, + { + .title = "Address", + .text = text, + }); +UX_FLOW_DEF_VALID( + ux_display_public_flow_3_step, + pbb, + address_approve(), + { + &C_icon_validate_14, + "Approve", + "address", + }); +UX_FLOW_DEF_VALID( + ux_display_public_flow_4_step, + pb, + user_approval_denied(), + { + &C_icon_crossmark, + "Reject", + }); - case BUTTON_EVT_RELEASED | BUTTON_LEFT: - ui_idle(); - break; - } - return 0; -} +UX_FLOW(ux_address_approval_flow, + &ux_display_public_flow_1_step, + &ux_display_public_flow_2_step, + &ux_display_public_flow_3_step, + &ux_display_public_flow_4_step +); -void -ui_address() +void ui_address_approval() { - step_address(); - if (ui_text_more()) { - UX_DISPLAY(bagl_ui_address_nanos, NULL); - } else { - ui_idle(); + if (G_ux.stack_count == 0) { + ux_stack_push(); } -} -#endif - -void -step_address() -{ - char checksummed[65]; - uint8_t publicKey[32]; - cx_ecfp_private_key_t privateKey; - - algorand_key_derive(0, &privateKey); - algorand_public_key(&privateKey, publicKey); - checksummed_addr(publicKey, checksummed); - ui_text_put(checksummed); + ux_flow_init(0, ux_address_approval_flow, NULL); } diff --git a/src/ui_idle.c b/src/ui_idle.c index dd56f522..d56eb2eb 100644 --- a/src/ui_idle.c +++ b/src/ui_idle.c @@ -2,75 +2,43 @@ #include "os_io_seproxyhal.h" #include "algo_ui.h" -#if defined(TARGET_NANOS) -static const ux_menu_entry_t menu_top[]; -static const ux_menu_entry_t menu_about[]; +UX_FLOW_DEF_NOCB( + ux_idle_flow_welcome_step, + nn, //pnn, + { + //"", //&C_icon_dashboard, + "Application", + "is ready", + }); +UX_FLOW_DEF_NOCB( + ux_idle_flow_version_step, + bn, + { + "Version", + APPVERSION, + }); +UX_FLOW_DEF_VALID( + ux_idle_flow_exit_step, + pb, + os_sched_exit(-1), + { + &C_icon_dashboard_x, + "Quit", + }); +UX_FLOW(ux_idle_flow, + &ux_idle_flow_welcome_step, + &ux_idle_flow_version_step, + &ux_idle_flow_exit_step, + FLOW_LOOP +); -static const ux_menu_entry_t menu_about[] = { - {NULL, NULL, 0, NULL, "Version", APPVERSION, 0, 0}, - {menu_top, NULL, 1, NULL, "Back", NULL, 0, 0}, - UX_MENU_END -}; - -static const ux_menu_entry_t menu_top[] = { - {NULL, ui_address, 0, NULL, "Address", NULL, 0, 0}, - {menu_about, NULL, 0, NULL, "About", NULL, 0, 0}, - {NULL, os_sched_exit, 0, NULL, "Quit app", NULL, 0, 0}, - UX_MENU_END -}; - -static const ux_menu_entry_t menu_loading[] = { - {NULL, NULL, 0, NULL, "Loading...", "Please wait", 0, 0}, - UX_MENU_END -}; -#endif - -#if defined(TARGET_NANOX) -UX_STEP_NOCB(ux_idle_flow_1_step, bn, {"Version", APPVERSION}); -UX_STEP_NOCB_INIT(ux_idle_flow_2_step, bnnn_paging, step_address(), {"Address", text}); -UX_STEP_VALID(ux_idle_flow_3_step, pb, os_sched_exit(-1), {&C_icon_dashboard, "Quit"}); - -const ux_flow_step_t * const ux_idle_flow [] = { - &ux_idle_flow_1_step, - &ux_idle_flow_2_step, - &ux_idle_flow_3_step, - FLOW_END_STEP, -}; - -UX_STEP_NOCB(ux_loading_1_step, bn, {"Loading...", "Please wait"}); - -const ux_flow_step_t * const ux_loading_flow [] = { - &ux_loading_1_step, - FLOW_END_STEP, -}; -#endif - -void -ui_loading() -{ -#if defined(TARGET_NANOX) - // reserve a display stack slot if none yet - if(G_ux.stack_count == 0) { - ux_stack_push(); - } - ux_flow_init(0, ux_loading_flow, NULL); -#endif -#if defined(TARGET_NANOS) - UX_MENU_DISPLAY(0, menu_loading, NULL); -#endif -} void ui_idle() { -#if defined(TARGET_NANOX) // reserve a display stack slot if none yet if(G_ux.stack_count == 0) { ux_stack_push(); } ux_flow_init(0, ux_idle_flow, NULL); -#endif -#if defined(TARGET_NANOS) - UX_MENU_DISPLAY(0, menu_top, NULL); -#endif } diff --git a/src/ui_text.c b/src/ui_text.c index 9d3391d5..7508cf1c 100644 --- a/src/ui_text.c +++ b/src/ui_text.c @@ -12,39 +12,6 @@ static int lineBufferPos; // 1 extra byte for the null termination char lineBuffer[MAX_CHARS_PER_LINE+2+1]; -int -ui_text_more() -{ - int linePos; - - if (text[lineBufferPos] == '\0') { - lineBuffer[0] = '\0'; - return 0; - } - - for (linePos = 0; linePos < MAX_CHARS_PER_LINE; linePos++) { - if (text[lineBufferPos] == '\0') { - break; - } - - if (text[lineBufferPos] == '\n') { - lineBufferPos++; - break; - } - - lineBuffer[linePos] = text[lineBufferPos]; - lineBufferPos++; - } - - if (text[lineBufferPos] != '\0') { - lineBuffer[linePos++] = '.'; - lineBuffer[linePos++] = '.'; - } - - lineBuffer[linePos++] = '\0'; - return 1; -} - void ui_text_putn(const char *msg, size_t maxlen) { @@ -60,8 +27,6 @@ ui_text_putn(const char *msg, size_t maxlen) lineBufferPos = 0; PRINTF("ui_text_putn: text %s\n", &text[0]); - - /* Caller should invoke ui_text_more() after ui_text_putn(). */ } void diff --git a/src/ui_txn.c b/src/ui_txn.c index 4b5271a2..6d1112ba 100644 --- a/src/ui_txn.c +++ b/src/ui_txn.c @@ -6,13 +6,28 @@ #include "algo_tx.h" #include "algo_addr.h" #include "algo_keys.h" +#include "algo_asa.h" #include "base64.h" #include "glyphs.h" +bool is_opt_in_tx(){ + if(current_txn.type == ASSET_XFER && + current_txn.payment.amount == 0 && + current_txn.asset_xfer.id != 0 && + memcmp(current_txn.asset_xfer.receiver, + current_txn.asset_xfer.sender, + sizeof(current_txn.asset_xfer.receiver)) == 0){ + return true; + } + return false; +} + +char caption[20]; + static char * u64str(uint64_t v) { - static char buf[24]; + static char buf[27]; char *p = &buf[sizeof(buf)]; *(--p) = '\0'; @@ -30,6 +45,81 @@ u64str(uint64_t v) return p; } +bool adjustDecimals(char *src, uint32_t srcLength, char *target, + uint32_t targetLength, uint8_t decimals) { + uint32_t startOffset; + uint32_t lastZeroOffset = 0; + uint32_t offset = 0; + if ((srcLength == 1) && (*src == '0')) { + if (targetLength < 2) { + return false; + } + target[0] = '0'; + target[1] = '\0'; + return true; + } + if (srcLength <= decimals) { + uint32_t delta = decimals - srcLength; + if (targetLength < srcLength + 1 + 2 + delta) { + return false; + } + target[offset++] = '0'; + target[offset++] = '.'; + for (uint32_t i = 0; i < delta; i++) { + target[offset++] = '0'; + } + startOffset = offset; + for (uint32_t i = 0; i < srcLength; i++) { + target[offset++] = src[i]; + } + target[offset] = '\0'; + } else { + uint32_t sourceOffset = 0; + uint32_t delta = srcLength - decimals; + if (targetLength < srcLength + 1 + 1) { + return false; + } + while (offset < delta) { + target[offset++] = src[sourceOffset++]; + } + if (decimals != 0) { + target[offset++] = '.'; + } + startOffset = offset; + while (sourceOffset < srcLength) { + target[offset++] = src[sourceOffset++]; + } + target[offset] = '\0'; + } + for (uint32_t i = startOffset; i < offset; i++) { + if (target[i] == '0') { + if (lastZeroOffset == 0) { + lastZeroOffset = i; + } + } else { + lastZeroOffset = 0; + } + } + if (lastZeroOffset != 0) { + target[lastZeroOffset] = '\0'; + if (target[lastZeroOffset - 1] == '.') { + target[lastZeroOffset - 1] = '\0'; + } + } + return true; +} + +static char* +amount_to_str(uint64_t amount, uint8_t decimals){ + char* result = u64str(amount); + char tmp[24]; + memcpy(tmp, result, sizeof(tmp)); + memset(result, 0, sizeof(tmp)); + adjustDecimals(tmp, strlen(tmp), result, 27, decimals); + result[26] = '\0'; + return result; +} + static int all_zero_key(uint8_t *buf) { @@ -53,7 +143,11 @@ static int step_txn_type() { break; case ASSET_XFER: - ui_text_put("Asset xfer"); + if(is_opt_in_tx()){ + ui_text_put("Opt-in"); + }else{ + ui_text_put("Asset xfer"); + } break; case ASSET_FREEZE: @@ -72,9 +166,7 @@ static int step_txn_type() { static int step_sender() { uint8_t publicKey[32]; - cx_ecfp_private_key_t privateKey; - algorand_key_derive(current_txn.accountId, &privateKey); - algorand_public_key(&privateKey, publicKey); + fetch_public_key(current_txn.accountId, publicKey); if (os_memcmp(publicKey, current_txn.sender, sizeof(current_txn.sender)) == 0) { return 0; } @@ -97,19 +189,19 @@ static int step_rekey() { } static int step_fee() { - ui_text_put(u64str(current_txn.fee)); + ui_text_put(amount_to_str(current_txn.fee, ALGORAND_DECIMALS)); return 1; } -static int step_firstvalid() { - ui_text_put(u64str(current_txn.firstValid)); - return 1; -} +// static int step_firstvalid() { +// ui_text_put(u64str(current_txn.firstValid)); +// return 1; +// } -static int step_lastvalid() { - ui_text_put(u64str(current_txn.lastValid)); - return 1; -} +// static int step_lastvalid() { +// ui_text_put(u64str(current_txn.lastValid)); +// return 1; +// } static const char* default_genesisID = "mainnet-v1.0"; static const uint8_t default_genesisHash[] = { @@ -166,7 +258,7 @@ static int step_receiver() { } static int step_amount() { - ui_text_put(u64str(current_txn.payment.amount)); + ui_text_put(amount_to_str(current_txn.payment.amount, ALGORAND_DECIMALS)); return 1; } @@ -220,12 +312,30 @@ static int step_participating() { } static int step_asset_xfer_id() { - ui_text_put(u64str(current_txn.asset_xfer.id)); + const algo_asset_info_t *asa = algo_asa_get(current_txn.asset_xfer.id); + const char *id = u64str(current_txn.asset_xfer.id); + + if (asa == NULL) { + snprintf(text, sizeof(text), "#%s", id); + } else { + snprintf(text, sizeof(text), "%s (#%s)", asa->name, id); + } return 1; } static int step_asset_xfer_amount() { - ui_text_put(u64str(current_txn.asset_xfer.amount)); + if(is_opt_in_tx()){ + return 0; + } + + const algo_asset_info_t *asa = algo_asa_get(current_txn.asset_xfer.id); + if (asa != NULL) { + snprintf(caption, sizeof(caption), "Amount (%s)", asa->unit); + ui_text_put(amount_to_str(current_txn.asset_xfer.amount, asa->decimals)); + } else { + snprintf(caption, sizeof(caption), "Amount (base unit)"); + ui_text_put(u64str(current_txn.asset_xfer.amount)); + } return 1; } @@ -241,7 +351,8 @@ static int step_asset_xfer_sender() { } static int step_asset_xfer_receiver() { - if (all_zero_key(current_txn.asset_xfer.receiver)) { + if (all_zero_key(current_txn.asset_xfer.receiver) || + is_opt_in_tx()) { return 0; } @@ -392,286 +503,229 @@ static int step_asset_config_clawback() { return step_asset_config_addr_helper(current_txn.asset_config.params.clawback); } -#if defined(TARGET_NANOX) -static unsigned int ux_last_step; - -ALGO_UX_STEP_NOCB_INIT(ALL_TYPES, 0, bn, step_txn_type(), {"Txn type", text}); -ALGO_UX_STEP_NOCB_INIT(ALL_TYPES, 1, bnnn_paging, step_sender(), {"Sender", text}); -ALGO_UX_STEP_NOCB_INIT(ALL_TYPES, 2, bnnn_paging, step_rekey(), {"RekeyTo", text}); -ALGO_UX_STEP_NOCB_INIT(ALL_TYPES, 3, bn, step_fee(), {"Fee (uAlg)", text}); -ALGO_UX_STEP_NOCB_INIT(ALL_TYPES, 4, bn, step_firstvalid(), {"First valid", text}); -ALGO_UX_STEP_NOCB_INIT(ALL_TYPES, 5, bn, step_lastvalid(), {"Last valid", text}); -ALGO_UX_STEP_NOCB_INIT(ALL_TYPES, 6, bn, step_genesisID(), {"Genesis ID", text}); -ALGO_UX_STEP_NOCB_INIT(ALL_TYPES, 7, bnnn_paging, step_genesisHash(), {"Genesis hash", text}); -ALGO_UX_STEP_NOCB_INIT(ALL_TYPES, 8, bn, step_note(), {"Note", text}); - -ALGO_UX_STEP_NOCB_INIT(PAYMENT, 9, bnnn_paging, step_receiver(), {"Receiver", text}); -ALGO_UX_STEP_NOCB_INIT(PAYMENT, 10, bn, step_amount(), {"Amount (uAlg)", text}); -ALGO_UX_STEP_NOCB_INIT(PAYMENT, 11, bnnn_paging, step_close(), {"Close to", text}); - -ALGO_UX_STEP_NOCB_INIT(KEYREG, 12, bnnn_paging, step_votepk(), {"Vote PK", text}); -ALGO_UX_STEP_NOCB_INIT(KEYREG, 13, bnnn_paging, step_vrfpk(), {"VRF PK", text}); -ALGO_UX_STEP_NOCB_INIT(KEYREG, 14, bn, step_votefirst(), {"Vote first", text}); -ALGO_UX_STEP_NOCB_INIT(KEYREG, 15, bn, step_votelast(), {"Vote last", text}); -ALGO_UX_STEP_NOCB_INIT(KEYREG, 16, bn, step_keydilution(), {"Key dilution", text}); -ALGO_UX_STEP_NOCB_INIT(KEYREG, 17, bn, step_participating(), {"Participating", text}); - -ALGO_UX_STEP_NOCB_INIT(ASSET_XFER, 18, bn, step_asset_xfer_id(), {"Asset ID", text}); -ALGO_UX_STEP_NOCB_INIT(ASSET_XFER, 19, bn, step_asset_xfer_amount(), {"Asset amt", text}); -ALGO_UX_STEP_NOCB_INIT(ASSET_XFER, 20, bnnn_paging, step_asset_xfer_sender(), {"Asset src", text}); -ALGO_UX_STEP_NOCB_INIT(ASSET_XFER, 21, bnnn_paging, step_asset_xfer_receiver(), {"Asset dst", text}); -ALGO_UX_STEP_NOCB_INIT(ASSET_XFER, 22, bnnn_paging, step_asset_xfer_close(), {"Asset close", text}); - -ALGO_UX_STEP_NOCB_INIT(ASSET_FREEZE, 23, bn, step_asset_freeze_id(), {"Asset ID", text}); -ALGO_UX_STEP_NOCB_INIT(ASSET_FREEZE, 24, bnnn_paging, step_asset_freeze_account(), {"Asset account", text}); -ALGO_UX_STEP_NOCB_INIT(ASSET_FREEZE, 25, bn, step_asset_freeze_flag(), {"Freeze flag", text}); - -ALGO_UX_STEP_NOCB_INIT(ASSET_CONFIG, 26, bn, step_asset_config_id(), {"Asset ID", text}); -ALGO_UX_STEP_NOCB_INIT(ASSET_CONFIG, 27, bn, step_asset_config_total(), {"Total units", text}); -ALGO_UX_STEP_NOCB_INIT(ASSET_CONFIG, 28, bn, step_asset_config_default_frozen(), {"Default frozen", text}); -ALGO_UX_STEP_NOCB_INIT(ASSET_CONFIG, 29, bnnn_paging, step_asset_config_unitname(), {"Unit name", text}); -ALGO_UX_STEP_NOCB_INIT(ASSET_CONFIG, 30, bn, step_asset_config_decimals(), {"Decimals", text}); -ALGO_UX_STEP_NOCB_INIT(ASSET_CONFIG, 31, bnnn_paging, step_asset_config_assetname(), {"Asset name", text}); -ALGO_UX_STEP_NOCB_INIT(ASSET_CONFIG, 32, bnnn_paging, step_asset_config_url(), {"URL", text}); -ALGO_UX_STEP_NOCB_INIT(ASSET_CONFIG, 33, bnnn_paging, step_asset_config_metadata_hash(), {"Metadata hash", text}); -ALGO_UX_STEP_NOCB_INIT(ASSET_CONFIG, 34, bnnn_paging, step_asset_config_manager(), {"Manager", text}); -ALGO_UX_STEP_NOCB_INIT(ASSET_CONFIG, 35, bnnn_paging, step_asset_config_reserve(), {"Reserve", text}); -ALGO_UX_STEP_NOCB_INIT(ASSET_CONFIG, 36, bnnn_paging, step_asset_config_freeze(), {"Freezer", text}); -ALGO_UX_STEP_NOCB_INIT(ASSET_CONFIG, 37, bnnn_paging, step_asset_config_clawback(), {"Clawback", text}); - -ALGO_UX_STEP(38, pbb, NULL, 0, txn_approve(), NULL, {&C_icon_validate_14, "Sign", "transaction"}); -ALGO_UX_STEP(39, pbb, NULL, 0, txn_deny(), NULL, {&C_icon_crossmark, "Cancel", "signature"}); - -const ux_flow_step_t * const ux_txn_flow [] = { - &txn_flow_0, - &txn_flow_1, - &txn_flow_2, - &txn_flow_3, - &txn_flow_4, - &txn_flow_5, - &txn_flow_6, - &txn_flow_7, - &txn_flow_8, - &txn_flow_9, - &txn_flow_10, - &txn_flow_11, - &txn_flow_12, - &txn_flow_13, - &txn_flow_14, - &txn_flow_15, - &txn_flow_16, - &txn_flow_17, - &txn_flow_18, - &txn_flow_19, - &txn_flow_20, - &txn_flow_21, - &txn_flow_22, - &txn_flow_23, - &txn_flow_24, - &txn_flow_25, - &txn_flow_26, - &txn_flow_27, - &txn_flow_28, - &txn_flow_29, - &txn_flow_30, - &txn_flow_31, - &txn_flow_32, - &txn_flow_33, - &txn_flow_34, - &txn_flow_35, - &txn_flow_36, - &txn_flow_37, - &txn_flow_38, - &txn_flow_39, - FLOW_END_STEP, -}; -#endif // TARGET_NANOX - -#if defined(TARGET_NANOS) -struct ux_step { - // The display callback returns a non-zero value if it placed information - // about the associated caption into lineBuffer, which should be displayed. - // If it returns 0, the approval flow moves on to the next step. The - // callback is invoked only if the transaction type matches txtype. - int txtype; - const char *caption; - int (*display)(void); -}; - -static unsigned int ux_current_step; -static const struct ux_step ux_steps[] = { - { ALL_TYPES, "Txn type", &step_txn_type }, - { ALL_TYPES, "Sender", &step_sender }, - { ALL_TYPES, "RekeyTo", &step_rekey }, - { ALL_TYPES, "Fee (uAlg)", &step_fee }, - { ALL_TYPES, "First valid", &step_firstvalid }, - { ALL_TYPES, "Last valid", &step_lastvalid }, - { ALL_TYPES, "Genesis ID", &step_genesisID }, - { ALL_TYPES, "Genesis hash", &step_genesisHash }, - { ALL_TYPES, "Note", &step_note }, - { PAYMENT, "Receiver", &step_receiver }, - { PAYMENT, "Amount (uAlg)", &step_amount }, - { PAYMENT, "Close to", &step_close }, - { KEYREG, "Vote PK", &step_votepk }, - { KEYREG, "VRF PK", &step_vrfpk }, - { KEYREG, "Vote first", &step_votefirst }, - { KEYREG, "Vote last", &step_votelast }, - { KEYREG, "Key dilution", &step_keydilution }, - { KEYREG, "Participating", &step_participating }, - { ASSET_XFER, "Asset ID", &step_asset_xfer_id }, - { ASSET_XFER, "Asset amt", &step_asset_xfer_amount }, - { ASSET_XFER, "Asset src", &step_asset_xfer_sender }, - { ASSET_XFER, "Asset dst", &step_asset_xfer_receiver }, - { ASSET_XFER, "Asset close", &step_asset_xfer_close }, - { ASSET_FREEZE, "Asset ID", &step_asset_freeze_id }, - { ASSET_FREEZE, "Asset account", &step_asset_freeze_account }, - { ASSET_FREEZE, "Freeze flag", &step_asset_freeze_flag }, - { ASSET_CONFIG, "Asset ID", &step_asset_config_id }, - { ASSET_CONFIG, "Total units", &step_asset_config_total }, - { ASSET_CONFIG, "Default frozen", &step_asset_config_default_frozen }, - { ASSET_CONFIG, "Unit name", &step_asset_config_unitname }, - { ASSET_CONFIG, "Decimals", &step_asset_config_decimals }, - { ASSET_CONFIG, "Asset name", &step_asset_config_assetname }, - { ASSET_CONFIG, "URL", &step_asset_config_url }, - { ASSET_CONFIG, "Metadata hash", &step_asset_config_metadata_hash }, - { ASSET_CONFIG, "Manager", &step_asset_config_manager }, - { ASSET_CONFIG, "Reserve", &step_asset_config_reserve }, - { ASSET_CONFIG, "Freezer", &step_asset_config_freeze }, - { ASSET_CONFIG, "Clawback", &step_asset_config_clawback }, -}; - -static const bagl_element_t bagl_ui_approval_nanos[] = { - { {BAGL_RECTANGLE, 0x00, 0, 0, 128, 32, 0, 0, BAGL_FILL, 0x000000, 0xFFFFFF, 0, 0}, - NULL, 0, 0, 0, NULL, NULL, NULL, }, - { {BAGL_LABELINE, 0x02, 0, 12, 128, 11, 0, 0, 0, 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_REGULAR_11px | BAGL_FONT_ALIGNMENT_CENTER, 0}, - "Sign transaction", 0, 0, 0, NULL, NULL, NULL, }, - { {BAGL_ICON, 0x00, 3, 12, 7, 7, 0, 0, 0, 0xFFFFFF, 0x000000, 0, BAGL_GLYPH_ICON_CROSS}, - NULL, 0, 0, 0, NULL, NULL, NULL, }, - { {BAGL_ICON, 0x00, 117, 13, 8, 6, 0, 0, 0, 0xFFFFFF, 0x000000, 0, BAGL_GLYPH_ICON_CHECK}, - NULL, 0, 0, 0, NULL, NULL, NULL, }, -}; - -static unsigned int -bagl_ui_approval_nanos_button(unsigned int button_mask, unsigned int button_mask_counter) -{ - switch (button_mask) { - case BUTTON_EVT_RELEASED | BUTTON_RIGHT: - txn_approve(); - break; - - case BUTTON_EVT_RELEASED | BUTTON_LEFT: - txn_deny(); - break; - } - return 0; -} - -static char captionBuffer[32]; - -static const bagl_element_t bagl_ui_step_nanos[] = { - { {BAGL_RECTANGLE, 0x00, 0, 0, 128, 32, 0, 0, BAGL_FILL, 0x000000, 0xFFFFFF, - 0, 0}, - NULL, 0, 0, 0, NULL, NULL, NULL, }, - - /* Caption */ - { {BAGL_LABELINE, 0x02, 0, 12, 128, 11, 0, 0, 0, 0xFFFFFF, 0x000000, - BAGL_FONT_OPEN_SANS_REGULAR_11px | BAGL_FONT_ALIGNMENT_CENTER, 0}, - captionBuffer, 0, 0, 0, NULL, NULL, NULL, }, - - /* Value */ - { {BAGL_LABELINE, 0x02, 23, 26, 82, 11, 0x80 | 10, 0, 0, 0xFFFFFF, 0x000000, - BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, 26}, - lineBuffer, 0, 0, 0, NULL, NULL, NULL, }, - - { {BAGL_ICON, 0x00, 3, 12, 7, 7, 0, 0, 0, 0xFFFFFF, 0x000000, - 0, BAGL_GLYPH_ICON_CROSS}, - NULL, 0, 0, 0, NULL, NULL, NULL, }, - { {BAGL_ICON, 0x00, 117, 13, 8, 6, 0, 0, 0, 0xFFFFFF, 0x000000, - 0, BAGL_GLYPH_ICON_RIGHT}, - NULL, 0, 0, 0, NULL, NULL, NULL, }, +typedef int (*format_function_t)(); +typedef struct{ + char* caption; + format_function_t value_setter; + uint8_t type; +} screen_t; + +#define SCREEN_DYN_CAPTION NULL + +screen_t const screen_table[] = { + {"Txn type", &step_txn_type, ALL_TYPES}, + {"Sender", &step_sender, ALL_TYPES}, + {"Rekey to", &step_rekey, ALL_TYPES}, + {"Fee (Alg)", &step_fee, ALL_TYPES}, + // {"First valid", step_firstvalid, ALL_TYPES}, + // {"Last valid", step_lastvalid, ALL_TYPES}, + {"Genesis ID", &step_genesisID, ALL_TYPES}, + {"Genesis hash", &step_genesisHash, ALL_TYPES}, + {"Note", &step_note, ALL_TYPES}, + {"Receiver", &step_receiver, PAYMENT}, + {"Amount (Alg)", step_amount, PAYMENT}, + {"Close to", &step_close, PAYMENT}, + {"Vote PK", &step_votepk, KEYREG}, + {"VRF PK", &step_vrfpk, KEYREG}, + {"Vote first", &step_votefirst, KEYREG}, + {"Vote last", &step_votelast, KEYREG}, + {"Key dilution", &step_keydilution, KEYREG}, + {"Participating", &step_participating, KEYREG}, + {"Asset ID", &step_asset_xfer_id, ASSET_XFER}, + {SCREEN_DYN_CAPTION, &step_asset_xfer_amount, ASSET_XFER}, + {"Asset src", &step_asset_xfer_sender, ASSET_XFER}, + {"Asset dst", &step_asset_xfer_receiver, ASSET_XFER}, + {"Asset close", &step_asset_xfer_close, ASSET_XFER}, + {"Asset ID", &step_asset_freeze_id, ASSET_FREEZE}, + {"Asset account", &step_asset_freeze_account, ASSET_FREEZE}, + {"Freeze flag", &step_asset_freeze_flag, ASSET_FREEZE}, + {"Asset ID", &step_asset_config_id, ASSET_CONFIG}, + {"Total units", &step_asset_config_total, ASSET_CONFIG}, + {"Default frozen", &step_asset_config_default_frozen, ASSET_CONFIG}, + {"Unit name", &step_asset_config_unitname, ASSET_CONFIG}, + {"Decimals", &step_asset_config_decimals, ASSET_CONFIG}, + {"Asset name", &step_asset_config_assetname, ASSET_CONFIG}, + {"URL", &step_asset_config_url, ASSET_CONFIG}, + {"Metadata hash", &step_asset_config_metadata_hash, ASSET_CONFIG}, + {"Manager", &step_asset_config_manager, ASSET_CONFIG}, + {"Reserve", &step_asset_config_reserve, ASSET_CONFIG}, + {"Freezer", &step_asset_config_freeze, ASSET_CONFIG}, + {"Clawback", &step_asset_config_clawback, ASSET_CONFIG} }; -static void bagl_ui_step_nanos_display(); - -static unsigned int -bagl_ui_step_nanos_button(unsigned int button_mask, unsigned int button_mask_counter) -{ - switch (button_mask) { - case BUTTON_EVT_RELEASED | BUTTON_RIGHT: - if (ui_text_more()) { - UX_REDISPLAY(); - return 0; +#define SCREEN_NUM (int8_t)(sizeof(screen_table)/sizeof(screen_t)) + +void display_next_state(bool is_upper_border); + +UX_STEP_NOCB( + ux_confirm_tx_init_flow_step, + pnn, + { + &C_icon_eye, + "Review", + "Transaction", + }); + +UX_STEP_INIT( + ux_init_upper_border, + NULL, + NULL, + { + display_next_state(true); + }); +UX_STEP_NOCB( + ux_variable_display, + bnnn_paging, + { + .title = caption, + .text = text, + }); +UX_STEP_INIT( + ux_init_lower_border, + NULL, + NULL, + { + display_next_state(false); + }); + +UX_FLOW_DEF_VALID( + ux_confirm_tx_finalize_step, + pnn, + txn_approve(), + { + &C_icon_validate_14, + "Sign", + "Transaction", + }); + +UX_FLOW_DEF_VALID( + ux_reject_tx_flow_step, + pnn, + user_approval_denied(), + { + &C_icon_crossmark, + "Cancel", + "Transaction" + }); + +UX_FLOW(ux_txn_flow, + &ux_confirm_tx_init_flow_step, + + &ux_init_upper_border, + &ux_variable_display, + &ux_init_lower_border, + + &ux_confirm_tx_finalize_step, + &ux_reject_tx_flow_step +); + +volatile int8_t current_data_index; + +bool set_state_data(bool forward){ + // Apply last formatter to fill the screen's buffer + do{ + current_data_index = forward ? current_data_index+1 : current_data_index-1; + if(screen_table[current_data_index].type == ALL_TYPES || + screen_table[current_data_index].type == current_txn.type){ + if(((format_function_t)PIC(screen_table[current_data_index].value_setter))() != 0){ + break; + } + } + } while(current_data_index >= 0 && + current_data_index < SCREEN_NUM); + + if(current_data_index < 0 || current_data_index >= SCREEN_NUM){ + return false; } - ux_current_step++; - bagl_ui_step_nanos_display(); - return 0; - - case BUTTON_EVT_RELEASED | BUTTON_LEFT: - txn_deny(); - return 0; - } - - return 0; -} - -static void -bagl_ui_step_nanos_display() -{ - while (1) { - if (ux_current_step >= sizeof(ux_steps) / sizeof(ux_steps[0])) { - UX_DISPLAY(bagl_ui_approval_nanos, NULL); - return; + if (screen_table[current_data_index].caption != SCREEN_DYN_CAPTION) { + strncpy(caption, + (char*)PIC(screen_table[current_data_index].caption), + sizeof(caption)); } - int txtype = ux_steps[ux_current_step].txtype; - if (txtype == ALL_TYPES || txtype == current_txn.type) { - const char* step_caption = (const char*) PIC(ux_steps[ux_current_step].caption); - int (*step_display)(void) = (int (*)(void)) PIC(ux_steps[ux_current_step].display); - if (step_display()) { - snprintf(captionBuffer, sizeof(captionBuffer), "%s", step_caption); - ui_text_more(); - UX_DISPLAY(bagl_ui_step_nanos, NULL); - return; - } + PRINTF("caption: %s\n", caption); + PRINTF("details: %s\n\n", text); + return true; +} + +volatile uint8_t current_state; + +#define INSIDE_BORDERS 0 +#define OUT_OF_BORDERS 1 + +void display_next_state(bool is_upper_border){ + + if(is_upper_border){ + if(current_state == OUT_OF_BORDERS){ // -> from first screen + current_state = INSIDE_BORDERS; + set_state_data(true); + ux_flow_next(); + } + else{ + if(set_state_data(false)){ // <- from middle, more screens available + ux_flow_next(); + } + else{ // <- from middle, no more screens available + current_state = OUT_OF_BORDERS; + ux_flow_prev(); + } + } + } + else // walking over the second border + { + if(current_state == OUT_OF_BORDERS){ // <- from last screen + current_state = INSIDE_BORDERS; + set_state_data(false); + ux_flow_prev(); + } + else{ + if(set_state_data(true)){ // -> from middle, more screens available + /*dirty hack to have coherent behavior on bnnn_paging when there are multiple screens*/ + G_ux.flow_stack[G_ux.stack_count-1].prev_index = G_ux.flow_stack[G_ux.stack_count-1].index-2; + G_ux.flow_stack[G_ux.stack_count-1].index--; + ux_flow_relayout(); + /*end of dirty hack*/ + } + else{ // -> from middle, no more screens available + current_state = OUT_OF_BORDERS; + ux_flow_next(); + } + } } - ux_current_step++; - } } -#endif // TARGET_NANOS -void -ui_txn() -{ + +void ui_txn(void) { PRINTF("Transaction:\n"); PRINTF(" Type: %d\n", current_txn.type); PRINTF(" Sender: %.*h\n", 32, current_txn.sender); - PRINTF(" Fee: %s\n", u64str(current_txn.fee)); + PRINTF(" Fee: %s\n", amount_to_str(current_txn.fee, ALGORAND_DECIMALS)); PRINTF(" First valid: %s\n", u64str(current_txn.firstValid)); PRINTF(" Last valid: %s\n", u64str(current_txn.lastValid)); PRINTF(" Genesis ID: %.*s\n", 32, current_txn.genesisID); PRINTF(" Genesis hash: %.*h\n", 32, current_txn.genesisHash); if (current_txn.type == PAYMENT) { PRINTF(" Receiver: %.*h\n", 32, current_txn.payment.receiver); - PRINTF(" Amount: %s\n", u64str(current_txn.payment.amount)); + PRINTF(" Amount: %s\n", amount_to_str(current_txn.payment.amount, ALGORAND_DECIMALS)); PRINTF(" Close to: %.*h\n", 32, current_txn.payment.close); } + if (current_txn.type == ASSET_XFER) { + PRINTF(" Sender: %.*h\n", 32, current_txn.asset_xfer.sender); + PRINTF(" Receiver: %.*h\n", 32, current_txn.asset_xfer.receiver); + PRINTF(" Amount: %s\n", u64str(current_txn.asset_xfer.amount)); + PRINTF(" Close to: %.*h\n", 32, current_txn.asset_xfer.close); + } if (current_txn.type == KEYREG) { PRINTF(" Vote PK: %.*h\n", 32, current_txn.keyreg.votepk); PRINTF(" VRF PK: %.*h\n", 32, current_txn.keyreg.vrfpk); } -#if defined(TARGET_NANOS) - ux_current_step = 0; - bagl_ui_step_nanos_display(); -#endif - -#if defined(TARGET_NANOX) - ux_last_step = 0; + current_data_index = -1; + current_state = OUT_OF_BORDERS; if (G_ux.stack_count == 0) { ux_stack_push(); } ux_flow_init(0, ux_txn_flow, NULL); -#endif } diff --git a/src/ux.c b/src/ux.c index 30cfbfe9..bd2aa107 100644 --- a/src/ux.c +++ b/src/ux.c @@ -1,12 +1,5 @@ #include "os.h" #include "os_io_seproxyhal.h" -#ifdef TARGET_NANOS -// This seems to be implicitly required by the SDK at link-time. -ux_state_t ux; -#endif - -#ifdef TARGET_NANOX ux_state_t G_ux; -bolos_ux_params_t G_ux_params; -#endif +bolos_ux_params_t G_ux_params; \ No newline at end of file diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 00000000..e6286eb0 --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1,5 @@ +__pycache__ +*.pyc +.*.swp +.*.swo + diff --git a/tests/Makefile b/tests/Makefile new file mode 100644 index 00000000..815a1c67 --- /dev/null +++ b/tests/Makefile @@ -0,0 +1,17 @@ +APP_ALGORAND_SRC=../ +APP_ALGORAND_CLI=$(APP_ALGORAND_SRC)/cli/ +APP_ALGORAND_BIN=$(APP_ALGORAND_SRC)/bin/ + +DEBUG=1 + + +.PHONY: test +test: $(APP_ALGORAND_BIN)/app.elf + PYTHONPATH=$(APP_ALGORAND_CLI) pytest --verbose --app $< test/ + +$(APP_ALGORAND_BIN)/app.elf: FORCE + $(MAKE) -j -C $(APP_ALGORAND_SRC) DEBUG=$(DEBUG) + + +.PHONY: FORCE +FORCE: diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 00000000..2e827ef0 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,130 @@ +# app-algorand-test + +## Overview + +Pytest-base test suite for the [app-algorand](https://github.com/LedgerHQ/app-algorand) Nano App. + +This test suite is mainly based on: + - `pytest` framework. + - Ě€speculos` Docker container. + - `ledgerblue` tool. + - Docker Python SDK. + - Algorand Python SDK. + + +## Install + +This test suite requires [https://docs.docker.com/engine/install/ubuntu/](Docker) engine +and assumes the [https://hub.docker.com/r/ledgerhq/speculos](`speculos`) image being pulled. +Python environment may be installed within a virtualenv: + + ``` + docker pull speculos + virtualenv env + env/bin/activate + pip install -r requirements.txt + ``` + +## Run tests + + ``` + make test + ``` + +## APDU Format for Multi-Account Support + +The format of the APDUs in app release that implements multi-account support has been kept backward compatible with +previous message format (1.0.7). Two messages have been modified to implement multi-account support. + +### `INS_GET_PUBLIC_KEY`: + +Original format of this instruction is fixed with no payload. +
+ ------------------------------------ + | CLA | INS | P1 | P2 | LC | + ------------------------------------ + | 0x80 | 0x03 | 0x00 | 0x00 | 0x00 | + ------------------------------------ ++ +New format enhances this APDU with a 4-byte payload that encodes an account number (big endian 32-bit unsigned integer). +This payload is optional so that former format may still be used. So user may send: +
+ ------------------------------------ + | CLA | INS | P1 | P2 | LC | + ------------------------------------ + | 0x80 | 0x03 | 0x00 | 0x00 | 0x00 | + ------------------------------------ ++or +
+ ---------------------------------------------------------------- + | CLA | INS | P1 | P2 | LC | PAYLOAD (4 bytes) | + ---------------------------------------------------------------- + | 0x80 | 0x03 | 0x00 | 0x00 | 0x04 | {account} | + ---------------------------------------------------------------- ++ +The account number is used to derive keys from BIP32 path `44'/283'/
+ ------------------------------------------------------------------------ - - - + | CLA | INS | P1 | P2 | LC | PAYLOAD (N1 bytes) + ------------------------------------------------------------------------ - - - + | 0x80 | 0x03 | 0x00 | 0x80 | N1 | {MessagePack Chunk#1} + ------------------------------------------------------------------------ - - - + ... + ------------------------------------------------------------------------ - - - + | CLA | INS | P1 | P2 | LC | PAYLOAD (Ni bytes) + ------------------------------------------------------------------------ - - - + | 0x80 | 0x03 | 0x80 | 0x80 | Ni | {MessagePack Chunk#i} + ------------------------------------------------------------------------ - - - + ... + ------------------------------------------------------------------------ - - - + | CLA | INS | P1 | P2 | LC | PAYLOAD (NI bytes) + ------------------------------------------------------------------------ - - - + | 0x80 | 0x03 | 0x80 | 0x00 | NI | {MessagePack Chunk#I} + ------------------------------------------------------------------------ - - - ++If one single APDU may contain a whole transaction, `P1` and `P2` are both `0x00`. + +New format enhances messaging with an optional account number that must be inserted +in the first chunk of the sequence. As an optional payload, bit `0` of field `P1` in +the first chunk must be set if present in the message. + +And as for `INS_GET_PUBLIC_KEY` instruction, it is a big-endian encoded 32-bit +unsigned integer word. + +The resulting sequence of chunks is as follows: +
+ ------------------------------------------------------------------------ - - - + | CLA | INS | P1 | P2 | LC | PAYLOAD (N1 bytes) + ------------------------------------------------------------------------ - - - + | 0x80 | 0x03 | 0x01 | 0x80 | N1 | {account (4 bytes)} + {MessagePack Chunk#1 (N1 - 4 bytes)} + ------------------------------------------------------------------------ - - - + ... + ------------------------------------------------------------------------ - - - + | CLA | INS | P1 | P2 | LC | PAYLOAD (Ni bytes) + ------------------------------------------------------------------------ - - - + | 0x80 | 0x03 | 0x80 | 0x80 | Ni | {MessagePack Chunk#i} + ------------------------------------------------------------------------ - - - + ... + ------------------------------------------------------------------------ - - - + | CLA | INS | P1 | P2 | LC | PAYLOAD (NI bytes) + ------------------------------------------------------------------------ - - - + | 0x80 | 0x03 | 0x80 | 0x00 | NI | {MessagePack Chunk#I} + ------------------------------------------------------------------------ - - - ++If one signle APDU is needed for the whole transaction along with the account number, +`P1` and `P2` are `0x01` and `0x00` respectively. + +If the account number is not inserted within the message, the former message format is used +(`P1` in the first chunk is `0x00`) and the account number defaults to `0x00` for the transaction +signature. + diff --git a/tests/pytest.ini b/tests/pytest.ini new file mode 100644 index 00000000..513fa78f --- /dev/null +++ b/tests/pytest.ini @@ -0,0 +1,5 @@ +[pytest] +log_cli = True +log_cli_format = %(asctime)s %(levelname)s %(message)s +log_cli_date_format = %Y-%m-%d %H:%M:%S +log_cli_level = DEBUG diff --git a/tests/requirements.txt b/tests/requirements.txt new file mode 100644 index 00000000..b8552c46 --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1,59 @@ +attrs==19.3.0 +backports.shutil-which==3.5.2 +certifi==2020.4.5.1 +cffi==1.14.0 +chardet==3.0.4 +click==7.1.2 +ConfigArgParse==1.2.3 +construct==2.10.56 +cryptography==2.9.2 +docker==4.2.1 +docutils==0.16 +ecdsa==0.15 +ECPy==0.10.0 +ed25519==1.5 +future==0.18.2 +hidapi==0.9.0.post2 +idna==2.9 +importlib-metadata==1.6.0 +intelhex==2.2.1 +jsonschema==3.2.0 +ledger-agent==0.9.0 +ledgerblue==0.1.31 +ledgerwallet==0.1.2 +libagent==0.14.1 +lockfile==0.12.2 +mnemonic==0.19 +more-itertools==8.4.0 +msgpack==1.0.0 +packaging==20.4 +pbkdf2==1.3 +Pillow==7.1.2 +pluggy==0.13.1 +protobuf==3.12.2 +py==1.8.2 +py-algorand-sdk==1.3.0 +pycparser==2.20 +pycrypto==2.6.1 +pycryptodomex==3.9.7 +pyelftools==0.26 +PyMsgBox==1.0.8 +PyNaCl==1.4.0 +pyparsing==2.4.7 +PyQt5==5.14.2 +PyQt5-sip==12.7.2 +pyrsistent==0.16.0 +pytest==5.4.3 +python-daemon==2.2.4 +python-u2flib-host==3.0.3 +requests==2.23.0 +secp256k1==0.13.2 +semantic-version==2.8.5 +semver==2.10.2 +six==1.15.0 +tabulate==0.8.7 +Unidecode==1.1.1 +urllib3==1.25.9 +wcwidth==0.2.4 +websocket-client==0.57.0 +zipp==3.1.0 diff --git a/tests/test/__init__.py b/tests/test/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test/conftest.py b/tests/test/conftest.py new file mode 100644 index 00000000..fdcbcb2c --- /dev/null +++ b/tests/test/conftest.py @@ -0,0 +1,66 @@ +import pytest + +from .speculos import SpeculosContainer + +import base64 +import msgpack +from algosdk import transaction + +import algomsgpack + + +@pytest.fixture(scope='session') +def app(pytestconfig): + return pytestconfig.option.app + + +@pytest.fixture(scope='session') +def apdu_port(pytestconfig): + return pytestconfig.option.apdu_port + + +@pytest.fixture(scope='session') +def speculos(app, apdu_port): + speculos = SpeculosContainer(app=app, apdu_port=apdu_port) + speculos.start() + print("Started container") + yield speculos + print("Stopping container") + speculos.stop() + + +@pytest.fixture(scope='session') +def dongle(speculos, pytestconfig): + dongle = speculos.connect(debug=pytestconfig.option.verbose > 0) + print("Connected dongle") + yield dongle + print("Disconnecting dongle") + dongle.close() + + +def pytest_addoption(parser, pluginmanager): + parser.addoption("--app", dest="app") + parser.addoption("--apdu_port", dest="apdu_port", type=int, default=9999) + + + + +def genTxns(): + yield transaction.PaymentTxn( + sender="YK54TGVZ37C7P76GKLXTY2LAH2522VD3U2434HRKE7NMXA65VHJVLFVOE4", + receiver="RNZZNMS5L35EF6IQHH24ISSYQIKTUTWKGCB4Q5PBYYSTVB5EYDQRVYWMLE", + fee=0.001, + flat_fee=True, + amt=1000000, + first=5667360, + last=5668360, + note="Hello World".encode(), + gen="testnet-v1.0", + gh="SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=" + ) + +def genTxnPayload(txns): + for txn in txns: + if isinstance(txn, Transaction): + txn = {"txn": txn.dictify()} + yield base64.b64decode(encoding.msgpack_encode(txn)) diff --git a/tests/test/dongle.py b/tests/test/dongle.py new file mode 100644 index 00000000..ba14036f --- /dev/null +++ b/tests/test/dongle.py @@ -0,0 +1,96 @@ +import time +import threading +import socket +import json +import logging +from contextlib import contextmanager + +import traceback + +import ledgerblue.commTCP +from ledgerblue.commException import CommException + + +logger = logging.getLogger('speculos') + + +class Dongle: + def __init__(self, apdu_port=9999, automation_port=None, button_port=None, + debug=False): + self.apdu_port = apdu_port + self.automation_port = automation_port + self.button_port = button_port + self.dongle = ledgerblue.commTCP.getDongle(server='127.0.0.1', + port=self.apdu_port, + debug=debug) + + def exchange(self, apdu, timeout=20000): + return bytes(self.dongle.exchange(apdu, timeout)) + + def close(self): + self.dongle.close() + + @contextmanager + def screen_event_handler(self, handler): + def do_handle_events(_handler, _fd): + buttons = Buttons(self.button_port) + try: + for line in _fd: + if callable(handler): + handler(json.loads(line.strip('\n')), buttons) + except ValueError: + pass + except Exception as e: + logger.error(e) + for l in traceback.extract_stack(): + logger.error(l) + finally: + buttons.close() + + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.connect(('127.0.0.1', self.automation_port)) + fd = s.makefile() + logger.info('Connected to 127.0.0.1:%d' % self.automation_port) + + t = threading.Thread(target=do_handle_events, + args=(handler, fd), + daemon=True) + t.start() + yield self + fd.close() + t.join() + + logger.info('Closing connection to 127.0.0.1:%d' % self.automation_port) + s.close() + + +class Buttons: + LEFT = b'L' + RIGHT = b'R' + LEFT_RELEASE = b'l' + RIGHT_RELEASE = b'r' + + def __init__(self, button_port): + self.button_port = button_port + self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.s.connect(('127.0.0.1', button_port)) + logger.info('Buttons: connected to port: %d' % self.button_port) + + def close(self): + logger.info('Buttons: closing connection to port: %d' % self.button_port) + self.s.close() + + def press(self, *args): + for action in args: + logger.info('Buttons: actions:%s' % action) + if type(action) == bytes: + self.s.send(action) + elif type(action) == str: + self.s.send(action.encode()) + elif type(action) == int or type(action) == float: + self.delay(seconds=action) + return self + + def delay(self, seconds=0.1): + time.sleep(seconds) + return self diff --git a/tests/test/speculos.py b/tests/test/speculos.py new file mode 100644 index 00000000..0688e6b5 --- /dev/null +++ b/tests/test/speculos.py @@ -0,0 +1,111 @@ +import os.path +import threading +import socket +import atexit +import logging + +import docker + +from . import dongle + + +CommException = dongle.CommException +logger = logging.getLogger('speculos') + + +class SpeculosContainer: + """ + `SpeculosContainer` handles running the Bolos App under test within + the `speculos` Docker` image. + + A `SpeculosContainer` instance is constructed with the Bolos App ELF + filename passed as `app` argument and with an optional tcp port passed + as `apdu_port` argument. + + The Docker container mounts the directory of the `app` within the + container on the `/app` mountpoint and exposes the `apdu_port` as tcp + port linked to the default Speculos APDU port (9999). + + The `start()` method starts running the container and starts a background + thread that reads and logs `stdout` and `stderr` output logs from the + container. Note that speculos is run in `headless` display mode. + + Besides the `connect()` method creates a `ledgerblue` tcp connection to + the `speculos` process through the `apdu_port` tcp port. + """ + + def __init__(self, app, apdu_port=9999, + automation_port=None, button_port=None): + self.app = app + self.apdu_port = apdu_port + self.automation_port = automation_port or (apdu_port + 1) + self.button_port = button_port or (apdu_port + 2) + self.docker = docker.from_env().containers + self.container = None + + def start(self): + self.container = self._run_speculos_container() + self.log_handler = self._log_speculos_output(self.container) + atexit.register(self.stop) + logger.info("Started docker container: %s (%s)" + % (self.container.image, self.container.name)) + + def stop(self): + logger.info("Stopping docker container: %s (%s)..." + % (self.container.image, self.container.name)) + self.container.stop() + self.log_handler.join() + + def connect(self, debug=False): + if self.container is None: + raise dongle.CommException("speculos not started yet") + return dongle.Dongle(self.apdu_port, + self.automation_port, + self.button_port, + debug=debug) + + def _run_speculos_container(self): + appdir = os.path.abspath(os.path.dirname(self.app)) + args = [ + '--display headless', + '--apdu-port 9999', + '--automation-port 10000', + '--button-port 10001', + '--log-level button:DEBUG', + '/app/%s' % os.path.basename(self.app) + ] + c = self.docker.create(image='ledgerhq/speculos', + command=' '.join(args), + volumes={appdir: {'bind': '/app', 'mode': 'ro'}}, + ports={ + '9999/tcp': self.apdu_port, + '10000/tcp': self.automation_port, + '10001/tcp': self.button_port, + }) + c.start() + return c + + + def _log_speculos_output(self, container): + # Synchronize on first log output from container + cv = threading.Condition() + started = False + + def do_log(): + for log in container.logs(stream=True, follow=True): + nonlocal started + if not started: + with cv: + started = True + cv.notify() + logger.info(log.decode('utf-8').strip('\n')) + + t = threading.Thread(target=do_log, daemon=True) + t.start() + with cv: + while not started: + cv.wait() + return t + + + diff --git a/tests/test/test_get_public_key.py b/tests/test/test_get_public_key.py new file mode 100644 index 00000000..99439003 --- /dev/null +++ b/tests/test/test_get_public_key.py @@ -0,0 +1,123 @@ +import pytest +import logging +import struct + +from . import speculos + + +def test_ins_with_no_payload(dongle): + """ + Test that sending `INS_GET_PUBLIC_KEY` (0x03) APDU without payload + returns a public key as a 32-byte long `bytes`. + """ + try: + apdu = struct.pack('>BBBBB', 0x80, 0x3, 0x0, 0x0, 0x0) + key = dongle.exchange(apdu) + assert type(key) == bytes + assert len(key) == 32 + except speculos.CommException as e: + logging.error(e) + assert False + + +def test_ins_with_4_bytes_payload(dongle): + """ + Test that sending `INS_GET_PUBLIC_KEY` (0x03) APDU with a + 4-byte (`uint32_t`) payload returns a public key as a + 32-byte long `bytes`. + """ + try: + apdu = struct.pack('>BBBBBI', 0x80, 0x3, 0x0, 0x0, 0x0, 0x0) + key = dongle.exchange(apdu) + assert len(key) == 32 + except speculos.CommException as e: + logging.error(e) + assert False + +labels = { + 'verify', 'address', 'approve' +} + +def getPubKey_ui_handler(event, buttons): + logging.warning(event) + label = sorted(event, key=lambda e: e['y'])[0]['text'].lower() + logging.warning('label => %s' % label) + if len(list(filter(lambda l: l in label, labels))) > 0: + if label == "approve": + buttons.press(buttons.RIGHT, buttons.LEFT, buttons.RIGHT_RELEASE, buttons.LEFT_RELEASE) + else: + buttons.press(buttons.RIGHT, buttons.RIGHT_RELEASE) + +def test_ins_with_4_bytes_payload_and_user_approval(dongle): + """ + Test that sending `INS_GET_PUBLIC_KEY` (0x03) APDU with a + 4-byte (`uint32_t`) payload returns a public key as a + 32-byte long `bytes`, after asking the user to approve the corresponding address + """ + try: + apdu = struct.pack('>BBBBBI', 0x80, 0x3, 0x80, 0x0, 0x0, 0x0) + + with dongle.screen_event_handler(getPubKey_ui_handler): + key = dongle.exchange(apdu) + + assert len(key) == 32 + except speculos.CommException as e: + logging.error(e) + assert False + + +@pytest.fixture(params=[1, 2, 3, 5, 8, 14]) +def invalid_size_apdu(request): + l = request.param + return struct.pack('>BBBBB%ds' % l, 0x80, 0x3, 0x0, 0x0, l, bytes(l)) + + +def test_ins_with_invalid_paylod_sizes(dongle, invalid_size_apdu): + """ + """ + with pytest.raises(speculos.CommException) as excinfo: + dongle.exchange(invalid_size_apdu) + assert excinfo.value.sw == 0x6a85 + + +def test_ins_without_payload_returns_account_0_key(dongle): + """ + """ + try: + apdu = struct.pack('>BBBBB', 0x80, 0x3, 0x0, 0x0, 0x0) + key1 = dongle.exchange(apdu) + assert type(key1) == bytes + assert len(key1) == 32 + apdu = struct.pack('>BBBBBI', 0x80, 0x3, 0x0, 0x0, 0x0, 0x0) + key2 = dongle.exchange(apdu) + assert type(key2) == bytes + assert len(key2) == 32 + + assert key1 == key2 + except speculos.CommException as e: + logging.error(e) + assert False + + +@pytest.fixture(params=[1, 2, 3, 5, 8, 14]) +def account_apdu(request): + account = request.param + return struct.pack('>BBBBBI', 0x80, 0x3, 0x0, 0x0, 0x4, account) + + +def test_ins_with_non_0_account_does_not_return_account_0_key(dongle, account_apdu): + """ + """ + try: + key1 = dongle.exchange(struct.pack('>BBBBB', 0x80, 0x3, 0x0, 0x0, 0x0)) + assert type(key1) == bytes + assert len(key1) == 32 + key2 = dongle.exchange(account_apdu) + assert type(key2) == bytes + assert len(key2) == 32 + + assert key1 != key2 + except speculos.CommException as e: + logging.error(e) + assert False + diff --git a/tests/test/test_sign_msgpack.py b/tests/test/test_sign_msgpack.py new file mode 100644 index 00000000..3b6c0c0f --- /dev/null +++ b/tests/test/test_sign_msgpack.py @@ -0,0 +1,120 @@ +import pytest +import logging +import struct +import base64 + +import msgpack +import nacl.signing + +import algosdk + +from . import speculos + + +labels = { + 'review', 'txn type', 'sender', 'fee', 'first valid', 'last valid', + 'genesis', 'note', 'receiver', 'amount', 'sign' +} + + +@pytest.fixture +def txn(): + txn = algosdk.transaction.PaymentTxn( + sender="YK54TGVZ37C7P76GKLXTY2LAH2522VD3U2434HRKE7NMXA65VHJVLFVOE4", + receiver="RNZZNMS5L35EF6IQHH24ISSYQIKTUTWKGCB4Q5PBYYSTVB5EYDQRVYWMLE", + fee=0.001, + flat_fee=True, + amt=1000000, + first=5667360, + last=5668360, + note="Hello World".encode(), + gen="testnet-v1.0", + gh="SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=" + ) + # txn = {"txn": txn.dictify()} + return base64.b64decode(algosdk.encoding.msgpack_encode(txn)) + + +def test_sign_msgpack_with_default_account(dongle, txn): + """ + """ + apdu = struct.pack('>BBBBB', 0x80, 0x3, 0x0, 0x0, 0x0) + pubKey = dongle.exchange(apdu) + + with dongle.screen_event_handler(txn_ui_handler): + logging.info(txn) + txnSig = sign_algo_txn(dongle, txn) + + assert len(txnSig) == 64 + verify_key = nacl.signing.VerifyKey(pubKey) + verify_key.verify(smessage=b'TX' + txn, signature=txnSig) + + +@pytest.mark.parametrize('account_id', [0, 1, 3, 7, 10, 42, 12345]) +def test_sign_msgpack_with_valid_account_id(dongle, txn, account_id): + """ + """ + apdu = struct.pack('>BBBBBI', 0x80, 0x3, 0x0, 0x0, 0x4, account_id) + pubKey = dongle.exchange(apdu) + + with dongle.screen_event_handler(txn_ui_handler): + logging.info(txn) + txnSig = sign_algo_txn(dongle=dongle, + txn=struct.pack('>I', account_id) + txn, + p1=0x1) + + assert len(txnSig) == 64 + verify_key = nacl.signing.VerifyKey(pubKey) + verify_key.verify(smessage=b'TX' + txn, signature=txnSig) + + +def test_sign_msgpack_returns_same_signature(dongle, txn): + """ + """ + with dongle.screen_event_handler(txn_ui_handler): + defaultTxnSig = sign_algo_txn(dongle, txn) + + with dongle.screen_event_handler(txn_ui_handler): + txnSig = sign_algo_txn(dongle=dongle, + txn=struct.pack('>I', 0x0) + txn, + p1=0x1) + + assert txnSig == defaultTxnSig + + +def txn_ui_handler(event, buttons): + logging.warning(event) + label = sorted(event, key=lambda e: e['y'])[0]['text'].lower() + logging.warning('label => %s' % label) + if len(list(filter(lambda l: l in label, labels))) > 0: + if label == "sign": + buttons.press(buttons.RIGHT, buttons.LEFT, buttons.RIGHT_RELEASE, buttons.LEFT_RELEASE) + else: + buttons.press(buttons.RIGHT, buttons.RIGHT_RELEASE) + + +def chunks(txn, chunk_size=250, first_chunk_size=250): + size = first_chunk_size + last = False + while not last: + chunk = txn[:size] + txn = txn[len(chunk):] + last = not txn + size = chunk_size + yield chunk, last + + +def apdus(chunks, p1=0x00, p2=0x80): + for chunk, last in chunks: + if last: + p2 &= ~0x80 + size = len(chunk) + yield struct.pack('>BBBBB%ds' % size, 0x80, 0x08, p1, p2, size, chunk) + p1 |= 0x80 + + +def sign_algo_txn(dongle, txn, p1=0x00): + for apdu in apdus(chunks(txn), p1=p1 & 0x7f): + sig = dongle.exchange(apdu) + return sig +