From 3ff22481e67f943f06427c4b66ff9c2520f4ea36 Mon Sep 17 00:00:00 2001 From: Ken Carpenter Date: Wed, 12 May 2021 18:05:45 -0700 Subject: [PATCH] V1.0.0 Firmware / V1.05 Bootloader --- DEVELOPMENT.md | 13 +- ports/stm32/Makefile | 2 +- ports/stm32/boards/Passport/.clang-format | 7 + ports/stm32/boards/Passport/.reuse/dep5 | 4 +- .../Passport/.vscode/c_cpp_properties.json | 16 + .../boards/Passport/.vscode/settings.json | 7 +- .../boards/Passport/LICENSES/GPL-3.0-only.txt | 4 +- .../Passport/LICENSES/GPL-3.0-or-later.txt | 4 +- ports/stm32/boards/Passport/adc.c | 438 +- ports/stm32/boards/Passport/adc.h | 16 +- ports/stm32/boards/Passport/backlight.h | 20 - ports/stm32/boards/Passport/bip39_utils.c | 12 +- ports/stm32/boards/Passport/bip39_utils.h | 8 +- ports/stm32/boards/Passport/bip39_word_info.c | 4109 +++++++++-------- ports/stm32/boards/Passport/board_init.c | 54 +- ports/stm32/boards/Passport/board_init.h | 2 +- .../boards/Passport/bootloader/.gitignore | 1 + .../stm32/boards/Passport/bootloader/Makefile | 219 +- .../Passport/bootloader/bootloader_graphics.c | 142 + .../Passport/bootloader/bootloader_graphics.h | 17 + .../stm32/boards/Passport/bootloader/flash.c | 137 +- .../stm32/boards/Passport/bootloader/flash.h | 16 +- .../boards/Passport/bootloader/link-script.ld | 35 +- ports/stm32/boards/Passport/bootloader/main.c | 374 +- .../boards/Passport/bootloader/se-atecc608a.c | 110 +- .../boards/Passport/bootloader/se-atecc608a.h | 6 +- .../stm32/boards/Passport/bootloader/secrets | Bin 0 -> 256 bytes .../Passport/bootloader/secrets-main-dev-se | Bin 0 -> 256 bytes .../boards/Passport/bootloader/secrets-old | Bin 0 -> 256 bytes .../Passport/bootloader/secrets.unknown | Bin 0 -> 256 bytes .../stm32/boards/Passport/bootloader/splash.c | 19 + .../stm32/boards/Passport/bootloader/splash.h | 6 + .../boards/Passport/bootloader/startup.S | 4 +- .../boards/Passport/bootloader/startup.s | 86 + ports/stm32/boards/Passport/bootloader/ui.c | 243 + ports/stm32/boards/Passport/bootloader/ui.h | 16 + .../stm32/boards/Passport/bootloader/update.c | 299 +- .../stm32/boards/Passport/bootloader/update.h | 6 +- .../stm32/boards/Passport/bootloader/verify.c | 123 +- .../stm32/boards/Passport/bootloader/verify.h | 11 +- .../boards/Passport/bootloader/version_info.h | 6 + ports/stm32/boards/Passport/busy_bar.c | 196 + ports/stm32/boards/Passport/busy_bar.h | 9 + .../boards/Passport/bytewords_word_info.c | 269 ++ ports/stm32/boards/Passport/camera-ovm7690.c | 34 +- ports/stm32/boards/Passport/camera-ovm7690.h | 5 +- ports/stm32/boards/Passport/clang-format.txt | 4 - .../boards/Passport/{ => common}/backlight.c | 30 +- ports/stm32/boards/Passport/common/delay.c | 4 +- ports/stm32/boards/Passport/common/display.c | 187 + .../stm32/boards/Passport/{ => common}/gpio.c | 4 +- ports/stm32/boards/Passport/common/hash.c | 100 +- .../Passport/{ => common}/keypad-adp-5587.c | 64 +- .../Passport/common/lcd-sharp-ls018B7dh02.c | 160 +- .../boards/Passport/common/passport_fonts.c | 845 ++++ ports/stm32/boards/Passport/common/pprng.c | 4 +- .../boards/Passport/common/ring_buffer.c | 72 + ports/stm32/boards/Passport/common/se.c | 315 +- ports/stm32/boards/Passport/common/spiflash.c | 4 +- ports/stm32/boards/Passport/common/utils.c | 96 +- ports/stm32/boards/Passport/debug-utils.c | 2 +- ports/stm32/boards/Passport/debug-utils.h | 2 +- ports/stm32/boards/Passport/dispatch.c | 223 +- ports/stm32/boards/Passport/dispatch.h | 29 +- .../Passport/docs/generic-wallet-export.md | 61 + .../stm32/boards/Passport/factory/test_ocd.py | 71 - .../stm32/boards/Passport/firmware_graphics.c | 142 + .../stm32/boards/Passport/firmware_graphics.h | 23 + ports/stm32/boards/Passport/frequency.c | 159 + ports/stm32/boards/Passport/frequency.h | 13 + ports/stm32/boards/Passport/graphics/README | 5 + .../boards/Passport/graphics/c/.gitignore | 5 + .../stm32/boards/Passport/graphics/c/Makefile | 24 + .../boards/Passport/graphics/c/busybar1.png | Bin 0 -> 108 bytes .../boards/Passport/graphics/c/busybar2.png | Bin 0 -> 118 bytes .../boards/Passport/graphics/c/busybar3.png | Bin 0 -> 131 bytes .../boards/Passport/graphics/c/busybar4.png | Bin 0 -> 136 bytes .../boards/Passport/graphics/c/busybar5.png | Bin 0 -> 125 bytes .../boards/Passport/graphics/c/busybar6.png | Bin 0 -> 126 bytes .../boards/Passport/graphics/c/busybar7.png | Bin 0 -> 120 bytes .../Passport/graphics/{ => c}/cbuild.py | 88 +- .../Passport/graphics/{ => c}/splash.png | Bin .../boards/Passport/graphics/graphics.py | 67 - .../boards/Passport/graphics/loading1.png | Bin 407 -> 0 bytes .../Passport/graphics/{ => py}/Makefile | 6 + .../Passport/graphics/{ => py}/README.md | 0 .../Passport/graphics/{ => py}/arrow_down.txt | 0 .../Passport/graphics/{ => py}/arrow_up.txt | 0 .../graphics/{ => py}/battery_100.png | Bin .../Passport/graphics/{ => py}/battery_25.png | Bin .../Passport/graphics/{ => py}/battery_50.png | Bin .../Passport/graphics/{ => py}/battery_75.png | Bin .../graphics/{ => py}/battery_low.png | Bin .../Passport/graphics/{ => py}/build.py | 14 +- .../Passport/graphics/{ => py}/cylon.py | 2 +- .../Passport/graphics/py/fcc-ce-logos.jpg | Bin 0 -> 3294 bytes .../Passport/graphics/py/fcc-ce-logos.png | Bin 0 -> 1091 bytes .../Passport/graphics/{ => py}/fruit.png | Bin .../boards/Passport/graphics/py/graphics.py | 87 + .../boards/Passport/graphics/py/ie_logo.py | Bin 0 -> 2707 bytes .../Passport/graphics/{ => py}/more_left.txt | 0 .../Passport/graphics/{ => py}/more_right.txt | 0 .../Passport/graphics/py/passphrase_icon.png | Bin 0 -> 175 bytes .../Passport/graphics/{ => py}/passport.png | Bin .../{box.png => py/pw_empty_box_lg.png} | Bin .../Passport/graphics/py/pw_empty_box_sm.png | Bin 0 -> 526 bytes .../{xbox.png => py/pw_filled_box_lg.png} | Bin .../Passport/graphics/py/pw_filled_box_sm.png | Bin 0 -> 559 bytes .../{tbox.png => py/pw_pressed_box_lg.png} | Bin .../graphics/py/pw_pressed_box_sm.png | Bin 0 -> 553 bytes .../Passport/graphics/{ => py}/scroll.txt | 0 .../boards/Passport/graphics/py/scrollbar.txt | 276 ++ .../boards/Passport/graphics/py/selected.png | Bin 0 -> 148 bytes .../Passport/graphics/{ => py}/sm_box.txt | 0 .../Passport/graphics/{ => py}/space.txt | 0 .../Passport/graphics/{ => py}/spin.txt | 0 .../boards/Passport/graphics/py/splash.png | Bin 0 -> 919 bytes .../graphics/{ => py}/tetris_pattern_0.png | Bin .../graphics/{ => py}/tetris_pattern_1.png | Bin .../graphics/{ => py}/tetris_pattern_2.png | Bin .../graphics/{ => py}/tetris_pattern_3.png | Bin .../graphics/{ => py}/tetris_pattern_4.png | Bin .../graphics/{ => py}/tetris_pattern_5.png | Bin .../graphics/{ => py}/tetris_pattern_6.png | Bin .../Passport/graphics/{ => py}/wedge.png | Bin .../Passport/graphics/{ => py}/wordmark.png | Bin ports/stm32/boards/Passport/graphics/py/x.png | Bin 0 -> 156 bytes .../boards/Passport/graphics/scrollbar.txt | 276 -- .../boards/Passport/graphics/selected.txt | 12 - ports/stm32/boards/Passport/identify.c | 105 +- .../stm32/boards/Passport/image_conversion.c | 2 +- .../stm32/boards/Passport/image_conversion.h | 2 +- .../stm32/boards/Passport/include/backlight.h | 21 + ports/stm32/boards/Passport/include/delay.h | 4 +- ports/stm32/boards/Passport/include/display.h | 34 + .../boards/Passport/include/firmware-keys.h | 89 +- .../stm32/boards/Passport/include/fwheader.h | 13 +- .../boards/Passport/{ => include}/gpio.h | 2 +- ports/stm32/boards/Passport/include/hash.h | 10 +- .../Passport/{ => include}/keypad-adp-5587.h | 6 +- .../Passport/include/lcd-sharp-ls018B7dh02.h | 8 +- .../boards/Passport/include/passport_fonts.h | 52 + ports/stm32/boards/Passport/include/pprng.h | 4 +- .../Passport/{ => include}/ring_buffer.h | 27 +- .../stm32/boards/Passport/include/se-config.h | 35 +- ports/stm32/boards/Passport/include/se.h | 10 +- .../stm32/boards/Passport/include/secresult.h | 23 + ports/stm32/boards/Passport/include/secrets.h | 3 +- .../stm32/boards/Passport/include/spiflash.h | 2 +- ports/stm32/boards/Passport/include/utils.h | 28 +- ports/stm32/boards/Passport/manifest.py | 34 +- ports/stm32/boards/Passport/modfoundation.c | 1282 +++-- ports/stm32/boards/Passport/modfoundation.h | 2 +- ports/stm32/boards/Passport/modtcc-codecs.c | 32 +- .../Passport/modules/accept_terms_ux.py | 76 + .../stm32/boards/Passport/modules/accounts.py | 141 + .../stm32/boards/Passport/modules/actions.py | 2035 +++++--- .../Passport/modules/address_explorer.py | 231 - ports/stm32/boards/Passport/modules/auth.py | 752 +-- .../stm32/boards/Passport/modules/backups.py | 802 ---- .../battery.pxd/QuickLook/Icon.tiff | Bin .../battery.pxd/QuickLook/Thumbnail.tiff | Bin .../data/086DBC0B-DCEF-4AA4-BBA9-464BB18375A5 | Bin .../data/18FA2DFE-89BE-4617-A245-FB33EB5955E9 | Bin .../data/382CF34A-D9D4-4DD0-A7D4-8982624BB2D4 | Bin .../data/3A1928B0-6F6D-4934-ACCB-865C1942A171 | Bin .../data/41C0228F-C5B5-410D-A7D6-F3BF45F86D6B | Bin .../data/5407B7C2-FB5C-4F3B-8E83-20C02E4178D5 | Bin .../data/6B9FA88C-D70C-48BD-B9FE-540F000D44FA | Bin .../battery.pxd/metadata.info | Bin .../boards/Passport/modules/battery_mon.py | 94 - .../boards/Passport/modules/bip39_utils.py | 16 +- .../stm32/boards/Passport/modules/callgate.py | 61 +- ports/stm32/boards/Passport/modules/chains.py | 10 +- .../stm32/boards/Passport/modules/choosers.py | 132 +- .../Passport/modules/collections/deque.py | 2 +- ports/stm32/boards/Passport/modules/common.py | 43 +- .../stm32/boards/Passport/modules/compat7z.py | 61 +- .../boards/Passport/modules/constants.py | 34 +- .../Passport/modules/data_codecs/__init__.py | 3 + .../modules/data_codecs/address_sampler.py | 26 + .../modules/data_codecs/data_decoder.py | 44 + .../modules/data_codecs/data_encoder.py | 29 + .../modules/data_codecs/data_format.py | 36 + .../modules/data_codecs/data_sampler.py | 20 + .../modules/data_codecs/http_sampler.py | 28 + .../data_codecs/multisig_config_sampler.py | 28 + .../modules/data_codecs/psbt_txn_sampler.py | 31 + .../Passport/modules/data_codecs/qr_codec.py | 69 + .../modules/data_codecs/qr_factory.py | 41 + .../Passport/modules/data_codecs/qr_type.py | 12 + .../modules/data_codecs/seed_sampler.py | 28 + .../Passport/modules/data_codecs/ur1_codec.py | 122 + .../Passport/modules/data_codecs/ur2_codec.py | 114 + .../boards/Passport/modules/descriptor.py | 60 + .../stm32/boards/Passport/modules/display.py | 274 +- .../boards/Passport/modules/exceptions.py | 22 + ports/stm32/boards/Passport/modules/export.py | 1103 +++++ ports/stm32/boards/Passport/modules/files.py | 40 +- .../boards/Passport/modules/flash_cache.py | 359 ++ ports/stm32/boards/Passport/modules/flow.py | 223 +- .../stm32/boards/Passport/modules/graphics.py | 40 +- .../stm32/boards/Passport/modules/history.py | 187 + ports/stm32/boards/Passport/modules/ie.py | 42 + ports/stm32/boards/Passport/modules/keypad.py | 56 +- ports/stm32/boards/Passport/modules/log.py | 21 + .../stm32/boards/Passport/modules/login_ux.py | 427 +- ports/stm32/boards/Passport/modules/main.py | 161 +- ports/stm32/boards/Passport/modules/menu.py | 106 +- .../stm32/boards/Passport/modules/multisig.py | 1254 ++--- .../boards/Passport/modules/new_wallet.py | 843 ++++ .../boards/Passport/modules/noise_source.py | 17 + .../stm32/boards/Passport/modules/opcodes.py | 4 +- .../boards/Passport/modules/passport_fonts.py | 415 +- .../stm32/boards/Passport/modules/periodic.py | 275 ++ .../stm32/boards/Passport/modules/pincodes.py | 98 +- ports/stm32/boards/Passport/modules/psbt.py | 504 +- .../Passport/modules/public_constants.py | 13 +- .../Passport/modules/schema_evolution.py | 40 + .../boards/Passport/modules/se_commands.py | 28 +- ports/stm32/boards/Passport/modules/seed.py | 97 +- .../boards/Passport/modules/seed_check_ux.py | 218 + .../{seed_phrase_ux.py => seed_entry_ux.py} | 116 +- .../boards/Passport/modules/self_test_ux.py | 74 + .../boards/Passport/modules/serializations.py | 40 +- .../stm32/boards/Passport/modules/settings.py | 238 +- ports/stm32/boards/Passport/modules/sffile.py | 22 +- ports/stm32/boards/Passport/modules/sflash.py | 24 +- ports/stm32/boards/Passport/modules/snake.py | 15 +- ports/stm32/boards/Passport/modules/sram4.py | 15 +- .../{stacksats.py => stacking_sats.py} | 163 +- ports/stm32/boards/Passport/modules/stash.py | 70 +- ports/stm32/boards/Passport/modules/stat.py | 148 + ports/stm32/boards/Passport/modules/uQR.py | 2 +- .../Passport/modules/uasyncio/__init__.py | 2 +- .../boards/Passport/modules/uasyncio/core.py | 2 +- .../Passport/modules/uasyncio/queues.py | 6 +- .../Passport/modules/uasyncio/synchro.py | 2 +- .../boards/Passport/modules/ur/__init__.py | 3 - .../stm32/boards/Passport/modules/ur1/bc32.py | 16 +- .../boards/Passport/modules/ur1/bech32.py | 2 +- .../Passport/modules/ur1/bech32_version.py | 2 +- .../boards/Passport/modules/ur1/decode_ur.py | 11 +- .../boards/Passport/modules/ur1/encode_ur.py | 10 +- .../boards/Passport/modules/ur1/mini_cbor.py | 2 +- .../boards/Passport/modules/ur1/utils.py | 9 +- .../boards/Passport/modules/ur2/__init__.py | 3 + .../Passport/modules/{ur => ur2}/bytewords.py | 2 +- .../Passport/modules/{ur => ur2}/cbor_lite.py | 4 +- .../Passport/modules/{ur => ur2}/constants.py | 2 +- .../Passport/modules/{ur => ur2}/crc32.py | 2 +- .../modules/{ur => ur2}/fountain_decoder.py | 63 +- .../modules/{ur => ur2}/fountain_encoder.py | 4 +- .../modules/{ur => ur2}/fountain_utils.py | 2 +- .../modules/{ur => ur2}/random_sampler.py | 2 +- .../boards/Passport/modules/{ur => ur2}/ur.py | 2 +- .../modules/{ur => ur2}/ur_decoder.py | 7 +- .../modules/{ur => ur2}/ur_encoder.py | 2 +- .../Passport/modules/{ur => ur2}/utils.py | 2 +- .../modules/{ur => ur2}/xoshiro256.py | 19 +- ports/stm32/boards/Passport/modules/utils.py | 567 ++- ports/stm32/boards/Passport/modules/ux.py | 1541 ++++--- .../stm32/boards/Passport/modules/version.py | 34 +- .../Passport/modules/wallets/bitcoin_core.py | 108 + .../Passport/modules/wallets/bluewallet.py | 24 + .../boards/Passport/modules/wallets/btcpay.py | 23 + .../Passport/modules/wallets/caravan.py | 19 + .../boards/Passport/modules/wallets/casa.py | 60 + .../Passport/modules/wallets/constants.py | 32 + .../Passport/modules/wallets/dux_reserve.py | 23 + .../Passport/modules/wallets/electrum.py | 141 + .../Passport/modules/wallets/fullynoded.py | 24 + .../modules/wallets/generic_json_wallet.py | 62 + .../Passport/modules/wallets/gordian.py | 24 + .../boards/Passport/modules/wallets/lily.py | 25 + .../modules/wallets/multisig_import.py | 58 + .../Passport/modules/wallets/multisig_json.py | 47 + .../Passport/modules/wallets/sparrow.py | 23 + .../Passport/modules/wallets/specter.py | 24 + .../Passport/modules/wallets/sw_wallets.py | 37 + .../boards/Passport/modules/wallets/utils.py | 181 + .../boards/Passport/modules/wallets/vault.py | 30 + .../boards/Passport/modules/wallets/wasabi.py | 54 + ports/stm32/boards/Passport/mpconfigboard.h | 12 +- ports/stm32/boards/Passport/mpconfigboard.mk | 36 +- ports/stm32/boards/Passport/passport.ld | 8 +- ports/stm32/boards/Passport/pins.c | 334 +- ports/stm32/boards/Passport/pins.h | 12 +- ports/stm32/boards/Passport/quirc.h | 2 +- ports/stm32/boards/Passport/ring_buffer.c | 72 - ports/stm32/boards/Passport/se-atecc608a.c | 231 +- ports/stm32/boards/Passport/se-atecc608a.h | 27 +- .../boards/Passport/stm32h7xx_hal_conf.h | 2 +- ports/stm32/boards/Passport/strnlen.c | 2 +- .../Passport/tools/add-secrets/Makefile | 78 + .../Passport/tools/add-secrets/add-secrets.c | 225 + .../tools/add-secrets/x86/release/add-secrets | Bin 0 -> 13416 bytes .../boards/Passport/tools/cosign/.gitignore | 1 + .../boards/Passport/tools/cosign/Makefile | 8 +- .../boards/Passport/tools/cosign/cosign.c | 68 +- .../Passport/tools/provisioning/provision.py | 376 ++ .../Passport/tools/pubkey-to-c/pubkey-to-c | 37 + .../tools/se_config_gen/se_config_gen.py | 255 + .../tools/se_config_gen/secel_config.py | 420 ++ .../tools/se_config_gen/secel_debug.py | 64 + .../Passport/tools/version_info/version_info | 19 + .../{utils => tools/word_list_gen}/README.md | 2 +- .../word_list_gen}/bip39_test.c | 2 +- .../word_list_gen}/bip39_words.c | 6 +- .../tools/word_list_gen/bytewords_words.c | 262 ++ .../tools/word_list_gen/word_list_gen | Bin 0 -> 101384 bytes .../word_list_gen/word_list_gen.c} | 70 +- .../modtrezorcrypto/modtrezorcrypto-bip32.h | 89 +- ports/stm32/boards/Passport/version.c | 4 +- ports/stm32/boards/Passport/version.h | 4 +- ports/stm32/img.raw | Bin 290400 -> 0 bytes ports/stm32/stm32_it.c | 53 +- ports/stm32/system_stm32.c | 1 + py/objstringio.c | 1 + 319 files changed, 21258 insertions(+), 10255 deletions(-) create mode 100644 ports/stm32/boards/Passport/.clang-format create mode 100644 ports/stm32/boards/Passport/.vscode/c_cpp_properties.json delete mode 100644 ports/stm32/boards/Passport/backlight.h create mode 100644 ports/stm32/boards/Passport/bootloader/.gitignore create mode 100644 ports/stm32/boards/Passport/bootloader/bootloader_graphics.c create mode 100644 ports/stm32/boards/Passport/bootloader/bootloader_graphics.h create mode 100644 ports/stm32/boards/Passport/bootloader/secrets create mode 100644 ports/stm32/boards/Passport/bootloader/secrets-main-dev-se create mode 100644 ports/stm32/boards/Passport/bootloader/secrets-old create mode 100644 ports/stm32/boards/Passport/bootloader/secrets.unknown create mode 100644 ports/stm32/boards/Passport/bootloader/splash.c create mode 100644 ports/stm32/boards/Passport/bootloader/splash.h create mode 100644 ports/stm32/boards/Passport/bootloader/startup.s create mode 100644 ports/stm32/boards/Passport/bootloader/ui.c create mode 100644 ports/stm32/boards/Passport/bootloader/ui.h create mode 100644 ports/stm32/boards/Passport/bootloader/version_info.h create mode 100644 ports/stm32/boards/Passport/busy_bar.c create mode 100644 ports/stm32/boards/Passport/busy_bar.h create mode 100644 ports/stm32/boards/Passport/bytewords_word_info.c delete mode 100644 ports/stm32/boards/Passport/clang-format.txt rename ports/stm32/boards/Passport/{ => common}/backlight.c (72%) create mode 100644 ports/stm32/boards/Passport/common/display.c rename ports/stm32/boards/Passport/{ => common}/gpio.c (86%) rename ports/stm32/boards/Passport/{ => common}/keypad-adp-5587.c (81%) create mode 100644 ports/stm32/boards/Passport/common/passport_fonts.c create mode 100644 ports/stm32/boards/Passport/common/ring_buffer.c create mode 100644 ports/stm32/boards/Passport/docs/generic-wallet-export.md delete mode 100644 ports/stm32/boards/Passport/factory/test_ocd.py create mode 100644 ports/stm32/boards/Passport/firmware_graphics.c create mode 100644 ports/stm32/boards/Passport/firmware_graphics.h create mode 100644 ports/stm32/boards/Passport/frequency.c create mode 100644 ports/stm32/boards/Passport/frequency.h create mode 100644 ports/stm32/boards/Passport/graphics/README create mode 100644 ports/stm32/boards/Passport/graphics/c/.gitignore create mode 100644 ports/stm32/boards/Passport/graphics/c/Makefile create mode 100644 ports/stm32/boards/Passport/graphics/c/busybar1.png create mode 100644 ports/stm32/boards/Passport/graphics/c/busybar2.png create mode 100644 ports/stm32/boards/Passport/graphics/c/busybar3.png create mode 100644 ports/stm32/boards/Passport/graphics/c/busybar4.png create mode 100644 ports/stm32/boards/Passport/graphics/c/busybar5.png create mode 100644 ports/stm32/boards/Passport/graphics/c/busybar6.png create mode 100644 ports/stm32/boards/Passport/graphics/c/busybar7.png rename ports/stm32/boards/Passport/graphics/{ => c}/cbuild.py (58%) rename ports/stm32/boards/Passport/graphics/{ => c}/splash.png (100%) delete mode 100644 ports/stm32/boards/Passport/graphics/graphics.py delete mode 100644 ports/stm32/boards/Passport/graphics/loading1.png rename ports/stm32/boards/Passport/graphics/{ => py}/Makefile (56%) rename ports/stm32/boards/Passport/graphics/{ => py}/README.md (100%) rename ports/stm32/boards/Passport/graphics/{ => py}/arrow_down.txt (100%) rename ports/stm32/boards/Passport/graphics/{ => py}/arrow_up.txt (100%) rename ports/stm32/boards/Passport/graphics/{ => py}/battery_100.png (100%) rename ports/stm32/boards/Passport/graphics/{ => py}/battery_25.png (100%) rename ports/stm32/boards/Passport/graphics/{ => py}/battery_50.png (100%) rename ports/stm32/boards/Passport/graphics/{ => py}/battery_75.png (100%) rename ports/stm32/boards/Passport/graphics/{ => py}/battery_low.png (100%) rename ports/stm32/boards/Passport/graphics/{ => py}/build.py (89%) rename ports/stm32/boards/Passport/graphics/{ => py}/cylon.py (91%) create mode 100644 ports/stm32/boards/Passport/graphics/py/fcc-ce-logos.jpg create mode 100644 ports/stm32/boards/Passport/graphics/py/fcc-ce-logos.png rename ports/stm32/boards/Passport/graphics/{ => py}/fruit.png (100%) create mode 100644 ports/stm32/boards/Passport/graphics/py/graphics.py create mode 100644 ports/stm32/boards/Passport/graphics/py/ie_logo.py rename ports/stm32/boards/Passport/graphics/{ => py}/more_left.txt (100%) rename ports/stm32/boards/Passport/graphics/{ => py}/more_right.txt (100%) create mode 100644 ports/stm32/boards/Passport/graphics/py/passphrase_icon.png rename ports/stm32/boards/Passport/graphics/{ => py}/passport.png (100%) rename ports/stm32/boards/Passport/graphics/{box.png => py/pw_empty_box_lg.png} (100%) create mode 100644 ports/stm32/boards/Passport/graphics/py/pw_empty_box_sm.png rename ports/stm32/boards/Passport/graphics/{xbox.png => py/pw_filled_box_lg.png} (100%) create mode 100644 ports/stm32/boards/Passport/graphics/py/pw_filled_box_sm.png rename ports/stm32/boards/Passport/graphics/{tbox.png => py/pw_pressed_box_lg.png} (100%) create mode 100644 ports/stm32/boards/Passport/graphics/py/pw_pressed_box_sm.png rename ports/stm32/boards/Passport/graphics/{ => py}/scroll.txt (100%) create mode 100644 ports/stm32/boards/Passport/graphics/py/scrollbar.txt create mode 100644 ports/stm32/boards/Passport/graphics/py/selected.png rename ports/stm32/boards/Passport/graphics/{ => py}/sm_box.txt (100%) rename ports/stm32/boards/Passport/graphics/{ => py}/space.txt (100%) rename ports/stm32/boards/Passport/graphics/{ => py}/spin.txt (100%) create mode 100644 ports/stm32/boards/Passport/graphics/py/splash.png rename ports/stm32/boards/Passport/graphics/{ => py}/tetris_pattern_0.png (100%) rename ports/stm32/boards/Passport/graphics/{ => py}/tetris_pattern_1.png (100%) rename ports/stm32/boards/Passport/graphics/{ => py}/tetris_pattern_2.png (100%) rename ports/stm32/boards/Passport/graphics/{ => py}/tetris_pattern_3.png (100%) rename ports/stm32/boards/Passport/graphics/{ => py}/tetris_pattern_4.png (100%) rename ports/stm32/boards/Passport/graphics/{ => py}/tetris_pattern_5.png (100%) rename ports/stm32/boards/Passport/graphics/{ => py}/tetris_pattern_6.png (100%) rename ports/stm32/boards/Passport/graphics/{ => py}/wedge.png (100%) rename ports/stm32/boards/Passport/graphics/{ => py}/wordmark.png (100%) create mode 100644 ports/stm32/boards/Passport/graphics/py/x.png delete mode 100644 ports/stm32/boards/Passport/graphics/scrollbar.txt delete mode 100644 ports/stm32/boards/Passport/graphics/selected.txt create mode 100644 ports/stm32/boards/Passport/include/backlight.h create mode 100644 ports/stm32/boards/Passport/include/display.h rename ports/stm32/boards/Passport/{ => include}/gpio.h (70%) rename ports/stm32/boards/Passport/{ => include}/keypad-adp-5587.h (96%) create mode 100644 ports/stm32/boards/Passport/include/passport_fonts.h rename ports/stm32/boards/Passport/{ => include}/ring_buffer.h (56%) create mode 100644 ports/stm32/boards/Passport/include/secresult.h create mode 100644 ports/stm32/boards/Passport/modules/accept_terms_ux.py create mode 100644 ports/stm32/boards/Passport/modules/accounts.py delete mode 100644 ports/stm32/boards/Passport/modules/address_explorer.py delete mode 100644 ports/stm32/boards/Passport/modules/backups.py rename ports/stm32/boards/Passport/{graphics => modules}/battery.pxd/QuickLook/Icon.tiff (100%) rename ports/stm32/boards/Passport/{graphics => modules}/battery.pxd/QuickLook/Thumbnail.tiff (100%) rename ports/stm32/boards/Passport/{graphics => modules}/battery.pxd/data/086DBC0B-DCEF-4AA4-BBA9-464BB18375A5 (100%) rename ports/stm32/boards/Passport/{graphics => modules}/battery.pxd/data/18FA2DFE-89BE-4617-A245-FB33EB5955E9 (100%) rename ports/stm32/boards/Passport/{graphics => modules}/battery.pxd/data/382CF34A-D9D4-4DD0-A7D4-8982624BB2D4 (100%) rename ports/stm32/boards/Passport/{graphics => modules}/battery.pxd/data/3A1928B0-6F6D-4934-ACCB-865C1942A171 (100%) rename ports/stm32/boards/Passport/{graphics => modules}/battery.pxd/data/41C0228F-C5B5-410D-A7D6-F3BF45F86D6B (100%) rename ports/stm32/boards/Passport/{graphics => modules}/battery.pxd/data/5407B7C2-FB5C-4F3B-8E83-20C02E4178D5 (100%) rename ports/stm32/boards/Passport/{graphics => modules}/battery.pxd/data/6B9FA88C-D70C-48BD-B9FE-540F000D44FA (100%) rename ports/stm32/boards/Passport/{graphics => modules}/battery.pxd/metadata.info (100%) delete mode 100644 ports/stm32/boards/Passport/modules/battery_mon.py create mode 100644 ports/stm32/boards/Passport/modules/data_codecs/__init__.py create mode 100644 ports/stm32/boards/Passport/modules/data_codecs/address_sampler.py create mode 100644 ports/stm32/boards/Passport/modules/data_codecs/data_decoder.py create mode 100644 ports/stm32/boards/Passport/modules/data_codecs/data_encoder.py create mode 100644 ports/stm32/boards/Passport/modules/data_codecs/data_format.py create mode 100644 ports/stm32/boards/Passport/modules/data_codecs/data_sampler.py create mode 100644 ports/stm32/boards/Passport/modules/data_codecs/http_sampler.py create mode 100644 ports/stm32/boards/Passport/modules/data_codecs/multisig_config_sampler.py create mode 100644 ports/stm32/boards/Passport/modules/data_codecs/psbt_txn_sampler.py create mode 100644 ports/stm32/boards/Passport/modules/data_codecs/qr_codec.py create mode 100644 ports/stm32/boards/Passport/modules/data_codecs/qr_factory.py create mode 100644 ports/stm32/boards/Passport/modules/data_codecs/qr_type.py create mode 100644 ports/stm32/boards/Passport/modules/data_codecs/seed_sampler.py create mode 100644 ports/stm32/boards/Passport/modules/data_codecs/ur1_codec.py create mode 100644 ports/stm32/boards/Passport/modules/data_codecs/ur2_codec.py create mode 100644 ports/stm32/boards/Passport/modules/descriptor.py create mode 100644 ports/stm32/boards/Passport/modules/exceptions.py create mode 100644 ports/stm32/boards/Passport/modules/export.py create mode 100644 ports/stm32/boards/Passport/modules/flash_cache.py create mode 100644 ports/stm32/boards/Passport/modules/history.py create mode 100644 ports/stm32/boards/Passport/modules/ie.py create mode 100644 ports/stm32/boards/Passport/modules/log.py create mode 100644 ports/stm32/boards/Passport/modules/new_wallet.py create mode 100644 ports/stm32/boards/Passport/modules/noise_source.py create mode 100644 ports/stm32/boards/Passport/modules/periodic.py create mode 100644 ports/stm32/boards/Passport/modules/schema_evolution.py create mode 100644 ports/stm32/boards/Passport/modules/seed_check_ux.py rename ports/stm32/boards/Passport/modules/{seed_phrase_ux.py => seed_entry_ux.py} (67%) create mode 100644 ports/stm32/boards/Passport/modules/self_test_ux.py rename ports/stm32/boards/Passport/modules/{stacksats.py => stacking_sats.py} (69%) create mode 100644 ports/stm32/boards/Passport/modules/stat.py delete mode 100644 ports/stm32/boards/Passport/modules/ur/__init__.py create mode 100644 ports/stm32/boards/Passport/modules/ur2/__init__.py rename ports/stm32/boards/Passport/modules/{ur => ur2}/bytewords.py (98%) rename ports/stm32/boards/Passport/modules/{ur => ur2}/cbor_lite.py (98%) rename ports/stm32/boards/Passport/modules/{ur => ur2}/constants.py (58%) rename ports/stm32/boards/Passport/modules/{ur => ur2}/crc32.py (88%) rename ports/stm32/boards/Passport/modules/{ur => ur2}/fountain_decoder.py (82%) rename ports/stm32/boards/Passport/modules/{ur => ur2}/fountain_encoder.py (96%) rename ports/stm32/boards/Passport/modules/{ur => ur2}/fountain_utils.py (94%) rename ports/stm32/boards/Passport/modules/{ur => ur2}/random_sampler.py (94%) rename ports/stm32/boards/Passport/modules/{ur => ur2}/ur.py (83%) rename ports/stm32/boards/Passport/modules/{ur => ur2}/ur_decoder.py (93%) rename ports/stm32/boards/Passport/modules/{ur => ur2}/ur_encoder.py (95%) rename ports/stm32/boards/Passport/modules/{ur => ur2}/utils.py (94%) rename ports/stm32/boards/Passport/modules/{ur => ur2}/xoshiro256.py (91%) create mode 100644 ports/stm32/boards/Passport/modules/wallets/bitcoin_core.py create mode 100644 ports/stm32/boards/Passport/modules/wallets/bluewallet.py create mode 100644 ports/stm32/boards/Passport/modules/wallets/btcpay.py create mode 100644 ports/stm32/boards/Passport/modules/wallets/caravan.py create mode 100644 ports/stm32/boards/Passport/modules/wallets/casa.py create mode 100644 ports/stm32/boards/Passport/modules/wallets/constants.py create mode 100644 ports/stm32/boards/Passport/modules/wallets/dux_reserve.py create mode 100644 ports/stm32/boards/Passport/modules/wallets/electrum.py create mode 100644 ports/stm32/boards/Passport/modules/wallets/fullynoded.py create mode 100644 ports/stm32/boards/Passport/modules/wallets/generic_json_wallet.py create mode 100644 ports/stm32/boards/Passport/modules/wallets/gordian.py create mode 100644 ports/stm32/boards/Passport/modules/wallets/lily.py create mode 100644 ports/stm32/boards/Passport/modules/wallets/multisig_import.py create mode 100644 ports/stm32/boards/Passport/modules/wallets/multisig_json.py create mode 100644 ports/stm32/boards/Passport/modules/wallets/sparrow.py create mode 100644 ports/stm32/boards/Passport/modules/wallets/specter.py create mode 100644 ports/stm32/boards/Passport/modules/wallets/sw_wallets.py create mode 100644 ports/stm32/boards/Passport/modules/wallets/utils.py create mode 100644 ports/stm32/boards/Passport/modules/wallets/vault.py create mode 100644 ports/stm32/boards/Passport/modules/wallets/wasabi.py delete mode 100644 ports/stm32/boards/Passport/ring_buffer.c create mode 100644 ports/stm32/boards/Passport/tools/add-secrets/Makefile create mode 100644 ports/stm32/boards/Passport/tools/add-secrets/add-secrets.c create mode 100755 ports/stm32/boards/Passport/tools/add-secrets/x86/release/add-secrets create mode 100644 ports/stm32/boards/Passport/tools/cosign/.gitignore create mode 100644 ports/stm32/boards/Passport/tools/provisioning/provision.py create mode 100755 ports/stm32/boards/Passport/tools/pubkey-to-c/pubkey-to-c create mode 100644 ports/stm32/boards/Passport/tools/se_config_gen/se_config_gen.py create mode 100644 ports/stm32/boards/Passport/tools/se_config_gen/secel_config.py create mode 100644 ports/stm32/boards/Passport/tools/se_config_gen/secel_debug.py create mode 100755 ports/stm32/boards/Passport/tools/version_info/version_info rename ports/stm32/boards/Passport/{utils => tools/word_list_gen}/README.md (86%) rename ports/stm32/boards/Passport/{utils => tools/word_list_gen}/bip39_test.c (96%) rename ports/stm32/boards/Passport/{utils => tools/word_list_gen}/bip39_words.c (99%) create mode 100644 ports/stm32/boards/Passport/tools/word_list_gen/bytewords_words.c create mode 100755 ports/stm32/boards/Passport/tools/word_list_gen/word_list_gen rename ports/stm32/boards/Passport/{utils/bip39_gen.c => tools/word_list_gen/word_list_gen.c} (52%) delete mode 100644 ports/stm32/img.raw diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 5a65f0c..e15071f 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -51,7 +51,7 @@ You will need several shell windows or tabs open to interact with the various to In one shell, make sure that you `cd` to the root `stm32` source folder, e.g., `cd ~/passport/ports/stm32`: make BOARD=Passport - + To include debug symbols for use in `ddd`, run the following: make BOARD=Passport DEBUG=1 @@ -71,6 +71,7 @@ private keys. First, you need to build the `cosign` tool and copy it somewhere in your `PATH`: + sudo apt-get install libssl-dev cd ports/stm32/boards/Passport/tools/cosign make cp x86/release/cosign ~/.local/bin # You can run `echo $PATH` to see the list of possible places you can put this file @@ -80,7 +81,7 @@ Next you need to sign the firmware twice. The `cosign` tool appends `-signed` t Assuming you are still in the `ports/stm32` folder run the following: # TODO: Update command arguments once final signing flow is in place - cosign -f build-Passport/firmware.bin -k 1 -v 0.9 + cosign -f build-Passport/firmware.bin -k 1 -v 0.9 cosign -f build-Passport/firmware-signed.bin -k 2 You can also dump the contents of the firmware header with the following command: @@ -124,16 +125,18 @@ We use `telnet` to connect to the OpenOCD Server. Open a third shell and run th From here can connect over JTAG and run a range of commands (see the help for OpenOCD for details): -Whenever you change any code in the `bootlaoder` folder or in the `common` folder, you will need to rebuild the bootloader (see above), and then flash it to the device with the following sequence in OpenOCD: +Whenever you change any code in the `bootloader` folder or in the `common` folder, you will need to rebuild the bootloader (see above), and then flash it to the device with the following sequence in OpenOCD: reset halt - flash write_image erase boards/Passport/bootloader/bootloader.bin 0x8000000 + flash write_image erase boards/Passport/bootloader/arm/release/bootloader.bin 0x8000000 reset +### TBD: Add docs on appending secrets to the end of the bootloader.bin file during development. + The following command sequence is one you will run repeatedly (i.e., after each build): reset halt - flash write_image erase build-Passport/firmware-signed-signed.bin 0x8020000 + flash write_image erase build-Passport/firmware-signed-signed.bin 0x8020000 reset These commands do the following: diff --git a/ports/stm32/Makefile b/ports/stm32/Makefile index f3d07d2..e21bbda 100644 --- a/ports/stm32/Makefile +++ b/ports/stm32/Makefile @@ -119,7 +119,7 @@ ifeq ($(DEBUG), 1) CFLAGS += -g -DPENDSV_DEBUG COPT = -O0 else -COPT += -Os -DNDEBUG +COPT += -O2 -DNDEBUG endif # Options for mpy-cross diff --git a/ports/stm32/boards/Passport/.clang-format b/ports/stm32/boards/Passport/.clang-format new file mode 100644 index 0000000..2c3a525 --- /dev/null +++ b/ports/stm32/boards/Passport/.clang-format @@ -0,0 +1,7 @@ +--- +# We'll use defaults from the LLVM style, but with 4 columns indentation. +BasedOnStyle: Mozilla +IndentWidth: 4 +--- +Language: Cpp +ColumnLimit: 120 diff --git a/ports/stm32/boards/Passport/.reuse/dep5 b/ports/stm32/boards/Passport/.reuse/dep5 index c0316e2..7cf701c 100644 --- a/ports/stm32/boards/Passport/.reuse/dep5 +++ b/ports/stm32/boards/Passport/.reuse/dep5 @@ -12,9 +12,9 @@ Copyright: 2015, Kenneth MacKay License: BSD-2-Clause Files: graphics/* -Copyright: 2020 Foundation Devices, Inc. +Copyright: 2020 Foundation Devices, Inc. License: GPL-3.0-or-later Files: .vscode/settings.json TODO.txt bootloader/se-config.h clang-format.txt include/se-config.h modules/graphics.py pins.csv utils/README.md -Copyright: 2020 Foundation Devices, Inc. +Copyright: 2020 Foundation Devices, Inc. License: GPL-3.0-or-later diff --git a/ports/stm32/boards/Passport/.vscode/c_cpp_properties.json b/ports/stm32/boards/Passport/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..67aec94 --- /dev/null +++ b/ports/stm32/boards/Passport/.vscode/c_cpp_properties.json @@ -0,0 +1,16 @@ +{ + "configurations": [ + { + "name": "Linux", + "includePath": [ + "${workspaceFolder}/**" + ], + "defines": [], + "compilerPath": "/usr/bin/gcc", + "cStandard": "gnu11", + "cppStandard": "gnu++14", + "intelliSenseMode": "gcc-x64" + } + ], + "version": 4 +} \ No newline at end of file diff --git a/ports/stm32/boards/Passport/.vscode/settings.json b/ports/stm32/boards/Passport/.vscode/settings.json index 0c3c261..cce8104 100644 --- a/ports/stm32/boards/Passport/.vscode/settings.json +++ b/ports/stm32/boards/Passport/.vscode/settings.json @@ -2,6 +2,9 @@ "files.associations": { "array": "c", "string": "c", - "string_view": "c" - } + "string_view": "c", + "algorithm": "c" + }, + "python.linting.pylintEnabled": true, + "python.linting.enabled": true } \ No newline at end of file diff --git a/ports/stm32/boards/Passport/LICENSES/GPL-3.0-only.txt b/ports/stm32/boards/Passport/LICENSES/GPL-3.0-only.txt index 5990771..0adc84e 100644 --- a/ports/stm32/boards/Passport/LICENSES/GPL-3.0-only.txt +++ b/ports/stm32/boards/Passport/LICENSES/GPL-3.0-only.txt @@ -574,10 +574,10 @@ version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with -this program. If not, see . +this program. If not, see . Also add information on how to contact you by electronic and paper mail. diff --git a/ports/stm32/boards/Passport/LICENSES/GPL-3.0-or-later.txt b/ports/stm32/boards/Passport/LICENSES/GPL-3.0-or-later.txt index 5990771..0adc84e 100644 --- a/ports/stm32/boards/Passport/LICENSES/GPL-3.0-or-later.txt +++ b/ports/stm32/boards/Passport/LICENSES/GPL-3.0-or-later.txt @@ -574,10 +574,10 @@ version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with -this program. If not, see . +this program. If not, see . Also add information on how to contact you by electronic and paper mail. diff --git a/ports/stm32/boards/Passport/adc.c b/ports/stm32/boards/Passport/adc.c index 63f3aff..2d424cf 100644 --- a/ports/stm32/boards/Passport/adc.c +++ b/ports/stm32/boards/Passport/adc.c @@ -1,18 +1,9 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // // Copyright 2020 - Foundation Devices Inc. // -/* - * Contains support for ADC3 - Board Revision - * and ADC2 for Power Monitor - * Init functions are called by board_init() - * read_boardrev() is called as needed, currently - * used by LCD display processing to determine active high/low for - * SPI1 NSS pin. - */ - #include #include #include @@ -47,19 +38,16 @@ static ADC_HandleTypeDef hadc3; static ADC_HandleTypeDef hadc2; -/* - * adc2_init() - Sets up ADC2 which is used for Power Monitor and Noise inputs. - */ -HAL_StatusTypeDef adc2_init(void) +static HAL_StatusTypeDef adc2_init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; - HAL_StatusTypeDef ret = HAL_OK; + HAL_StatusTypeDef rc; hadc2.Instance = ADC2; - ret = HAL_ADC_DeInit(&hadc2); - if(ret != HAL_OK) { - printf("Failed to DeInit ADC2\r\n"); - return ret; + rc = HAL_ADC_DeInit(&hadc2); + if (rc != HAL_OK) { + printf("Failed to DeInit ADC2\n"); + return rc; } __HAL_RCC_ADC12_CLK_ENABLE(); @@ -107,25 +95,82 @@ HAL_StatusTypeDef adc2_init(void) hadc3.Init.Oversampling.TriggeredMode = ADC_TRIGGEREDMODE_SINGLE_TRIGGER; hadc3.Init.Oversampling.OversamplingStopReset = ADC_REGOVERSAMPLING_CONTINUED_MODE; - ret = HAL_ADC_Init(&hadc2); - if (ret != HAL_OK) + rc = HAL_ADC_Init(&hadc2); + if (rc != HAL_OK) { - printf("Failed to init ADC2\r\n");; - return ret; + printf("Failed to init ADC2\n");; + return rc; } /* Run the ADC calibration in single-ended mode */ - ret = HAL_ADCEx_Calibration_Start(&hadc2, ADC_CALIB_OFFSET, ADC_SINGLE_ENDED); - if (ret != HAL_OK) + rc = HAL_ADCEx_Calibration_Start(&hadc2, ADC_CALIB_OFFSET, ADC_SINGLE_ENDED); + if (rc != HAL_OK) { - printf("ADC3 calibration failed\r\n"); - return ret; + printf("ADC3 calibration failed\n"); + return rc; } - return ret; + return HAL_OK; } -void enable_noise(void) { +HAL_StatusTypeDef adc3_init(void) +{ + HAL_StatusTypeDef rc; + + hadc3.Instance = ADC3; + rc = HAL_ADC_DeInit(&hadc3); + if (rc != HAL_OK) + { + printf("Failed to deinit ADC3\n"); + return rc; + } + + __HAL_RCC_ADC3_CLK_ENABLE(); + + /**ADC3 GPIO Configuration + PC2_C ------> ADC3_INP0 - ALS_OUT + PC3_C ------> ADC3_INP1 - BDREV + */ + HAL_SYSCFG_AnalogSwitchConfig(SYSCFG_SWITCH_PC2 | SYSCFG_SWITCH_PC3, + SYSCFG_SWITCH_PC2_OPEN | SYSCFG_SWITCH_PC3_OPEN); + + hadc3.Instance = ADC3; + hadc3.Init.ClockPrescaler = ADC_CLOCK_ASYNC_DIV2; // 4 + hadc3.Init.Resolution = ADC_RESOLUTION_16B; + hadc3.Init.ScanConvMode = ADC_SCAN_DISABLE; + hadc3.Init.EOCSelection = ADC_EOC_SINGLE_CONV; + hadc3.Init.LowPowerAutoWait = DISABLE; + hadc3.Init.ContinuousConvMode = ENABLE; // DIS + hadc3.Init.NbrOfConversion = 1; + hadc3.Init.DiscontinuousConvMode = DISABLE; + hadc3.Init.ExternalTrigConv = ADC_SOFTWARE_START; + hadc3.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE; + hadc3.Init.ConversionDataManagement = ADC_CONVERSIONDATA_DR; + hadc3.Init.Overrun = ADC_OVR_DATA_OVERWRITTEN; + hadc3.Init.LeftBitShift = ADC_LEFTBITSHIFT_NONE; + + /* + * Perform oversampling to read multiple samples + * and compute the average in HW. + */ + hadc3.Init.OversamplingMode = ENABLE; + hadc3.Init.Oversampling.Ratio = 0x20; /* Bit for 32x oversampling */ + hadc3.Init.Oversampling.RightBitShift = ADC_RIGHTBITSHIFT_5; + hadc3.Init.Oversampling.TriggeredMode = ADC_TRIGGEREDMODE_SINGLE_TRIGGER; + hadc3.Init.Oversampling.OversamplingStopReset = ADC_REGOVERSAMPLING_CONTINUED_MODE; + + rc = HAL_ADC_Init(&hadc3); + if (rc != HAL_OK) + { + printf("ADC3 init failed\n"); + return rc; + } + + return HAL_OK; +} + +void adc_enable_noise(void) +{ GPIO_InitTypeDef GPIO_InitStruct = {0}; /* @@ -143,13 +188,13 @@ void enable_noise(void) { HAL_GPIO_WritePin(GPIOD, GPIO_PIN_10, 1); } -void disable_noise(void) { +void adc_disable_noise(void) +{ /* * PD8 Amp2_enable * PD9 Amp1_enable * PD10 Noise Bias enable */ - HAL_GPIO_WritePin(GPIOD, GPIO_PIN_8, 0); HAL_GPIO_WritePin(GPIOD, GPIO_PIN_9, 0); HAL_GPIO_WritePin(GPIOD, GPIO_PIN_10, 0); @@ -160,8 +205,12 @@ void disable_noise(void) { * read_noise_inputs() - Reads the two noise output channels and returns * the count values read. */ -HAL_StatusTypeDef read_noise_inputs(uint32_t * noise1, uint32_t * noise2) { - HAL_StatusTypeDef ret = HAL_OK; +int adc_read_noise_inputs( + uint32_t *noise1, + uint32_t *noise2 +) +{ + HAL_StatusTypeDef rc; ADC_ChannelConfTypeDef sConfig = {0}; /* Configure Noiseout 1 input Channel */ @@ -174,35 +223,35 @@ HAL_StatusTypeDef read_noise_inputs(uint32_t * noise1, uint32_t * noise2) { sConfig.Offset = 0; sConfig.OffsetRightShift = DISABLE; /* No Right Offset Shift */ sConfig.OffsetSignedSaturation = DISABLE; /* No Signed Saturation */ - ret = HAL_ADC_ConfigChannel(&hadc2, &sConfig); - if (ret != HAL_OK) + rc = HAL_ADC_ConfigChannel(&hadc2, &sConfig); + if (rc != HAL_OK) { - printf("Failed to config ADC2 channel 8\r\n");; - return ret; + printf("Failed to config ADC2 channel 8\n");; + return -1; } /* Start processing for current (I) values */ - ret = HAL_ADC_Start(&hadc2); - if (ret != HAL_OK) + rc = HAL_ADC_Start(&hadc2); + if (rc != HAL_OK) { - printf("ADC2 start failed\r\n"); - return ret; + printf("ADC2 start failed\n"); + return -1; } - ret = HAL_ADC_PollForConversion(&hadc2, HAL_MAX_DELAY); - if (ret != HAL_OK) + rc = HAL_ADC_PollForConversion(&hadc2, HAL_MAX_DELAY); + if (rc != HAL_OK) { - printf("ADC2 poll for conversion failed\r\n"); - return ret; + printf("ADC2 poll for conversion failed\n"); + return -1; } *noise1 = HAL_ADC_GetValue(&hadc2); - ret = HAL_ADC_Stop(&hadc2); - if (ret != HAL_OK) + rc = HAL_ADC_Stop(&hadc2); + if (rc != HAL_OK) { - printf("ADC2 start failed\r\n"); - return ret; + printf("ADC2 start failed\n"); + return -1; } /* Configure Noiseout 2 input Channel */ @@ -210,45 +259,49 @@ HAL_StatusTypeDef read_noise_inputs(uint32_t * noise1, uint32_t * noise2) { sConfig.Channel = ADC_CHANNEL_10; sConfig.Rank = ADC_REGULAR_RANK_1; sConfig.SamplingTime = ADC_SAMPLETIME_8CYCLES_5; - ret = HAL_ADC_ConfigChannel(&hadc2, &sConfig); - if (ret != HAL_OK) + rc = HAL_ADC_ConfigChannel(&hadc2, &sConfig); + if (rc != HAL_OK) { - printf("Failed to config ADC2 channel 4\r\n");; - return ret; + printf("Failed to config ADC2 channel 4\n");; + return -1; } /* Now sample for Noise output 2 */ - ret = HAL_ADC_Start(&hadc2); - if (ret != HAL_OK) + rc = HAL_ADC_Start(&hadc2); + if (rc != HAL_OK) { - printf("ADC2 start failed\r\n"); - return ret; + printf("ADC2 start failed\n"); + return -1; } - ret = HAL_ADC_PollForConversion(&hadc2, HAL_MAX_DELAY); - if (ret != HAL_OK) + rc = HAL_ADC_PollForConversion(&hadc2, HAL_MAX_DELAY); + if (rc != HAL_OK) { - printf("ADC2 poll for conversion failed\r\n"); - return ret; + printf("ADC2 poll for conversion failed\n"); + return -1; } *noise2 = HAL_ADC_GetValue(&hadc2); - ret = HAL_ADC_Stop(&hadc2); - if (ret != HAL_OK) + rc = HAL_ADC_Stop(&hadc2); + if (rc != HAL_OK) { - printf("ADC2 start failed\r\n"); - return ret; + printf("ADC2 start failed\n"); + return -1; } - return ret; + return 0; } /* - * read_powermon() Reads the power monitor current and voltage channels + * adc_read_powermon() Reads the power monitor current and voltage channels */ -HAL_StatusTypeDef read_powermon(uint16_t * current, uint16_t * voltage) { - HAL_StatusTypeDef ret = HAL_OK; +int adc_read_powermon( + uint16_t *current, + uint16_t *voltage +) +{ + HAL_StatusTypeDef rc; ADC_ChannelConfTypeDef sConfig = {0}; uint32_t adc_value_i; @@ -264,26 +317,26 @@ HAL_StatusTypeDef read_powermon(uint16_t * current, uint16_t * voltage) { sConfig.Offset = 0; sConfig.OffsetRightShift = DISABLE; /* No Right Offset Shift */ sConfig.OffsetSignedSaturation = DISABLE; /* No Signed Saturation */ - ret = HAL_ADC_ConfigChannel(&hadc2, &sConfig); - if (ret != HAL_OK) + rc = HAL_ADC_ConfigChannel(&hadc2, &sConfig); + if (rc != HAL_OK) { - printf("Failed to config ADC2 channel 8\r\n");; - return ret; + printf("Failed to config ADC2 channel 8\n");; + return -1; } // Start processing for current (I) values - ret = HAL_ADC_Start(&hadc2); - if (ret != HAL_OK) + rc = HAL_ADC_Start(&hadc2); + if (rc != HAL_OK) { - printf("ADC2 start failed\r\n"); - return ret; + printf("ADC2 start failed\n"); + return -1; } - ret = HAL_ADC_PollForConversion(&hadc2, HAL_MAX_DELAY); - if (ret != HAL_OK) + rc = HAL_ADC_PollForConversion(&hadc2, HAL_MAX_DELAY); + if (rc != HAL_OK) { - printf("ADC2 poll for conversion failed\r\n"); - return ret; + printf("ADC2 poll for conversion failed\n"); + return -1; } adc_value_i = HAL_ADC_GetValue(&hadc2); @@ -294,11 +347,11 @@ HAL_StatusTypeDef read_powermon(uint16_t * current, uint16_t * voltage) { *current = ((adc_value_i * REF_VOLTAGE_MV) / MAX_SAMPLES_CNT) / PWRMON_I_SENSE_RESISTOR; - ret = HAL_ADC_Stop(&hadc2); - if (ret != HAL_OK) + rc = HAL_ADC_Stop(&hadc2); + if (rc != HAL_OK) { - printf("ADC2 start failed\r\n"); - return ret; + printf("ADC2 start failed\n"); + return -1; } /* Switch to the voltage channel */ @@ -306,166 +359,177 @@ HAL_StatusTypeDef read_powermon(uint16_t * current, uint16_t * voltage) { sConfig.Channel = ADC_CHANNEL_4; sConfig.Rank = ADC_REGULAR_RANK_1; sConfig.SamplingTime = ADC_SAMPLETIME_8CYCLES_5; - ret = HAL_ADC_ConfigChannel(&hadc2, &sConfig); - if (ret != HAL_OK) + rc = HAL_ADC_ConfigChannel(&hadc2, &sConfig); + if (rc != HAL_OK) { - printf("Failed to config ADC2 channel 4\r\n");; - return ret; + printf("Failed to config ADC2 channel 4\n");; + return -1; } /* Now sample for voltage (V) */ - ret = HAL_ADC_Start(&hadc2); - if (ret != HAL_OK) + rc = HAL_ADC_Start(&hadc2); + if (rc != HAL_OK) { - printf("ADC2 start failed\r\n"); - return ret; + printf("ADC2 start failed\n"); + return -1; } - ret = HAL_ADC_PollForConversion(&hadc2, HAL_MAX_DELAY); - if (ret != HAL_OK) + rc = HAL_ADC_PollForConversion(&hadc2, HAL_MAX_DELAY); + if (rc != HAL_OK) { - printf("ADC2 poll for conversion failed\r\n"); - return ret; + printf("ADC2 poll for conversion failed\n"); + return -1; } adc_value_v = HAL_ADC_GetValue(&hadc2); *voltage = (adc_value_v * REF_VOLTAGE_MV) / MAX_SAMPLES_CNT; - ret = HAL_ADC_Stop(&hadc2); - if (ret != HAL_OK) + rc = HAL_ADC_Stop(&hadc2); + if (rc != HAL_OK) { - printf("ADC2 start failed\r\n"); - return ret; + printf("ADC2 start failed\n"); + return -1; } - return ret; + return 0; } /* - * adc3_init() - Set up ADC3 which is used for the board revision + * adc_read_als() - Reads the ambient light sensor channel + * and returns a numeric value based on the milli-volts + * read divided by the number of milli-volts per revision. */ -HAL_StatusTypeDef adc3_init(void) +int adc_read_als( + uint16_t *als +) { - HAL_StatusTypeDef ret = HAL_OK; - + HAL_StatusTypeDef rc; ADC_ChannelConfTypeDef sConfig = {0}; + uint32_t adc_value; + uint16_t millivolts; - hadc3.Instance = ADC3; - ret = HAL_ADC_DeInit(&hadc3); - if (ret != HAL_OK) - { - printf("Failed to deinit ADC3\r\n"); - return ret; - } - - /* - * PC3 ----> ADC3 INP1 - */ - __HAL_RCC_ADC3_CLK_ENABLE(); - - /**ADC3 GPIO Configuration - PC3_C ------> ADC3_INP1 - */ - HAL_SYSCFG_AnalogSwitchConfig(SYSCFG_SWITCH_PC3, SYSCFG_SWITCH_PC3_OPEN); - - hadc3.Instance = ADC3; - hadc3.Init.ClockPrescaler = ADC_CLOCK_ASYNC_DIV2; // 4 - hadc3.Init.Resolution = ADC_RESOLUTION_16B; - hadc3.Init.ScanConvMode = ADC_SCAN_DISABLE; - hadc3.Init.EOCSelection = ADC_EOC_SINGLE_CONV; - hadc3.Init.LowPowerAutoWait = DISABLE; - hadc3.Init.ContinuousConvMode = ENABLE; // DIS - hadc3.Init.NbrOfConversion = 1; - hadc3.Init.DiscontinuousConvMode = DISABLE; - hadc3.Init.ExternalTrigConv = ADC_SOFTWARE_START; - hadc3.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE; - hadc3.Init.ConversionDataManagement = ADC_CONVERSIONDATA_DR; - hadc3.Init.Overrun = ADC_OVR_DATA_OVERWRITTEN; - hadc3.Init.LeftBitShift = ADC_LEFTBITSHIFT_NONE; + *als = 0; - /* - * Perform oversampling to read multiple samples - * and compute the average in HW. - */ - hadc3.Init.OversamplingMode = ENABLE; - hadc3.Init.Oversampling.Ratio = 0x20; /* Bit for 32x oversampling */ - hadc3.Init.Oversampling.RightBitShift = ADC_RIGHTBITSHIFT_5; - hadc3.Init.Oversampling.TriggeredMode = ADC_TRIGGEREDMODE_SINGLE_TRIGGER; - hadc3.Init.Oversampling.OversamplingStopReset = ADC_REGOVERSAMPLING_CONTINUED_MODE; - - ret = HAL_ADC_Init(&hadc3); - if (ret != HAL_OK) - { - printf("ADC3 init failed\r\n"); - return ret; - } /** Configure Regular Channel */ - sConfig.Channel = ADC_CHANNEL_1; + sConfig.Channel = ADC_CHANNEL_0; sConfig.Rank = ADC_REGULAR_RANK_1; sConfig.SamplingTime = ADC_SAMPLETIME_8CYCLES_5; sConfig.SingleDiff = ADC_SINGLE_ENDED; sConfig.OffsetNumber = ADC_OFFSET_NONE; sConfig.Offset = 0; - ret = HAL_ADC_ConfigChannel(&hadc3, &sConfig); - if (ret != HAL_OK) + rc = HAL_ADC_ConfigChannel(&hadc3, &sConfig); + if (rc != HAL_OK) { - printf("Failed to config ADC3 channel\r\n"); + printf("Failed to config ADC3 channel\n"); + return -1; } /* Run the ADC calibration in single-ended mode */ - ret = HAL_ADCEx_Calibration_Start(&hadc3, ADC_CALIB_OFFSET, ADC_SINGLE_ENDED); - if (ret != HAL_OK) + rc = HAL_ADCEx_Calibration_Start(&hadc3, ADC_CALIB_OFFSET, ADC_SINGLE_ENDED); + if (rc != HAL_OK) + { + printf("ADC3 calibration failed\n"); + return -1; + } + + rc = HAL_ADC_Start(&hadc3); + if (rc != HAL_OK) + { + printf("ADC3 start failed\n"); + return -1; + } + + rc = HAL_ADC_PollForConversion(&hadc3, HAL_MAX_DELAY); + if (rc != HAL_OK) { - printf("ADC3 calibration failed\r\n"); + printf("ADC3 poll for conversion failed\n"); + return -1; } + adc_value = HAL_ADC_GetValue(&hadc3); + HAL_ADC_Stop(&hadc3); + + millivolts = (((adc_value) * REF_VOLTAGE_MV) / MAX_SAMPLES_CNT); - return ret; + *als = millivolts; /* Upper-level code will scale this as needed */ + return 0; } /* - * read_boardrev() - Reads the board revision channel + * adc_read_boardrev() - Reads the board revision channel * and returns a numeric value based on the milli-volts * read divided by the number of milli-volts per revision. */ -HAL_StatusTypeDef read_boardrev(uint16_t * board_rev) +int adc_read_boardrev( + uint16_t *board_rev +) { - HAL_StatusTypeDef ret = HAL_OK; + HAL_StatusTypeDef rc; + ADC_ChannelConfTypeDef sConfig = {0}; uint32_t adc_value; uint16_t millivolts; - ret = HAL_ADC_Start(&hadc3); - if (ret != HAL_OK) + *board_rev = 0; + + /** Configure Regular Channel + */ + sConfig.Channel = ADC_CHANNEL_1; + sConfig.Rank = ADC_REGULAR_RANK_1; + sConfig.SamplingTime = ADC_SAMPLETIME_8CYCLES_5; + sConfig.SingleDiff = ADC_SINGLE_ENDED; + sConfig.OffsetNumber = ADC_OFFSET_NONE; + sConfig.Offset = 0; + rc = HAL_ADC_ConfigChannel(&hadc3, &sConfig); + if (rc != HAL_OK) { - printf("ADC3 start failed\r\n"); - return ret; + printf("Failed to config ADC3 channel\n"); + return -1; } - ret = HAL_ADC_PollForConversion(&hadc3, HAL_MAX_DELAY); - if (ret != HAL_OK) + /* Run the ADC calibration in single-ended mode */ + rc = HAL_ADCEx_Calibration_Start(&hadc3, ADC_CALIB_OFFSET, ADC_SINGLE_ENDED); + if (rc != HAL_OK) { - printf("ADC3 poll for conversion failed\r\n"); - return ret; + printf("ADC3 calibration failed\n"); + return -1; + } + + rc = HAL_ADC_Start(&hadc3); + if (rc != HAL_OK) + { + printf("ADC3 start failed\n"); + return -1; + } + + rc = HAL_ADC_PollForConversion(&hadc3, HAL_MAX_DELAY); + if (rc != HAL_OK) + { + printf("ADC3 poll for conversion failed\n"); + return -1; } adc_value = HAL_ADC_GetValue(&hadc3); HAL_ADC_Stop(&hadc3); - /* - * The ADC consistently reads low when taking a single sample. - * For now add an offset to make sure that we can properly compute the board rev number - * TODO: This will need to be tested with the next board revision to make sure that it tracks properly. - */ - - millivolts = (((adc_value) * REF_VOLTAGE_MV) / MAX_SAMPLES_CNT); // + BOARD_REV_MV_OFFSET; + millivolts = (((adc_value) * REF_VOLTAGE_MV) / MAX_SAMPLES_CNT); - /* - * Determine the board revision based on the milli-volts, probably will - * need a tolerance for example if we are looking for 100 mv increments - * then maybe have a +/- 6 mv window. - * - */ + printf("[%s] millivolts: %u\n", __func__, millivolts); *board_rev = millivolts / MILLIVOLTS_PER_REVISION; - return ret; + return 0; +} + +int adc_init(void) +{ + HAL_StatusTypeDef rc; + + rc = adc2_init(); + if (rc != HAL_OK) + return -1; + + rc = adc3_init(); + if (rc != HAL_OK) + return -1; + + return 0; } diff --git a/ports/stm32/boards/Passport/adc.h b/ports/stm32/boards/Passport/adc.h index f73d754..40f5764 100644 --- a/ports/stm32/boards/Passport/adc.h +++ b/ports/stm32/boards/Passport/adc.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // // Copyright 2020 - Foundation Devices Inc. @@ -7,12 +7,12 @@ #ifndef _ADC_H_ #define _ADC_H_ -extern HAL_StatusTypeDef adc3_init(void); -extern HAL_StatusTypeDef adc2_init(void); -extern HAL_StatusTypeDef read_boardrev(uint16_t * board_rev); -extern HAL_StatusTypeDef read_powermon(uint16_t * current, uint16_t * voltage); -extern void enable_noise(void); -extern void disable_noise(void); -extern HAL_StatusTypeDef read_noise_inputs(uint32_t * noise1, uint32_t * noise2); +extern int adc_init(void); +extern int adc_read_als(uint16_t *als); +extern int adc_read_boardrev(uint16_t *board_rev); +extern int adc_read_powermon(uint16_t *current, uint16_t *voltage); +extern void adc_enable_noise(void); +extern void adc_disable_noise(void); +extern int adc_read_noise_inputs(uint32_t *noise1, uint32_t *noise2); #endif //_ADC_H_ diff --git a/ports/stm32/boards/Passport/backlight.h b/ports/stm32/boards/Passport/backlight.h deleted file mode 100644 index 9ff4543..0000000 --- a/ports/stm32/boards/Passport/backlight.h +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. -// SPDX-License-Identifier: GPL-3.0-or-later -// -// SPDX-FileCopyrightText: 2018 Coinkite, Inc. -// SPDX-License-Identifier: GPL-3.0-only -// -// Backlight driver for LED - -#ifndef STM32_BACKLIGHT_H -#define STM32_BACKLIGHT_H - -#include "stm32h7xx_hal.h" - -#include -#include - -void backlight_init(void); -void backlight_intensity(uint16_t intensity); - -#endif //STM32_BACKLIGHT_H diff --git a/ports/stm32/boards/Passport/bip39_utils.c b/ports/stm32/boards/Passport/bip39_utils.c index 3f28a2f..1bcf05e 100644 --- a/ports/stm32/boards/Passport/bip39_utils.c +++ b/ports/stm32/boards/Passport/bip39_utils.c @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // @@ -12,7 +12,7 @@ #include "bip39_utils.h" -extern word_info_t word_info[]; +extern word_info_t bip39_word_info[]; #ifdef UNUSED_CODE uint32_t letter_to_number(char ch) { @@ -77,19 +77,19 @@ uint8_t starts_with(const char* s, const char* prefix) { } // Fills in `matches` with a comma-separated list of matching words -void get_words_matching_prefix(char* prefix, char* matches, uint32_t matches_len, uint32_t max_matches) { +void get_words_matching_prefix(char* prefix, char* matches, uint32_t matches_len, uint32_t max_matches, const word_info_t* word_info, uint32_t num_words) { char* pnext_match = matches; char candidate_keypad_digits[MAX_WORD_LEN + 1]; uint32_t num_matches = 0; uint32_t total_written = 0; - for (uint32_t i = 0; i < NUM_WORDS; i++) { + for (uint32_t i = 0; i < num_words; i++) { snprintf(candidate_keypad_digits, MAX_WORD_LEN + 1, "%lu", word_info[i].keypad_digits); if (starts_with(candidate_keypad_digits, prefix)) { // This is a match, so convert the offsets to a real string and append to the buffer uint32_t len = word_info_to_string(candidate_keypad_digits, word_info[i].offsets, pnext_match); if (total_written + len > matches_len - 1) { - // Don't write this one, as there is not enough room + // Don't write this one, as there is not enough room break; } total_written += len; @@ -112,5 +112,3 @@ void get_words_matching_prefix(char* prefix, char* matches, uint32_t matches_len } *pnext_match = 0; } - - diff --git a/ports/stm32/boards/Passport/bip39_utils.h b/ports/stm32/boards/Passport/bip39_utils.h index fbff164..69a29f7 100644 --- a/ports/stm32/boards/Passport/bip39_utils.h +++ b/ports/stm32/boards/Passport/bip39_utils.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // @@ -6,7 +6,7 @@ // This structure stores the keypad digits required for a word (max 8 digits) in `keypad_digits`. // -// The `offsets` field contains 8 sets of 2 bits each. Each 2-bit value is +// The `offsets` field contains 8 sets of 2 bits each. Each 2-bit value is // an offset from the corresponding keypad digit: // // Examples: @@ -15,7 +15,7 @@ // On the '7' key, offset of 'p' is 0, 'q' is 1 and 'r' is 2, and 's' is 3. // // If keypad_digits is '234', representing the word 'beg', then the offsets -// are 'b'=b01, 'e'=b01, 'g'=b00. The bits are encoded starting at the high end of the +// are 'b'=b01, 'e'=b01, 'g'=b00. The bits are encoded starting at the high end of the // 16-bit value, so the final `offsets` value for '234' and 'beg' is b0101000000000000 or 0x5000. typedef struct { @@ -23,4 +23,4 @@ typedef struct { uint16_t offsets; } word_info_t; -void get_words_matching_prefix(char* prefix, char* matches, uint32_t matches_len, uint32_t max_matches); \ No newline at end of file +void get_words_matching_prefix(char* prefix, char* matches, uint32_t matches_len, uint32_t max_matches, const word_info_t* word_info, uint32_t num_words); diff --git a/ports/stm32/boards/Passport/bip39_word_info.c b/ports/stm32/boards/Passport/bip39_word_info.c index 64754aa..387d50d 100644 --- a/ports/stm32/boards/Passport/bip39_word_info.c +++ b/ports/stm32/boards/Passport/bip39_word_info.c @@ -1,2056 +1,2061 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2021 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // -#include "bip39_utils.h" +#include -word_info_t word_info[] = { - {2226366, 0x1124}, //abandon - {2245489, 0x1a88}, //ability - {2253, 0x1900}, //able - {22688, 0x1900}, //about - {22683, 0x1a40}, //above - {227368, 0x1d40}, //absent - {227672, 0x1e90}, //absorb - {22787228, 0x1c88}, //abstract - {227873, 0x1d80}, //absurd - {22873, 0x1740}, //abuse - {222377, 0x29f0}, //access - {22243368, 0x2a14}, //accident - {2226868, 0x2a50}, //account - {222873, 0x29d0}, //accuse - {2244383, 0x2664}, //achieve - {2243, 0x2800}, //acid - {22687842, 0x29ca}, //acoustic - {2278473, 0x25a4}, //acquire - {227677, 0x2af0}, //across - {228, 0x2000}, //act - {228466, 0x2290}, //action - {22867, 0x2280}, //actor - {2287377, 0x227c}, //actress - {228825, 0x2120}, //actual - {23278, 0x0000}, //adapt - {233, 0x0000}, //add - {233428, 0x0280}, //addict - {2337377, 0x027c}, //address - {235878, 0x01c0}, //adjust - {23648, 0x0200}, //admit - {23858, 0x0600}, //adult - {2382623, 0x0864}, //advance - {238423, 0x0a90}, //advice - {2376242, 0x1a68}, //aerobic - {233247, 0x28a0}, //affair - {233673, 0x2a80}, //afford - {237243, 0x2880}, //afraid - {24246, 0x0240}, //again - {243, 0x0400}, //age - {24368, 0x0500}, //agent - {24733, 0x0940}, //agree - {24323, 0x1400}, //ahead - {246, 0x2000}, //aim - {247, 0x2800}, //air - {2477678, 0x28a0}, //airport - {24753, 0x2e40}, //aisle - {25276, 0x2200}, //alarm - {25286, 0x2500}, //album - {2526465, 0x2a68}, //alcohol - {25378, 0x2600}, //alert - {25436, 0x2940}, //alien - {255, 0x2800}, //all - {25539, 0x2980}, //alley - {25569, 0x2a00}, //allow - {256678, 0x22c0}, //almost - {25663, 0x2940}, //alone - {25742, 0x2100}, //alpha - {2573239, 0x2908}, //already - {2576, 0x2e00}, //also - {25837, 0x2180}, //alter - {259297, 0x20b0}, //always - {2628387, 0x0058}, //amateur - {2629464, 0x0390}, //amazing - {26664, 0x0900}, //among - {266868, 0x0940}, //amount - {268733, 0x0740}, //amused - {2625978, 0x12b0}, //analyst - {262467, 0x19a0}, //anchor - {2624368, 0x1a50}, //ancient - {26437, 0x1180}, //anger - {26453, 0x1240}, //angle - {26479, 0x1280}, //angry - {264625, 0x1820}, //animal - {26553, 0x1640}, //ankle - {26668623, 0x1659}, //announce - {266825, 0x1520}, //annual - {2668437, 0x1858}, //another - {267937, 0x1c60}, //answer - {2683662, 0x1150}, //antenna - {2684783, 0x1254}, //antique - {2694389, 0x1648}, //anxiety - {269, 0x1800}, //any - {27278, 0x0200}, //apart - {2765649, 0x0a88}, //apology - {277327, 0x0120}, //appear - {27753, 0x0240}, //apple - {2777683, 0x02a4}, //approve - {27745, 0x0a80}, //april - {2724, 0x2900}, //arch - {272842, 0x28a0}, //arctic - {2732, 0x2400}, //area - {27362, 0x2500}, //arena - {27483, 0x2140}, //argue - {276, 0x2000}, //arm - {27633, 0x2100}, //armed - {27667, 0x2280}, //armor - {2769, 0x2200}, //army - {276863, 0x2940}, //around - {2772643, 0x2844}, //arrange - {277378, 0x29c0}, //arrest - {277483, 0x2a90}, //arrive - {27769, 0x2a00}, //arrow - {278, 0x2000}, //art - {27833228, 0x2188}, //artefact - {278478, 0x22c0}, //artist - {2789675, 0x20a4}, //artwork - {275, 0x3400}, //ask - {277328, 0x3180}, //aspect - {2772858, 0x3c60}, //assault - {27738, 0x3d00}, //asset - {277478, 0x3ec0}, //assist - {277863, 0x3d10}, //assume - {278462, 0x3100}, //asthma - {2845383, 0x0644}, //athlete - {2866, 0x0800}, //atom - {288225, 0x0090}, //attack - {288363, 0x0140}, //attend - {28848833, 0x0211}, //attitude - {2887228, 0x0220}, //attract - {2828466, 0x18a4}, //auction - {28348, 0x1200}, //audit - {284878, 0x11c0}, //august - {2868, 0x1400}, //aunt - {288467, 0x11a0}, //author - {2886, 0x1200}, //auto - {288866, 0x1110}, //autumn - {2837243, 0x2604}, //average - {2862236, 0x2a08}, //avocado - {28643, 0x2a00}, //avoid - {29253, 0x0140}, //awake - {29273, 0x0240}, //aware - {2929, 0x0200}, //away - {2937663, 0x0784}, //awesome - {29385, 0x0980}, //awful - {2959273, 0x0420}, //awkward - {2947, 0x1b00}, //axis - {2229, 0x4600}, //baby - {22243567, 0x496a}, //bachelor - {22266, 0x4a40}, //bacon - {22343, 0x4040}, //badge - {224, 0x4000}, //bag - {2252623, 0x4864}, //balance - {2252669, 0x4a98}, //balcony - {2255, 0x4a00}, //ball - {226266, 0x41a0}, //bamboo - {226262, 0x4440}, //banana - {226637, 0x4560}, //banner - {227, 0x4800}, //bar - {227359, 0x49a0}, //barely - {2274246, 0x4824}, //bargain - {227735, 0x4a60}, //barrel - {2273, 0x4d00}, //base - {22742, 0x4e80}, //basic - {227538, 0x4d40}, //basket - {228853, 0x4090}, //battle - {23224, 0x5240}, //beach - {2326, 0x5100}, //bean - {232889, 0x5120}, //beauty - {2322873, 0x5874}, //because - {232663, 0x5a10}, //become - {2333, 0x5600}, //beef - {233673, 0x5a90}, //before - {23446, 0x5240}, //begin - {234283, 0x5490}, //behave - {234463, 0x5640}, //behind - {2354383, 0x5a64}, //believe - {23569, 0x5a00}, //below - {2358, 0x5800}, //belt - {23624, 0x5640}, //bench - {2363348, 0x55a0}, //benefit - {2378, 0x5c00}, //best - {238729, 0x5220}, //betray - {238837, 0x5060}, //better - {2389336, 0x5054}, //between - {239663, 0x5a40}, //beyond - {2429253, 0x6aa4}, //bicycle - {243, 0x6000}, //bid - {2453, 0x6500}, //bike - {2463, 0x6400}, //bind - {2465649, 0x6a88}, //biology - {2473, 0x6800}, //bird - {24784, 0x6840}, //birth - {248837, 0x6060}, //bitter - {25225, 0x6240}, //black - {25233, 0x6040}, //blade - {25263, 0x6040}, //blame - {2526538, 0x6150}, //blanket - {25278, 0x6300}, //blast - {25325, 0x6440}, //bleak - {25377, 0x67c0}, //bless - {25463, 0x6900}, //blind - {25663, 0x6a00}, //blood - {2567766, 0x6be0}, //blossom - {256873, 0x69d0}, //blouse - {2583, 0x6500}, //blue - {2587, 0x6600}, //blur - {25874, 0x6740}, //blush - {26273, 0x6200}, //board - {2628, 0x6000}, //boat - {2639, 0x6200}, //body - {2645, 0x6a00}, //boil - {2662, 0x6100}, //bomb - {2663, 0x6500}, //bone - {26687, 0x65c0}, //bonus - {2665, 0x6900}, //book - {26678, 0x6b00}, //boost - {267337, 0x6860}, //border - {267464, 0x6a40}, //boring - {267769, 0x6a80}, //borrow - {2677, 0x6f00}, //boss - {268866, 0x6080}, //bottom - {268623, 0x6590}, //bounce - {269, 0x6400}, //box - {269, 0x6800}, //boy - {2722538, 0x6250}, //bracket - {27246, 0x6240}, //brain - {27263, 0x6100}, //brand - {27277, 0x63c0}, //brass - {27283, 0x6240}, //brave - {27323, 0x6400}, //bread - {273393, 0x65d0}, //breeze - {27425, 0x6a40}, //brick - {274343, 0x6810}, //bridge - {27433, 0x6980}, //brief - {274448, 0x6840}, //bright - {27464, 0x6900}, //bring - {27475, 0x6b40}, //brisk - {27622654, 0x6aaa}, //broccoli - {276536, 0x6950}, //broken - {276693, 0x69d0}, //bronze - {27666, 0x6a00}, //broom - {2768437, 0x6858}, //brother - {27696, 0x6840}, //brown - {27874, 0x6740}, //brush - {282253, 0x5590}, //bubble - {28339, 0x5080}, //buddy - {283438, 0x5040}, //budget - {2833256, 0x5a28}, //buffalo - {28453, 0x5a00}, //build - {2852, 0x5900}, //bulb - {2855, 0x5900}, //bulk - {285538, 0x5a40}, //bullet - {286353, 0x5490}, //bundle - {286537, 0x5560}, //bunker - {287336, 0x5850}, //burden - {287437, 0x5860}, //burger - {28778, 0x5b00}, //burst - {287, 0x5c00}, //bus - {28746377, 0x5e5f}, //business - {2879, 0x5e00}, //busy - {288837, 0x5060}, //butter - {28937, 0x5980}, //buyer - {2899, 0x5f00}, //buzz - {2222243, 0x8504}, //cabbage - {22246, 0x8640}, //cabin - {22253, 0x8640}, //cable - {222887, 0x8870}, //cactus - {2243, 0x8100}, //cage - {2253, 0x8500}, //cake - {2255, 0x8a00}, //call - {2256, 0x8800}, //calm - {226372, 0x8180}, //camera - {2267, 0x8000}, //camp - {226, 0x8400}, //can - {22625, 0x8480}, //canal - {226235, 0x8660}, //cancel - {22639, 0x8480}, //candy - {226666, 0x8590}, //cannon - {22663, 0x8640}, //canoe - {226827, 0x8630}, //canvas - {226966, 0x8690}, //canyon - {2272253, 0x8064}, //capable - {2274825, 0x8208}, //capital - {2278246, 0x8024}, //captain - {227, 0x8800}, //car - {227266, 0x8990}, //carbon - {2273, 0x8800}, //card - {22746, 0x8880}, //cargo - {227738, 0x8840}, //carpet - {22779, 0x8a80}, //carry - {2278, 0x8800}, //cart - {2273, 0x8d00}, //case - {2274, 0x8d00}, //cash - {227466, 0x8e60}, //casino - {227853, 0x8c90}, //castle - {227825, 0x8d20}, //casual - {228, 0x8000}, //cat - {2282564, 0x80a0}, //catalog - {22824, 0x8240}, //catch - {22834679, 0x812a}, //category - {228853, 0x8090}, //cattle - {228448, 0x8440}, //caught - {22873, 0x8740}, //cause - {2288466, 0x84a4}, //caution - {2283, 0x8900}, //cave - {2345464, 0x9a90}, //ceiling - {235379, 0x99a0}, //celery - {236368, 0x9140}, //cement - {236787, 0x9770}, //census - {2368879, 0x9468}, //century - {237325, 0x9920}, //cereal - {2378246, 0x9824}, //certain - {24247, 0x9280}, //chair - {24255, 0x9240}, //chalk - {24267466, 0x9029}, //champion - {242643, 0x9110}, //change - {24267, 0x92c0}, //chaos - {2427837, 0x9018}, //chapter - {242743, 0x9210}, //charge - {24273, 0x9340}, //chase - {2428, 0x9000}, //chat - {24327, 0x9400}, //cheap - {24325, 0x9640}, //check - {243373, 0x95d0}, //cheese - {2433, 0x9600}, //chef - {243779, 0x96a0}, //cherry - {24378, 0x9700}, //chest - {2442536, 0x9a54}, //chicken - {24433, 0x9980}, //chief - {24453, 0x9a00}, //child - {2446639, 0x9858}, //chimney - {246423, 0x9a90}, //choice - {246673, 0x9ad0}, //choose - {2476642, 0x9a68}, //chronic - {2482553, 0x9664}, //chuckle - {24865, 0x9540}, //chunk - {24876, 0x9640}, //churn - {24427, 0xa080}, //cigar - {24662666, 0xa509}, //cinnamon - {247253, 0xaa90}, //circle - {2484936, 0xa2d4}, //citizen - {2489, 0xa200}, //city - {24845, 0xaa80}, //civil - {25246, 0xa200}, //claim - {2527, 0xa000}, //clap - {2527439, 0xa2a8}, //clarify - {2529, 0xa000}, //claw - {2529, 0xa200}, //clay - {25326, 0xa440}, //clean - {25375, 0xa640}, //clerk - {253837, 0xa660}, //clever - {25425, 0xaa40}, //click - {254368, 0xa940}, //client - {25433, 0xaa80}, //cliff - {25462, 0xa840}, //climb - {254642, 0xa9a0}, //clinic - {2547, 0xa800}, //clip - {25625, 0xaa40}, //clock - {2564, 0xa800}, //clog - {25673, 0xab40}, //close - {25684, 0xa840}, //cloth - {25683, 0xa900}, //cloud - {25696, 0xa840}, //clown - {2582, 0xa500}, //club - {25867, 0xa400}, //clump - {2587837, 0xa718}, //cluster - {258824, 0xa490}, //clutch - {26224, 0xa240}, //coach - {26278, 0xa300}, //coast - {2626688, 0xaa50}, //coconut - {2633, 0xa100}, //code - {263333, 0xaa50}, //coffee - {2645, 0xaa00}, //coil - {2646, 0xa900}, //coin - {2655328, 0xaa60}, //collect - {26567, 0xaa80}, //color - {265866, 0xa910}, //column - {2662463, 0xa194}, //combine - {2663, 0xa100}, //come - {2663678, 0xa2a0}, //comfort - {26642, 0xa280}, //comic - {266666, 0xa090}, //common - {2667269, 0xa018}, //company - {2662378, 0xa660}, //concert - {2663828, 0xa460}, //conduct - {2663476, 0xa6a0}, //confirm - {26647377, 0xa49f}, //congress - {2666328, 0xa560}, //connect - {26674337, 0xa786}, //consider - {2668765, 0xa4a8}, //control - {26684623, 0xa699}, //convince - {2665, 0xa900}, //cook - {2665, 0xaa00}, //cool - {267737, 0xa060}, //copper - {2679, 0xa200}, //copy - {26725, 0xa880}, //coral - {2673, 0xa900}, //core - {2676, 0xa900}, //corn - {2677328, 0xaa60}, //correct - {2678, 0xac00}, //cost - {268866, 0xa090}, //cotton - {26824, 0xa640}, //couch - {2686879, 0xa528}, //country - {268753, 0xa490}, //couple - {268773, 0xa6d0}, //course - {268746, 0xa790}, //cousin - {26837, 0xa980}, //cover - {269683, 0xaa10}, //coyote - {27225, 0xa240}, //crack - {272353, 0xa090}, //cradle - {27238, 0xa200}, //craft - {2726, 0xa000}, //cram - {27263, 0xa140}, //crane - {27274, 0xa340}, //crash - {272837, 0xa060}, //crater - {27295, 0xa080}, //crawl - {27299, 0xa380}, //crazy - {27326, 0xa400}, //cream - {273348, 0xa480}, //credit - {27335, 0xa540}, //creek - {2739, 0xa400}, //crew - {2742538, 0xaa50}, //cricket - {27463, 0xa840}, //crime - {27477, 0xab00}, //crisp - {274842, 0xa8a0}, //critic - {2767, 0xa800}, //crop - {27677, 0xabc0}, //cross - {276824, 0xa990}, //crouch - {27693, 0xa800}, //crowd - {2782425, 0xa688}, //crucial - {27835, 0xa580}, //cruel - {278473, 0xa6d0}, //cruise - {2786253, 0xa464}, //crumble - {278624, 0xa590}, //crunch - {27874, 0xa740}, //crush - {279, 0xa800}, //cry - {2797825, 0xab08}, //crystal - {2823, 0x9500}, //cube - {2858873, 0x9864}, //culture - {287, 0x9000}, //cup - {28726273, 0x9188}, //cupboard - {2874687, 0x9a9c}, //curious - {2877368, 0x9a50}, //current - {2878246, 0x9824}, //curtain - {28783, 0x9a40}, //curve - {2874466, 0x9da4}, //cushion - {287866, 0x9c80}, //custom - {2883, 0x9100}, //cute - {29253, 0xaa40}, //cycle - {323, 0x0000}, //dad - {326243, 0x0010}, //damage - {3267, 0x0000}, //damp - {32623, 0x0640}, //dance - {326437, 0x0460}, //danger - {327464, 0x0a40}, //daring - {3274, 0x0d00}, //dash - {32844837, 0x0446}, //daughter - {3296, 0x0100}, //dawn - {329, 0x0800}, //day - {3325, 0x1200}, //deal - {332283, 0x1410}, //debate - {332747, 0x16b0}, //debris - {332233, 0x1810}, //decade - {33236237, 0x1916}, //december - {332433, 0x1a10}, //decide - {3325463, 0x1a94}, //decline - {33267283, 0x1a81}, //decorate - {33273273, 0x1a4d}, //decrease - {3337, 0x1600}, //deer - {3333673, 0x1974}, //defense - {333463, 0x1a50}, //define - {3339, 0x1a00}, //defy - {334733, 0x1250}, //degree - {33529, 0x1880}, //delay - {3354837, 0x1a98}, //deliver - {336263, 0x1040}, //demand - {336473, 0x12d0}, //demise - {336425, 0x1620}, //denial - {3368478, 0x14b0}, //dentist - {3369, 0x1600}, //deny - {337278, 0x1080}, //depart - {337363, 0x1140}, //depend - {3376748, 0x12e0}, //deposit - {33784, 0x1040}, //depth - {337889, 0x1120}, //deputy - {337483, 0x1a90}, //derive - {33727423, 0x1ea5}, //describe - {337378, 0x1d80}, //desert - {337446, 0x1e10}, //design - {3375, 0x1d00}, //desk - {3377247, 0x1c28}, //despair - {3378769, 0x1ca8}, //destroy - {338245, 0x10a0}, //detail - {338328, 0x1180}, //detect - {3383567, 0x19a0}, //develop - {338423, 0x1a90}, //device - {338683, 0x1a10}, //devote - {3424726, 0x2080}, //diagram - {3425, 0x2200}, //dial - {3426663, 0x2090}, //diamond - {34279, 0x2280}, //diary - {3423, 0x2900}, //dice - {343735, 0x2760}, //diesel - {3438, 0x2400}, //diet - {343337, 0x2a60}, //differ - {3444825, 0x2208}, //digital - {3446489, 0x2188}, //dignity - {3453662, 0x2900}, //dilemma - {346637, 0x2560}, //dinner - {34667287, 0x26c6}, //dinosaur - {347328, 0x2980}, //direct - {3478, 0x2800}, //dirt - {34724733, 0x2c25}, //disagree - {34726837, 0x2ea6}, //discover - {3473273, 0x2d34}, //disease - {3474, 0x2d00}, //dish - {3476477, 0x2cbc}, //dismiss - {34767337, 0x2e86}, //disorder - {3477529, 0x2c88}, //display - {34782623, 0x2c19}, //distance - {348378, 0x2980}, //divert - {348433, 0x2a10}, //divide - {3486723, 0x2aa4}, //divorce - {34999, 0x2f80}, //dizzy - {362867, 0x28a0}, //doctor - {36286368, 0x2914}, //document - {364, 0x2000}, //dog - {3655, 0x2a00}, //doll - {3657446, 0x2864}, //dolphin - {366246, 0x2090}, //domain - {366283, 0x2410}, //donate - {366539, 0x2560}, //donkey - {36667, 0x2680}, //donor - {3667, 0x2a00}, //door - {3673, 0x2d00}, //dose - {368253, 0x2590}, //double - {3683, 0x2900}, //dove - {37238, 0x2200}, //draft - {372466, 0x2090}, //dragon - {37262, 0x2000}, //drama - {3727842, 0x2328}, //drastic - {3729, 0x2000}, //draw - {37326, 0x2400}, //dream - {37377, 0x27c0}, //dress - {37438, 0x2a00}, //drift - {37455, 0x2a80}, //drill - {37465, 0x2940}, //drink - {3747, 0x2800}, //drip - {37483, 0x2a40}, //drive - {3767, 0x2800}, //drop - {3786, 0x2400}, //drum - {379, 0x2800}, //dry - {3825, 0x1900}, //duck - {3862, 0x1100}, //dumb - {3863, 0x1500}, //dune - {387464, 0x1a40}, //during - {3878, 0x1c00}, //dust - {38824, 0x1240}, //dutch - {3889, 0x1200}, //duty - {39273, 0x0280}, //dwarf - {3962642, 0x2428}, //dynamic - {32437, 0x4180}, //eager - {32453, 0x4240}, //eagle - {32759, 0x4a80}, //early - {3276, 0x4900}, //earn - {32784, 0x4840}, //earth - {327459, 0x4ea0}, //easily - {3278, 0x4c00}, //east - {3279, 0x4e00}, //easy - {3246, 0x6600}, //echo - {3265649, 0x6a88}, //ecology - {3266669, 0x6988}, //economy - {3343, 0x4100}, //edge - {3348, 0x4800}, //edit - {3382283, 0x4604}, //educate - {333678, 0x6a80}, //effort - {344, 0x4000}, //egg - {34448, 0x6100}, //eight - {348437, 0x6160}, //either - {35269, 0x6600}, //elbow - {35337, 0x6180}, //elder - {35328742, 0x662a}, //electric - {3534268, 0x6410}, //elegant - {3536368, 0x6450}, //element - {35374268, 0x6444}, //elephant - {35382867, 0x660a}, //elevator - {35483, 0x6840}, //elite - {3573, 0x6d00}, //else - {362275, 0x4490}, //embark - {362639, 0x4620}, //embody - {3627223, 0x4624}, //embrace - {363743, 0x4610}, //emerge - {3668466, 0x48a4}, //emotion - {367569, 0x42a0}, //employ - {3676937, 0x4218}, //empower - {36789, 0x4080}, //empty - {362253, 0x5190}, //enable - {36228, 0x5200}, //enact - {363, 0x5000}, //end - {3635377, 0x527c}, //endless - {3636773, 0x52b4}, //endorse - {36369, 0x5480}, //enemy - {363749, 0x5620}, //energy - {3636723, 0x5aa4}, //enforce - {364243, 0x5010}, //engage - {364463, 0x5250}, //engine - {3642623, 0x5464}, //enhance - {36569, 0x5280}, //enjoy - {365478, 0x5ac0}, //enlist - {366844, 0x5910}, //enough - {367424, 0x5a90}, //enrich - {367655, 0x5aa0}, //enroll - {367873, 0x5d90}, //ensure - {36837, 0x5180}, //enter - {368473, 0x5290}, //entire - {36879, 0x5280}, //entry - {36835673, 0x59a1}, //envelope - {3747633, 0x4b84}, //episode - {37825, 0x5480}, //equal - {37847, 0x5600}, //equip - {372, 0x6000}, //era - {37273, 0x6340}, //erase - {37633, 0x6840}, //erode - {3767466, 0x6ba4}, //erosion - {37767, 0x6a80}, //error - {37878, 0x6400}, //erupt - {372273, 0x7810}, //escape - {37729, 0x7c80}, //essay - {3773623, 0x7d64}, //essence - {378283, 0x7010}, //estate - {3837625, 0x4648}, //eternal - {384427, 0x46b0}, //ethics - {38433623, 0x6859}, //evidence - {3845, 0x6a00}, //evil - {38653, 0x6940}, //evoke - {386583, 0x6a90}, //evolve - {39228, 0x5200}, //exact - {3926753, 0x5024}, //example - {392377, 0x59f0}, //excess - {39242643, 0x5911}, //exchange - {392483, 0x5a10}, //excite - {3925833, 0x5a44}, //exclude - {392873, 0x59d0}, //excuse - {3932883, 0x5644}, //execute - {39372473, 0x56ad}, //exercise - {3942878, 0x5470}, //exhaust - {3944248, 0x5660}, //exhibit - {39453, 0x5a40}, //exile - {39478, 0x5b00}, //exist - {3948, 0x5800}, //exit - {396842, 0x58a0}, //exotic - {397263, 0x5040}, //expand - {397328, 0x5180}, //expect - {397473, 0x5290}, //expire - {3975246, 0x5224}, //explain - {397673, 0x52d0}, //expose - {3977377, 0x527c}, //express - {398363, 0x5140}, //extend - {39872, 0x5200}, //extra - {393, 0x6400}, //eye - {3932769, 0x65a0}, //eyebrow - {322742, 0x86a0}, //fabric - {3223, 0x8900}, //face - {3228589, 0x8988}, //faculty - {3233, 0x8100}, //fade - {32468, 0x8900}, //faint - {32484, 0x8840}, //faith - {3255, 0x8a00}, //fall - {32573, 0x8b40}, //false - {3263, 0x8100}, //fame - {326459, 0x82a0}, //family - {326687, 0x8270}, //famous - {326, 0x8400}, //fan - {32629, 0x8680}, //fancy - {3268279, 0x8438}, //fantasy - {3276, 0x8800}, //farm - {3274466, 0x8da4}, //fashion - {328, 0x8000}, //fat - {32825, 0x8080}, //fatal - {328437, 0x8160}, //father - {3284483, 0x8214}, //fatigue - {32858, 0x8600}, //fault - {32867483, 0x8aa1}, //favorite - {3328873, 0x9064}, //feature - {33278279, 0x964a}, //february - {3333725, 0x9188}, //federal - {333, 0x9400}, //fee - {3333, 0x9400}, //feed - {3335, 0x9600}, //feel - {336253, 0x9090}, //female - {33623, 0x9640}, //fence - {33784825, 0x9ca2}, //festival - {33824, 0x9240}, //fetch - {33837, 0x9980}, //fever - {339, 0x9000}, //few - {34237, 0xa580}, //fiber - {3428466, 0xa8a4}, //fiction - {34353, 0xa600}, //field - {344873, 0xa190}, //figure - {3453, 0xa900}, //file - {3456, 0xa800}, //film - {345837, 0xa860}, //filter - {34625, 0xa480}, //final - {3463, 0xa400}, //find - {3463, 0xa500}, //fine - {346437, 0xa460}, //finger - {346474, 0xa6d0}, //finish - {3473, 0xa900}, //fire - {3476, 0xa800}, //firm - {34778, 0xab00}, //first - {347225, 0xae20}, //fiscal - {3474, 0xad00}, //fish - {348, 0xa000}, //fit - {3486377, 0xa17c}, //fitness - {349, 0xa400}, //fix - {3524, 0xa000}, //flag - {35263, 0xa040}, //flame - {35274, 0xa340}, //flash - {3528, 0xa000}, //flat - {352867, 0xa2a0}, //flavor - {3533, 0xa500}, //flee - {354448, 0xa840}, //flight - {3547, 0xa800}, //flip - {35628, 0xa800}, //float - {35625, 0xaa40}, //flock - {35667, 0xaa80}, //floor - {356937, 0xa860}, //flower - {35843, 0xa600}, //fluid - {35874, 0xa740}, //flush - {359, 0xa800}, //fly - {3626, 0xa000}, //foam - {36287, 0xa9c0}, //focus - {364, 0xa000}, //fog - {3645, 0xaa00}, //foil - {3653, 0xa800}, //fold - {365569, 0xaa80}, //follow - {3663, 0xa800}, //food - {3668, 0xa800}, //foot - {36723, 0xaa40}, //force - {367378, 0xa9c0}, //forest - {367438, 0xa840}, //forget - {3675, 0xa900}, //fork - {3678863, 0xa854}, //fortune - {36786, 0xa900}, //forum - {3679273, 0xa820}, //forward - {367745, 0xafa0}, //fossil - {367837, 0xac60}, //foster - {36863, 0xa500}, //found - {369, 0xa400}, //fox - {3724453, 0xa0a4}, //fragile - {37263, 0xa040}, //frame - {37378368, 0xa554}, //frequent - {37374, 0xa740}, //fresh - {374363, 0xa940}, //friend - {374643, 0xa910}, //fringe - {3764, 0xa800}, //frog - {37668, 0xa900}, //front - {37678, 0xab00}, //frost - {37696, 0xa840}, //frown - {376936, 0xab50}, //frozen - {37848, 0xa600}, //fruit - {3835, 0x9600}, //fuel - {386, 0x9400}, //fun - {38669, 0x9580}, //funny - {3876223, 0x9924}, //furnace - {3879, 0x9a00}, //fury - {388873, 0x9190}, //future - {423438, 0x0040}, //gadget - {4246, 0x0900}, //gain - {425299, 0x0860}, //galaxy - {4255379, 0x0a68}, //gallery - {4263, 0x0100}, //game - {427, 0x0000}, //gap - {427243, 0x0810}, //garage - {4272243, 0x0904}, //garbage - {427336, 0x0850}, //garden - {427542, 0x0aa0}, //garlic - {4276368, 0x0850}, //garment - {427, 0x0c00}, //gas - {4277, 0x0c00}, //gasp - {4283, 0x0100}, //gate - {428437, 0x0160}, //gather - {42843, 0x0440}, //gauge - {4293, 0x0d00}, //gaze - {4363725, 0x1588}, //general - {436487, 0x1670}, //genius - {43673, 0x1640}, //genre - {436853, 0x1490}, //gentle - {4368463, 0x1594}, //genuine - {4378873, 0x1c64}, //gesture - {44678, 0x1b00}, //ghost - {44268, 0x2100}, //giant - {4438, 0x2800}, //gift - {444453, 0x2090}, //giggle - {446437, 0x2460}, //ginger - {4472333, 0x28a4}, //giraffe - {4475, 0x2a00}, //girl - {4483, 0x2900}, //give - {4523, 0x2000}, //glad - {452623, 0x2190}, //glance - {45273, 0x2240}, //glare - {45277, 0x23c0}, //glass - {45433, 0x2840}, //glide - {4546773, 0x2834}, //glimpse - {45623, 0x2940}, //globe - {45666, 0x2a00}, //gloom - {45679, 0x2a80}, //glory - {45683, 0x2a40}, //glove - {4569, 0x2800}, //glow - {4583, 0x2500}, //glue - {4628, 0x2000}, //goat - {4633377, 0x207c}, //goddess - {4653, 0x2800}, //gold - {4663, 0x2800}, //good - {46673, 0x2b40}, //goose - {4674552, 0x2aa0}, //gorilla - {467735, 0x2c60}, //gospel - {467747, 0x2f80}, //gossip - {468376, 0x2990}, //govern - {4696, 0x2100}, //gown - {4722, 0x2100}, //grab - {47223, 0x2240}, //grace - {47246, 0x2240}, //grain - {47268, 0x2100}, //grant - {47273, 0x2040}, //grape - {47277, 0x23c0}, //grass - {4728489, 0x2288}, //gravity - {47328, 0x2400}, //great - {47336, 0x2540}, //green - {4743, 0x2800}, //grid - {47433, 0x2980}, //grief - {4748, 0x2800}, //grit - {4762379, 0x2a68}, //grocery - {47687, 0x2900}, //group - {4769, 0x2800}, //grow - {47868, 0x2500}, //grunt - {48273, 0x1200}, //guard - {48377, 0x17c0}, //guess - {48433, 0x1840}, //guide - {48458, 0x1a00}, //guilt - {484827, 0x1820}, //guitar - {486, 0x1400}, //gun - {496, 0x2000}, //gym - {42248, 0x4600}, //habit - {4247, 0x4a00}, //hair - {4253, 0x4a00}, //half - {426637, 0x4060}, //hammer - {4267837, 0x4318}, //hamster - {4263, 0x4400}, //hand - {42779, 0x4080}, //happy - {427267, 0x49a0}, //harbor - {4273, 0x4800}, //hard - {42774, 0x4b40}, //harsh - {4278378, 0x4a70}, //harvest - {428, 0x4000}, //hat - {4283, 0x4900}, //have - {4295, 0x4100}, //hawk - {429273, 0x4c80}, //hazard - {4323, 0x5000}, //head - {432584, 0x5210}, //health - {43278, 0x5200}, //heart - {43289, 0x5280}, //heavy - {43343464, 0x5058}, //hedgehog - {434448, 0x5840}, //height - {43556, 0x5a80}, //hello - {435638, 0x5840}, //helmet - {4357, 0x5800}, //help - {436, 0x5400}, //hen - {4376, 0x5a00}, //hero - {443336, 0x6050}, //hidden - {4444, 0x6100}, //high - {4455, 0x6a00}, //hill - {4468, 0x6400}, //hint - {447, 0x6000}, //hip - {4473, 0x6900}, //hire - {4478679, 0x6ca8}, //history - {46229, 0x6580}, //hobby - {462539, 0x6960}, //hockey - {4653, 0x6800}, //hold - {4653, 0x6900}, //hole - {4654329, 0x6a08}, //holiday - {465569, 0x6a80}, //hollow - {4663, 0x6100}, //home - {46639, 0x6580}, //honey - {4663, 0x6800}, //hood - {4673, 0x6100}, //hope - {4676, 0x6900}, //horn - {467767, 0x6aa0}, //horror - {46773, 0x6b40}, //horse - {46774825, 0x6c82}, //hospital - {4678, 0x6c00}, //host - {46835, 0x6180}, //hotel - {4687, 0x6600}, //hour - {46837, 0x6980}, //hover - {482, 0x5400}, //hub - {4843, 0x5100}, //huge - {48626, 0x5040}, //human - {486253, 0x5190}, //humble - {48667, 0x5280}, //humor - {4863733, 0x5490}, //hundred - {486479, 0x54a0}, //hungry - {4868, 0x5400}, //hunt - {487353, 0x5890}, //hurdle - {48779, 0x5a80}, //hurry - {4878, 0x5800}, //hurt - {4872263, 0x5d10}, //husband - {492743, 0x6680}, //hybrid - {423, 0xa400}, //ice - {4266, 0xa900}, //icon - {4332, 0x8400}, //idea - {43368439, 0x852a}, //identify - {4353, 0x8900}, //idle - {446673, 0x8690}, //ignore - {455, 0xa800}, //ill - {4553425, 0xa908}, //illegal - {4556377, 0xa97c}, //illness - {46243, 0x8040}, //image - {4648283, 0x8804}, //imitate - {4663673, 0x8174}, //immense - {466863, 0x8150}, //immune - {467228, 0x8080}, //impact - {467673, 0x82d0}, //impose - {4677683, 0x82a4}, //improve - {4678573, 0x81b4}, //impulse - {4624, 0x9900}, //inch - {4625833, 0x9a44}, //include - {462663, 0x9a10}, //income - {46273273, 0x9a4d}, //increase - {46339, 0x9140}, //index - {46342283, 0x9281}, //indicate - {463667, 0x92a0}, //indoor - {46387879, 0x91ca}, //industry - {463268, 0x9840}, //infant - {4635428, 0x9aa0}, //inflict - {463676, 0x9a80}, //inform - {464253, 0x9490}, //inhale - {4643748, 0x95a0}, //inherit - {4648425, 0x9888}, //initial - {465328, 0x9180}, //inject - {465879, 0x91a0}, //injury - {466283, 0x9010}, //inmate - {46637, 0x9580}, //inner - {46662368, 0x9694}, //innocent - {46788, 0x9100}, //input - {4678479, 0x95a8}, //inquiry - {467263, 0x9c50}, //insane - {467328, 0x9d80}, //insect - {467433, 0x9e10}, //inside - {4677473, 0x9ca4}, //inspire - {4678255, 0x9c28}, //install - {468228, 0x9080}, //intact - {46837378, 0x919c}, //interest - {4686, 0x9200}, //into - {468378, 0x99c0}, //invest - {468483, 0x9a10}, //invite - {4686583, 0x9aa4}, //involve - {4766, 0xa900}, //iron - {475263, 0xb840}, //island - {4765283, 0xba04}, //isolate - {47783, 0xbd40}, //issue - {4836, 0x8400}, //item - {48679, 0xaa80}, //ivory - {522538, 0x0940}, //jacket - {524827, 0x0120}, //jaguar - {527, 0x0800}, //jar - {5299, 0x0f00}, //jazz - {5325687, 0x129c}, //jealous - {53267, 0x11c0}, //jeans - {53559, 0x1a80}, //jelly - {53935, 0x1180}, //jewel - {562, 0x2400}, //job - {5646, 0x2900}, //join - {5653, 0x2500}, //joke - {5687639, 0x2658}, //journey - {569, 0x2800}, //joy - {58343, 0x1040}, //judge - {58423, 0x1a40}, //juice - {5867, 0x1000}, //jump - {586453, 0x1490}, //jungle - {586467, 0x16a0}, //junior - {5865, 0x1500}, //junk - {5878, 0x1c00}, //just - {52642766, 0x442a}, //kangaroo - {5336, 0x5500}, //keen - {5337, 0x5400}, //keep - {5382487, 0x5250}, //ketchup - {539, 0x5800}, //key - {5425, 0x6900}, //kick - {543, 0x6000}, //kid - {543639, 0x6160}, //kidney - {5463, 0x6400}, //kind - {5464366, 0x6420}, //kingdom - {5477, 0x6f00}, //kiss - {548, 0x6000}, //kit - {5482436, 0x6254}, //kitchen - {5483, 0x6100}, //kite - {548836, 0x6050}, //kitten - {5494, 0x6200}, //kiwi - {5633, 0x5500}, //knee - {56433, 0x5a40}, //knife - {56625, 0x5a40}, //knock - {5669, 0x5800}, //know - {522, 0x8400}, //lab - {52235, 0x8580}, //label - {52267, 0x8680}, //labor - {523337, 0x8060}, //ladder - {5239, 0x8200}, //lady - {5253, 0x8500}, //lake - {5267, 0x8000}, //lamp - {52648243, 0x8441}, //language - {527867, 0x8080}, //laptop - {52743, 0x8840}, //large - {52837, 0x8180}, //later - {52846, 0x8240}, //latin - {52844, 0x8440}, //laugh - {5286379, 0x8528}, //laundry - {5282, 0x8800}, //lava - {529, 0x8000}, //law - {5296, 0x8100}, //lawn - {5297848, 0x8360}, //lawsuit - {52937, 0x8980}, //layer - {5299, 0x8e00}, //lazy - {532337, 0x9060}, //leader - {5323, 0x9200}, //leaf - {53276, 0x9240}, //learn - {53283, 0x9240}, //leave - {5328873, 0x9864}, //lecture - {5338, 0x9800}, //left - {534, 0x9000}, //leg - {53425, 0x9080}, //legal - {534363, 0x9140}, //legend - {5347873, 0x9b64}, //leisure - {53666, 0x9240}, //lemon - {5363, 0x9400}, //lend - {536484, 0x9410}, //length - {5367, 0x9700}, //lens - {5367273, 0x9820}, //leopard - {537766, 0x9f90}, //lesson - {538837, 0x9060}, //letter - {53835, 0x9980}, //level - {5427, 0xa200}, //liar - {5423789, 0xa588}, //liberty - {5427279, 0xa628}, //library - {5423673, 0xa974}, //license - {5433, 0xa900}, //life - {5438, 0xa800}, //lift - {54448, 0xa100}, //light - {5453, 0xa500}, //like - {5462, 0xa100}, //limb - {54648, 0xa200}, //limit - {5465, 0xa500}, //link - {5466, 0xa900}, //lion - {547843, 0xa580}, //liquid - {5478, 0xac00}, //list - {548853, 0xa090}, //little - {5483, 0xa900}, //live - {549273, 0xac80}, //lizard - {5623, 0xa000}, //load - {5626, 0xa100}, //loan - {5627837, 0xa718}, //lobster - {56225, 0xa880}, //local - {5625, 0xa900}, //lock - {56442, 0xa280}, //logic - {566359, 0xa5a0}, //lonely - {5664, 0xa400}, //long - {5667, 0xa800}, //loop - {5688379, 0xa068}, //lottery - {5683, 0xa400}, //loud - {568643, 0xa510}, //lounge - {5683, 0xa900}, //love - {56925, 0xa880}, //loyal - {58259, 0x9980}, //lucky - {5844243, 0x9004}, //luggage - {586237, 0x9160}, //lumber - {58627, 0x9480}, //lunar - {58624, 0x9640}, //lunch - {589879, 0x95a0}, //luxury - {597427, 0xaab0}, //lyrics - {6224463, 0x0994}, //machine - {623, 0x0000}, //mad - {62442, 0x0280}, //magic - {624638, 0x0140}, //magnet - {6243, 0x0800}, //maid - {6245, 0x0a00}, //mail - {6246, 0x0900}, //main - {62567, 0x0280}, //major - {6253, 0x0500}, //make - {626625, 0x0020}, //mammal - {626, 0x0400}, //man - {626243, 0x0410}, //manage - {6263283, 0x0404}, //mandate - {62646, 0x0480}, //mango - {6267466, 0x07a4}, //mansion - {626825, 0x0520}, //manual - {62753, 0x0240}, //maple - {627253, 0x0990}, //marble - {62724, 0x0a40}, //march - {627446, 0x0890}, //margin - {627463, 0x0a50}, //marine - {627538, 0x0940}, //market - {62774243, 0x0a81}, //marriage - {6275, 0x0d00}, //mask - {6277, 0x0f00}, //mass - {627837, 0x0c60}, //master - {62824, 0x0240}, //match - {62837425, 0x01a2}, //material - {6284, 0x0100}, //math - {628749, 0x0290}, //matrix - {628837, 0x0060}, //matter - {6294686, 0x0610}, //maximum - {6293, 0x0d00}, //maze - {632369, 0x1080}, //meadow - {6326, 0x1100}, //mean - {6327873, 0x1364}, //measure - {6328, 0x1000}, //meat - {63242642, 0x191a}, //mechanic - {63325, 0x1080}, //medal - {63342, 0x1200}, //media - {635639, 0x1a20}, //melody - {6358, 0x1800}, //melt - {636237, 0x1160}, //member - {636679, 0x12a0}, //memory - {6368466, 0x14a4}, //mention - {6368, 0x1500}, //menu - {63729, 0x1a80}, //mercy - {63743, 0x1840}, //merge - {63748, 0x1a00}, //merit - {63779, 0x1a80}, //merry - {6374, 0x1d00}, //mesh - {6377243, 0x1f04}, //message - {63825, 0x1080}, //metal - {638463, 0x1180}, //method - {643353, 0x2090}, //middle - {64364448, 0x2184}, //midnight - {6455, 0x2900}, //milk - {6455466, 0x2aa4}, //million - {64642, 0x2280}, //mimic - {6463, 0x2400}, //mind - {6464686, 0x2610}, //minimum - {64667, 0x2680}, //minor - {646883, 0x2510}, //minute - {6472253, 0x28a4}, //miracle - {647767, 0x2aa0}, //mirror - {647379, 0x2da0}, //misery - {6477, 0x2f00}, //miss - {6478253, 0x2c14}, //mistake - {649, 0x2400}, //mix - {64933, 0x2500}, //mixed - {6498873, 0x2464}, //mixture - {662453, 0x2690}, //mobile - {66335, 0x2180}, //model - {663439, 0x22a0}, //modify - {666, 0x2000}, //mom - {666368, 0x2140}, //moment - {6664867, 0x2628}, //monitor - {666539, 0x2560}, //monkey - {6667837, 0x2718}, //monster - {66684, 0x2440}, //month - {6666, 0x2900}, //moon - {66725, 0x2880}, //moral - {6673, 0x2900}, //more - {6676464, 0x2990}, //morning - {66778486, 0x2d62}, //mosquito - {668437, 0x2160}, //mother - {668466, 0x2290}, //motion - {66867, 0x2280}, //motor - {66868246, 0x2509}, //mountain - {66873, 0x2740}, //mouse - {6683, 0x2900}, //move - {66843, 0x2a40}, //movie - {6824, 0x1900}, //much - {683346, 0x1a90}, //muffin - {6853, 0x1900}, //mule - {68584759, 0x188a}, //multiply - {687253, 0x1e90}, //muscle - {687386, 0x1d40}, //museum - {68747666, 0x1da8}, //mushroom - {68742, 0x1e80}, //music - {6878, 0x1c00}, //must - {688825, 0x1120}, //mutual - {697353, 0x2da0}, //myself - {6978379, 0x2c68}, //mystery - {6984, 0x2100}, //myth - {62483, 0x4a40}, //naive - {6263, 0x4100}, //name - {627546, 0x4190}, //napkin - {627769, 0x4a80}, //narrow - {62789, 0x4c80}, //nasty - {628466, 0x4290}, //nation - {628873, 0x4190}, //nature - {6327, 0x5200}, //near - {6325, 0x5900}, //neck - {6333, 0x5400}, //need - {63428483, 0x5029}, //negative - {6345328, 0x5260}, //neglect - {6348437, 0x5858}, //neither - {637439, 0x5140}, //nephew - {63783, 0x5a40}, //nerve - {6378, 0x5c00}, //nest - {638, 0x5000}, //net - {6389675, 0x50a4}, //network - {6388725, 0x5488}, //neutral - {63837, 0x5980}, //never - {6397, 0x5300}, //news - {6398, 0x5400}, //next - {6423, 0x6900}, //nice - {64448, 0x6100}, //night - {66253, 0x6640}, //noble - {66473, 0x6b40}, //noise - {6664633, 0x6254}, //nominee - {666353, 0x6890}, //noodle - {667625, 0x6820}, //normal - {66784, 0x6840}, //north - {6673, 0x6d00}, //nose - {6682253, 0x6064}, //notable - {6683, 0x6100}, //note - {6684464, 0x6190}, //nothing - {668423, 0x6290}, //notice - {66835, 0x6980}, //novel - {669, 0x6000}, //now - {6825327, 0x5a48}, //nuclear - {686237, 0x5160}, //number - {68773, 0x5b40}, //nurse - {688, 0x5000}, //nut - {625, 0x8400}, //oak - {6239, 0x9600}, //obey - {625328, 0x9180}, //object - {625443, 0x9a10}, //oblige - {6272873, 0x9e64}, //obscure - {6273783, 0x9da4}, //observe - {628246, 0x9090}, //obtain - {6284687, 0x9a9c}, //obvious - {62287, 0xa980}, //occur - {62326, 0xa440}, //ocean - {6286237, 0xa258}, //october - {6367, 0x8a00}, //odor - {633, 0xa800}, //off - {63337, 0xa980}, //offer - {633423, 0xaa90}, //office - {63836, 0xa140}, //often - {645, 0xa800}, //oil - {6529, 0x9200}, //okay - {653, 0xa000}, //old - {65483, 0xaa40}, //olive - {6596742, 0xa828}, //olympic - {6648, 0x8800}, //omit - {6623, 0x9900}, //once - {663, 0x9400}, //one - {66466, 0x9a40}, //onion - {665463, 0x9a50}, //online - {6659, 0x9a00}, //only - {6736, 0x8500}, //open - {67372, 0x8600}, //opera - {6746466, 0x89a4}, //opinion - {677673, 0x82d0}, //oppose - {678466, 0x8290}, //option - {672643, 0xa110}, //orange - {67248, 0xa600}, //orbit - {6724273, 0xa920}, //orchard - {67337, 0xa180}, //order - {67346279, 0xa24a}, //ordinary - {67426, 0xa040}, //organ - {674368, 0xa940}, //orient - {67444625, 0xa892}, //original - {677426, 0xa110}, //orphan - {6787424, 0xb2a4}, //ostrich - {68437, 0x8580}, //other - {6883667, 0x90a8}, //outdoor - {68837, 0x9180}, //outer - {688788, 0x9040}, //output - {6887433, 0x9384}, //outside - {6825, 0xa200}, //oval - {6836, 0xa500}, //oven - {6837, 0xa600}, //over - {696, 0x8400}, //own - {69637, 0x8580}, //owner - {699436, 0x9850}, //oxygen - {697837, 0xac60}, //oyster - {69663, 0xb940}, //ozone - {7228, 0x0800}, //pact - {723353, 0x0090}, //paddle - {7243, 0x0100}, //page - {7247, 0x0a00}, //pair - {725223, 0x0890}, //palace - {7256, 0x0800}, //palm - {72632, 0x0400}, //panda - {72635, 0x0580}, //panel - {72642, 0x0680}, //panic - {7268437, 0x0458}, //panther - {72737, 0x0180}, //paper - {727233, 0x0810}, //parade - {727368, 0x0940}, //parent - {7275, 0x0900}, //park - {727768, 0x0a80}, //parrot - {72789, 0x0880}, //party - {7277, 0x0f00}, //pass - {72824, 0x0240}, //patch - {7284, 0x0100}, //path - {7284368, 0x0250}, //patient - {728765, 0x02a0}, //patrol - {7288376, 0x0064}, //pattern - {72873, 0x0740}, //pause - {7283, 0x0900}, //pave - {7296368, 0x0850}, //payment - {73223, 0x1240}, //peace - {732688, 0x1140}, //peanut - {7327, 0x1200}, //pear - {7327268, 0x1310}, //peasant - {7354226, 0x1a84}, //pelican - {736, 0x1400}, //pen - {7362589, 0x1488}, //penalty - {736245, 0x16a0}, //pencil - {736753, 0x1890}, //people - {737737, 0x1060}, //pepper - {7373328, 0x1a60}, //perfect - {737648, 0x1880}, //permit - {737766, 0x1b90}, //person - {738, 0x1000}, //pet - {74663, 0x1940}, //phone - {74686, 0x1880}, //photo - {747273, 0x18d0}, //phrase - {74974225, 0x1ba2}, //physical - {74266, 0x2180}, //piano - {742642, 0x29a0}, //picnic - {7428873, 0x2864}, //picture - {74323, 0x2640}, //piece - {744, 0x2000}, //pig - {744366, 0x2190}, //pigeon - {7455, 0x2a00}, //pill - {74568, 0x2a00}, //pilot - {7465, 0x2500}, //pink - {7466337, 0x2958}, //pioneer - {7473, 0x2100}, //pipe - {747865, 0x2ca0}, //pistol - {74824, 0x2240}, //pitch - {74992, 0x2f00}, //pizza - {75223, 0x2240}, //place - {752638, 0x2140}, //planet - {7527842, 0x2328}, //plastic - {75283, 0x2040}, //plate - {7529, 0x2200}, //play - {753273, 0x24d0}, //please - {753343, 0x2410}, //pledge - {75825, 0x2640}, //pluck - {7584, 0x2400}, //plug - {758643, 0x2510}, //plunge - {7636, 0x2400}, //poem - {7638, 0x2400}, //poet - {76468, 0x2900}, //point - {76527, 0x2880}, //polar - {7653, 0x2900}, //pole - {765423, 0x2a90}, //police - {7663, 0x2400}, //pond - {7669, 0x2600}, //pony - {7665, 0x2a00}, //pool - {7678527, 0x2188}, //popular - {7678466, 0x28a4}, //portion - {76748466, 0x2e29}, //position - {76774253, 0x2f99}, //possible - {7678, 0x2c00}, //post - {768286, 0x2020}, //potato - {7688379, 0x2068}, //pottery - {7683789, 0x2988}, //poverty - {769337, 0x2060}, //powder - {76937, 0x2180}, //power - {77228423, 0x2229}, //practice - {772473, 0x22d0}, //praise - {7733428, 0x24a0}, //predict - {773337, 0x2660}, //prefer - {7737273, 0x2424}, //prepare - {7737368, 0x2750}, //present - {773889, 0x2420}, //pretty - {7738368, 0x2650}, //prevent - {77423, 0x2a40}, //price - {77433, 0x2840}, //pride - {7746279, 0x2828}, //primary - {77468, 0x2900}, //print - {77467489, 0x2aa2}, //priority - {774766, 0x2b90}, //prison - {7748283, 0x2a04}, //private - {77493, 0x2b40}, //prize - {7762536, 0x2990}, //problem - {7762377, 0x2a7c}, //process - {7763823, 0x2864}, //produce - {776348, 0x2a80}, //profit - {7764726, 0x2880}, //program - {7765328, 0x2860}, //project - {7766683, 0x2884}, //promote - {77663, 0x2a80}, //proof - {77673789, 0x2862}, //property - {7767737, 0x2b18}, //prosper - {7768328, 0x2860}, //protect - {77683, 0x2900}, //proud - {7768433, 0x2a84}, //provide - {782542, 0x16a0}, //public - {7833464, 0x1090}, //pudding - {7855, 0x1a00}, //pull - {7857, 0x1800}, //pulp - {78573, 0x1b40}, //pulse - {7867546, 0x1064}, //pumpkin - {78624, 0x1640}, //punch - {78745, 0x1280}, //pupil - {78779, 0x1080}, //puppy - {78724273, 0x1a4d}, //purchase - {787489, 0x1a20}, //purity - {7877673, 0x18b4}, //purpose - {78773, 0x1b40}, //purse - {7874, 0x1d00}, //push - {788, 0x1000}, //put - {789953, 0x1f90}, //puzzle - {7972643, 0x2820}, //pyramid - {7825489, 0x5288}, //quality - {7826886, 0x5110}, //quantum - {7827837, 0x5218}, //quarter - {78378466, 0x5729}, //question - {78425, 0x5a40}, //quick - {7848, 0x5800}, //quit - {7849, 0x5b00}, //quiz - {78683, 0x5840}, //quote - {722248, 0x8580}, //rabbit - {7222666, 0x8aa4}, //raccoon - {7223, 0x8900}, //race - {7225, 0x8900}, //rack - {72327, 0x8080}, //radar - {72346, 0x8280}, //radio - {7245, 0x8a00}, //rail - {7246, 0x8900}, //rain - {72473, 0x8b40}, //raise - {72559, 0x8a80}, //rally - {7267, 0x8000}, //ramp - {72624, 0x8640}, //ranch - {726366, 0x8480}, //random - {72643, 0x8440}, //range - {72743, 0x8200}, //rapid - {7273, 0x8900}, //rare - {7283, 0x8100}, //rate - {728437, 0x8160}, //rather - {72836, 0x8940}, //raven - {729, 0x8000}, //raw - {72967, 0x8e80}, //razor - {73239, 0x9080}, //ready - {7325, 0x9200}, //real - {732766, 0x9390}, //reason - {73235, 0x9580}, //rebel - {7328453, 0x95a0}, //rebuild - {732255, 0x98a0}, //recall - {7323483, 0x99a4}, //receive - {732473, 0x9a10}, //recipe - {732673, 0x9a80}, //record - {7329253, 0x9aa4}, //recycle - {733823, 0x9190}, //reduce - {7335328, 0x9a60}, //reflect - {733676, 0x9a80}, //reform - {733873, 0x99d0}, //refuse - {734466, 0x9290}, //region - {734738, 0x9240}, //regret - {7348527, 0x9188}, //regular - {735328, 0x9180}, //reject - {73529, 0x9840}, //relax - {7353273, 0x9934}, //release - {735433, 0x9a60}, //relief - {7359, 0x9a00}, //rely - {736246, 0x9090}, //remain - {73636237, 0x9116}, //remember - {736463, 0x9240}, //remind - {736683, 0x9290}, //remove - {736337, 0x9460}, //render - {73639, 0x9500}, //renew - {7368, 0x9400}, //rent - {736736, 0x9850}, //reopen - {737247, 0x90a0}, //repair - {737328, 0x9100}, //repeat - {7375223, 0x9224}, //replace - {737678, 0x9280}, //report - {7378473, 0x95a4}, //require - {737283, 0x9e50}, //rescue - {73736253, 0x9d19}, //resemble - {737478, 0x9ec0}, //resist - {73768723, 0x9e69}, //resource - {73776673, 0x9c9d}, //response - {737858, 0x9d80}, //result - {738473, 0x9290}, //retire - {7387328, 0x9240}, //retreat - {738876, 0x9190}, //return - {7386466, 0x95a4}, //reunion - {738325, 0x9920}, //reveal - {738439, 0x9a40}, //review - {739273, 0x9080}, //reward - {749846, 0x9840}, //rhythm - {742, 0xa400}, //rib - {742266, 0xa590}, //ribbon - {7423, 0xa900}, //rice - {7424, 0xa900}, //rich - {7433, 0xa100}, //ride - {74343, 0xa040}, //ridge - {74353, 0xaa40}, //rifle - {74448, 0xa100}, //right - {74443, 0xa200}, //rigid - {7464, 0xa400}, //ring - {7468, 0xa800}, //riot - {747753, 0xa090}, //ripple - {7475, 0xad00}, //risk - {748825, 0xa120}, //ritual - {74825, 0xa880}, //rival - {74837, 0xa980}, //river - {7623, 0xa000}, //road - {76278, 0xa300}, //roast - {76268, 0xa600}, //robot - {762878, 0xa5c0}, //robust - {762538, 0xa940}, //rocket - {7662623, 0xa064}, //romance - {7663, 0xaa00}, //roof - {766543, 0xa990}, //rookie - {7666, 0xa800}, //room - {7673, 0xad00}, //rose - {768283, 0xa010}, //rotate - {76844, 0xa440}, //rough - {76863, 0xa500}, //round - {76883, 0xa440}, //route - {76925, 0xa880}, //royal - {782237, 0x9560}, //rubber - {7833, 0x9100}, //rude - {784, 0x9000}, //rug - {7853, 0x9900}, //rule - {786, 0x9400}, //run - {786929, 0x9420}, //runway - {78725, 0x9880}, //rural - {723, 0xc000}, //sad - {723353, 0xc090}, //saddle - {7236377, 0xc17c}, //sadness - {7233, 0xc900}, //safe - {7245, 0xca00}, //sail - {72523, 0xc800}, //salad - {725666, 0xc890}, //salmon - {72566, 0xca40}, //salon - {7258, 0xc800}, //salt - {725883, 0xc910}, //salute - {7263, 0xc100}, //same - {726753, 0xc090}, //sample - {7263, 0xc400}, //sand - {7284739, 0xc2e8}, //satisfy - {7286744, 0xc2d8}, //satoshi - {72823, 0xc640}, //sauce - {7287243, 0xc704}, //sausage - {7283, 0xc900}, //save - {729, 0xc800}, //say - {72253, 0xe240}, //scale - {7226, 0xe100}, //scan - {72273, 0xe240}, //scare - {7228837, 0xe018}, //scatter - {72363, 0xe540}, //scene - {724363, 0xe510}, //scheme - {724665, 0xe6a0}, //school - {7243623, 0xe964}, //science - {72477677, 0xebeb}, //scissors - {72677466, 0xea29}, //scorpion - {72688, 0xe900}, //scout - {72727, 0xe800}, //scrap - {727336, 0xe950}, //screen - {727478, 0xea00}, //script - {72782, 0xe940}, //scrub - {732, 0xd000}, //sea - {732724, 0xd290}, //search - {732766, 0xd390}, //season - {7328, 0xd000}, //seat - {732663, 0xda40}, //second - {732738, 0xda40}, //secret - {7328466, 0xd8a4}, //section - {73287489, 0xd9a2}, //security - {7333, 0xd400}, //seed - {7335, 0xd500}, //seek - {7346368, 0xd050}, //segment - {735328, 0xd980}, //select - {7355, 0xda00}, //sell - {7364627, 0xd248}, //seminar - {736467, 0xd6a0}, //senior - {73673, 0xd740}, //sense - {73683623, 0xd459}, //sentence - {737437, 0xda70}, //series - {7378423, 0xdaa4}, //service - {7377466, 0xdfa4}, //session - {738853, 0xd090}, //settle - {73887, 0xd100}, //setup - {73836, 0xd940}, //seven - {742369, 0xd080}, //shadow - {74238, 0xd200}, //shaft - {7425569, 0xd2a0}, //shallow - {74273, 0xd240}, //share - {7433, 0xd400}, //shed - {74355, 0xd680}, //shell - {7437433, 0xd6a8}, //sheriff - {744353, 0xd980}, //shield - {74438, 0xda00}, //shift - {74463, 0xd940}, //shine - {7447, 0xd800}, //ship - {744837, 0xda60}, //shiver - {74625, 0xda40}, //shock - {7463, 0xd900}, //shoe - {74668, 0xda00}, //shoot - {7467, 0xd800}, //shop - {74678, 0xda00}, //short - {74685337, 0xd986}, //shoulder - {74683, 0xda40}, //shove - {747467, 0xda00}, //shrimp - {74784, 0xd900}, //shrug - {7483353, 0xd6a4}, //shuffle - {749, 0xd800}, //shy - {7425464, 0xe690}, //sibling - {7425, 0xe900}, //sick - {7433, 0xe100}, //side - {74343, 0xe440}, //siege - {74448, 0xe100}, //sight - {7446, 0xe100}, //sign - {745368, 0xe940}, //silent - {7455, 0xe900}, //silk - {74559, 0xea80}, //silly - {745837, 0xea60}, //silver - {7464527, 0xe288}, //similar - {746753, 0xe090}, //simple - {74623, 0xe640}, //since - {7464, 0xe400}, //sing - {74736, 0xe940}, //siren - {747837, 0xec60}, //sister - {7488283, 0xe104}, //situate - {749, 0xe400}, //six - {7493, 0xed00}, //size - {75283, 0xd040}, //skate - {753824, 0xd490}, //sketch - {754, 0xd800}, //ski - {75455, 0xda80}, //skill - {7546, 0xd900}, //skin - {75478, 0xda00}, //skirt - {75855, 0xd680}, //skull - {7522, 0xe100}, //slab - {7526, 0xe000}, //slam - {75337, 0xe500}, //sleep - {7536337, 0xe518}, //slender - {75423, 0xea40}, //slice - {75433, 0xe840}, //slide - {754448, 0xe840}, //slight - {7546, 0xe800}, //slim - {756426, 0xe810}, //slogan - {7568, 0xe800}, //slot - {7569, 0xe800}, //slow - {75874, 0xe740}, //slush - {76255, 0xc280}, //small - {76278, 0xc200}, //smart - {76453, 0xca40}, //smile - {76653, 0xc940}, //smoke - {766684, 0xca10}, //smooth - {76225, 0xd240}, //snack - {76253, 0xd140}, //snake - {7627, 0xd000}, //snap - {76433, 0xda80}, //sniff - {7669, 0xd800}, //snow - {7627, 0xe000}, //soap - {762237, 0xea60}, //soccer - {762425, 0xea20}, //social - {7625, 0xe900}, //sock - {7632, 0xe000}, //soda - {7638, 0xe800}, //soft - {76527, 0xe880}, //solar - {7653437, 0xe898}, //soldier - {76543, 0xea00}, //solid - {76588466, 0xe929}, //solution - {76583, 0xea40}, //solve - {7663663, 0xe194}, //someone - {7664, 0xe400}, //song - {7666, 0xe900}, //soon - {76779, 0xea80}, //sorry - {7678, 0xe800}, //sort - {7685, 0xe600}, //soul - {76863, 0xe500}, //sound - {7687, 0xe400}, //soup - {768723, 0xe690}, //source - {76884, 0xe440}, //south - {77223, 0xc240}, //space - {77273, 0xc240}, //spare - {7728425, 0xc088}, //spatial - {77296, 0xc040}, //spawn - {77325, 0xc440}, //speak - {7732425, 0xc688}, //special - {77333, 0xc500}, //speed - {77355, 0xc680}, //spell - {77363, 0xc500}, //spend - {774373, 0xc590}, //sphere - {77423, 0xca40}, //spice - {774337, 0xc860}, //spider - {77453, 0xc940}, //spike - {7746, 0xc900}, //spin - {774748, 0xca80}, //spirit - {77548, 0xca00}, //split - {77645, 0xca80}, //spoil - {7766767, 0xc9e8}, //sponsor - {77666, 0xca40}, //spoon - {77678, 0xca00}, //sport - {7768, 0xc800}, //spot - {77729, 0xc880}, //spray - {777323, 0xc900}, //spread - {777464, 0xca40}, //spring - {779, 0xc800}, //spy - {778273, 0xd490}, //square - {7783393, 0xd574}, //squeeze - {77847735, 0xd6a6}, //squirrel - {782253, 0xc190}, //stable - {7823486, 0xc090}, //stadium - {78233, 0xc280}, //staff - {78243, 0xc040}, //stage - {782477, 0xc2b0}, //stairs - {78267, 0xc000}, //stamp - {78263, 0xc100}, //stand - {78278, 0xc200}, //start - {78283, 0xc040}, //state - {7829, 0xc200}, //stay - {78325, 0xc440}, //steak - {78335, 0xc580}, //steel - {7836, 0xc400}, //stem - {7837, 0xc400}, //step - {783736, 0xc660}, //stereo - {78425, 0xca40}, //stick - {78455, 0xca80}, //still - {78464, 0xc900}, //sting - {78625, 0xca40}, //stock - {7866224, 0xc824}, //stomach - {78663, 0xc940}, //stone - {78665, 0xca80}, //stool - {78679, 0xca80}, //story - {78683, 0xca40}, //stove - {78728349, 0xc812}, //strategy - {787338, 0xc940}, //street - {787453, 0xca50}, //strike - {787664, 0xca40}, //strong - {78784453, 0xc909}, //struggle - {7883368, 0xc450}, //student - {78833, 0xc680}, //stuff - {7886253, 0xc464}, //stumble - {78953, 0xca40}, //style - {7825328, 0xd460}, //subject - {782648, 0xd480}, //submit - {782929, 0xd420}, //subway - {7822377, 0xda7c}, //success - {7824, 0xd900}, //such - {783336, 0xd050}, //sudden - {783337, 0xda60}, //suffer - {78427, 0xd080}, //sugar - {7844378, 0xd070}, //suggest - {7848, 0xd800}, //suit - {786637, 0xd060}, //summer - {786, 0xd400}, //sun - {78669, 0xd580}, //sunny - {786738, 0xd740}, //sunset - {78737, 0xd180}, //super - {787759, 0xd0a0}, //supply - {7877363, 0xd244}, //supreme - {7873, 0xd900}, //sure - {7873223, 0xda24}, //surface - {78743, 0xd840}, //surge - {78777473, 0xd8ad}, //surprise - {78776863, 0xda94}, //surround - {787839, 0xda60}, //survey - {7877328, 0xdc60}, //suspect - {7878246, 0xdc24}, //sustain - {7925569, 0xc2a0}, //swallow - {79267, 0xc000}, //swamp - {7927, 0xc000}, //swap - {79276, 0xc200}, //swarm - {79327, 0xc480}, //swear - {79338, 0xc500}, //sweet - {79438, 0xca00}, //swift - {7946, 0xc800}, //swim - {79464, 0xc900}, //swing - {794824, 0xc890}, //switch - {79673, 0xca00}, //sword - {796265, 0xe1a0}, //symbol - {7967866, 0xe020}, //symptom - {79787, 0xe900}, //syrup - {797836, 0xec40}, //system - {82253, 0x0640}, //table - {822553, 0x0990}, //tackle - {824, 0x0000}, //tag - {8245, 0x0a00}, //tail - {825368, 0x0940}, //talent - {8255, 0x0900}, //talk - {8265, 0x0500}, //tank - {8273, 0x0100}, //tape - {827438, 0x0840}, //target - {8275, 0x0d00}, //task - {82783, 0x0c40}, //taste - {828866, 0x00a0}, //tattoo - {8294, 0x0600}, //taxi - {83224, 0x1240}, //teach - {8326, 0x1000}, //team - {8355, 0x1a00}, //tell - {836, 0x1400}, //ten - {836268, 0x1440}, //tenant - {836647, 0x15b0}, //tennis - {8368, 0x1400}, //tent - {8376, 0x1800}, //term - {8378, 0x1c00}, //test - {8398, 0x1400}, //text - {84265, 0x1140}, //thank - {8428, 0x1000}, //that - {84363, 0x1440}, //theme - {8436, 0x1500}, //then - {843679, 0x16a0}, //theory - {84373, 0x1640}, //there - {8439, 0x1600}, //they - {84464, 0x1900}, //thing - {8447, 0x1b00}, //this - {8468448, 0x1910}, //thought - {84733, 0x1940}, //three - {847483, 0x1a90}, //thrive - {84769, 0x1a00}, //throw - {84862, 0x1440}, //thumb - {8486337, 0x1518}, //thunder - {842538, 0x2940}, //ticket - {8433, 0x2100}, //tide - {84437, 0x2180}, //tiger - {8458, 0x2800}, //tilt - {846237, 0x2160}, //timber - {8463, 0x2100}, //time - {8469, 0x2600}, //tiny - {847, 0x2000}, //tip - {84733, 0x2900}, //tired - {847783, 0x2f50}, //tissue - {84853, 0x2240}, //title - {86278, 0x2300}, //toast - {8622226, 0x24a8}, //tobacco - {86329, 0x2080}, //today - {8633537, 0x2098}, //toddler - {863, 0x2400}, //toe - {86438437, 0x2116}, //together - {864538, 0x2a40}, //toilet - {86536, 0x2540}, //token - {866286, 0x2020}, //tomato - {86667769, 0x22a8}, //tomorrow - {8663, 0x2500}, //tone - {866483, 0x2450}, //tongue - {8664448, 0x2610}, //tonight - {8665, 0x2a00}, //tool - {86684, 0x2840}, //tooth - {867, 0x2000}, //top - {86742, 0x2280}, //topic - {867753, 0x2090}, //topple - {86724, 0x2a40}, //torch - {8676236, 0x2908}, //tornado - {86786473, 0x28ad}, //tortoise - {8677, 0x2f00}, //toss - {86825, 0x2080}, //total - {8687478, 0x26b0}, //tourist - {869273, 0x2080}, //toward - {86937, 0x2180}, //tower - {8696, 0x2100}, //town - {869, 0x2800}, //toy - {87225, 0x2240}, //track - {87233, 0x2040}, //trade - {8723342, 0x22a8}, //traffic - {872442, 0x20a0}, //tragic - {87246, 0x2240}, //train - {87267337, 0x21e6}, //transfer - {8727, 0x2000}, //trap - {87274, 0x2340}, //trash - {872835, 0x2260}, //travel - {8729, 0x2200}, //tray - {87328, 0x2400}, //treat - {8733, 0x2500}, //tree - {87363, 0x2500}, //trend - {87425, 0x2880}, //trial - {87423, 0x2940}, //tribe - {87425, 0x2a40}, //trick - {8744437, 0x2818}, //trigger - {8746, 0x2800}, //trim - {8747, 0x2800}, //trip - {876749, 0x2860}, //trophy - {8768253, 0x2964}, //trouble - {87825, 0x2640}, //truck - {8783, 0x2500}, //true - {87859, 0x2680}, //truly - {8786738, 0x2410}, //trumpet - {87878, 0x2700}, //trust - {87884, 0x2440}, //truth - {879, 0x2800}, //try - {8823, 0x1500}, //tube - {8848466, 0x18a4}, //tuition - {886253, 0x1190}, //tumble - {8862, 0x1400}, //tuna - {886635, 0x1560}, //tunnel - {887539, 0x1960}, //turkey - {8876, 0x1900}, //turn - {887853, 0x1890}, //turtle - {893583, 0x0690}, //twelve - {893689, 0x0520}, //twenty - {89423, 0x0a40}, //twice - {8946, 0x0900}, //twin - {89478, 0x0b00}, //twist - {896, 0x0800}, //two - {8973, 0x2100}, //type - {8974225, 0x2288}, //typical - {8459, 0x4a00}, //ugly - {86273552, 0x4668}, //umbrella - {862253, 0x5190}, //unable - {8629273, 0x5024}, //unaware - {86253, 0x5a40}, //uncle - {8626837, 0x5a98}, //uncover - {86337, 0x5180}, //under - {8636, 0x5200}, //undo - {863247, 0x58a0}, //unfair - {863653, 0x5a80}, //unfold - {8642779, 0x5408}, //unhappy - {8643676, 0x5aa0}, //uniform - {864783, 0x5950}, //unique - {8648, 0x5800}, //unit - {86483773, 0x5a6d}, //universe - {8656696, 0x5584}, //unknown - {865625, 0x5a90}, //unlock - {86845, 0x5280}, //until - {8687825, 0x5748}, //unusual - {868345, 0x59a0}, //unveil - {873283, 0x4010}, //update - {8747233, 0x4204}, //upgrade - {874653, 0x4680}, //uphold - {8766, 0x4900}, //upon - {87737, 0x4180}, //upper - {87738, 0x4d00}, //upset - {87226, 0x6440}, //urban - {8743, 0x6100}, //urge - {87243, 0x7040}, //usage - {873, 0x7400}, //use - {8733, 0x7400}, //used - {873385, 0x7660}, //useful - {8735377, 0x767c}, //useless - {87825, 0x7480}, //usual - {8845489, 0x4a88}, //utility - {822268, 0x8840}, //vacant - {822886, 0x8940}, //vacuum - {82483, 0x8140}, //vague - {82543, 0x8a00}, //valid - {825539, 0x8a60}, //valley - {82583, 0x8a40}, //valve - {826, 0x8400}, //van - {826474, 0x86d0}, //vanish - {82767, 0x8280}, //vapor - {8274687, 0x8a9c}, //various - {8278, 0x8c00}, //vast - {82858, 0x8600}, //vault - {8344253, 0x96a4}, //vehicle - {835838, 0x9a40}, //velvet - {836367, 0x94a0}, //vendor - {8368873, 0x9464}, //venture - {83683, 0x9540}, //venue - {8372, 0x9900}, //verb - {837439, 0x9aa0}, //verify - {8377466, 0x9ba4}, //version - {8379, 0x9a00}, //very - {837735, 0x9f60}, //vessel - {8383726, 0x9184}, //veteran - {842253, 0xa190}, //viable - {8427268, 0xa610}, //vibrant - {8424687, 0xaa9c}, //vicious - {8428679, 0xa8a8}, //victory - {84336, 0xa180}, //video - {8439, 0xa400}, //view - {8455243, 0xaa04}, //village - {8468243, 0xa404}, //vintage - {846546, 0xaa90}, //violin - {8478825, 0xa848}, //virtual - {84787, 0xa9c0}, //virus - {8472, 0xac00}, //visa - {84748, 0xae00}, //visit - {847825, 0xad20}, //visual - {84825, 0xa080}, //vital - {84843, 0xaa00}, //vivid - {86225, 0xa880}, //vocal - {86423, 0xaa40}, //voice - {8643, 0xa800}, //void - {8652266, 0xaa18}, //volcano - {865863, 0xa910}, //volume - {8683, 0xa100}, //vote - {869243, 0xa810}, //voyage - {9243, 0x0100}, //wage - {92466, 0x0240}, //wagon - {9248, 0x0800}, //wait - {9255, 0x0900}, //walk - {9255, 0x0a00}, //wall - {925688, 0x0940}, //walnut - {9268, 0x0400}, //want - {9273273, 0x0a24}, //warfare - {9276, 0x0800}, //warm - {9277467, 0x0aa8}, //warrior - {9274, 0x0d00}, //wash - {9277, 0x0c00}, //wasp - {92783, 0x0c40}, //waste - {92837, 0x0180}, //water - {9283, 0x0900}, //wave - {929, 0x0800}, //way - {932584, 0x1210}, //wealth - {932766, 0x1090}, //weapon - {9327, 0x1200}, //wear - {932735, 0x1360}, //weasel - {9328437, 0x1058}, //weather - {932, 0x1400}, //web - {9333464, 0x1090}, //wedding - {9335363, 0x1550}, //weekend - {93473, 0x1a00}, //weird - {9352663, 0x1a84}, //welcome - {9378, 0x1c00}, //west - {938, 0x1000}, //wet - {94253, 0x1240}, //whale - {9428, 0x1000}, //what - {94328, 0x1400}, //wheat - {94335, 0x1580}, //wheel - {9436, 0x1500}, //when - {94373, 0x1640}, //where - {9447, 0x1800}, //whip - {9447737, 0x1b18}, //whisper - {9433, 0x2100}, //wide - {94384, 0x2040}, //width - {9433, 0x2900}, //wife - {9453, 0x2800}, //wild - {9455, 0x2a00}, //will - {946, 0x2400}, //win - {946369, 0x2480}, //window - {9463, 0x2500}, //wine - {9464, 0x2400}, //wing - {9465, 0x2500}, //wink - {946637, 0x2560}, //winner - {946837, 0x2460}, //winter - {9473, 0x2900}, //wire - {947366, 0x2c80}, //wisdom - {9473, 0x2d00}, //wise - {9474, 0x2d00}, //wish - {9486377, 0x217c}, //witness - {9653, 0x2a00}, //wolf - {96626, 0x2040}, //woman - {966337, 0x2460}, //wonder - {9663, 0x2800}, //wood - {9665, 0x2a00}, //wool - {9673, 0x2800}, //word - {9675, 0x2900}, //work - {96753, 0x2a00}, //world - {96779, 0x2a80}, //worry - {96784, 0x2840}, //worth - {9727, 0x2000}, //wrap - {97325, 0x2640}, //wreck - {9737853, 0x2724}, //wrestle - {97478, 0x2b00}, //wrist - {97483, 0x2840}, //write - {97664, 0x2900}, //wrong - {9273, 0x8800}, //yard - {9327, 0x9200}, //year - {935569, 0x9a80}, //yellow - {968, 0xa400}, //you - {96864, 0xa500}, //young - {96884, 0xa440}, //youth - {93272, 0xd600}, //zebra - {9376, 0xda00}, //zero - {9663, 0xe500}, //zone - {966, 0xe800} //zoo -}; \ No newline at end of file +typedef struct { + uint32_t keypad_digits; + uint16_t offsets; +} word_info_t; + +word_info_t bip39_word_info[] = { + {224, 0x4000}, // bag + {226, 0x8400}, // can + {227, 0x4800}, // bar + {227, 0x8800}, // car + {228, 0x2000}, // act + {228, 0x8000}, // cat + {233, 0x0000}, // add + {243, 0x0400}, // age + {243, 0x6000}, // bid + {246, 0x2000}, // aim + {247, 0x2800}, // air + {255, 0x2800}, // all + {269, 0x1800}, // any + {269, 0x6400}, // box + {269, 0x6800}, // boy + {275, 0x3400}, // ask + {276, 0x2000}, // arm + {278, 0x2000}, // art + {279, 0xa800}, // cry + {287, 0x5c00}, // bus + {287, 0x9000}, // cup + {323, 0x0000}, // dad + {326, 0x8400}, // fan + {328, 0x8000}, // fat + {329, 0x0800}, // day + {333, 0x9400}, // fee + {339, 0x9000}, // few + {344, 0x4000}, // egg + {348, 0xa000}, // fit + {349, 0xa400}, // fix + {359, 0xa800}, // fly + {363, 0x5000}, // end + {364, 0x2000}, // dog + {364, 0xa000}, // fog + {369, 0xa400}, // fox + {372, 0x6000}, // era + {379, 0x2800}, // dry + {386, 0x9400}, // fun + {393, 0x6400}, // eye + {423, 0xa400}, // ice + {427, 0x0000}, // gap + {427, 0x0c00}, // gas + {428, 0x4000}, // hat + {436, 0x5400}, // hen + {447, 0x6000}, // hip + {455, 0xa800}, // ill + {482, 0x5400}, // hub + {486, 0x1400}, // gun + {496, 0x2000}, // gym + {522, 0x8400}, // lab + {527, 0x0800}, // jar + {529, 0x8000}, // law + {534, 0x9000}, // leg + {539, 0x5800}, // key + {543, 0x6000}, // kid + {548, 0x6000}, // kit + {562, 0x2400}, // job + {569, 0x2800}, // joy + {623, 0x0000}, // mad + {625, 0x8400}, // oak + {626, 0x0400}, // man + {633, 0xa800}, // off + {638, 0x5000}, // net + {645, 0xa800}, // oil + {649, 0x2400}, // mix + {653, 0xa000}, // old + {663, 0x9400}, // one + {666, 0x2000}, // mom + {669, 0x6000}, // now + {688, 0x5000}, // nut + {696, 0x8400}, // own + {723, 0xc000}, // sad + {729, 0x8000}, // raw + {729, 0xc800}, // say + {732, 0xd000}, // sea + {736, 0x1400}, // pen + {738, 0x1000}, // pet + {742, 0xa400}, // rib + {744, 0x2000}, // pig + {749, 0xd800}, // shy + {749, 0xe400}, // six + {754, 0xd800}, // ski + {779, 0xc800}, // spy + {784, 0x9000}, // rug + {786, 0x9400}, // run + {786, 0xd400}, // sun + {788, 0x1000}, // put + {824, 0x0000}, // tag + {826, 0x8400}, // van + {836, 0x1400}, // ten + {847, 0x2000}, // tip + {863, 0x2400}, // toe + {867, 0x2000}, // top + {869, 0x2800}, // toy + {873, 0x7400}, // use + {879, 0x2800}, // try + {896, 0x0800}, // two + {929, 0x0800}, // way + {932, 0x1400}, // web + {938, 0x1000}, // wet + {946, 0x2400}, // win + {966, 0xe800}, // zoo + {968, 0xa400}, // you + {2229, 0x4600}, // baby + {2243, 0x2800}, // acid + {2243, 0x8100}, // cage + {2253, 0x1900}, // able + {2253, 0x8500}, // cake + {2255, 0x4a00}, // ball + {2255, 0x8a00}, // call + {2256, 0x8800}, // calm + {2267, 0x8000}, // camp + {2273, 0x4d00}, // base + {2273, 0x8800}, // card + {2273, 0x8d00}, // case + {2274, 0x8d00}, // cash + {2278, 0x8800}, // cart + {2283, 0x8900}, // cave + {2326, 0x5100}, // bean + {2333, 0x5600}, // beef + {2358, 0x5800}, // belt + {2378, 0x5c00}, // best + {2428, 0x9000}, // chat + {2433, 0x9600}, // chef + {2453, 0x6500}, // bike + {2463, 0x6400}, // bind + {2473, 0x6800}, // bird + {2489, 0xa200}, // city + {2527, 0xa000}, // clap + {2529, 0xa000}, // claw + {2529, 0xa200}, // clay + {2547, 0xa800}, // clip + {2564, 0xa800}, // clog + {2576, 0x2e00}, // also + {2582, 0xa500}, // club + {2583, 0x6500}, // blue + {2587, 0x6600}, // blur + {2628, 0x6000}, // boat + {2633, 0xa100}, // code + {2639, 0x6200}, // body + {2645, 0x6a00}, // boil + {2645, 0xaa00}, // coil + {2646, 0xa900}, // coin + {2662, 0x6100}, // bomb + {2663, 0x6500}, // bone + {2663, 0xa100}, // come + {2665, 0x6900}, // book + {2665, 0xa900}, // cook + {2665, 0xaa00}, // cool + {2673, 0xa900}, // core + {2676, 0xa900}, // corn + {2677, 0x6f00}, // boss + {2678, 0xac00}, // cost + {2679, 0xa200}, // copy + {2724, 0x2900}, // arch + {2726, 0xa000}, // cram + {2732, 0x2400}, // area + {2739, 0xa400}, // crew + {2767, 0xa800}, // crop + {2769, 0x2200}, // army + {2823, 0x9500}, // cube + {2852, 0x5900}, // bulb + {2855, 0x5900}, // bulk + {2866, 0x0800}, // atom + {2868, 0x1400}, // aunt + {2879, 0x5e00}, // busy + {2883, 0x9100}, // cute + {2886, 0x1200}, // auto + {2899, 0x5f00}, // buzz + {2929, 0x0200}, // away + {2947, 0x1b00}, // axis + {3223, 0x8900}, // face + {3233, 0x8100}, // fade + {3246, 0x6600}, // echo + {3255, 0x8a00}, // fall + {3263, 0x8100}, // fame + {3267, 0x0000}, // damp + {3274, 0x0d00}, // dash + {3276, 0x4900}, // earn + {3276, 0x8800}, // farm + {3278, 0x4c00}, // east + {3279, 0x4e00}, // easy + {3296, 0x0100}, // dawn + {3325, 0x1200}, // deal + {3333, 0x9400}, // feed + {3335, 0x9600}, // feel + {3337, 0x1600}, // deer + {3339, 0x1a00}, // defy + {3343, 0x4100}, // edge + {3348, 0x4800}, // edit + {3369, 0x1600}, // deny + {3375, 0x1d00}, // desk + {3423, 0x2900}, // dice + {3425, 0x2200}, // dial + {3438, 0x2400}, // diet + {3453, 0xa900}, // file + {3456, 0xa800}, // film + {3463, 0xa400}, // find + {3463, 0xa500}, // fine + {3473, 0xa900}, // fire + {3474, 0x2d00}, // dish + {3474, 0xad00}, // fish + {3476, 0xa800}, // firm + {3478, 0x2800}, // dirt + {3524, 0xa000}, // flag + {3528, 0xa000}, // flat + {3533, 0xa500}, // flee + {3547, 0xa800}, // flip + {3573, 0x6d00}, // else + {3626, 0xa000}, // foam + {3645, 0xaa00}, // foil + {3653, 0xa800}, // fold + {3655, 0x2a00}, // doll + {3663, 0xa800}, // food + {3667, 0x2a00}, // door + {3668, 0xa800}, // foot + {3673, 0x2d00}, // dose + {3675, 0xa900}, // fork + {3683, 0x2900}, // dove + {3729, 0x2000}, // draw + {3747, 0x2800}, // drip + {3764, 0xa800}, // frog + {3767, 0x2800}, // drop + {3786, 0x2400}, // drum + {3825, 0x1900}, // duck + {3835, 0x9600}, // fuel + {3845, 0x6a00}, // evil + {3862, 0x1100}, // dumb + {3863, 0x1500}, // dune + {3878, 0x1c00}, // dust + {3879, 0x9a00}, // fury + {3889, 0x1200}, // duty + {3948, 0x5800}, // exit + {4246, 0x0900}, // gain + {4247, 0x4a00}, // hair + {4253, 0x4a00}, // half + {4263, 0x0100}, // game + {4263, 0x4400}, // hand + {4266, 0xa900}, // icon + {4273, 0x4800}, // hard + {4277, 0x0c00}, // gasp + {4283, 0x0100}, // gate + {4283, 0x4900}, // have + {4293, 0x0d00}, // gaze + {4295, 0x4100}, // hawk + {4323, 0x5000}, // head + {4332, 0x8400}, // idea + {4353, 0x8900}, // idle + {4357, 0x5800}, // help + {4376, 0x5a00}, // hero + {4438, 0x2800}, // gift + {4444, 0x6100}, // high + {4455, 0x6a00}, // hill + {4468, 0x6400}, // hint + {4473, 0x6900}, // hire + {4475, 0x2a00}, // girl + {4483, 0x2900}, // give + {4523, 0x2000}, // glad + {4569, 0x2800}, // glow + {4583, 0x2500}, // glue + {4624, 0x9900}, // inch + {4628, 0x2000}, // goat + {4653, 0x2800}, // gold + {4653, 0x6800}, // hold + {4653, 0x6900}, // hole + {4663, 0x2800}, // good + {4663, 0x6100}, // home + {4663, 0x6800}, // hood + {4673, 0x6100}, // hope + {4676, 0x6900}, // horn + {4678, 0x6c00}, // host + {4686, 0x9200}, // into + {4687, 0x6600}, // hour + {4696, 0x2100}, // gown + {4722, 0x2100}, // grab + {4743, 0x2800}, // grid + {4748, 0x2800}, // grit + {4766, 0xa900}, // iron + {4769, 0x2800}, // grow + {4836, 0x8400}, // item + {4843, 0x5100}, // huge + {4868, 0x5400}, // hunt + {4878, 0x5800}, // hurt + {5239, 0x8200}, // lady + {5253, 0x8500}, // lake + {5267, 0x8000}, // lamp + {5282, 0x8800}, // lava + {5296, 0x8100}, // lawn + {5299, 0x0f00}, // jazz + {5299, 0x8e00}, // lazy + {5323, 0x9200}, // leaf + {5336, 0x5500}, // keen + {5337, 0x5400}, // keep + {5338, 0x9800}, // left + {5363, 0x9400}, // lend + {5367, 0x9700}, // lens + {5425, 0x6900}, // kick + {5427, 0xa200}, // liar + {5433, 0xa900}, // life + {5438, 0xa800}, // lift + {5453, 0xa500}, // like + {5462, 0xa100}, // limb + {5463, 0x6400}, // kind + {5465, 0xa500}, // link + {5466, 0xa900}, // lion + {5477, 0x6f00}, // kiss + {5478, 0xac00}, // list + {5483, 0x6100}, // kite + {5483, 0xa900}, // live + {5494, 0x6200}, // kiwi + {5623, 0xa000}, // load + {5625, 0xa900}, // lock + {5626, 0xa100}, // loan + {5633, 0x5500}, // knee + {5646, 0x2900}, // join + {5653, 0x2500}, // joke + {5664, 0xa400}, // long + {5667, 0xa800}, // loop + {5669, 0x5800}, // know + {5683, 0xa400}, // loud + {5683, 0xa900}, // love + {5865, 0x1500}, // junk + {5867, 0x1000}, // jump + {5878, 0x1c00}, // just + {6239, 0x9600}, // obey + {6243, 0x0800}, // maid + {6245, 0x0a00}, // mail + {6246, 0x0900}, // main + {6253, 0x0500}, // make + {6263, 0x4100}, // name + {6275, 0x0d00}, // mask + {6277, 0x0f00}, // mass + {6284, 0x0100}, // math + {6293, 0x0d00}, // maze + {6325, 0x5900}, // neck + {6326, 0x1100}, // mean + {6327, 0x5200}, // near + {6328, 0x1000}, // meat + {6333, 0x5400}, // need + {6358, 0x1800}, // melt + {6367, 0x8a00}, // odor + {6368, 0x1500}, // menu + {6374, 0x1d00}, // mesh + {6378, 0x5c00}, // nest + {6397, 0x5300}, // news + {6398, 0x5400}, // next + {6423, 0x6900}, // nice + {6455, 0x2900}, // milk + {6463, 0x2400}, // mind + {6477, 0x2f00}, // miss + {6529, 0x9200}, // okay + {6623, 0x9900}, // once + {6648, 0x8800}, // omit + {6659, 0x9a00}, // only + {6666, 0x2900}, // moon + {6673, 0x2900}, // more + {6673, 0x6d00}, // nose + {6683, 0x2900}, // move + {6683, 0x6100}, // note + {6736, 0x8500}, // open + {6824, 0x1900}, // much + {6825, 0xa200}, // oval + {6836, 0xa500}, // oven + {6837, 0xa600}, // over + {6853, 0x1900}, // mule + {6878, 0x1c00}, // must + {6984, 0x2100}, // myth + {7223, 0x8900}, // race + {7225, 0x8900}, // rack + {7226, 0xe100}, // scan + {7228, 0x0800}, // pact + {7233, 0xc900}, // safe + {7243, 0x0100}, // page + {7245, 0x8a00}, // rail + {7245, 0xca00}, // sail + {7246, 0x8900}, // rain + {7247, 0x0a00}, // pair + {7256, 0x0800}, // palm + {7258, 0xc800}, // salt + {7263, 0xc100}, // same + {7263, 0xc400}, // sand + {7267, 0x8000}, // ramp + {7273, 0x8900}, // rare + {7275, 0x0900}, // park + {7277, 0x0f00}, // pass + {7283, 0x0900}, // pave + {7283, 0x8100}, // rate + {7283, 0xc900}, // save + {7284, 0x0100}, // path + {7325, 0x9200}, // real + {7327, 0x1200}, // pear + {7328, 0xd000}, // seat + {7333, 0xd400}, // seed + {7335, 0xd500}, // seek + {7355, 0xda00}, // sell + {7359, 0x9a00}, // rely + {7368, 0x9400}, // rent + {7423, 0xa900}, // rice + {7424, 0xa900}, // rich + {7425, 0xe900}, // sick + {7433, 0xa100}, // ride + {7433, 0xd400}, // shed + {7433, 0xe100}, // side + {7446, 0xe100}, // sign + {7447, 0xd800}, // ship + {7455, 0x2a00}, // pill + {7455, 0xe900}, // silk + {7463, 0xd900}, // shoe + {7464, 0xa400}, // ring + {7464, 0xe400}, // sing + {7465, 0x2500}, // pink + {7467, 0xd800}, // shop + {7468, 0xa800}, // riot + {7473, 0x2100}, // pipe + {7475, 0xad00}, // risk + {7493, 0xed00}, // size + {7522, 0xe100}, // slab + {7526, 0xe000}, // slam + {7529, 0x2200}, // play + {7546, 0xd900}, // skin + {7546, 0xe800}, // slim + {7568, 0xe800}, // slot + {7569, 0xe800}, // slow + {7584, 0x2400}, // plug + {7623, 0xa000}, // road + {7625, 0xe900}, // sock + {7627, 0xd000}, // snap + {7627, 0xe000}, // soap + {7632, 0xe000}, // soda + {7636, 0x2400}, // poem + {7638, 0x2400}, // poet + {7638, 0xe800}, // soft + {7653, 0x2900}, // pole + {7663, 0x2400}, // pond + {7663, 0xaa00}, // roof + {7664, 0xe400}, // song + {7665, 0x2a00}, // pool + {7666, 0xa800}, // room + {7666, 0xe900}, // soon + {7669, 0x2600}, // pony + {7669, 0xd800}, // snow + {7673, 0xad00}, // rose + {7678, 0x2c00}, // post + {7678, 0xe800}, // sort + {7685, 0xe600}, // soul + {7687, 0xe400}, // soup + {7746, 0xc900}, // spin + {7768, 0xc800}, // spot + {7824, 0xd900}, // such + {7829, 0xc200}, // stay + {7833, 0x9100}, // rude + {7836, 0xc400}, // stem + {7837, 0xc400}, // step + {7848, 0x5800}, // quit + {7848, 0xd800}, // suit + {7849, 0x5b00}, // quiz + {7853, 0x9900}, // rule + {7855, 0x1a00}, // pull + {7857, 0x1800}, // pulp + {7873, 0xd900}, // sure + {7874, 0x1d00}, // push + {7927, 0xc000}, // swap + {7946, 0xc800}, // swim + {8245, 0x0a00}, // tail + {8255, 0x0900}, // talk + {8265, 0x0500}, // tank + {8273, 0x0100}, // tape + {8275, 0x0d00}, // task + {8278, 0x8c00}, // vast + {8294, 0x0600}, // taxi + {8326, 0x1000}, // team + {8355, 0x1a00}, // tell + {8368, 0x1400}, // tent + {8372, 0x9900}, // verb + {8376, 0x1800}, // term + {8378, 0x1c00}, // test + {8379, 0x9a00}, // very + {8398, 0x1400}, // text + {8428, 0x1000}, // that + {8433, 0x2100}, // tide + {8436, 0x1500}, // then + {8439, 0x1600}, // they + {8439, 0xa400}, // view + {8447, 0x1b00}, // this + {8458, 0x2800}, // tilt + {8459, 0x4a00}, // ugly + {8463, 0x2100}, // time + {8469, 0x2600}, // tiny + {8472, 0xac00}, // visa + {8636, 0x5200}, // undo + {8643, 0xa800}, // void + {8648, 0x5800}, // unit + {8663, 0x2500}, // tone + {8665, 0x2a00}, // tool + {8677, 0x2f00}, // toss + {8683, 0xa100}, // vote + {8696, 0x2100}, // town + {8727, 0x2000}, // trap + {8729, 0x2200}, // tray + {8733, 0x2500}, // tree + {8733, 0x7400}, // used + {8743, 0x6100}, // urge + {8746, 0x2800}, // trim + {8747, 0x2800}, // trip + {8766, 0x4900}, // upon + {8783, 0x2500}, // true + {8823, 0x1500}, // tube + {8862, 0x1400}, // tuna + {8876, 0x1900}, // turn + {8946, 0x0900}, // twin + {8973, 0x2100}, // type + {9243, 0x0100}, // wage + {9248, 0x0800}, // wait + {9255, 0x0900}, // walk + {9255, 0x0a00}, // wall + {9268, 0x0400}, // want + {9273, 0x8800}, // yard + {9274, 0x0d00}, // wash + {9276, 0x0800}, // warm + {9277, 0x0c00}, // wasp + {9283, 0x0900}, // wave + {9327, 0x1200}, // wear + {9327, 0x9200}, // year + {9376, 0xda00}, // zero + {9378, 0x1c00}, // west + {9428, 0x1000}, // what + {9433, 0x2100}, // wide + {9433, 0x2900}, // wife + {9436, 0x1500}, // when + {9447, 0x1800}, // whip + {9453, 0x2800}, // wild + {9455, 0x2a00}, // will + {9463, 0x2500}, // wine + {9464, 0x2400}, // wing + {9465, 0x2500}, // wink + {9473, 0x2900}, // wire + {9473, 0x2d00}, // wise + {9474, 0x2d00}, // wish + {9653, 0x2a00}, // wolf + {9663, 0x2800}, // wood + {9663, 0xe500}, // zone + {9665, 0x2a00}, // wool + {9673, 0x2800}, // word + {9675, 0x2900}, // work + {9727, 0x2000}, // wrap + {22246, 0x8640}, // cabin + {22253, 0x8640}, // cable + {22266, 0x4a40}, // bacon + {22343, 0x4040}, // badge + {22625, 0x8480}, // canal + {22639, 0x8480}, // candy + {22663, 0x8640}, // canoe + {22683, 0x1a40}, // above + {22688, 0x1900}, // about + {22742, 0x4e80}, // basic + {22746, 0x8880}, // cargo + {22779, 0x8a80}, // carry + {22824, 0x8240}, // catch + {22867, 0x2280}, // actor + {22873, 0x1740}, // abuse + {22873, 0x8740}, // cause + {23224, 0x5240}, // beach + {23278, 0x0000}, // adapt + {23446, 0x5240}, // begin + {23569, 0x5a00}, // below + {23624, 0x5640}, // bench + {23648, 0x0200}, // admit + {23858, 0x0600}, // adult + {24246, 0x0240}, // again + {24247, 0x9280}, // chair + {24255, 0x9240}, // chalk + {24267, 0x92c0}, // chaos + {24273, 0x9340}, // chase + {24323, 0x1400}, // ahead + {24325, 0x9640}, // check + {24327, 0x9400}, // cheap + {24368, 0x0500}, // agent + {24378, 0x9700}, // chest + {24427, 0xa080}, // cigar + {24433, 0x9980}, // chief + {24453, 0x9a00}, // child + {24733, 0x0940}, // agree + {24753, 0x2e40}, // aisle + {24784, 0x6840}, // birth + {24845, 0xaa80}, // civil + {24865, 0x9540}, // chunk + {24876, 0x9640}, // churn + {25225, 0x6240}, // black + {25233, 0x6040}, // blade + {25246, 0xa200}, // claim + {25263, 0x6040}, // blame + {25276, 0x2200}, // alarm + {25278, 0x6300}, // blast + {25286, 0x2500}, // album + {25325, 0x6440}, // bleak + {25326, 0xa440}, // clean + {25375, 0xa640}, // clerk + {25377, 0x67c0}, // bless + {25378, 0x2600}, // alert + {25425, 0xaa40}, // click + {25433, 0xaa80}, // cliff + {25436, 0x2940}, // alien + {25462, 0xa840}, // climb + {25463, 0x6900}, // blind + {25539, 0x2980}, // alley + {25569, 0x2a00}, // allow + {25625, 0xaa40}, // clock + {25663, 0x2940}, // alone + {25663, 0x6a00}, // blood + {25673, 0xab40}, // close + {25683, 0xa900}, // cloud + {25684, 0xa840}, // cloth + {25696, 0xa840}, // clown + {25742, 0x2100}, // alpha + {25837, 0x2180}, // alter + {25867, 0xa400}, // clump + {25874, 0x6740}, // blush + {26224, 0xa240}, // coach + {26273, 0x6200}, // board + {26278, 0xa300}, // coast + {26437, 0x1180}, // anger + {26453, 0x1240}, // angle + {26479, 0x1280}, // angry + {26553, 0x1640}, // ankle + {26567, 0xaa80}, // color + {26642, 0xa280}, // comic + {26664, 0x0900}, // among + {26678, 0x6b00}, // boost + {26687, 0x65c0}, // bonus + {26725, 0xa880}, // coral + {26824, 0xa640}, // couch + {26837, 0xa980}, // cover + {27225, 0xa240}, // crack + {27238, 0xa200}, // craft + {27246, 0x6240}, // brain + {27263, 0x6100}, // brand + {27263, 0xa140}, // crane + {27274, 0xa340}, // crash + {27277, 0x63c0}, // brass + {27278, 0x0200}, // apart + {27283, 0x6240}, // brave + {27295, 0xa080}, // crawl + {27299, 0xa380}, // crazy + {27323, 0x6400}, // bread + {27326, 0xa400}, // cream + {27335, 0xa540}, // creek + {27362, 0x2500}, // arena + {27425, 0x6a40}, // brick + {27433, 0x6980}, // brief + {27463, 0xa840}, // crime + {27464, 0x6900}, // bring + {27475, 0x6b40}, // brisk + {27477, 0xab00}, // crisp + {27483, 0x2140}, // argue + {27633, 0x2100}, // armed + {27666, 0x6a00}, // broom + {27667, 0x2280}, // armor + {27677, 0xabc0}, // cross + {27693, 0xa800}, // crowd + {27696, 0x6840}, // brown + {27738, 0x3d00}, // asset + {27745, 0x0a80}, // april + {27753, 0x0240}, // apple + {27769, 0x2a00}, // arrow + {27835, 0xa580}, // cruel + {27874, 0x6740}, // brush + {27874, 0xa740}, // crush + {28339, 0x5080}, // buddy + {28348, 0x1200}, // audit + {28453, 0x5a00}, // build + {28643, 0x2a00}, // avoid + {28778, 0x5b00}, // burst + {28783, 0x9a40}, // curve + {28937, 0x5980}, // buyer + {29253, 0x0140}, // awake + {29253, 0xaa40}, // cycle + {29273, 0x0240}, // aware + {29385, 0x0980}, // awful + {32437, 0x4180}, // eager + {32453, 0x4240}, // eagle + {32468, 0x8900}, // faint + {32484, 0x8840}, // faith + {32573, 0x8b40}, // false + {32623, 0x0640}, // dance + {32629, 0x8680}, // fancy + {32759, 0x4a80}, // early + {32784, 0x4840}, // earth + {32825, 0x8080}, // fatal + {32858, 0x8600}, // fault + {33529, 0x1880}, // delay + {33623, 0x9640}, // fence + {33784, 0x1040}, // depth + {33824, 0x9240}, // fetch + {33837, 0x9980}, // fever + {34237, 0xa580}, // fiber + {34279, 0x2280}, // diary + {34353, 0xa600}, // field + {34448, 0x6100}, // eight + {34625, 0xa480}, // final + {34778, 0xab00}, // first + {34999, 0x2f80}, // dizzy + {35263, 0xa040}, // flame + {35269, 0x6600}, // elbow + {35274, 0xa340}, // flash + {35337, 0x6180}, // elder + {35483, 0x6840}, // elite + {35625, 0xaa40}, // flock + {35628, 0xa800}, // float + {35667, 0xaa80}, // floor + {35843, 0xa600}, // fluid + {35874, 0xa740}, // flush + {36228, 0x5200}, // enact + {36287, 0xa9c0}, // focus + {36369, 0x5480}, // enemy + {36569, 0x5280}, // enjoy + {36667, 0x2680}, // donor + {36723, 0xaa40}, // force + {36786, 0xa900}, // forum + {36789, 0x4080}, // empty + {36837, 0x5180}, // enter + {36863, 0xa500}, // found + {36879, 0x5280}, // entry + {37238, 0x2200}, // draft + {37262, 0x2000}, // drama + {37263, 0xa040}, // frame + {37273, 0x6340}, // erase + {37326, 0x2400}, // dream + {37374, 0xa740}, // fresh + {37377, 0x27c0}, // dress + {37438, 0x2a00}, // drift + {37455, 0x2a80}, // drill + {37465, 0x2940}, // drink + {37483, 0x2a40}, // drive + {37633, 0x6840}, // erode + {37668, 0xa900}, // front + {37678, 0xab00}, // frost + {37696, 0xa840}, // frown + {37729, 0x7c80}, // essay + {37767, 0x6a80}, // error + {37825, 0x5480}, // equal + {37847, 0x5600}, // equip + {37848, 0xa600}, // fruit + {37878, 0x6400}, // erupt + {38653, 0x6940}, // evoke + {38669, 0x9580}, // funny + {38824, 0x1240}, // dutch + {39228, 0x5200}, // exact + {39273, 0x0280}, // dwarf + {39453, 0x5a40}, // exile + {39478, 0x5b00}, // exist + {39872, 0x5200}, // extra + {42248, 0x4600}, // habit + {42774, 0x4b40}, // harsh + {42779, 0x4080}, // happy + {42843, 0x0440}, // gauge + {43278, 0x5200}, // heart + {43289, 0x5280}, // heavy + {43556, 0x5a80}, // hello + {43673, 0x1640}, // genre + {44268, 0x2100}, // giant + {44678, 0x1b00}, // ghost + {45273, 0x2240}, // glare + {45277, 0x23c0}, // glass + {45433, 0x2840}, // glide + {45623, 0x2940}, // globe + {45666, 0x2a00}, // gloom + {45679, 0x2a80}, // glory + {45683, 0x2a40}, // glove + {46229, 0x6580}, // hobby + {46243, 0x8040}, // image + {46339, 0x9140}, // index + {46637, 0x9580}, // inner + {46639, 0x6580}, // honey + {46673, 0x2b40}, // goose + {46773, 0x6b40}, // horse + {46788, 0x9100}, // input + {46835, 0x6180}, // hotel + {46837, 0x6980}, // hover + {47223, 0x2240}, // grace + {47246, 0x2240}, // grain + {47268, 0x2100}, // grant + {47273, 0x2040}, // grape + {47277, 0x23c0}, // grass + {47328, 0x2400}, // great + {47336, 0x2540}, // green + {47433, 0x2980}, // grief + {47687, 0x2900}, // group + {47783, 0xbd40}, // issue + {47868, 0x2500}, // grunt + {48273, 0x1200}, // guard + {48377, 0x17c0}, // guess + {48433, 0x1840}, // guide + {48458, 0x1a00}, // guilt + {48626, 0x5040}, // human + {48667, 0x5280}, // humor + {48679, 0xaa80}, // ivory + {48779, 0x5a80}, // hurry + {52235, 0x8580}, // label + {52267, 0x8680}, // labor + {52743, 0x8840}, // large + {52837, 0x8180}, // later + {52844, 0x8440}, // laugh + {52846, 0x8240}, // latin + {52937, 0x8980}, // layer + {53267, 0x11c0}, // jeans + {53276, 0x9240}, // learn + {53283, 0x9240}, // leave + {53425, 0x9080}, // legal + {53559, 0x1a80}, // jelly + {53666, 0x9240}, // lemon + {53835, 0x9980}, // level + {53935, 0x1180}, // jewel + {54448, 0xa100}, // light + {54648, 0xa200}, // limit + {56225, 0xa880}, // local + {56433, 0x5a40}, // knife + {56442, 0xa280}, // logic + {56625, 0x5a40}, // knock + {56925, 0xa880}, // loyal + {58259, 0x9980}, // lucky + {58343, 0x1040}, // judge + {58423, 0x1a40}, // juice + {58624, 0x9640}, // lunch + {58627, 0x9480}, // lunar + {62287, 0xa980}, // occur + {62326, 0xa440}, // ocean + {62442, 0x0280}, // magic + {62483, 0x4a40}, // naive + {62567, 0x0280}, // major + {62646, 0x0480}, // mango + {62724, 0x0a40}, // march + {62753, 0x0240}, // maple + {62789, 0x4c80}, // nasty + {62824, 0x0240}, // match + {63325, 0x1080}, // medal + {63337, 0xa980}, // offer + {63342, 0x1200}, // media + {63729, 0x1a80}, // mercy + {63743, 0x1840}, // merge + {63748, 0x1a00}, // merit + {63779, 0x1a80}, // merry + {63783, 0x5a40}, // nerve + {63825, 0x1080}, // metal + {63836, 0xa140}, // often + {63837, 0x5980}, // never + {64448, 0x6100}, // night + {64642, 0x2280}, // mimic + {64667, 0x2680}, // minor + {64933, 0x2500}, // mixed + {65483, 0xaa40}, // olive + {66253, 0x6640}, // noble + {66335, 0x2180}, // model + {66466, 0x9a40}, // onion + {66473, 0x6b40}, // noise + {66684, 0x2440}, // month + {66725, 0x2880}, // moral + {66784, 0x6840}, // north + {66835, 0x6980}, // novel + {66843, 0x2a40}, // movie + {66867, 0x2280}, // motor + {66873, 0x2740}, // mouse + {67248, 0xa600}, // orbit + {67337, 0xa180}, // order + {67372, 0x8600}, // opera + {67426, 0xa040}, // organ + {68437, 0x8580}, // other + {68742, 0x1e80}, // music + {68773, 0x5b40}, // nurse + {68837, 0x9180}, // outer + {69637, 0x8580}, // owner + {69663, 0xb940}, // ozone + {72253, 0xe240}, // scale + {72273, 0xe240}, // scare + {72327, 0x8080}, // radar + {72346, 0x8280}, // radio + {72363, 0xe540}, // scene + {72473, 0x8b40}, // raise + {72523, 0xc800}, // salad + {72559, 0x8a80}, // rally + {72566, 0xca40}, // salon + {72624, 0x8640}, // ranch + {72632, 0x0400}, // panda + {72635, 0x0580}, // panel + {72642, 0x0680}, // panic + {72643, 0x8440}, // range + {72688, 0xe900}, // scout + {72727, 0xe800}, // scrap + {72737, 0x0180}, // paper + {72743, 0x8200}, // rapid + {72782, 0xe940}, // scrub + {72789, 0x0880}, // party + {72823, 0xc640}, // sauce + {72824, 0x0240}, // patch + {72836, 0x8940}, // raven + {72873, 0x0740}, // pause + {72967, 0x8e80}, // razor + {73223, 0x1240}, // peace + {73235, 0x9580}, // rebel + {73239, 0x9080}, // ready + {73529, 0x9840}, // relax + {73639, 0x9500}, // renew + {73673, 0xd740}, // sense + {73836, 0xd940}, // seven + {73887, 0xd100}, // setup + {74238, 0xd200}, // shaft + {74266, 0x2180}, // piano + {74273, 0xd240}, // share + {74323, 0x2640}, // piece + {74343, 0xa040}, // ridge + {74343, 0xe440}, // siege + {74353, 0xaa40}, // rifle + {74355, 0xd680}, // shell + {74438, 0xda00}, // shift + {74443, 0xa200}, // rigid + {74448, 0xa100}, // right + {74448, 0xe100}, // sight + {74463, 0xd940}, // shine + {74559, 0xea80}, // silly + {74568, 0x2a00}, // pilot + {74623, 0xe640}, // since + {74625, 0xda40}, // shock + {74663, 0x1940}, // phone + {74668, 0xda00}, // shoot + {74678, 0xda00}, // short + {74683, 0xda40}, // shove + {74686, 0x1880}, // photo + {74736, 0xe940}, // siren + {74784, 0xd900}, // shrug + {74824, 0x2240}, // pitch + {74825, 0xa880}, // rival + {74837, 0xa980}, // river + {74992, 0x2f00}, // pizza + {75223, 0x2240}, // place + {75283, 0x2040}, // plate + {75283, 0xd040}, // skate + {75337, 0xe500}, // sleep + {75423, 0xea40}, // slice + {75433, 0xe840}, // slide + {75455, 0xda80}, // skill + {75478, 0xda00}, // skirt + {75825, 0x2640}, // pluck + {75855, 0xd680}, // skull + {75874, 0xe740}, // slush + {76225, 0xd240}, // snack + {76253, 0xd140}, // snake + {76255, 0xc280}, // small + {76268, 0xa600}, // robot + {76278, 0xa300}, // roast + {76278, 0xc200}, // smart + {76433, 0xda80}, // sniff + {76453, 0xca40}, // smile + {76468, 0x2900}, // point + {76527, 0x2880}, // polar + {76527, 0xe880}, // solar + {76543, 0xea00}, // solid + {76583, 0xea40}, // solve + {76653, 0xc940}, // smoke + {76779, 0xea80}, // sorry + {76844, 0xa440}, // rough + {76863, 0xa500}, // round + {76863, 0xe500}, // sound + {76883, 0xa440}, // route + {76884, 0xe440}, // south + {76925, 0xa880}, // royal + {76937, 0x2180}, // power + {77223, 0xc240}, // space + {77273, 0xc240}, // spare + {77296, 0xc040}, // spawn + {77325, 0xc440}, // speak + {77333, 0xc500}, // speed + {77355, 0xc680}, // spell + {77363, 0xc500}, // spend + {77423, 0x2a40}, // price + {77423, 0xca40}, // spice + {77433, 0x2840}, // pride + {77453, 0xc940}, // spike + {77468, 0x2900}, // print + {77493, 0x2b40}, // prize + {77548, 0xca00}, // split + {77645, 0xca80}, // spoil + {77663, 0x2a80}, // proof + {77666, 0xca40}, // spoon + {77678, 0xca00}, // sport + {77683, 0x2900}, // proud + {77729, 0xc880}, // spray + {78233, 0xc280}, // staff + {78243, 0xc040}, // stage + {78263, 0xc100}, // stand + {78267, 0xc000}, // stamp + {78278, 0xc200}, // start + {78283, 0xc040}, // state + {78325, 0xc440}, // steak + {78335, 0xc580}, // steel + {78425, 0x5a40}, // quick + {78425, 0xca40}, // stick + {78427, 0xd080}, // sugar + {78455, 0xca80}, // still + {78464, 0xc900}, // sting + {78573, 0x1b40}, // pulse + {78624, 0x1640}, // punch + {78625, 0xca40}, // stock + {78663, 0xc940}, // stone + {78665, 0xca80}, // stool + {78669, 0xd580}, // sunny + {78679, 0xca80}, // story + {78683, 0x5840}, // quote + {78683, 0xca40}, // stove + {78725, 0x9880}, // rural + {78737, 0xd180}, // super + {78743, 0xd840}, // surge + {78745, 0x1280}, // pupil + {78773, 0x1b40}, // purse + {78779, 0x1080}, // puppy + {78833, 0xc680}, // stuff + {78953, 0xca40}, // style + {79267, 0xc000}, // swamp + {79276, 0xc200}, // swarm + {79327, 0xc480}, // swear + {79338, 0xc500}, // sweet + {79438, 0xca00}, // swift + {79464, 0xc900}, // swing + {79673, 0xca00}, // sword + {79787, 0xe900}, // syrup + {82253, 0x0640}, // table + {82483, 0x8140}, // vague + {82543, 0x8a00}, // valid + {82583, 0x8a40}, // valve + {82767, 0x8280}, // vapor + {82783, 0x0c40}, // taste + {82858, 0x8600}, // vault + {83224, 0x1240}, // teach + {83683, 0x9540}, // venue + {84265, 0x1140}, // thank + {84336, 0xa180}, // video + {84363, 0x1440}, // theme + {84373, 0x1640}, // there + {84437, 0x2180}, // tiger + {84464, 0x1900}, // thing + {84733, 0x1940}, // three + {84733, 0x2900}, // tired + {84748, 0xae00}, // visit + {84769, 0x1a00}, // throw + {84787, 0xa9c0}, // virus + {84825, 0xa080}, // vital + {84843, 0xaa00}, // vivid + {84853, 0x2240}, // title + {84862, 0x1440}, // thumb + {86225, 0xa880}, // vocal + {86253, 0x5a40}, // uncle + {86278, 0x2300}, // toast + {86329, 0x2080}, // today + {86337, 0x5180}, // under + {86423, 0xaa40}, // voice + {86536, 0x2540}, // token + {86684, 0x2840}, // tooth + {86724, 0x2a40}, // torch + {86742, 0x2280}, // topic + {86825, 0x2080}, // total + {86845, 0x5280}, // until + {86937, 0x2180}, // tower + {87225, 0x2240}, // track + {87226, 0x6440}, // urban + {87233, 0x2040}, // trade + {87243, 0x7040}, // usage + {87246, 0x2240}, // train + {87274, 0x2340}, // trash + {87328, 0x2400}, // treat + {87363, 0x2500}, // trend + {87423, 0x2940}, // tribe + {87425, 0x2880}, // trial + {87425, 0x2a40}, // trick + {87737, 0x4180}, // upper + {87738, 0x4d00}, // upset + {87825, 0x2640}, // truck + {87825, 0x7480}, // usual + {87859, 0x2680}, // truly + {87878, 0x2700}, // trust + {87884, 0x2440}, // truth + {89423, 0x0a40}, // twice + {89478, 0x0b00}, // twist + {92466, 0x0240}, // wagon + {92783, 0x0c40}, // waste + {92837, 0x0180}, // water + {93272, 0xd600}, // zebra + {93473, 0x1a00}, // weird + {94253, 0x1240}, // whale + {94328, 0x1400}, // wheat + {94335, 0x1580}, // wheel + {94373, 0x1640}, // where + {94384, 0x2040}, // width + {96626, 0x2040}, // woman + {96753, 0x2a00}, // world + {96779, 0x2a80}, // worry + {96784, 0x2840}, // worth + {96864, 0xa500}, // young + {96884, 0xa440}, // youth + {97325, 0x2640}, // wreck + {97478, 0x2b00}, // wrist + {97483, 0x2840}, // write + {97664, 0x2900}, // wrong + {222377, 0x29f0}, // access + {222873, 0x29d0}, // accuse + {222887, 0x8870}, // cactus + {226235, 0x8660}, // cancel + {226262, 0x4440}, // banana + {226266, 0x41a0}, // bamboo + {226372, 0x8180}, // camera + {226637, 0x4560}, // banner + {226666, 0x8590}, // cannon + {226827, 0x8630}, // canvas + {226966, 0x8690}, // canyon + {227266, 0x8990}, // carbon + {227359, 0x49a0}, // barely + {227368, 0x1d40}, // absent + {227466, 0x8e60}, // casino + {227538, 0x4d40}, // basket + {227672, 0x1e90}, // absorb + {227677, 0x2af0}, // across + {227735, 0x4a60}, // barrel + {227738, 0x8840}, // carpet + {227825, 0x8d20}, // casual + {227853, 0x8c90}, // castle + {227873, 0x1d80}, // absurd + {228448, 0x8440}, // caught + {228466, 0x2290}, // action + {228825, 0x2120}, // actual + {228853, 0x4090}, // battle + {228853, 0x8090}, // cattle + {232663, 0x5a10}, // become + {232889, 0x5120}, // beauty + {233247, 0x28a0}, // affair + {233428, 0x0280}, // addict + {233673, 0x2a80}, // afford + {233673, 0x5a90}, // before + {234283, 0x5490}, // behave + {234463, 0x5640}, // behind + {235379, 0x99a0}, // celery + {235878, 0x01c0}, // adjust + {236368, 0x9140}, // cement + {236787, 0x9770}, // census + {237243, 0x2880}, // afraid + {237325, 0x9920}, // cereal + {238423, 0x0a90}, // advice + {238729, 0x5220}, // betray + {238837, 0x5060}, // better + {239663, 0x5a40}, // beyond + {242643, 0x9110}, // change + {242743, 0x9210}, // charge + {243373, 0x95d0}, // cheese + {243779, 0x96a0}, // cherry + {246423, 0x9a90}, // choice + {246673, 0x9ad0}, // choose + {247253, 0xaa90}, // circle + {248837, 0x6060}, // bitter + {253837, 0xa660}, // clever + {254368, 0xa940}, // client + {254642, 0xa9a0}, // clinic + {256678, 0x22c0}, // almost + {256873, 0x69d0}, // blouse + {258824, 0xa490}, // clutch + {259297, 0x20b0}, // always + {262467, 0x19a0}, // anchor + {263333, 0xaa50}, // coffee + {264625, 0x1820}, // animal + {265866, 0xa910}, // column + {266666, 0xa090}, // common + {266825, 0x1520}, // annual + {266868, 0x0940}, // amount + {267337, 0x6860}, // border + {267464, 0x6a40}, // boring + {267737, 0xa060}, // copper + {267769, 0x6a80}, // borrow + {267937, 0x1c60}, // answer + {268623, 0x6590}, // bounce + {268733, 0x0740}, // amused + {268746, 0xa790}, // cousin + {268753, 0xa490}, // couple + {268773, 0xa6d0}, // course + {268866, 0x6080}, // bottom + {268866, 0xa090}, // cotton + {269683, 0xaa10}, // coyote + {272353, 0xa090}, // cradle + {272837, 0xa060}, // crater + {272842, 0x28a0}, // arctic + {273348, 0xa480}, // credit + {273393, 0x65d0}, // breeze + {274343, 0x6810}, // bridge + {274448, 0x6840}, // bright + {274842, 0xa8a0}, // critic + {276536, 0x6950}, // broken + {276693, 0x69d0}, // bronze + {276824, 0xa990}, // crouch + {276863, 0x2940}, // around + {277327, 0x0120}, // appear + {277328, 0x3180}, // aspect + {277378, 0x29c0}, // arrest + {277478, 0x3ec0}, // assist + {277483, 0x2a90}, // arrive + {277863, 0x3d10}, // assume + {278462, 0x3100}, // asthma + {278473, 0xa6d0}, // cruise + {278478, 0x22c0}, // artist + {278624, 0xa590}, // crunch + {282253, 0x5590}, // bubble + {283438, 0x5040}, // budget + {284878, 0x11c0}, // august + {285538, 0x5a40}, // bullet + {286353, 0x5490}, // bundle + {286537, 0x5560}, // bunker + {287336, 0x5850}, // burden + {287437, 0x5860}, // burger + {287866, 0x9c80}, // custom + {288225, 0x0090}, // attack + {288363, 0x0140}, // attend + {288467, 0x11a0}, // author + {288837, 0x5060}, // butter + {288866, 0x1110}, // autumn + {322742, 0x86a0}, // fabric + {326243, 0x0010}, // damage + {326437, 0x0460}, // danger + {326459, 0x82a0}, // family + {326687, 0x8270}, // famous + {327459, 0x4ea0}, // easily + {327464, 0x0a40}, // daring + {328437, 0x8160}, // father + {332233, 0x1810}, // decade + {332283, 0x1410}, // debate + {332433, 0x1a10}, // decide + {332747, 0x16b0}, // debris + {333463, 0x1a50}, // define + {333678, 0x6a80}, // effort + {334733, 0x1250}, // degree + {336253, 0x9090}, // female + {336263, 0x1040}, // demand + {336425, 0x1620}, // denial + {336473, 0x12d0}, // demise + {337278, 0x1080}, // depart + {337363, 0x1140}, // depend + {337378, 0x1d80}, // desert + {337446, 0x1e10}, // design + {337483, 0x1a90}, // derive + {337889, 0x1120}, // deputy + {338245, 0x10a0}, // detail + {338328, 0x1180}, // detect + {338423, 0x1a90}, // device + {338683, 0x1a10}, // devote + {343337, 0x2a60}, // differ + {343735, 0x2760}, // diesel + {344873, 0xa190}, // figure + {345837, 0xa860}, // filter + {346437, 0xa460}, // finger + {346474, 0xa6d0}, // finish + {346637, 0x2560}, // dinner + {347225, 0xae20}, // fiscal + {347328, 0x2980}, // direct + {348378, 0x2980}, // divert + {348433, 0x2a10}, // divide + {348437, 0x6160}, // either + {352867, 0xa2a0}, // flavor + {354448, 0xa840}, // flight + {356937, 0xa860}, // flower + {362253, 0x5190}, // enable + {362275, 0x4490}, // embark + {362639, 0x4620}, // embody + {362867, 0x28a0}, // doctor + {363743, 0x4610}, // emerge + {363749, 0x5620}, // energy + {364243, 0x5010}, // engage + {364463, 0x5250}, // engine + {365478, 0x5ac0}, // enlist + {365569, 0xaa80}, // follow + {366246, 0x2090}, // domain + {366283, 0x2410}, // donate + {366539, 0x2560}, // donkey + {366844, 0x5910}, // enough + {367378, 0xa9c0}, // forest + {367424, 0x5a90}, // enrich + {367438, 0xa840}, // forget + {367569, 0x42a0}, // employ + {367655, 0x5aa0}, // enroll + {367745, 0xafa0}, // fossil + {367837, 0xac60}, // foster + {367873, 0x5d90}, // ensure + {368253, 0x2590}, // double + {368473, 0x5290}, // entire + {372273, 0x7810}, // escape + {372466, 0x2090}, // dragon + {374363, 0xa940}, // friend + {374643, 0xa910}, // fringe + {376936, 0xab50}, // frozen + {378283, 0x7010}, // estate + {384427, 0x46b0}, // ethics + {386583, 0x6a90}, // evolve + {387464, 0x1a40}, // during + {388873, 0x9190}, // future + {392377, 0x59f0}, // excess + {392483, 0x5a10}, // excite + {392873, 0x59d0}, // excuse + {396842, 0x58a0}, // exotic + {397263, 0x5040}, // expand + {397328, 0x5180}, // expect + {397473, 0x5290}, // expire + {397673, 0x52d0}, // expose + {398363, 0x5140}, // extend + {423438, 0x0040}, // gadget + {425299, 0x0860}, // galaxy + {426637, 0x4060}, // hammer + {427243, 0x0810}, // garage + {427267, 0x49a0}, // harbor + {427336, 0x0850}, // garden + {427542, 0x0aa0}, // garlic + {428437, 0x0160}, // gather + {429273, 0x4c80}, // hazard + {432584, 0x5210}, // health + {434448, 0x5840}, // height + {435638, 0x5840}, // helmet + {436487, 0x1670}, // genius + {436853, 0x1490}, // gentle + {443336, 0x6050}, // hidden + {444453, 0x2090}, // giggle + {446437, 0x2460}, // ginger + {446673, 0x8690}, // ignore + {452623, 0x2190}, // glance + {462539, 0x6960}, // hockey + {462663, 0x9a10}, // income + {463268, 0x9840}, // infant + {463667, 0x92a0}, // indoor + {463676, 0x9a80}, // inform + {464253, 0x9490}, // inhale + {465328, 0x9180}, // inject + {465569, 0x6a80}, // hollow + {465879, 0x91a0}, // injury + {466283, 0x9010}, // inmate + {466863, 0x8150}, // immune + {467228, 0x8080}, // impact + {467263, 0x9c50}, // insane + {467328, 0x9d80}, // insect + {467433, 0x9e10}, // inside + {467673, 0x82d0}, // impose + {467735, 0x2c60}, // gospel + {467747, 0x2f80}, // gossip + {467767, 0x6aa0}, // horror + {468228, 0x9080}, // intact + {468376, 0x2990}, // govern + {468378, 0x99c0}, // invest + {468483, 0x9a10}, // invite + {475263, 0xb840}, // island + {484827, 0x1820}, // guitar + {486253, 0x5190}, // humble + {486479, 0x54a0}, // hungry + {487353, 0x5890}, // hurdle + {492743, 0x6680}, // hybrid + {522538, 0x0940}, // jacket + {523337, 0x8060}, // ladder + {524827, 0x0120}, // jaguar + {527867, 0x8080}, // laptop + {532337, 0x9060}, // leader + {534363, 0x9140}, // legend + {536484, 0x9410}, // length + {537766, 0x9f90}, // lesson + {538837, 0x9060}, // letter + {543639, 0x6160}, // kidney + {547843, 0xa580}, // liquid + {548836, 0x6050}, // kitten + {548853, 0xa090}, // little + {549273, 0xac80}, // lizard + {566359, 0xa5a0}, // lonely + {568643, 0xa510}, // lounge + {586237, 0x9160}, // lumber + {586453, 0x1490}, // jungle + {586467, 0x16a0}, // junior + {589879, 0x95a0}, // luxury + {597427, 0xaab0}, // lyrics + {624638, 0x0140}, // magnet + {625328, 0x9180}, // object + {625443, 0x9a10}, // oblige + {626243, 0x0410}, // manage + {626625, 0x0020}, // mammal + {626825, 0x0520}, // manual + {627253, 0x0990}, // marble + {627446, 0x0890}, // margin + {627463, 0x0a50}, // marine + {627538, 0x0940}, // market + {627546, 0x4190}, // napkin + {627769, 0x4a80}, // narrow + {627837, 0x0c60}, // master + {628246, 0x9090}, // obtain + {628466, 0x4290}, // nation + {628749, 0x0290}, // matrix + {628837, 0x0060}, // matter + {628873, 0x4190}, // nature + {632369, 0x1080}, // meadow + {633423, 0xaa90}, // office + {635639, 0x1a20}, // melody + {636237, 0x1160}, // member + {636679, 0x12a0}, // memory + {637439, 0x5140}, // nephew + {638463, 0x1180}, // method + {643353, 0x2090}, // middle + {646883, 0x2510}, // minute + {647379, 0x2da0}, // misery + {647767, 0x2aa0}, // mirror + {662453, 0x2690}, // mobile + {663439, 0x22a0}, // modify + {665463, 0x9a50}, // online + {666353, 0x6890}, // noodle + {666368, 0x2140}, // moment + {666539, 0x2560}, // monkey + {667625, 0x6820}, // normal + {668423, 0x6290}, // notice + {668437, 0x2160}, // mother + {668466, 0x2290}, // motion + {672643, 0xa110}, // orange + {674368, 0xa940}, // orient + {677426, 0xa110}, // orphan + {677673, 0x82d0}, // oppose + {678466, 0x8290}, // option + {683346, 0x1a90}, // muffin + {686237, 0x5160}, // number + {687253, 0x1e90}, // muscle + {687386, 0x1d40}, // museum + {688788, 0x9040}, // output + {688825, 0x1120}, // mutual + {697353, 0x2da0}, // myself + {697837, 0xac60}, // oyster + {699436, 0x9850}, // oxygen + {722248, 0x8580}, // rabbit + {723353, 0x0090}, // paddle + {723353, 0xc090}, // saddle + {724363, 0xe510}, // scheme + {724665, 0xe6a0}, // school + {725223, 0x0890}, // palace + {725666, 0xc890}, // salmon + {725883, 0xc910}, // salute + {726366, 0x8480}, // random + {726753, 0xc090}, // sample + {727233, 0x0810}, // parade + {727336, 0xe950}, // screen + {727368, 0x0940}, // parent + {727478, 0xea00}, // script + {727768, 0x0a80}, // parrot + {728437, 0x8160}, // rather + {728765, 0x02a0}, // patrol + {732255, 0x98a0}, // recall + {732473, 0x9a10}, // recipe + {732663, 0xda40}, // second + {732673, 0x9a80}, // record + {732688, 0x1140}, // peanut + {732724, 0xd290}, // search + {732738, 0xda40}, // secret + {732766, 0x9390}, // reason + {732766, 0xd390}, // season + {733676, 0x9a80}, // reform + {733823, 0x9190}, // reduce + {733873, 0x99d0}, // refuse + {734466, 0x9290}, // region + {734738, 0x9240}, // regret + {735328, 0x9180}, // reject + {735328, 0xd980}, // select + {735433, 0x9a60}, // relief + {736245, 0x16a0}, // pencil + {736246, 0x9090}, // remain + {736337, 0x9460}, // render + {736463, 0x9240}, // remind + {736467, 0xd6a0}, // senior + {736683, 0x9290}, // remove + {736736, 0x9850}, // reopen + {736753, 0x1890}, // people + {737247, 0x90a0}, // repair + {737283, 0x9e50}, // rescue + {737328, 0x9100}, // repeat + {737437, 0xda70}, // series + {737478, 0x9ec0}, // resist + {737648, 0x1880}, // permit + {737678, 0x9280}, // report + {737737, 0x1060}, // pepper + {737766, 0x1b90}, // person + {737858, 0x9d80}, // result + {738325, 0x9920}, // reveal + {738439, 0x9a40}, // review + {738473, 0x9290}, // retire + {738853, 0xd090}, // settle + {738876, 0x9190}, // return + {739273, 0x9080}, // reward + {742266, 0xa590}, // ribbon + {742369, 0xd080}, // shadow + {742642, 0x29a0}, // picnic + {744353, 0xd980}, // shield + {744366, 0x2190}, // pigeon + {744837, 0xda60}, // shiver + {745368, 0xe940}, // silent + {745837, 0xea60}, // silver + {746753, 0xe090}, // simple + {747273, 0x18d0}, // phrase + {747467, 0xda00}, // shrimp + {747753, 0xa090}, // ripple + {747837, 0xec60}, // sister + {747865, 0x2ca0}, // pistol + {748825, 0xa120}, // ritual + {749846, 0x9840}, // rhythm + {752638, 0x2140}, // planet + {753273, 0x24d0}, // please + {753343, 0x2410}, // pledge + {753824, 0xd490}, // sketch + {754448, 0xe840}, // slight + {756426, 0xe810}, // slogan + {758643, 0x2510}, // plunge + {762237, 0xea60}, // soccer + {762425, 0xea20}, // social + {762538, 0xa940}, // rocket + {762878, 0xa5c0}, // robust + {765423, 0x2a90}, // police + {766543, 0xa990}, // rookie + {766684, 0xca10}, // smooth + {768283, 0xa010}, // rotate + {768286, 0x2020}, // potato + {768723, 0xe690}, // source + {769337, 0x2060}, // powder + {772473, 0x22d0}, // praise + {773337, 0x2660}, // prefer + {773889, 0x2420}, // pretty + {774337, 0xc860}, // spider + {774373, 0xc590}, // sphere + {774748, 0xca80}, // spirit + {774766, 0x2b90}, // prison + {776348, 0x2a80}, // profit + {777323, 0xc900}, // spread + {777464, 0xca40}, // spring + {778273, 0xd490}, // square + {782237, 0x9560}, // rubber + {782253, 0xc190}, // stable + {782477, 0xc2b0}, // stairs + {782542, 0x16a0}, // public + {782648, 0xd480}, // submit + {782929, 0xd420}, // subway + {783336, 0xd050}, // sudden + {783337, 0xda60}, // suffer + {783736, 0xc660}, // stereo + {786637, 0xd060}, // summer + {786738, 0xd740}, // sunset + {786929, 0x9420}, // runway + {787338, 0xc940}, // street + {787453, 0xca50}, // strike + {787489, 0x1a20}, // purity + {787664, 0xca40}, // strong + {787759, 0xd0a0}, // supply + {787839, 0xda60}, // survey + {789953, 0x1f90}, // puzzle + {794824, 0xc890}, // switch + {796265, 0xe1a0}, // symbol + {797836, 0xec40}, // system + {822268, 0x8840}, // vacant + {822553, 0x0990}, // tackle + {822886, 0x8940}, // vacuum + {825368, 0x0940}, // talent + {825539, 0x8a60}, // valley + {826474, 0x86d0}, // vanish + {827438, 0x0840}, // target + {828866, 0x00a0}, // tattoo + {835838, 0x9a40}, // velvet + {836268, 0x1440}, // tenant + {836367, 0x94a0}, // vendor + {836647, 0x15b0}, // tennis + {837439, 0x9aa0}, // verify + {837735, 0x9f60}, // vessel + {842253, 0xa190}, // viable + {842538, 0x2940}, // ticket + {843679, 0x16a0}, // theory + {846237, 0x2160}, // timber + {846546, 0xaa90}, // violin + {847483, 0x1a90}, // thrive + {847783, 0x2f50}, // tissue + {847825, 0xad20}, // visual + {862253, 0x5190}, // unable + {863247, 0x58a0}, // unfair + {863653, 0x5a80}, // unfold + {864538, 0x2a40}, // toilet + {864783, 0x5950}, // unique + {865625, 0x5a90}, // unlock + {865863, 0xa910}, // volume + {866286, 0x2020}, // tomato + {866483, 0x2450}, // tongue + {867753, 0x2090}, // topple + {868345, 0x59a0}, // unveil + {869243, 0xa810}, // voyage + {869273, 0x2080}, // toward + {872442, 0x20a0}, // tragic + {872835, 0x2260}, // travel + {873283, 0x4010}, // update + {873385, 0x7660}, // useful + {874653, 0x4680}, // uphold + {876749, 0x2860}, // trophy + {886253, 0x1190}, // tumble + {886635, 0x1560}, // tunnel + {887539, 0x1960}, // turkey + {887853, 0x1890}, // turtle + {893583, 0x0690}, // twelve + {893689, 0x0520}, // twenty + {925688, 0x0940}, // walnut + {932584, 0x1210}, // wealth + {932735, 0x1360}, // weasel + {932766, 0x1090}, // weapon + {935569, 0x9a80}, // yellow + {946369, 0x2480}, // window + {946637, 0x2560}, // winner + {946837, 0x2460}, // winter + {947366, 0x2c80}, // wisdom + {966337, 0x2460}, // wonder + {2222243, 0x8504}, // cabbage + {2226366, 0x1124}, // abandon + {2226868, 0x2a50}, // account + {2244383, 0x2664}, // achieve + {2245489, 0x1a88}, // ability + {2252623, 0x4864}, // balance + {2252669, 0x4a98}, // balcony + {2272253, 0x8064}, // capable + {2274246, 0x4824}, // bargain + {2274825, 0x8208}, // capital + {2278246, 0x8024}, // captain + {2278473, 0x25a4}, // acquire + {2282564, 0x80a0}, // catalog + {2287377, 0x227c}, // actress + {2288466, 0x84a4}, // caution + {2322873, 0x5874}, // because + {2337377, 0x027c}, // address + {2345464, 0x9a90}, // ceiling + {2354383, 0x5a64}, // believe + {2363348, 0x55a0}, // benefit + {2368879, 0x9468}, // century + {2376242, 0x1a68}, // aerobic + {2378246, 0x9824}, // certain + {2382623, 0x0864}, // advance + {2389336, 0x5054}, // between + {2427837, 0x9018}, // chapter + {2429253, 0x6aa4}, // bicycle + {2442536, 0x9a54}, // chicken + {2446639, 0x9858}, // chimney + {2465649, 0x6a88}, // biology + {2476642, 0x9a68}, // chronic + {2477678, 0x28a0}, // airport + {2482553, 0x9664}, // chuckle + {2484936, 0xa2d4}, // citizen + {2526465, 0x2a68}, // alcohol + {2526538, 0x6150}, // blanket + {2527439, 0xa2a8}, // clarify + {2567766, 0x6be0}, // blossom + {2573239, 0x2908}, // already + {2587837, 0xa718}, // cluster + {2624368, 0x1a50}, // ancient + {2625978, 0x12b0}, // analyst + {2626688, 0xaa50}, // coconut + {2628387, 0x0058}, // amateur + {2629464, 0x0390}, // amazing + {2655328, 0xaa60}, // collect + {2662378, 0xa660}, // concert + {2662463, 0xa194}, // combine + {2663476, 0xa6a0}, // confirm + {2663678, 0xa2a0}, // comfort + {2663828, 0xa460}, // conduct + {2666328, 0xa560}, // connect + {2667269, 0xa018}, // company + {2668437, 0x1858}, // another + {2668765, 0xa4a8}, // control + {2677328, 0xaa60}, // correct + {2683662, 0x1150}, // antenna + {2684783, 0x1254}, // antique + {2686879, 0xa528}, // country + {2694389, 0x1648}, // anxiety + {2722538, 0x6250}, // bracket + {2742538, 0xaa50}, // cricket + {2765649, 0x0a88}, // apology + {2768437, 0x6858}, // brother + {2772643, 0x2844}, // arrange + {2772858, 0x3c60}, // assault + {2777683, 0x02a4}, // approve + {2782425, 0xa688}, // crucial + {2786253, 0xa464}, // crumble + {2789675, 0x20a4}, // artwork + {2797825, 0xab08}, // crystal + {2828466, 0x18a4}, // auction + {2833256, 0x5a28}, // buffalo + {2837243, 0x2604}, // average + {2845383, 0x0644}, // athlete + {2858873, 0x9864}, // culture + {2862236, 0x2a08}, // avocado + {2874466, 0x9da4}, // cushion + {2874687, 0x9a9c}, // curious + {2877368, 0x9a50}, // current + {2878246, 0x9824}, // curtain + {2887228, 0x0220}, // attract + {2937663, 0x0784}, // awesome + {2959273, 0x0420}, // awkward + {3228589, 0x8988}, // faculty + {3265649, 0x6a88}, // ecology + {3266669, 0x6988}, // economy + {3268279, 0x8438}, // fantasy + {3274466, 0x8da4}, // fashion + {3284483, 0x8214}, // fatigue + {3325463, 0x1a94}, // decline + {3328873, 0x9064}, // feature + {3333673, 0x1974}, // defense + {3333725, 0x9188}, // federal + {3354837, 0x1a98}, // deliver + {3368478, 0x14b0}, // dentist + {3376748, 0x12e0}, // deposit + {3377247, 0x1c28}, // despair + {3378769, 0x1ca8}, // destroy + {3382283, 0x4604}, // educate + {3383567, 0x19a0}, // develop + {3424726, 0x2080}, // diagram + {3426663, 0x2090}, // diamond + {3428466, 0xa8a4}, // fiction + {3444825, 0x2208}, // digital + {3446489, 0x2188}, // dignity + {3453662, 0x2900}, // dilemma + {3473273, 0x2d34}, // disease + {3476477, 0x2cbc}, // dismiss + {3477529, 0x2c88}, // display + {3486377, 0xa17c}, // fitness + {3486723, 0x2aa4}, // divorce + {3534268, 0x6410}, // elegant + {3536368, 0x6450}, // element + {3627223, 0x4624}, // embrace + {3635377, 0x527c}, // endless + {3636723, 0x5aa4}, // enforce + {3636773, 0x52b4}, // endorse + {3642623, 0x5464}, // enhance + {3657446, 0x2864}, // dolphin + {3668466, 0x48a4}, // emotion + {3676937, 0x4218}, // empower + {3678863, 0xa854}, // fortune + {3679273, 0xa820}, // forward + {3724453, 0xa0a4}, // fragile + {3727842, 0x2328}, // drastic + {3747633, 0x4b84}, // episode + {3767466, 0x6ba4}, // erosion + {3773623, 0x7d64}, // essence + {3837625, 0x4648}, // eternal + {3876223, 0x9924}, // furnace + {3925833, 0x5a44}, // exclude + {3926753, 0x5024}, // example + {3932769, 0x65a0}, // eyebrow + {3932883, 0x5644}, // execute + {3942878, 0x5470}, // exhaust + {3944248, 0x5660}, // exhibit + {3962642, 0x2428}, // dynamic + {3975246, 0x5224}, // explain + {3977377, 0x527c}, // express + {4255379, 0x0a68}, // gallery + {4267837, 0x4318}, // hamster + {4272243, 0x0904}, // garbage + {4276368, 0x0850}, // garment + {4278378, 0x4a70}, // harvest + {4363725, 0x1588}, // general + {4368463, 0x1594}, // genuine + {4378873, 0x1c64}, // gesture + {4472333, 0x28a4}, // giraffe + {4478679, 0x6ca8}, // history + {4546773, 0x2834}, // glimpse + {4553425, 0xa908}, // illegal + {4556377, 0xa97c}, // illness + {4625833, 0x9a44}, // include + {4633377, 0x207c}, // goddess + {4635428, 0x9aa0}, // inflict + {4643748, 0x95a0}, // inherit + {4648283, 0x8804}, // imitate + {4648425, 0x9888}, // initial + {4654329, 0x6a08}, // holiday + {4663673, 0x8174}, // immense + {4674552, 0x2aa0}, // gorilla + {4677473, 0x9ca4}, // inspire + {4677683, 0x82a4}, // improve + {4678255, 0x9c28}, // install + {4678479, 0x95a8}, // inquiry + {4678573, 0x81b4}, // impulse + {4686583, 0x9aa4}, // involve + {4728489, 0x2288}, // gravity + {4762379, 0x2a68}, // grocery + {4765283, 0xba04}, // isolate + {4863733, 0x5490}, // hundred + {4872263, 0x5d10}, // husband + {5286379, 0x8528}, // laundry + {5297848, 0x8360}, // lawsuit + {5325687, 0x129c}, // jealous + {5328873, 0x9864}, // lecture + {5347873, 0x9b64}, // leisure + {5367273, 0x9820}, // leopard + {5382487, 0x5250}, // ketchup + {5423673, 0xa974}, // license + {5423789, 0xa588}, // liberty + {5427279, 0xa628}, // library + {5464366, 0x6420}, // kingdom + {5482436, 0x6254}, // kitchen + {5627837, 0xa718}, // lobster + {5687639, 0x2658}, // journey + {5688379, 0xa068}, // lottery + {5844243, 0x9004}, // luggage + {6224463, 0x0994}, // machine + {6263283, 0x0404}, // mandate + {6267466, 0x07a4}, // mansion + {6272873, 0x9e64}, // obscure + {6273783, 0x9da4}, // observe + {6284687, 0x9a9c}, // obvious + {6286237, 0xa258}, // october + {6294686, 0x0610}, // maximum + {6327873, 0x1364}, // measure + {6345328, 0x5260}, // neglect + {6348437, 0x5858}, // neither + {6368466, 0x14a4}, // mention + {6377243, 0x1f04}, // message + {6388725, 0x5488}, // neutral + {6389675, 0x50a4}, // network + {6455466, 0x2aa4}, // million + {6464686, 0x2610}, // minimum + {6472253, 0x28a4}, // miracle + {6478253, 0x2c14}, // mistake + {6498873, 0x2464}, // mixture + {6596742, 0xa828}, // olympic + {6664633, 0x6254}, // nominee + {6664867, 0x2628}, // monitor + {6667837, 0x2718}, // monster + {6676464, 0x2990}, // morning + {6682253, 0x6064}, // notable + {6684464, 0x6190}, // nothing + {6724273, 0xa920}, // orchard + {6746466, 0x89a4}, // opinion + {6787424, 0xb2a4}, // ostrich + {6825327, 0x5a48}, // nuclear + {6883667, 0x90a8}, // outdoor + {6887433, 0x9384}, // outside + {6978379, 0x2c68}, // mystery + {7222666, 0x8aa4}, // raccoon + {7228837, 0xe018}, // scatter + {7236377, 0xc17c}, // sadness + {7243623, 0xe964}, // science + {7268437, 0x0458}, // panther + {7284368, 0x0250}, // patient + {7284739, 0xc2e8}, // satisfy + {7286744, 0xc2d8}, // satoshi + {7287243, 0xc704}, // sausage + {7288376, 0x0064}, // pattern + {7296368, 0x0850}, // payment + {7323483, 0x99a4}, // receive + {7327268, 0x1310}, // peasant + {7328453, 0x95a0}, // rebuild + {7328466, 0xd8a4}, // section + {7329253, 0x9aa4}, // recycle + {7335328, 0x9a60}, // reflect + {7346368, 0xd050}, // segment + {7348527, 0x9188}, // regular + {7353273, 0x9934}, // release + {7354226, 0x1a84}, // pelican + {7362589, 0x1488}, // penalty + {7364627, 0xd248}, // seminar + {7373328, 0x1a60}, // perfect + {7375223, 0x9224}, // replace + {7377466, 0xdfa4}, // session + {7378423, 0xdaa4}, // service + {7378473, 0x95a4}, // require + {7386466, 0x95a4}, // reunion + {7387328, 0x9240}, // retreat + {7425464, 0xe690}, // sibling + {7425569, 0xd2a0}, // shallow + {7428873, 0x2864}, // picture + {7437433, 0xd6a8}, // sheriff + {7464527, 0xe288}, // similar + {7466337, 0x2958}, // pioneer + {7483353, 0xd6a4}, // shuffle + {7488283, 0xe104}, // situate + {7527842, 0x2328}, // plastic + {7536337, 0xe518}, // slender + {7653437, 0xe898}, // soldier + {7662623, 0xa064}, // romance + {7663663, 0xe194}, // someone + {7678466, 0x28a4}, // portion + {7678527, 0x2188}, // popular + {7683789, 0x2988}, // poverty + {7688379, 0x2068}, // pottery + {7728425, 0xc088}, // spatial + {7732425, 0xc688}, // special + {7733428, 0x24a0}, // predict + {7737273, 0x2424}, // prepare + {7737368, 0x2750}, // present + {7738368, 0x2650}, // prevent + {7746279, 0x2828}, // primary + {7748283, 0x2a04}, // private + {7762377, 0x2a7c}, // process + {7762536, 0x2990}, // problem + {7763823, 0x2864}, // produce + {7764726, 0x2880}, // program + {7765328, 0x2860}, // project + {7766683, 0x2884}, // promote + {7766767, 0xc9e8}, // sponsor + {7767737, 0x2b18}, // prosper + {7768328, 0x2860}, // protect + {7768433, 0x2a84}, // provide + {7783393, 0xd574}, // squeeze + {7822377, 0xda7c}, // success + {7823486, 0xc090}, // stadium + {7825328, 0xd460}, // subject + {7825489, 0x5288}, // quality + {7826886, 0x5110}, // quantum + {7827837, 0x5218}, // quarter + {7833464, 0x1090}, // pudding + {7844378, 0xd070}, // suggest + {7866224, 0xc824}, // stomach + {7867546, 0x1064}, // pumpkin + {7873223, 0xda24}, // surface + {7877328, 0xdc60}, // suspect + {7877363, 0xd244}, // supreme + {7877673, 0x18b4}, // purpose + {7878246, 0xdc24}, // sustain + {7883368, 0xc450}, // student + {7886253, 0xc464}, // stumble + {7925569, 0xc2a0}, // swallow + {7967866, 0xe020}, // symptom + {7972643, 0x2820}, // pyramid + {8274687, 0x8a9c}, // various + {8344253, 0x96a4}, // vehicle + {8368873, 0x9464}, // venture + {8377466, 0x9ba4}, // version + {8383726, 0x9184}, // veteran + {8424687, 0xaa9c}, // vicious + {8427268, 0xa610}, // vibrant + {8428679, 0xa8a8}, // victory + {8455243, 0xaa04}, // village + {8468243, 0xa404}, // vintage + {8468448, 0x1910}, // thought + {8478825, 0xa848}, // virtual + {8486337, 0x1518}, // thunder + {8622226, 0x24a8}, // tobacco + {8626837, 0x5a98}, // uncover + {8629273, 0x5024}, // unaware + {8633537, 0x2098}, // toddler + {8642779, 0x5408}, // unhappy + {8643676, 0x5aa0}, // uniform + {8652266, 0xaa18}, // volcano + {8656696, 0x5584}, // unknown + {8664448, 0x2610}, // tonight + {8676236, 0x2908}, // tornado + {8687478, 0x26b0}, // tourist + {8687825, 0x5748}, // unusual + {8723342, 0x22a8}, // traffic + {8735377, 0x767c}, // useless + {8744437, 0x2818}, // trigger + {8747233, 0x4204}, // upgrade + {8768253, 0x2964}, // trouble + {8786738, 0x2410}, // trumpet + {8845489, 0x4a88}, // utility + {8848466, 0x18a4}, // tuition + {8974225, 0x2288}, // typical + {9273273, 0x0a24}, // warfare + {9277467, 0x0aa8}, // warrior + {9328437, 0x1058}, // weather + {9333464, 0x1090}, // wedding + {9335363, 0x1550}, // weekend + {9352663, 0x1a84}, // welcome + {9447737, 0x1b18}, // whisper + {9486377, 0x217c}, // witness + {9737853, 0x2724}, // wrestle + {22243368, 0x2a14}, // accident + {22243567, 0x496a}, // bachelor + {22687842, 0x29ca}, // acoustic + {22787228, 0x1c88}, // abstract + {22834679, 0x812a}, // category + {24267466, 0x9029}, // champion + {24662666, 0xa509}, // cinnamon + {26647377, 0xa49f}, // congress + {26668623, 0x1659}, // announce + {26674337, 0xa786}, // consider + {26684623, 0xa699}, // convince + {27622654, 0x6aaa}, // broccoli + {27833228, 0x2188}, // artefact + {28726273, 0x9188}, // cupboard + {28746377, 0x5e5f}, // business + {28848833, 0x0211}, // attitude + {32844837, 0x0446}, // daughter + {32867483, 0x8aa1}, // favorite + {33236237, 0x1916}, // december + {33267283, 0x1a81}, // decorate + {33273273, 0x1a4d}, // decrease + {33278279, 0x964a}, // february + {33727423, 0x1ea5}, // describe + {33784825, 0x9ca2}, // festival + {34667287, 0x26c6}, // dinosaur + {34724733, 0x2c25}, // disagree + {34726837, 0x2ea6}, // discover + {34767337, 0x2e86}, // disorder + {34782623, 0x2c19}, // distance + {35328742, 0x662a}, // electric + {35374268, 0x6444}, // elephant + {35382867, 0x660a}, // elevator + {36286368, 0x2914}, // document + {36835673, 0x59a1}, // envelope + {37378368, 0xa554}, // frequent + {38433623, 0x6859}, // evidence + {39242643, 0x5911}, // exchange + {39372473, 0x56ad}, // exercise + {43343464, 0x5058}, // hedgehog + {43368439, 0x852a}, // identify + {46273273, 0x9a4d}, // increase + {46342283, 0x9281}, // indicate + {46387879, 0x91ca}, // industry + {46662368, 0x9694}, // innocent + {46774825, 0x6c82}, // hospital + {46837378, 0x919c}, // interest + {52642766, 0x442a}, // kangaroo + {52648243, 0x8441}, // language + {62774243, 0x0a81}, // marriage + {62837425, 0x01a2}, // material + {63242642, 0x191a}, // mechanic + {63428483, 0x5029}, // negative + {64364448, 0x2184}, // midnight + {66778486, 0x2d62}, // mosquito + {66868246, 0x2509}, // mountain + {67346279, 0xa24a}, // ordinary + {67444625, 0xa892}, // original + {68584759, 0x188a}, // multiply + {68747666, 0x1da8}, // mushroom + {72477677, 0xebeb}, // scissors + {72677466, 0xea29}, // scorpion + {73287489, 0xd9a2}, // security + {73636237, 0x9116}, // remember + {73683623, 0xd459}, // sentence + {73736253, 0x9d19}, // resemble + {73768723, 0x9e69}, // resource + {73776673, 0x9c9d}, // response + {74685337, 0xd986}, // shoulder + {74974225, 0x1ba2}, // physical + {76588466, 0xe929}, // solution + {76748466, 0x2e29}, // position + {76774253, 0x2f99}, // possible + {77228423, 0x2229}, // practice + {77467489, 0x2aa2}, // priority + {77673789, 0x2862}, // property + {77847735, 0xd6a6}, // squirrel + {78378466, 0x5729}, // question + {78724273, 0x1a4d}, // purchase + {78728349, 0xc812}, // strategy + {78776863, 0xda94}, // surround + {78777473, 0xd8ad}, // surprise + {78784453, 0xc909}, // struggle + {86273552, 0x4668}, // umbrella + {86438437, 0x2116}, // together + {86483773, 0x5a6d}, // universe + {86667769, 0x22a8}, // tomorrow + {86786473, 0x28ad}, // tortoise + {87267337, 0x21e6} // transfer +}; diff --git a/ports/stm32/boards/Passport/board_init.c b/ports/stm32/boards/Passport/board_init.c index e97cad9..fc05336 100644 --- a/ports/stm32/boards/Passport/board_init.c +++ b/ports/stm32/boards/Passport/board_init.c @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // @@ -6,13 +6,18 @@ #include "stm32h7xx_hal.h" -#include "gpio.h" -#include "backlight.h" #include "adc.h" +#include "backlight.h" #include "camera-ovm7690.h" -#include "lcd-sharp-ls018B7dh02.h" +#include "display.h" +#include "frequency.h" +#include "gpio.h" #include "image_conversion.h" -#include "py/mphal.h" +#include "lcd-sharp-ls018B7dh02.h" +#include "busy_bar.h" +#include "se.h" +#include "utils.h" +#include "se.h" #define QR_IMAGE_SIZE (CAMERA_WIDTH * CAMERA_HEIGHT) #define VIEWFINDER_IMAGE_SIZE ((240 * 303) / 8) @@ -20,28 +25,27 @@ uint8_t qr[QR_IMAGE_SIZE]; uint8_t dp[VIEWFINDER_IMAGE_SIZE]; -void Passport_board_init(void) +void +Passport_board_init(void) { + /* Enable the console UART */ + frequency_update_console_uart(); + printf("[%s]\n", __func__); + printf("%lu, %lu, %lu, %lu, %lu\n", HAL_RCC_GetSysClockFreq(), SystemCoreClock, HAL_RCC_GetHCLKFreq(), HAL_RCC_GetPCLK1Freq(), HAL_RCC_GetPCLK2Freq()); + + set_stack_sentinel(); + gpio_init(); - backlight_init(); - lcd_init(); + // backlight_init(); Not necessary as we call backlight_minimal_init() from the Backlight class in modfoundation.c + display_init(false); camera_init(); - adc2_init(); - adc3_init(); - -#if 0 - backlight_intensity(250); - camera_on(); - while (1) - { - camera_snapshot(); - convert_rgb565_to_grayscale_and_mono(camera_frame_buffer, qr, CAMERA_HEIGHT, CAMERA_WIDTH, dp, 240, 240); - lcd_update(dp, 0); - HAL_Delay(10); - } -#endif -} + adc_init(); + busy_bar_init(); + se_setup(); -void Passport_board_early_init(void) -{ + // check_stack("Passport_board_init() complete", true); } + +void +Passport_board_early_init(void) +{} diff --git a/ports/stm32/boards/Passport/board_init.h b/ports/stm32/boards/Passport/board_init.h index 3aa6bf9..ce3721a 100644 --- a/ports/stm32/boards/Passport/board_init.h +++ b/ports/stm32/boards/Passport/board_init.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // diff --git a/ports/stm32/boards/Passport/bootloader/.gitignore b/ports/stm32/boards/Passport/bootloader/.gitignore new file mode 100644 index 0000000..b7a88c4 --- /dev/null +++ b/ports/stm32/boards/Passport/bootloader/.gitignore @@ -0,0 +1 @@ +version_info.c diff --git a/ports/stm32/boards/Passport/bootloader/Makefile b/ports/stm32/boards/Passport/bootloader/Makefile index 7068ad2..8d55e12 100644 --- a/ports/stm32/boards/Passport/bootloader/Makefile +++ b/ports/stm32/boards/Passport/bootloader/Makefile @@ -1,7 +1,7 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: GPL-3.0-or-later # -# SPDX-FileCopyrightText: 2018 Coinkite, Inc. +# SPDX-FileCopyrightText: 2018 Coinkite, Inc. # SPDX-License-Identifier: GPL-3.0-only # # "Bootloader" Makefile @@ -12,6 +12,8 @@ # clobber - delete all build products # +BOOTLOADER_VERSION = 1.05 + # Toolchain TOOLCHAIN = arm-none-eabi- CC = $(TOOLCHAIN)gcc @@ -31,40 +33,58 @@ PASSPORT_PATH = .. VPATH = $(HAL_DRIVERS_PATH) VPATH += $(HAL_CMSIS_PATH) +VPATH += $(HAL_CMSIS_PATH)/gcc VPATH += $(PASSPORT_PATH)/common VPATH += $(PASSPORT_PATH)/common/micro-ecc # Basename of all targets TARGET_NAME = bootloader +TARGETDIR = arm # Source files. Important: Add them also to link-script.ld to control placement. -OBJS += startup.o -OBJS += main.o -OBJS += flash.o -OBJS += update.o -OBJS += verify.o -OBJS += se-atecc608a.o - -OBJS += delay.o -OBJS += hash.o -OBJS += pprng.o -OBJS += se.o -OBJS += sha256.o -OBJS += spiflash.o -OBJS += uECC.o -OBJS += utils.o - -OBJS += system_stm32h7xx.o - -OBJS += stm32h7xx_hal.o -OBJS += stm32h7xx_hal_rcc.o -OBJS += stm32h7xx_hal_rcc_ex.o -OBJS += stm32h7xx_hal_gpio.o -OBJS += stm32h7xx_hal_cortex.o -OBJS += stm32h7xx_hal_pwr.o -OBJS += stm32h7xx_hal_pwr_ex.o -OBJS += stm32h7xx_hal_spi.o -OBJS += stm32h7xx_hal_dma.o +# Files specific to the bootloader +SOURCES = startup_stm32h753xx.c +SOURCES += startup.c +SOURCES += bootloader_graphics.c +SOURCES += main.c +SOURCES += flash.c +SOURCES += splash.c +SOURCES += update.c +SOURCES += se-atecc608a.c +SOURCES += ui.c +SOURCES += verify.c +SOURCES += version_info.c + +# Common files between bootloader and MP +SOURCES += backlight.c +SOURCES += delay.c +SOURCES += display.c +SOURCES += gpio.c +SOURCES += hash.c +SOURCES += lcd-sharp-ls018B7dh02.c +SOURCES += passport_fonts.c +SOURCES += pprng.c +SOURCES += se.c +SOURCES += sha256.c +SOURCES += spiflash.c +SOURCES += uECC.c +SOURCES += utils.c + +SOURCES += system_stm32h7xx.c + +SOURCES += stm32h7xx_hal.c +SOURCES += stm32h7xx_hal_rcc.c +SOURCES += stm32h7xx_hal_rcc_ex.c +SOURCES += stm32h7xx_hal_gpio.c +SOURCES += stm32h7xx_hal_cortex.c +SOURCES += stm32h7xx_hal_pwr.c +SOURCES += stm32h7xx_hal_pwr_ex.c +SOURCES += stm32h7xx_hal_spi.c +SOURCES += stm32h7xx_hal_dma.c + +# Required for LCD support +SOURCES += stm32h7xx_hal_tim.c +SOURCES += stm32h7xx_hal_tim_ex.c # Where we will end up in the memory map (at start of flash) BL_FLASH_BASE = 0x08000000 @@ -72,32 +92,52 @@ BL_FLASH_SIZE = 0x20000 BL_FLASH_LAST = 0x08020000 # SRAM4 is reserved for us. -BL_SRAM_BASE = 0x38000000 -BL_SRAM_SIZE = 0x00008000 +BL_SRAM_BASE = 0x20000000 +BL_SRAM_SIZE = 0x00020000 -# Final 2k bytes reserved for data (not code) -# - must be page-aligned, contains pairing secret -#BL_NVROM_BASE = 0x0801F000 # final area for ROM secrets -#BL_NVROM_SIZE = 0x1000 -BL_NVROM_BASE = 0x081C0000 # temporary for development -BL_NVROM_SIZE = 0x1000 +# Final 1k bytes reserved for data (not code) +BL_NVROM_BASE = 0x0801FF00 # final area for ROM secrets +#BL_NVROM_BASE = 0x081C0000 # temporary for development +BL_NVROM_SIZE = 0x100 # Compiler flags. -CFLAGS = -Wall --std=gnu99 -g +CFLAGS = -Wall -Werror --std=gnu99 +CFLAGS += -Wno-address-of-packed-member CFLAGS += -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hard CFLAGS += -ffunction-sections -fdata-sections CFLAGS += -mtune=cortex-m7 -mcpu=cortex-m7 -DSTM32H753xx CFLAGS += -I. -I$(PASSPORT_PATH)/include -I$(PASSPORT_PATH)/common/micro-ecc CFLAGS += -DPASSPORT_BOOTLOADER -#CFLAGS += -DUSE_CRYPTO -CFLAGS += -DDEMO +CFLAGS += -DUSE_CRYPTO +ifeq ($(findstring production,$(MAKECMDGOALS)),production) +CFLAGS += -DPRODUCTION_BUILD +endif +#CFLAGS += -DCONVERSION_BUILD +ifeq ($(findstring locked,$(MAKECMDGOALS)),locked) +CFLAGS += -DLOCKED +endif # Pass in the locations of stuff CFLAGS += -D BL_FLASH_BASE=$(BL_FLASH_BASE) -D BL_FLASH_SIZE=$(BL_FLASH_SIZE) CFLAGS += -D BL_NVROM_BASE=$(BL_NVROM_BASE) -D BL_NVROM_SIZE=$(BL_NVROM_SIZE) -CFLAGS += -D BL_SRAM_BASE=$(BL_SRAM_BASE) -D BL_SRAM_SIZE=$(BL_SRAM_SIZE) +CFLAGS += -D BL_SRAM_BASE=$(BL_SRAM_BASE) -D BL_SRAM_SIZE=$(BL_SRAM_SIZE) CFLAGS += -D BL_FLASH_LAST=$(BL_FLASH_LAST) +ifeq ($(findstring debug,$(MAKECMDGOALS)),debug) +OBJDIR = $(TARGETDIR)/debug +CFLAGS += -g -DDEBUG +LDFLAGS += -g +else +OBJDIR = $(TARGETDIR)/release +CFLAGS += -O2 +# Add keypad support for release builds +SOURCES += keypad-adp-5587.c +SOURCES += ring_buffer.c +SOURCES += stm32h7xx_hal_i2c.c +endif + +OBJECTS = $(addprefix $(OBJDIR)/,$(SOURCES:.c=.o)) + CC_SYMBOLS = -mcpu=cortex-m7 # Header file search path @@ -112,7 +152,7 @@ CFLAGS += $(foreach INC,$(INC_PATHS),-I$(INC)) # LINKER_SCRIPT = link-script.ld -LDFLAGS += -flto -Wl,--gc-sections --specs=nano.specs -Wl,-T$(LINKER_SCRIPT) +LDFLAGS += -flto -Wl,--gc-sections -specs=nano.specs -Wl,-T$(LINKER_SCRIPT) LDFLAGS += -nostartfiles LDFLAGS += -Wl,--defsym,BL_FLASH_BASE=$(BL_FLASH_BASE) LDFLAGS += -Wl,--defsym,BL_FLASH_SIZE=$(BL_FLASH_SIZE) @@ -120,100 +160,63 @@ LDFLAGS += -Wl,--defsym,BL_NVROM_BASE=$(BL_NVROM_BASE) LDFLAGS += -Wl,--defsym,BL_NVROM_SIZE=$(BL_NVROM_SIZE) LDFLAGS += -Wl,--defsym,BL_SRAM_BASE=$(BL_SRAM_BASE) LDFLAGS += -Wl,--defsym,BL_SRAM_SIZE=$(BL_SRAM_SIZE) -LDFLAGS += -Wl,-Map=$(TARGET_NAME).map +LDFLAGS += -Wl,-Map=$(OBJDIR)/$(TARGET_NAME).map ASFLAGS += -Wa,--defsym,BL_FLASH_BASE=$(BL_FLASH_BASE) -Wa,--defsym,BL_FLASH_SIZE=$(BL_FLASH_SIZE) ASFLAGS += -Wa,--defsym,BL_SRAM_BASE=$(BL_SRAM_BASE) -Wa,--defsym,BL_SRAM_SIZE=$(BL_SRAM_SIZE) -TARGET_ELF = $(TARGET_NAME).elf -TARGETS = $(TARGET_NAME).bin +TARGET_ELF = $(OBJDIR)/$(TARGET_NAME).elf +TARGETS = $(OBJDIR)/$(TARGET_NAME).bin + +all: version $(TARGETS) + +debug: version $(TARGETS) -all: $(TARGETS) +locked: version $(TARGETS) + +production: version $(TARGETS) # recompile on any change, because with a small project like this... -$(OBJS): Makefile +$(OBJECTS): Makefile $(TARGETS): $(TARGET_ELF) Makefile # link step -$(TARGET_ELF): $(OBJS) $(LINKER_SCRIPT) Makefile - $(CC) $(CFLAGS) -o $(TARGET_ELF) $(LDFLAGS) $(OBJS) +$(TARGET_ELF): $(OBJECTS) $(LINKER_SCRIPT) Makefile + $(CC) $(CFLAGS) $(LDFLAGS) -o $(TARGET_ELF) $(OBJECTS) $(SIZE) -Ax $@ -%.o: %.S +$(OBJDIR)/%.o: %.s + @rm -f $@ + @[ -d $(dir $@) ] || mkdir -p $(dir $@) $(CC) $(CFLAGS) -c -o $@ $< -%.o: %.c +$(OBJDIR)/%.o: %.c + @rm -f $@ + @[ -d $(dir $@) ] || mkdir -p $(dir $@) $(CC) $(CFLAGS) -c -MMD -MP -o $@ $< # raw binary, forced to right size, pad w/ 0xff -%.bin: $(TARGET_ELF) - $(OBJCOPY) -O binary --pad-to $(BL_FLASH_LAST) --gap-fill 0xff $< $@.tmp +$(OBJDIR)/%.bin: $(TARGET_ELF) + $(OBJCOPY) -O binary --pad-to $$(($(BL_FLASH_LAST) - $(BL_NVROM_SIZE))) --gap-fill 0x00 $< $@.tmp dd bs=$$(($(BL_FLASH_SIZE) * 1024)) count=1 if=$@.tmp of=$@ @$(RM) $@.tmp ifneq ($(MAKECMDGOALS),clean) --include $(OBJS:.o=.d) +-include $(OBJECTS:.o=.d) endif # make a 'release' build release: code-committed check-fontawesome clean all capture release: CFLAGS += -DRELEASE=1 -Werror -check-fontawesome: - # You must have commerical license for Font Awesome (altho fallback looks ok) - test -f assets/FontAwesome5Pro-Light-300.otf - -.PHONY: code-committed -code-committed: - @echo "" - @echo "Are all changes commited already?" - git diff --stat --exit-code . - @echo '... yes' - -# these files are what we capture and store for each release. -DELIVERABLES = $(TARGET_NAME).bin - -checksums.txt: $(DELIVERABLES) - shasum -a 256 $(DELIVERABLES) > $@ - -# Track released versions -.PHONY: capture -capture: version.txt version-full.txt $(DELIVERABLES) checksums.txt - V=`cat version.txt` && cat checksums.txt > releases/$$V.txt && cat version-full.txt >> releases/$$V.txt && mkdir -p releases/$$V; cp $(DELIVERABLES) releases/$$V - @echo - @echo " Version: " `cat version.txt` - @echo - V=`cat version.txt` && git tag -am "Bootloader version $$V" "bootloader-"$$V - git add -f releases/*/bootloader.* releases/*.txt - -# Pull out the version string from binary object (already linked in) and -# construct a text file (version.txt) with those contents -version.txt version-full.txt: version.o Makefile - $(OBJCOPY) -O binary -j .rodata.version_string version.o version-tmp.txt - cat version-tmp.txt | sed -e 's/ .*//' | sed -e 's/ .*//' > version.txt - cat version-tmp.txt | tr '\0' '\n' > version-full.txt - @echo - @echo "Version string: " `cat version-full.txt` - @echo - $(RM) version-tmp.txt - -# nice version numbers. -BUILD_TIME = $(shell date '+%Y%m%d.%H%M%S') -BRANCH = $(shell git rev-parse --abbrev-ref HEAD) -SHA_VERSION = $(shell git rev-parse --short HEAD) -GIT_HASH = "$(BRANCH)@$(SHA_VERSION)" -version.o: CFLAGS += -DBUILD_TIME='"$(BUILD_TIME)"' -DGIT_HASH='$(GIT_HASH)' -version.o: Makefile - clean: - $(RM) $(OBJS) - $(RM) $(OBJS:.o=.d) + @$(RM) -r $(TARGETDIR) -clobber: clean - $(RM) $(TARGETS) +version: + @$(TOP)/boards/Passport/tools/version_info/version_info version_info.c $(BOOTLOADER_VERSION) + @[ -d $(dir $(OBJDIR)/version_info.o) ] || mkdir -p $(dir $(OBJDIR)/version_info.o) + $(CC) $(CFLAGS) -c -MMD -MP -o $(OBJDIR)/version_info.o version_info.c -debug: - @echo CFLAGS = $(CFLAGS) - @echo - @echo OBJS = $(OBJS) +.PHONY: all clean install version +.SECONDARY: diff --git a/ports/stm32/boards/Passport/bootloader/bootloader_graphics.c b/ports/stm32/boards/Passport/bootloader/bootloader_graphics.c new file mode 100644 index 0000000..cddfd3b --- /dev/null +++ b/ports/stm32/boards/Passport/bootloader/bootloader_graphics.c @@ -0,0 +1,142 @@ +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-License-Identifier: GPL-3.0-or-later +// +// +// Autogenerated - Do not edit! +// + +#include "bootloader_graphics.h" + +uint8_t splash_data[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x0f, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7e, 0x00, 0x0f, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7e, 0x00, 0x0f, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x0f, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x0f, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xfe, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xfe, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xfe, 0x00, 0x0f, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xfe, 0x00, 0x0f, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xfe, 0x00, 0x0f, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xfe, 0x00, 0x0f, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xfe, 0x00, 0x0f, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xfe, 0x00, 0x0f, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xfe, 0x00, 0x0f, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xfe, 0x00, 0x0f, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xe0, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xe0, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xe0, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xe0, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xe0, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xe0, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xe0, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xe0, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xe0, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xe0, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xe0, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xe0, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xe0, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xe0, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0xe0, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xe0, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xe0, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xe0, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xe0, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xe0, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xe0, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xe0, 0x00, 0x7f, 0xfe, 0x00, 0x0f, 0xff, 0xc0, 0x00, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xc0, 0x00, 0x3f, 0xfc, 0x00, 0x07, 0xff, 0x80, 0x00, 0x7f, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xff, 0x80, 0x0f, 0x80, 0x0e, 0x0e, 0x01, 0xc0, 0x70, 0x1f, 0xe0, 0x00, 0x10, 0x03, 0xff, 0x80, 0x70, 0x00, 0xf8, 0x01, 0xc0, 0x70, + 0xff, 0x80, 0x1f, 0xc0, 0x0e, 0x0e, 0x01, 0xc0, 0x70, 0x1f, 0xf0, 0x00, 0x38, 0x03, 0xff, 0x80, 0x70, 0x01, 0xfc, 0x01, 0xc0, 0x70, + 0xff, 0x80, 0x3f, 0xe0, 0x0e, 0x0e, 0x01, 0xe0, 0x70, 0x1f, 0xf8, 0x00, 0x38, 0x03, 0xff, 0x80, 0x70, 0x03, 0xfe, 0x01, 0xe0, 0x70, + 0xe0, 0x00, 0x78, 0xf0, 0x0e, 0x0e, 0x01, 0xf0, 0x70, 0x1c, 0x3c, 0x00, 0x7c, 0x00, 0x38, 0x00, 0x70, 0x07, 0x8f, 0x01, 0xf0, 0x70, + 0xe0, 0x00, 0xf0, 0x78, 0x0e, 0x0e, 0x01, 0xf8, 0x70, 0x1c, 0x1e, 0x00, 0x7c, 0x00, 0x38, 0x00, 0x70, 0x0f, 0x07, 0x81, 0xf8, 0x70, + 0xfe, 0x00, 0xe0, 0x38, 0x0e, 0x0e, 0x01, 0xfc, 0x70, 0x1c, 0x0e, 0x00, 0xfe, 0x00, 0x38, 0x00, 0x70, 0x0e, 0x03, 0x81, 0xfc, 0x70, + 0xfe, 0x00, 0xe0, 0x38, 0x0e, 0x0e, 0x01, 0xde, 0x70, 0x1c, 0x0e, 0x00, 0xee, 0x00, 0x38, 0x00, 0x70, 0x0e, 0x03, 0x81, 0xde, 0x70, + 0xfe, 0x00, 0xe0, 0x38, 0x0e, 0x0e, 0x01, 0xcf, 0x70, 0x1c, 0x0e, 0x01, 0xef, 0x00, 0x38, 0x00, 0x70, 0x0e, 0x03, 0x81, 0xcf, 0x70, + 0xe0, 0x00, 0xf0, 0x78, 0x0e, 0x0e, 0x01, 0xc7, 0xf0, 0x1c, 0x1e, 0x01, 0xc7, 0x00, 0x38, 0x00, 0x70, 0x0f, 0x07, 0x81, 0xc7, 0xf0, + 0xe0, 0x00, 0x78, 0xf0, 0x0f, 0x1e, 0x01, 0xc3, 0xf0, 0x1c, 0x3c, 0x03, 0xc7, 0x80, 0x38, 0x00, 0x70, 0x07, 0x8f, 0x01, 0xc3, 0xf0, + 0xe0, 0x00, 0x3f, 0xe0, 0x07, 0xfc, 0x01, 0xc1, 0xf0, 0x1f, 0xf8, 0x03, 0x83, 0x80, 0x38, 0x00, 0x70, 0x03, 0xfe, 0x01, 0xc1, 0xf0, + 0xe0, 0x00, 0x1f, 0xc0, 0x07, 0xfc, 0x01, 0xc0, 0xf0, 0x1f, 0xf0, 0x07, 0x83, 0xc0, 0x38, 0x00, 0x70, 0x01, 0xfc, 0x01, 0xc0, 0xf0, + 0xe0, 0x00, 0x0f, 0x80, 0x03, 0xf8, 0x01, 0xc0, 0x70, 0x1f, 0xe0, 0x07, 0x01, 0xc0, 0x38, 0x00, 0x70, 0x00, 0xf8, 0x01, 0xc0, 0x70, +}; +Image splash_img = { 172, 129, 22, splash_data }; + diff --git a/ports/stm32/boards/Passport/bootloader/bootloader_graphics.h b/ports/stm32/boards/Passport/bootloader/bootloader_graphics.h new file mode 100644 index 0000000..f6810cd --- /dev/null +++ b/ports/stm32/boards/Passport/bootloader/bootloader_graphics.h @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-License-Identifier: GPL-3.0-or-later +// +// +// Autogenerated - Do not edit! +// + +#include + +typedef struct _Image { + int16_t width; + int16_t height; + int16_t byte_width; + uint8_t* data; +} Image; + +extern Image splash_img; diff --git a/ports/stm32/boards/Passport/bootloader/flash.c b/ports/stm32/boards/Passport/bootloader/flash.c index d769346..b62e49c 100644 --- a/ports/stm32/boards/Passport/bootloader/flash.c +++ b/ports/stm32/boards/Passport/bootloader/flash.c @@ -1,13 +1,13 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // -// SPDX-FileCopyrightText: 2018 Coinkite, Inc. +// SPDX-FileCopyrightText: 2018 Coinkite, Inc. // SPDX-License-Identifier: GPL-3.0-only // // (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard // and is covered by GPLv3 license found in COPYING. // -// +// // flash.c -- manage flash and its sensitive contents. // @@ -40,7 +40,7 @@ static inline bool is_pairing_secret_programmed( return false; } -static inline bool is_se_programmed(void) +static inline secresult is_se_programmed(void) { int rc; uint8_t config[128] = {0}; @@ -50,8 +50,8 @@ static inline bool is_se_programmed(void) LOCKUP_FOREVER(); /* Can't talk to the SE */ if ((config[86] != 0x55) && (config[87] != 0x55)) - return true; - return false; + return SEC_TRUE; + return SEC_FALSE; } // See FLASH_WaitForLastOperation((uint32_t)FLASH_TIMEOUT_VALUE) @@ -113,11 +113,11 @@ void flash_lock(void) // see HAL_FLASH_Lock(); SET_BIT(FLASH->CR1, FLASH_CR_LOCK); if (READ_BIT(FLASH->CR1, FLASH_CR_LOCK)) { - return; //INCONSISTENT("failed to lock bank 1"); + return; } SET_BIT(FLASH->CR2, FLASH_CR_LOCK); if (READ_BIT(FLASH->CR2, FLASH_CR_LOCK)) { - return; //INCONSISTENT("failed to lock bank 2"); + return; } } @@ -131,7 +131,7 @@ void flash_unlock(void) WRITE_REG(FLASH->KEYR1, FLASH_KEY2); if(READ_BIT(FLASH->CR1, FLASH_CR_LOCK)) { - return; //INCONSISTENT("failed to unlock bank 1"); + return; } } if (READ_BIT(FLASH->CR2, FLASH_CR_LOCK)) { @@ -140,11 +140,12 @@ void flash_unlock(void) WRITE_REG(FLASH->KEYR2, FLASH_KEY2); if(READ_BIT(FLASH->CR2, FLASH_CR_LOCK)) { - return; //INCONSISTENT("failed to unlock bank 2"); + return; } } } +__attribute__((section(".ramfunc"))) int flash_ob_lock(bool lock) { if (!lock) @@ -155,7 +156,7 @@ int flash_ob_lock(bool lock) /* Authorizes the Option Byte registers programming */ WRITE_REG(FLASH->OPTKEYR, FLASH_OPT_KEY1); WRITE_REG(FLASH->OPTKEYR, FLASH_OPT_KEY2); - + /* Verify that the Option Bytes are unlocked */ if (READ_BIT(FLASH->OPTCR, FLASH_OPTCR_OPTLOCK) != 0U) { @@ -168,7 +169,7 @@ int flash_ob_lock(bool lock) /* see HAL_FLASH_OB_Lock() */ /* Set the OPTLOCK Bit to lock the FLASH Option Byte Registers access */ SET_BIT(FLASH->OPTCR, FLASH_OPTCR_OPTLOCK); - + /* Verify that the Option Bytes are locked */ if (READ_BIT(FLASH->OPTCR, FLASH_OPTCR_OPTLOCK) == 0U) { @@ -289,18 +290,26 @@ static int flash_rom_secrets(rom_secrets_t *secrets) { __IO uint32_t *src_secrets = (__IO uint32_t *)secrets; __IO uint32_t *flash_secrets = (__IO uint32_t *)(BL_NVROM_BASE); + __IO uint32_t empty[FLASH_NB_32BITWORD_IN_FLASHWORD] = {0}; uint32_t flash_word_len = sizeof(uint32_t) * FLASH_NB_32BITWORD_IN_FLASHWORD; uint32_t pos = (uint32_t)src_secrets; uint32_t dest = (uint32_t)flash_secrets; + uint32_t zeros = (uint32_t)empty; int i; flash_unlock(); - for (i = 0; i < sizeof(rom_secrets_t); i += flash_word_len, pos += flash_word_len, dest += flash_word_len) { - if (flash_burn(dest, pos)) { + for (i = 0; i < sizeof(rom_secrets_t); i += flash_word_len, pos += flash_word_len, dest += flash_word_len) + { + if (flash_burn(dest, pos)) + return -1; + } + + for (; i < BL_NVROM_SIZE; i += flash_word_len, dest += flash_word_len) + { + if (flash_burn(dest, zeros)) return -1; - } } flash_lock(); @@ -351,6 +360,24 @@ static int flash_bootloader(rom_secrets_t *secrets) } #endif /* JUST_PROGRAM_ROM_SECRETS */ +__attribute__((section(".ramfunc"))) +void flash_lockdown_hard(void) +{ +#ifdef LOCKED + _flash_wait_done(FLASH_BANK_1); + _flash_wait_done(FLASH_BANK_2); + flash_ob_lock(false); + + MODIFY_REG(FLASH->OPTSR_PRG, FLASH_OPTSR_RDP, (uint32_t)OB_RDP_LEVEL_2); + + _flash_wait_done(FLASH_BANK_1); + _flash_wait_done(FLASH_BANK_2); + SET_BIT(FLASH->OPTCR, FLASH_OPTCR_OPTSTART); + + flash_ob_lock(true); +#endif /* LOCKED */ +} + static void pick_pairing_secret(rom_secrets_t *local) { uint32_t secret[8]; @@ -376,11 +403,11 @@ static void pick_pairing_secret(rom_secrets_t *local) *pos = rng_sample(); } - pos = (uint32_t *)&local->otp_key_long; - len = sizeof(local->otp_key_long); - for (i = 0; i < len; i += sizeof(uint32_t), ++pos) { - *pos = rng_sample(); - } + // pos = (uint32_t *)&local->otp_key_long; + // len = sizeof(local->otp_key_long); + // for (i = 0; i < len; i += sizeof(uint32_t), ++pos) { + // *pos = rng_sample(); + // } pos = (uint32_t *)&local->hash_cache_secret; len = sizeof(local->hash_cache_secret); @@ -389,11 +416,12 @@ static void pick_pairing_secret(rom_secrets_t *local) } } -void flash_first_boot(void) +secresult flash_first_boot(void) { int rc; - uint8_t fw_hash[HASH_LEN]; - uint8_t board_hash[HASH_LEN]; + uint8_t fw_hash[HASH_LEN] = {0}; + uint8_t board_hash[HASH_LEN] = {0}; + uint8_t zeros[HASH_LEN] = {0}; passport_firmware_header_t *fwhdr = (passport_firmware_header_t *)FW_HDR; uint8_t *fwptr = (uint8_t *)fwhdr + FW_HEADER_SIZE; bool secrets_already_programmed; @@ -401,17 +429,17 @@ void flash_first_boot(void) rom_secrets_t local_secrets = {0}; if (sizeof(rom_secrets_t) > 2048) - LOCKUP_FOREVER(); + return ERR_ROM_SECRETS_TOO_BIG; - if (!verify_header(fwhdr)) - LOCKUP_FOREVER(); + if (verify_header(fwhdr) != SEC_TRUE) + return ERR_INVALID_FIRMWARE_HEADER; hash_fw(&fwhdr->info, fwptr, fwhdr->info.fwlength, fw_hash, sizeof(fw_hash)); - if (!verify_signature(fwhdr, fw_hash, sizeof(fw_hash))) - LOCKUP_FOREVER(); + if (verify_signature(fwhdr, fw_hash, sizeof(fw_hash)) == SEC_FALSE) + return ERR_INVALID_FIRMWARE_SIGNATURE; - secrets_already_programmed = is_pairing_secret_programmed(rom_secrets->pairing_secret, + secrets_already_programmed = is_pairing_secret_programmed(rom_secrets->pairing_secret, sizeof(rom_secrets->pairing_secret)); if (secrets_already_programmed) @@ -421,7 +449,7 @@ void flash_first_boot(void) rc = se_setup_config(&local_secrets); if (rc != 0) - LOCKUP_FOREVER(); + return ERR_UNABLE_TO_CONFIGURE_SE; if (!secrets_already_programmed) { @@ -433,41 +461,40 @@ void flash_first_boot(void) #endif /* JUST_PROGRAM_ROM_SECRETS */ HAL_ResumeTick(); if (rc < 0) - LOCKUP_FOREVER(); - } + return ERR_UNABLE_TO_WRITE_ROM_SECRETS; + } -#ifdef DEMO - memset(board_hash, 0, sizeof(board_hash)); -#else + // We need to lockdown the flash BEFORE programming the first board_hash into the SE so that the correct + // option bytes from the MCU are included in the hash. + flash_lockdown_hard(); + +#ifdef PRODUCTION_BUILD hash_board(fw_hash, sizeof(fw_hash), board_hash, sizeof(board_hash)); -#endif /* DEMO */ +#else + memset(board_hash, 0, sizeof(board_hash)); +#endif /* PRODUCTION_BUILD */ - rc = se_program_board_hash(board_hash, sizeof(board_hash)); + rc = se_program_board_hash(zeros, board_hash, sizeof(board_hash)); if (rc < 0) - LOCKUP_FOREVER(); + return ERR_UNABLE_TO_UPDATE_FIRMWARE_HASH_IN_SE; - flash_lockdown_hard((uint32_t)OB_RDP_LEVEL_2); + return SEC_TRUE; } -void flash_lockdown_hard(uint32_t rdp_level) +secresult flash_is_programmed(void) { -#ifndef FIXME /* Enable once we're almost ready to release and don't - * forget to move the secrets back into 0 - */ - return; -#else - flash_ob_lock(false); - - MODIFY_REG(FLASH->OPTSR_PRG, FLASH_OPTSR_RDP, rdp_level); + if (!is_pairing_secret_programmed(rom_secrets->pairing_secret, sizeof(rom_secrets->pairing_secret))) + return SEC_FALSE; - flash_ob_lock(true); -#endif /* FIXME */ + return is_se_programmed(); } -bool flash_is_programmed(void) +#ifdef LOCKED +secresult flash_is_locked(void) { - if (!is_pairing_secret_programmed(rom_secrets->pairing_secret, sizeof(rom_secrets->pairing_secret))) - return false; - - return is_se_programmed(); + uint32_t rdp_level = READ_BIT(FLASH->OPTSR_CUR, FLASH_OPTSR_RDP); + if (rdp_level == OB_RDP_LEVEL_2) + return SEC_TRUE; + return SEC_FALSE; } +#endif /* LOCKED */ diff --git a/ports/stm32/boards/Passport/bootloader/flash.h b/ports/stm32/boards/Passport/bootloader/flash.h index f301c36..216ff64 100644 --- a/ports/stm32/boards/Passport/bootloader/flash.h +++ b/ports/stm32/boards/Passport/bootloader/flash.h @@ -1,7 +1,7 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // -// SPDX-FileCopyrightText: 2018 Coinkite, Inc. +// SPDX-FileCopyrightText: 2018 Coinkite, Inc. // SPDX-License-Identifier: GPL-3.0-only // /* @@ -14,10 +14,15 @@ #include "stm32h7xx_hal.h" +#include "secresult.h" + // Details of the OTP area. 64-bit slots. #define OPT_FLASH_BASE 0x1FFF7000 #define NUM_OPT_SLOTS 128 +#define USER_SETTINGS_FLASH_ADDR 0x81E0000 + + static inline bool flash_is_security_level2(void) { return ((FLASH->OPTSR_CUR & FLASH_OPTSR_RDP_Msk) == OB_RDP_LEVEL_2); @@ -29,8 +34,9 @@ extern void flash_unlock(void); extern int flash_burn(uint32_t flash_address, uint32_t data_address); extern int flash_sector_erase(uint32_t address); extern void flash_test(void); -extern void flash_first_boot(void); -extern void flash_lockdown_hard(uint32_t rdp_level_code); -extern bool flash_is_programmed(void); +extern secresult flash_first_boot(void); +extern void flash_lockdown_hard(void); +extern secresult flash_is_programmed(void); +extern secresult flash_is_locked(void); // EOF diff --git a/ports/stm32/boards/Passport/bootloader/link-script.ld b/ports/stm32/boards/Passport/bootloader/link-script.ld index ab4ec3a..e02bf3c 100644 --- a/ports/stm32/boards/Passport/bootloader/link-script.ld +++ b/ports/stm32/boards/Passport/bootloader/link-script.ld @@ -1,8 +1,8 @@ /* - SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. + SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. SPDX-License-Identifier: GPL-3.0-or-later - SPDX-FileCopyrightText: 2018 Coinkite, Inc. + SPDX-FileCopyrightText: 2018 Coinkite, Inc. SPDX-License-Identifier: GPL-3.0-only */ @@ -26,24 +26,29 @@ SEARCH_DIR(.) /* Memory Spaces Definitions */ MEMORY { - rom (rx) : ORIGIN = BL_FLASH_BASE, LENGTH = BL_FLASH_SIZE - ram (rwx) : ORIGIN = BL_SRAM_BASE, LENGTH = BL_SRAM_SIZE - itcm (rwx) : ORIGIN = 0x00000000, LENGTH = 0x10000 + rom (rx) : ORIGIN = BL_FLASH_BASE, LENGTH = BL_FLASH_SIZE + ram (rwx) : ORIGIN = BL_SRAM_BASE, LENGTH = BL_SRAM_SIZE + itcm (rwx) : ORIGIN = 0x00000000, LENGTH = 0x10000 } /* The stack size used by the bootloader. */ -STACK_SIZE = DEFINED(STACK_SIZE) ? STACK_SIZE : DEFINED(__stack_size__) ? __stack_size__ : 0x4000; +STACK_SIZE = DEFINED(STACK_SIZE) ? STACK_SIZE : DEFINED(__stack_size__) ? __stack_size__ : 0x8000; /* Section Definitions */ SECTIONS { + .isr_vector : + { + . = ALIGN(4); + KEEP(*(.isr_vector)) /* Startup code */ + . = ALIGN(4); + } > rom + .text : { . = ALIGN(4); _sfixed = .; KEEP(*(.entry_code)) - KEEP(*(.outside_pcrop)) - main.o(.text*) . = ALIGN(256); /* important: this pulls in library (libgcc) stuff here */ @@ -55,7 +60,7 @@ SECTIONS } > rom - /* .ARM.exidx is sorted, so has to go in its own output section. */ + /* .ARM.exidx is sorted, so has to go in its own output section. */ PROVIDE_HIDDEN (__exidx_start = .); .ARM.exidx : { @@ -121,18 +126,6 @@ SECTIONS /* Some very manual linking! I've tried doing it right, and couldn't get it to work well */ addr_rom_secrets = BL_NVROM_BASE; - /* if you initialize a global var to some non-zero value, then that data ends up - in .relocate as read-only data and used briefly at startup (when copied to RAM). - We don't want to support that, so we're checking here that hasn't happened. */ -/* - ASSERT(_pdg_hack == _erelocate, - "Sorry, no initialized data support! Set to zero or remove.") -*/ - /* ensure binary fits */ -/* - ASSERT(_erelocate - BL_CODE_BASE + _etext <= BL_FLASH_BASE + BL_FLASH_SIZE, - "Binary is too big to fit!!!") -*/ . = ALIGN(4); _end = . ; } diff --git a/ports/stm32/boards/Passport/bootloader/main.c b/ports/stm32/boards/Passport/bootloader/main.c index ebaf75d..eb04d6f 100644 --- a/ports/stm32/boards/Passport/bootloader/main.c +++ b/ports/stm32/boards/Passport/bootloader/main.c @@ -1,17 +1,16 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // -// SPDX-FileCopyrightText: 2018 Coinkite, Inc. +// SPDX-FileCopyrightText: 2018 Coinkite, Inc. // SPDX-License-Identifier: GPL-3.0-only // -#include - #include +#include #include "../stm32h7xx_hal_conf.h" -#include "stm32h7xx_ll_cortex.h" - +#include "delay.h" +#include "fwheader.h" #include "pprng.h" #include "secrets.h" #include "se.h" @@ -22,11 +21,42 @@ #include "verify.h" #include "update.h" +#include "backlight.h" +#include "display.h" +#include "lcd-sharp-ls018B7dh02.h" +#ifndef DEBUG +#include "keypad-adp-5587.h" +#endif /* DEBUG */ +#include "splash.h" +#include "ui.h" +#include "gpio.h" +#include "version_info.h" +#include "hash.h" +#include "secresult.h" + +/* + * This is an empty function to satisfy the linker requirement for init + * when the startup_stm32h753xx.s file was pulled into the bootloader + * build to define the full vector table. + */ +void _init(void) +{ +} + void SysTick_Handler(void) { HAL_IncTick(); } - +#ifndef DEBUG +void EXTI15_10_IRQHandler(void) +{ + if (__HAL_GPIO_EXTI_GET_FLAG(1 << 12)) + { + __HAL_GPIO_EXTI_CLEAR_FLAG(1 << 12); + keypad_ISR(); + } +} +#endif /* DEBUG */ static void SystemClock_Config(void) { HAL_StatusTypeDef rc; @@ -42,7 +72,7 @@ static void SystemClock_Config(void) __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1); while(!__HAL_PWR_GET_FLAG(PWR_FLAG_VOSRDY)) {} - + /* Enable HSE Oscillator and activate PLL with HSE as source */ RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE | RCC_OSCILLATORTYPE_HSI48; RCC_OscInitStruct.HSEState = RCC_HSE_ON; @@ -58,7 +88,7 @@ static void SystemClock_Config(void) RCC_OscInitStruct.PLL.PLLQ = 120; RCC_OscInitStruct.PLL.PLLR = 2; RCC_OscInitStruct.PLL.PLLFRACN = 0; - + RCC_OscInitStruct.PLL.PLLVCOSEL = RCC_PLL1VCOWIDE; RCC_OscInitStruct.PLL.PLLRGE = RCC_PLL1VCIRANGE_1; rc = HAL_RCC_OscConfig(&RCC_OscInitStruct); @@ -91,10 +121,10 @@ static void SystemClock_Config(void) RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; RCC_ClkInitStruct.SYSCLKDivider = RCC_SYSCLK_DIV1; RCC_ClkInitStruct.AHBCLKDivider = RCC_HCLK_DIV2; - RCC_ClkInitStruct.APB3CLKDivider = RCC_APB3_DIV2; - RCC_ClkInitStruct.APB1CLKDivider = RCC_APB1_DIV2; - RCC_ClkInitStruct.APB2CLKDivider = RCC_APB2_DIV2; - RCC_ClkInitStruct.APB4CLKDivider = RCC_APB4_DIV2; + RCC_ClkInitStruct.APB3CLKDivider = RCC_APB3_DIV2; + RCC_ClkInitStruct.APB1CLKDivider = RCC_APB1_DIV2; + RCC_ClkInitStruct.APB2CLKDivider = RCC_APB2_DIV2; + RCC_ClkInitStruct.APB4CLKDivider = RCC_APB4_DIV2; rc = HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4); if (rc != HAL_OK) { @@ -113,7 +143,32 @@ static void SystemClock_Config(void) __HAL_RCC_D2SRAM3_CLK_ENABLE(); } -void MPU_Config(void) +// Recover from ECC errors during firmware updates +void HardFault_Handler(void) +{ + uint32_t cfsr = SCB->CFSR; + + if (cfsr & 0x8000) { + uint32_t faultaddr = (uint32_t)SCB->BFAR; + uint32_t fw_sector_start = FW_START; + uint32_t fw_sector_end = FW_END; + + if ((faultaddr >= fw_sector_start) && (faultaddr < fw_sector_end)) { + uint32_t faultsector = faultaddr & 0xFFF0000; + + flash_unlock(); + flash_sector_erase(faultsector); + flash_lock(); + + /* Reset the board */ + passport_reset(); + } + } + + while (1); +} + +static void MPU_Config(void) { MPU_Region_InitTypeDef MPU_InitStruct; @@ -180,7 +235,6 @@ void MPU_Config(void) MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE; HAL_MPU_ConfigRegion(&MPU_InitStruct); -#if 0 /* FIXME...enabling this causes an IACCVIOL! */ /* Configure SRAM4 region as non-executable */ memset(&MPU_InitStruct, 0, sizeof(MPU_InitStruct)); MPU_InitStruct.Enable = MPU_REGION_ENABLE; @@ -195,7 +249,6 @@ void MPU_Config(void) MPU_InitStruct.SubRegionDisable = 0x00; MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE; HAL_MPU_ConfigRegion(&MPU_InitStruct); -#endif /* Configure ITCM region as non-executable */ memset(&MPU_InitStruct, 0, sizeof(MPU_InitStruct)); @@ -212,23 +265,142 @@ void MPU_Config(void) MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE; HAL_MPU_ConfigRegion(&MPU_InitStruct); + /* Configure DTCM region as non-executable */ + memset(&MPU_InitStruct, 0, sizeof(MPU_InitStruct)); + MPU_InitStruct.Enable = MPU_REGION_ENABLE; + MPU_InitStruct.BaseAddress = 0x20000000; + MPU_InitStruct.Size = MPU_REGION_SIZE_128KB; + MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS; + MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE; + MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE; + MPU_InitStruct.IsShareable = MPU_ACCESS_SHAREABLE; + MPU_InitStruct.Number = MPU_REGION_NUMBER5; + MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0; + MPU_InitStruct.SubRegionDisable = 0x00; + MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE; + HAL_MPU_ConfigRegion(&MPU_InitStruct); + + /* Configure Backup region as non-executable */ + memset(&MPU_InitStruct, 0, sizeof(MPU_InitStruct)); + MPU_InitStruct.Enable = MPU_REGION_ENABLE; + MPU_InitStruct.BaseAddress = 0x38800000; + MPU_InitStruct.Size = MPU_REGION_SIZE_4KB; + MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS; + MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE; + MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE; + MPU_InitStruct.IsShareable = MPU_ACCESS_SHAREABLE; + MPU_InitStruct.Number = MPU_REGION_NUMBER5; + MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0; + MPU_InitStruct.SubRegionDisable = 0x00; + MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE; + HAL_MPU_ConfigRegion(&MPU_InitStruct); /* Enable MPU */ HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT); +} + +static void version(void) +{ + passport_firmware_header_t *fwhdr = (passport_firmware_header_t *)FW_HDR; + char version[22] = {0}; + + strcpy(version, "Version "); + strcat(version, (char *)fwhdr->info.fwversion); + show_splash(version); +} + +#ifndef DEBUG + +static void show_more_info(void) +{ + char message[80]; + + // For the firmware header and hash + uint8_t fw_hash[HASH_LEN]; + passport_firmware_header_t *fwhdr = (passport_firmware_header_t *)FW_HDR; + + uint8_t page = 0; + while (true) { + switch (page) { + case 0: + strcpy(message, "\nVersion:\n"); + strcat(message, build_version); + strcat(message, "\n\nBuild Date:\n"); + strcat(message, build_date); + + if (ui_show_message("Bootloader Info", message, "SHUTDOWN", "NEXT", true)){ + page++; + } else { + display_clean_shutdown(); + } + break; + + case 1: + strcpy(message, "\nVersion:\n"); + strcat(message, (char*)fwhdr->info.fwversion); + strcat(message, "\n\nBuild Date:\n"); + strcat(message, (char*)fwhdr->info.fwdate); + if (ui_show_message("Firmware Info", message, "BACK", "NEXT", true)){ + page++; + } else { + page--; + } + break; + + case 2: { + message[0] = '\n'; + message[1] = 0; + hash_fw_user((uint8_t*)fwhdr, FW_HEADER_SIZE + fwhdr->info.fwlength, fw_hash, sizeof(fw_hash), false); + + bytes_to_hex_str(fw_hash, 32, &message[1], 8, '\n'); + + if (ui_show_message("Download Hash", message, "BACK", "NEXT", true)){ + page++; + } else { + page--; + } + break; + } + + case 3: { + message[0] = '\n'; + message[1] = 0; + hash_fw_user((uint8_t*)fwhdr, FW_HEADER_SIZE + fwhdr->info.fwlength, fw_hash, sizeof(fw_hash), true); + + bytes_to_hex_str(fw_hash, 32, &message[1], 8, '\n'); + + if (ui_show_message("Build Hash", message, "BACK", "START", true)){ + return; + } else { + page--; + } + break; + } + } + } +} +#endif /* DEBUG */ + +void random_boot_delay() { + // Random delay to make cold-boot stepping attacks harder: 0 - 100ms + uint32_t ms_to_delay = rng_sample() % 50; + delay_ms(ms_to_delay); } int main(void) { HAL_StatusTypeDef rc; - +#ifndef DEBUG + uint8_t keycount; + uint8_t key; +#endif /* DEBUG */ SystemInit(); rc = HAL_Init(); if (rc != HAL_OK) - { - while(1) { ; } - } + LOCKUP_FOREVER(); + #if 0 /* This is interfering with firmware boot after an update. It * appears that the data cache is getting in the way of the * reset handler properly copying over the data section into SRAM. @@ -238,27 +410,163 @@ int main(void) #endif SystemClock_Config(); + // Set Brown-out level early on to reset on glitch attempts + MODIFY_REG(FLASH->OPTSR_PRG, FLASH_OPTSR_BOR_LEV, (uint32_t)OB_BOR_LEVEL2); + +#ifdef LOCKED + // Ensure RDP level 2 on every boot in case of shenanigans + if (!flash_is_security_level2()) { + flash_lockdown_hard(); + } +#endif /* LOCKED */ + rng_setup(); + + random_boot_delay(); + se_setup(); - /* Check for first-boot condition */ - if (!flash_is_programmed()) - flash_first_boot(); + // Force LED to red every time we restart for consistency + se_set_gpio(0); - /* Validate our pairing secret */ - if (!se_valid_secret(rom_secrets->pairing_secret)) - LOCKUP_FOREVER(); + // Initialize the LCD driver and clear the display + backlight_init(); + backlight_intensity(100); + display_init(true); - /* Validate the internal firmware */ - if (!verify_current_firmware()) - LOCKUP_FOREVER(); +#ifndef DEBUG + keypad_init(); + gpio_init(); + +#endif /* DEBUG */ + + show_splash(""); + + random_boot_delay(); + + // Check for first-boot condition + if (flash_is_programmed() == SEC_FALSE) { + secresult result = flash_first_boot(); + switch (result) { + case SEC_TRUE: + // All good! + break; + + case ERR_ROM_SECRETS_TOO_BIG: + ui_show_fatal_error("ROM Secrets area is larger than 2048 bytes."); + break; + + case ERR_INVALID_FIRMWARE_HEADER: + ui_show_fatal_error("Invalid firmware header found during first boot."); + break; + + case ERR_INVALID_FIRMWARE_SIGNATURE: + ui_show_fatal_error("Invalid firmware signature found during first boot."); + break; - /* Check for firmware update */ - if (is_firmware_update_present()) + case ERR_UNABLE_TO_CONFIGURE_SE: + ui_show_fatal_error("Unable to configure the Secure Element during first boot."); + break; + + case ERR_UNABLE_TO_WRITE_ROM_SECRETS: + ui_show_fatal_error("Unable to flash ROM secrets to end of bootloader flash block during first boot."); + break; + + case ERR_UNABLE_TO_UPDATE_FIRMWARE_HASH_IN_SE: + ui_show_fatal_error("Unable to program firmware hash into security chip during first boot."); + break; + + default: + ui_show_fatal_error("Unexpected error on first boot."); + break; + } + } + + // Increment the boot counter + uint32_t counter_result; + if (se_add_counter(&counter_result, 1, 1) != 0) { + ui_show_fatal_error("Unable to increment boot counter in the Secure Element. Device may have been tampered with.\n\nThis Passport is now permanently disabled."); + } + + // Validate our pairing secret + if (!se_valid_secret(rom_secrets->pairing_secret)) { + ui_show_fatal_error("Unable to connect to the Secure Element.\n\nThis Passport is now permanently disabled."); + } + + // Check for firmware update + if (is_firmware_update_present() == SEC_TRUE) { update_firmware(); + } + + // Validate the internal firmware + secresult result = verify_current_firmware(true); + switch (result) { + case SEC_TRUE: + // All good! + break; + + case ERR_INVALID_FIRMWARE_HEADER: + ui_show_fatal_error("Invalid firmware header found.\n\nThis Passport is now permanently disabled."); + break; + + case ERR_INVALID_FIRMWARE_SIGNATURE: + ui_show_fatal_error("The installed firmware was not signed by a valid key.\n\nThis Passport is now permanently disabled."); + break; - /* Setup MPU */ + case ERR_FIRMWARE_HASH_DOES_NOT_MATCH_SE: + ui_show_fatal_error("The installed firmware hash does not match that expected by the Secure Element.\n\nThis Passport is now permanently disabled."); + break; + + default: + ui_show_fatal_error("Unexpected error when verifying current firmware."); + break; + } + + random_boot_delay(); + + // Setup MPU MPU_Config(); - /* From here we'll boot to Micropython */ + version(); + +#ifndef DEBUG + /* + * Delay for 3 seconds to allow the user to press a key indicating that + * they would like to see board info or show the self test (in Python). + */ + delay_ms(3000); + + // We use the first byte in sram4 to pass a parameter that we check for on the MicroPython side + // to see if user wants to view the self-test. + uint8_t* p_sram4 = (uint8_t*)0x38000000; + *p_sram4 = 0; + + keycount = ring_buffer_dequeue(&key); + if (keycount > 0) + { + // The '1' key + if ((key & 0x7f) == 112) + { + show_more_info(); + } + + // The '7' key + if ((key & 0x7f) == 107) + { + // Setting this byte to 1 signals main.py to show the self-test and serial number + *p_sram4 = 1; + } + } +#endif + + // Show a warning message if non-Foundation firmware is loaded on the device + if (is_user_signed_firmware_installed() == SEC_TRUE) { + if (ui_show_message("Firmware Warning", "\nCustom, non-Foundation firmware is loaded on this Passport.\n\nOK to continue?", "NO", "YES", true)){ + // Continue booting + } else { + display_clean_shutdown(); + } + } + + // From here we'll boot to Micropython: see stm32_main() in /ports/stm32/main.c } diff --git a/ports/stm32/boards/Passport/bootloader/se-atecc608a.c b/ports/stm32/boards/Passport/bootloader/se-atecc608a.c index 31d93a2..b033b81 100644 --- a/ports/stm32/boards/Passport/bootloader/se-atecc608a.c +++ b/ports/stm32/boards/Passport/bootloader/se-atecc608a.c @@ -1,9 +1,10 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // -// SPDX-FileCopyrightText: 2018 Coinkite, Inc. +// SPDX-FileCopyrightText: 2018 Coinkite, Inc. // SPDX-License-Identifier: GPL-3.0-only // + #include #include #include @@ -17,14 +18,12 @@ #include "se-config.h" #include "sha256.h" -#define INCONSISTENT(x) /* FIXME */ - static int se_write_data_slot(int slot_num, uint8_t *data, int len, bool lock_it) { -#ifdef FIXME - ASSERT(len >= 32); - ASSERT(len <= 416); -#endif + if ((len < 32) || (len > 416) || (lock_it && slot_num == 8)) { + return -1; + } + for (int blk = 0, xlen = len; xlen > 0; blk++, xlen -= 32) { int rc; @@ -38,10 +37,6 @@ static int se_write_data_slot(int slot_num, uint8_t *data, int len, bool lock_it } if (lock_it) { -#ifdef FIXME - ASSERT(slot_num != 8); // no support for mega slot 8 - ASSERT(len == 32); // probably not a limitation here -#endif // Assume 36/72-byte long slot, which will be partially written, and rest // should be ones. const int slot_len = (slot_num <= 7) ? 36 : 72; @@ -82,34 +77,7 @@ static int se_lock_data_zone(void) return se_read1(); } -#if 0 /* Not used now */ -// Read a 4-byte area from config area, or -1 if fail. -// -static int se_read_config_word(int offset, uint8_t *dest) -{ - offset &= 0x7f; - - // read 32 bits (aligned) - se_write(OP_Read, 0x00, offset/4, NULL, 0); - - int rv = se_read(dest, 4); - if (rv < 0) - return -1; - - return 0; -} - -// Read a byte from config area. -// -static int se_read_config_byte(int offset) -{ - uint8_t tmp[4]; - se_read_config_word(offset, tmp); - - return tmp[offset % 4]; -} -#endif /* 0 */ static int se_config_write(uint8_t *config) { // send all 128 bytes, less some that can't be written. @@ -143,10 +111,10 @@ static int se_config_write(uint8_t *config) // secret in cleartext. They could then restore original chip and access freely. // // PASSPORT NOTE: We can eliminate the above by having the factory bootloader -// be different than the normal bootloader. The factory bootloader +// be different than the normal bootloader. The factory bootloader // will have the one-time setup code only, not the runtime code. // The normal bootloader will NOT have the one-time setup code, -// but WILL have the main runtime code. So swapping in blank +// but WILL have the main runtime code. So swapping in blank // SE would not trigger us to write the pairing secret in the clear. // int se_setup_config(rom_secrets_t *secrets) @@ -212,7 +180,7 @@ int se_setup_config(rom_secrets_t *secrets) case 15: break; - case KEYNUM_pairing: + case KEYNUM_pairing_secret: if (se_write_data_slot(kn, secrets->pairing_secret, 32, false)) return -4; break; @@ -226,38 +194,56 @@ int se_setup_config(rom_secrets_t *secrets) // - stretching pin/words attempts (iterated may times) // See mathcheck.py for details. uint8_t tmp[32]; - + rng_buffer(tmp, sizeof(tmp)); - + if (se_write_data_slot(kn, tmp, 32, true)) return -5; } break; - case KEYNUM_main_pin: + case KEYNUM_pin_hash: case KEYNUM_lastgood: - case KEYNUM_firmware: + case KEYNUM_firmware_hash: if (se_write_data_slot(kn, zeros, 32, false)) return -6; break; - case KEYNUM_secret: - if (se_write_data_slot(kn, zeros, 72, false)) + case KEYNUM_supply_chain: { + // SCV key is in user settings flash + uint8_t* supply_chain_key = (uint8_t*)USER_SETTINGS_FLASH_ADDR; + bool is_erased = true; + for (uint32_t i=0; i<32; i++) { + if (supply_chain_key[i] != 0xFF) { + is_erased = false; + } + } + + // If the scv key is not set in flash, then don't proceed, else validation will never work! + if (is_erased) { + return -11; + } + + int rc = se_write_data_slot(kn, supply_chain_key, 32, false); + + // Always erase the supply chain key, even if the write failed + flash_sector_erase(USER_SETTINGS_FLASH_ADDR); + + if (rc) return -7; + } break; - case KEYNUM_long_secret: - { - uint8_t long_zeros[416] = {0}; - if (se_write_data_slot(kn, long_zeros, 416, false)) + case KEYNUM_seed: + case KEYNUM_user_fw_pubkey: + if (se_write_data_slot(kn, zeros, 72, false)) return -8; - } break; case KEYNUM_match_count: { uint32_t buf[32/4] = { 1024, 1024 }; - if (se_write_data_slot(KEYNUM_match_count, (uint8_t *)buf,sizeof(buf),false)) + if (se_write_data_slot(KEYNUM_match_count, (uint8_t *)buf,sizeof(buf),false)) return -9; } break; @@ -323,7 +309,7 @@ int se_set_gpio_secure(uint8_t *digest) if (rc < 0) return -1; - rc = se_checkmac(KEYNUM_firmware, digest); + rc = se_checkmac(KEYNUM_firmware_hash, digest); if (rc < 0) return -1; @@ -335,25 +321,23 @@ int se_set_gpio_secure(uint8_t *digest) } int se_program_board_hash( - uint8_t *board_hash, + uint8_t *previous_hash, + uint8_t *new_hash, uint8_t hash_len ) { -#ifdef DEMO - uint8_t zeros[HASH_LEN] = {0}; - - return se_encrypted_write(KEYNUM_firmware, KEYNUM_main_pin, zeros, board_hash, hash_len); +#ifdef PRODUCTION_BUILD + return se_encrypted_write(KEYNUM_firmware_hash, KEYNUM_firmware_hash, previous_hash, new_hash, hash_len); #else - /* We don't know how we're going to do this yet */ return 0; -#endif /* DEMO */ +#endif /* PRODUCTION_BUILD */ } bool se_valid_secret( uint8_t *secret ) { - int rc = se_checkmac(KEYNUM_pairing, secret); + int rc = se_checkmac(KEYNUM_pairing_secret, secret); if (rc < 0) return false; return true; diff --git a/ports/stm32/boards/Passport/bootloader/se-atecc608a.h b/ports/stm32/boards/Passport/bootloader/se-atecc608a.h index 23a32ab..5858432 100644 --- a/ports/stm32/boards/Passport/bootloader/se-atecc608a.h +++ b/ports/stm32/boards/Passport/bootloader/se-atecc608a.h @@ -1,7 +1,7 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // -// SPDX-FileCopyrightText: 2018 Coinkite, Inc. +// SPDX-FileCopyrightText: 2018 Coinkite, Inc. // SPDX-License-Identifier: GPL-3.0-only // // Copyright 2020 - Foundation Devices Inc. @@ -17,7 +17,7 @@ extern int se_setup_config(rom_secrets_t *secrets); extern uint8_t se_get_gpio(void); extern int se_set_gpio(int state); extern int se_set_gpio_secure(uint8_t *digest); -extern int se_program_board_hash(uint8_t *board_hash, uint8_t hash_len); +extern int se_program_board_hash(uint8_t *previous_hash, uint8_t *new_hash, uint8_t hash_len); extern bool se_valid_secret(uint8_t *secret); #endif //_SECURE_ELEMENT_ATECC608A_H_ diff --git a/ports/stm32/boards/Passport/bootloader/secrets b/ports/stm32/boards/Passport/bootloader/secrets new file mode 100644 index 0000000000000000000000000000000000000000..6de6bab6a3920e0ec9833c4a9fb9fd53bee71817 GIT binary patch literal 256 zcmcES^$fS&BH|=mE4fwS>*we8$M3S11UB)|EIEgdaMOI+{03!eUz^?v!=Rh&v+?*140z^LrH?3b~;`8%I)nd-f= zhrGYv@7A06?cLwCam>qa=gMF2xqCSN;7w)ATbXwE@=Ha2C`?U-}qZ|&*z;;EaoQ;Rr-uk2#A z64m)@yp}!E_kho*bB9|aRO^brrCWcCd$BDqTK?@9t^N+~EPRL G^aTJgwnh~I literal 0 HcmV?d00001 diff --git a/ports/stm32/boards/Passport/bootloader/splash.c b/ports/stm32/boards/Passport/bootloader/splash.c new file mode 100644 index 0000000..74e3350 --- /dev/null +++ b/ports/stm32/boards/Passport/bootloader/splash.c @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-License-Identifier: GPL-3.0-or-later +// +// splash.c - Splash screen shown at during initialization + +#include "display.h" + +#include "bootloader_graphics.h" + +void show_splash(char* message) +{ + uint16_t x = SCREEN_WIDTH / 2 - splash_img.width / 2; + uint16_t y = SCREEN_HEIGHT / 2 - splash_img.height / 2; + + display_clear(0); + display_image(x, y, splash_img.width, splash_img.height, splash_img.data, DRAW_MODE_NORMAL); + display_text(message, CENTER_X, SCREEN_HEIGHT - 68, &FontSmall, false); + display_show(); +} diff --git a/ports/stm32/boards/Passport/bootloader/splash.h b/ports/stm32/boards/Passport/bootloader/splash.h new file mode 100644 index 0000000..525e248 --- /dev/null +++ b/ports/stm32/boards/Passport/bootloader/splash.h @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-License-Identifier: GPL-3.0-or-later +// +// splash.h - Splash screen shown at during initialization + +extern void show_splash(char* message); diff --git a/ports/stm32/boards/Passport/bootloader/startup.S b/ports/stm32/boards/Passport/bootloader/startup.S index c69a66f..40b8ba9 100644 --- a/ports/stm32/boards/Passport/bootloader/startup.S +++ b/ports/stm32/boards/Passport/bootloader/startup.S @@ -1,8 +1,8 @@ /* - SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. + SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. SPDX-License-Identifier: GPL-3.0-or-later - SPDX-FileCopyrightText: 2018 Coinkite, Inc. + SPDX-FileCopyrightText: 2018 Coinkite, Inc. SPDX-License-Identifier: GPL-3.0-only */ /* starting value for the top of our stack. */ diff --git a/ports/stm32/boards/Passport/bootloader/startup.s b/ports/stm32/boards/Passport/bootloader/startup.s new file mode 100644 index 0000000..e8c6795 --- /dev/null +++ b/ports/stm32/boards/Passport/bootloader/startup.s @@ -0,0 +1,86 @@ +/* + SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. + SPDX-License-Identifier: GPL-3.0-or-later + + SPDX-FileCopyrightText: 2018 Coinkite, Inc. + SPDX-License-Identifier: GPL-3.0-only +*/ + .thumb + .syntax unified + + .global Reset_Handler + .type Reset_Handler, %function + +Reset_Handler: + + /* Load the stack pointer */ + ldr sp, =_estack + + /* Initialise the data section */ + ldr r1, =_sidata + ldr r2, =_sdata + ldr r3, =_edata + b .data_copy_entry +.data_copy_loop: + ldr r0, [r1], #4 /* Should be 4-aligned to be as fast as possible */ + str r0, [r2], #4 +.data_copy_entry: + cmp r2, r3 + bcc .data_copy_loop + + /* Zero out the BSS section */ + movs r0, #0 + ldr r1, =_sbss + ldr r2, =_ebss + b .bss_zero_entry +.bss_zero_loop: + str r0, [r1], #4 /* Should be 4-aligned to be as fast as possible */ +.bss_zero_entry: + cmp r1, r2 + bcc .bss_zero_loop + + /* Initialise the sram section */ + ldr r1, =_siram + ldr r2, =_sram + ldr r3, =_eram + b .ram_copy_entry +.ram_copy_loop: + ldr r0, [r1], #4 /* Should be 4-aligned to be as fast as possible */ + str r0, [r2], #4 +.ram_copy_entry: + cmp r2, r3 + bcc .ram_copy_loop + + bl main + + /* + * get a ptr to real code + * load R1 with 0x08020800 value: start of firmware + */ + movw r1, (0x08020800 >> 12) + lsl r1, 12 + orr.w r1, r1, #0x800 + + /* set stack pointer to their preference */ + ldr r0, [r1] + mov sp, r0 + + /* Disabled + * We cannot change to user mode here because the micropython code + * depends on being in supervisor mode. SystemInit() is invoked + * from the Reset_Handler() and the vector table (along with other + * SCB accesses) is set at the start of stm32_main()...both + * important things in the startup processing. + */ + /* We are in supervisor mode out of reset...drop down to user mode */ +/* + mrs r3, CONTROL + orr.w r3, r3, #1 + msr CONTROL, r3 +*/ + /* Read reset vector, and jump to it. */ + mov r0, 1 /* set reset_mode arg: 1=normal? */ + ldr lr, [r1, 4] + bx lr + + .end diff --git a/ports/stm32/boards/Passport/bootloader/ui.c b/ports/stm32/boards/Passport/bootloader/ui.c new file mode 100644 index 0000000..9f438fb --- /dev/null +++ b/ports/stm32/boards/Passport/bootloader/ui.c @@ -0,0 +1,243 @@ +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-License-Identifier: GPL-3.0-or-later +// +// ui.c - Simple UI elements for the bootloader + +#include + +#include "ui.h" +#include "gpio.h" +#include "delay.h" +#include "utils.h" +#include "display.h" +#include "passport_fonts.h" +#include "ring_buffer.h" +#include "lcd-sharp-ls018B7dh02.h" + +#define HEADER_HEIGHT 40 +#define FOOTER_HEIGHT 32 +#define SIDE_MARGIN 4 +#define TOP_MARGIN 4 + +void ui_draw_header(char* title) { + uint16_t title_y = 10; + + // Title + display_text(title, CENTER_X, title_y, &FontSmall, false); + + // Divider + display_fill_rect(0, HEADER_HEIGHT-4, SCREEN_WIDTH, 2, 1); +} + +void ui_draw_button(uint16_t x, uint16_t y, uint16_t w, uint16_t h, char* label, bool is_pressed) { + if (is_pressed) { + display_fill_rect(x, y, w, h, 1); + } else { + display_rect(x, y, w, h, 1); + } + + // Measure text and center it in the button + uint16_t label_width = display_measure_text(label, &FontTiny); + + x = x + (w / 2 - label_width / 2); + y = y + (h / 2 - FontTiny.ascent / 2); + + display_text(label, x, y - 1, &FontTiny, is_pressed); +} + +void ui_draw_footer(char* left_btn, bool is_left_pressed, char* right_btn, bool is_right_pressed) { + + uint16_t btn_w = SCREEN_WIDTH / 2; + + // Draw left button + ui_draw_button(-1, SCREEN_HEIGHT - FOOTER_HEIGHT + 1, btn_w + 1, + FOOTER_HEIGHT, left_btn, is_left_pressed); + + // Draw right button + ui_draw_button(btn_w - 1, SCREEN_HEIGHT - FOOTER_HEIGHT + 1, + btn_w + 2, FOOTER_HEIGHT, right_btn, is_right_pressed); +} + +void ui_draw_wrapped_text(uint16_t x, uint16_t y, uint16_t max_width, char* text, bool center) { + // Buffer to hold each wrapped line + char line[80]; + uint16_t curr_y = y; + + while (*text != 0) { + uint16_t sp = 0; + uint16_t last_space = 0; + uint16_t line_width = 0; + uint16_t first_non_space = 0; + uint16_t text_len = strlen(text); + uint16_t sp_skip = 0; + + // Skip leading spaces + while (true) { + if (text[sp] == ' ') { + sp++; + first_non_space = sp; + } else if (text[sp] == '\n') { + sp++; + first_non_space = sp; + curr_y += FontSmall.leading; + } else { + break; + } + } + + while (sp < text_len) { + char ch = text[sp]; + if (ch == ' ') { + last_space = sp; + } + else if (ch == '\n') { + // Time to break the line - Skip over this character after copying and rendering the line with sp_skip + sp_skip = 1; + break; + } + + uint16_t ch_width = display_get_char_width(ch, &FontSmall); + line_width += ch_width; + if (line_width >= max_width) { + // If we found a space, we can break there, but if we didn't + // then just break before we go over. + if (last_space != 0) { + sp = last_space; + } + break; + } + sp++; + } + + // Copy to prepare for rendering + strncpy(line, text + first_non_space, sp-first_non_space); + line[sp-first_non_space] = 0; + text = text + sp + sp_skip; + + + // Draw the line + display_text(line, center ? CENTER_X : SIDE_MARGIN, curr_y, &FontSmall, false); + + curr_y += FontSmall.leading; + } +} + +#ifndef DEBUG +static bool poll_for_key(uint8_t* p_key, bool* p_is_key_down) { + uint8_t key; + uint8_t count = ring_buffer_dequeue(&key); + + if (count == 0) { + return false; + } + + *p_key = key & 0x7F; + *p_is_key_down = (key & 0x80) ? true : false; + + return true; +} +#endif // DEBUG + +// Show message and then delay or wait for button press +bool ui_show_message(char* title, char* message, char* left_btn, char *right_btn, bool center) { + bool exit = false; + bool result = false; + bool is_left_pressed = false; + bool is_right_pressed = false; + + do { + display_clear(0); + + // Draw the text + ui_draw_wrapped_text(SIDE_MARGIN, HEADER_HEIGHT + TOP_MARGIN, SCREEN_WIDTH - SIDE_MARGIN * 2, message, center); + + // Draw the header + ui_draw_header(title); + + // Draw the footer + ui_draw_footer(left_btn, is_left_pressed, right_btn, is_right_pressed); + display_show(); + +#ifdef DEBUG + delay_ms(5000); + result = true; + } while (exit); +#else + // Only poll if we are not exiting + if (!exit) { + // Poll for key + uint8_t key; + bool is_key_down; + bool key_read; + do { + key_read = poll_for_key(&key, &is_key_down); + } while (!key_read); + + // Handle key + if (key_read) { + if (is_key_down) { + switch (key) { + case 99: // 'y' + is_right_pressed = true; + break; + + case 113: // 'x' + is_left_pressed = true; + break; + } + } else { + switch (key) { + case 99: // 'y' + is_right_pressed = false; + exit = true; + result = true; + continue; + + case 113: // 'x' + is_left_pressed = false; + exit = true; + result = false; + continue; + } + } + } else { + delay_ms(50); + } + } + } while (!exit); +#endif // DEBUG + + return result; +} + +// Show the error message and give user the option to SHUTDOWN, or view +// CONTACT information. Then have option to go BACK to the error. +// NOTE: This function never returns! +void ui_show_fatal_error(char* error) { + bool show_error = true; + + while (true) { + if (show_error) { + // Show the error + if (ui_show_message("Fatal Error", error, "CONTACT", "SHUTDOWN", true)) { + display_clean_shutdown(); + } else { + show_error = false; + } + } else { + // Show Contact Info + if (ui_show_message("Contact", "\nContact us at:\n\nhello@foundationdevices.com", + "BACK", "SHUTDOWN", true)) { + display_clean_shutdown(); + } else { + show_error = true; + } + } + } +} + +void ui_show_hex_buffer(char* title, uint8_t* data, uint32_t length) { + char buf[512]; + bytes_to_hex_str(data, length, buf, 8, '\n'); + ui_show_message(title, buf, "SHUTDOWN", "CONTINUE", true); +} diff --git a/ports/stm32/boards/Passport/bootloader/ui.h b/ports/stm32/boards/Passport/bootloader/ui.h new file mode 100644 index 0000000..7652836 --- /dev/null +++ b/ports/stm32/boards/Passport/bootloader/ui.h @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-License-Identifier: GPL-3.0-or-later +// +// ui.h - Simple UI elements for the bootloader + +#include +#include + +// UI elements +void ui_draw_header(char* title); +void ui_draw_footer(char* left_btn, bool is_left_pressed, char* right_btn, bool is_right_pressed); +void ui_draw_button(uint16_t x, uint16_t y, uint16_t w, uint16_t h, char* label, bool is_pressed); +void ui_draw_wrapped_text(uint16_t x, uint16_t y, uint16_t max_width, char* text, bool center); +bool ui_show_message(char* title, char* message, char* left_btn, char*right_btn, bool center); +void ui_show_fatal_error(char* message); +void ui_show_hex_buffer(char* title, uint8_t* buf, uint32_t length); diff --git a/ports/stm32/boards/Passport/bootloader/update.c b/ports/stm32/boards/Passport/bootloader/update.c index 3984034..f219f0f 100644 --- a/ports/stm32/boards/Passport/bootloader/update.c +++ b/ports/stm32/boards/Passport/bootloader/update.c @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2018 Coinkite, Inc. +// SPDX-FileCopyrightText: 2018 Coinkite, Inc. // SPDX-License-Identifier: GPL-3.0-only // /* @@ -9,15 +9,35 @@ * */ #include +#include +#include "display.h" #include "fwheader.h" +#include "hash.h" +#include "se-config.h" #include "sha256.h" #include "spiflash.h" +#include "splash.h" #include "utils.h" +#include "se-atecc608a.h" #include "verify.h" #include "flash.h" #include "update.h" +#include "ui.h" +#include "gpio.h" +#include "firmware-keys.h" + +// Global so we can compare with it later in do_update() +static uint8_t spi_hdr_hash[HASH_LEN] = {0}; + +static void clear_update_from_spi_flash() +{ + uint8_t zeros[FW_HEADER_SIZE] = {0}; + + spi_write(0, 256, zeros); + spi_write(256, sizeof(zeros), zeros); +} static void calculate_spi_hash( passport_firmware_header_t *hdr, @@ -26,12 +46,12 @@ static void calculate_spi_hash( ) { SHA256_CTX ctx; - uint32_t pos = FW_HEADER_SIZE; + uint32_t pos = FW_HEADER_SIZE + 256; // Skip over the update hash page uint32_t remaining = hdr->info.fwlength; uint8_t *buf = (uint8_t *)D1_AXISRAM_BASE; /* Working memory */ sha256_init(&ctx); - + sha256_update(&ctx, (uint8_t *)&hdr->info, sizeof(fw_info_t)); while (remaining > 0) @@ -62,6 +82,44 @@ out: return; } +static void calculate_spi_hdr_hash( + passport_firmware_header_t *hdr, + uint8_t *hash, + uint8_t hashlen +) +{ + SHA256_CTX ctx; + + sha256_init(&ctx); + sha256_update(&ctx, (uint8_t *)hdr, sizeof(passport_firmware_header_t)); + sha256_final(&ctx, hash); + + /* double SHA256 */ + sha256_init(&ctx); + sha256_update(&ctx, hash, hashlen); + sha256_final(&ctx, hash); +} + +// Hash the spi hash with the device hash value -- used to prevent external attacker from beign able to insert a firmware +// update directly in external SPI flash. They won't be able to replicate this hash. +static void calculate_update_hash( + uint8_t *spi_hash, + uint8_t spi_hashlen, + uint8_t *update_hash, + uint8_t update_hashlen +) +{ + SHA256_CTX ctx; + + uint8_t device_hash[HASH_LEN]; + get_device_hash(device_hash); + + sha256_init(&ctx); + sha256_update(&ctx, (uint8_t *)spi_hash, spi_hashlen); + sha256_update(&ctx, device_hash, sizeof(device_hash)); + sha256_final(&ctx, update_hash); +} + static int do_update(uint32_t size) { int rc; @@ -69,17 +127,62 @@ static int do_update(uint32_t size) uint32_t pos; uint32_t addr; uint32_t data[FLASH_NB_32BITWORD_IN_FLASHWORD] __attribute__((aligned(8))); + uint32_t total = FW_END - FW_START; + uint8_t percent_done = 0; + uint8_t last_percent_done = 255; + uint8_t curr_spi_hdr_hash[HASH_LEN] = {0}; + uint32_t remaining_bytes_to_hash = sizeof(passport_firmware_header_t); + secresult not_checked = SEC_TRUE; + SHA256_CTX ctx; + + sha256_init(&ctx); flash_unlock(); + // Make sure header still fits in one page or this check will be more complex. + if (sizeof(passport_firmware_header_t) > 256) { + clear_update_from_spi_flash(); + ui_show_fatal_error("sizeof(passport_firmware_header_t) > 256"); + } + for (pos = 0, addr = FW_START; pos < size; pos += flash_word_len, addr += flash_word_len) { - if (spi_read(pos, sizeof(data), (uint8_t *)data) != HAL_OK) + // We read starting 256 bytes in as the first page holds the update request hash + if (spi_read(pos + 256, sizeof(data), (uint8_t *)data) != HAL_OK) { rc = -1; break; } + // TOCTOU check by hashing the header again and comparing to the hash we took earlier when we verified it. + if (remaining_bytes_to_hash > 0) { + // Calculate the running hash 32 bytes at a time until we reach sizeof(passport_firmware_header_t) + size_t hash_size = MIN(remaining_bytes_to_hash, flash_word_len); + sha256_update(&ctx, (uint8_t *)data, hash_size); + remaining_bytes_to_hash -= hash_size; + } + + if (not_checked == SEC_TRUE && remaining_bytes_to_hash == 0) { + // Finalize the hash and check it + sha256_final(&ctx, curr_spi_hdr_hash); + + /* double SHA256 */ + sha256_init(&ctx); + sha256_update(&ctx, curr_spi_hdr_hash, HASH_LEN); + sha256_final(&ctx, curr_spi_hdr_hash); + + // ui_show_hex_buffer("Prev Hash", spi_hdr_hash, HASH_LEN); + // ui_show_hex_buffer("TOCTOU Hash", curr_spi_hdr_hash, HASH_LEN); + + // Compare the hashes + if (memcmp(curr_spi_hdr_hash, spi_hdr_hash, HASH_LEN) != 0) { + // Someone may be hacking on the SPI flash! + clear_update_from_spi_flash(); + ui_show_fatal_error("\nSPI flash appears to have been actively modified during firmware update."); + } + not_checked = SEC_FALSE; + } + if (addr % FLASH_SECTOR_SIZE == 0) { rc = flash_sector_erase(addr); @@ -90,26 +193,69 @@ static int do_update(uint32_t size) rc = flash_burn(addr, (uint32_t)data); if (rc < 0) break; + + /* Update the progress bar only if the percentage changed */ + percent_done = (uint8_t)((float)pos/(float)total * 100.0f); + + if (percent_done != last_percent_done) + { + display_progress_bar(PROGRESS_BAR_MARGIN, PROGRESS_BAR_Y, SCREEN_WIDTH - (PROGRESS_BAR_MARGIN * 2), PROGRESS_BAR_HEIGHT, percent_done); + /* Showing just the lines that changed is much faster and avoids full-screen flicker */ + display_show_lines(PROGRESS_BAR_Y, PROGRESS_BAR_Y + PROGRESS_BAR_HEIGHT); + last_percent_done = percent_done; + } } + /* Clear the remainder of flash */ + memset(data, 0, sizeof(data)); + for (; addr < FW_END; pos += flash_word_len, addr += flash_word_len) + { + if (addr % FLASH_SECTOR_SIZE == 0) + { + rc = flash_sector_erase(addr); + if (rc < 0) + break; + } + + rc = flash_burn(addr, (uint32_t)data); + if (rc < 0) + break; + + /* Update the progress bar only if the percentage changed */ + percent_done = (uint8_t)((float)pos/(float)total * 100.0f); + + if (percent_done != last_percent_done) + { + display_progress_bar(PROGRESS_BAR_MARGIN, PROGRESS_BAR_Y, SCREEN_WIDTH - (PROGRESS_BAR_MARGIN * 2), PROGRESS_BAR_HEIGHT, percent_done); + /* Showing just the lines that changed is much faster and avoids full-screen flicker */ + display_show_lines(PROGRESS_BAR_Y, PROGRESS_BAR_Y + PROGRESS_BAR_HEIGHT); + last_percent_done = percent_done; + } + } + + /* Make sure the progress bar goes to 100 */ + display_progress_bar(PROGRESS_BAR_MARGIN, PROGRESS_BAR_Y, SCREEN_WIDTH - (PROGRESS_BAR_MARGIN * 2), PROGRESS_BAR_HEIGHT, 100); + display_show_lines(PROGRESS_BAR_Y, PROGRESS_BAR_Y + PROGRESS_BAR_HEIGHT); + flash_lock(); return rc; } -bool is_firmware_update_present(void) +secresult is_firmware_update_present(void) { passport_firmware_header_t hdr = {}; if (spi_setup() != HAL_OK) - return false; + return SEC_FALSE; - if (spi_read(0, sizeof(hdr), (void *)&hdr) != HAL_OK) - return false; + // Skip first page of flash + if (spi_read(256, sizeof(hdr), (void *)&hdr) != HAL_OK) + return SEC_FALSE; if (!verify_header(&hdr)) - return false; + return SEC_FALSE; - return true; + return SEC_TRUE; } void update_firmware(void) @@ -117,32 +263,137 @@ void update_firmware(void) int rc; passport_firmware_header_t *internalhdr = FW_HDR; passport_firmware_header_t spihdr = {0}; - uint8_t fw_hash[HASH_LEN] = {0}; - uint8_t zeros[FW_HEADER_SIZE] = {0}; - + uint8_t internal_fw_hash[HASH_LEN] = {0}; + uint8_t spi_fw_hash[HASH_LEN] = {0}; + uint8_t current_board_hash[HASH_LEN] = {0}; + uint8_t new_board_hash[HASH_LEN] = {0}; + uint8_t actual_update_hash[HASH_LEN] = {0}; + uint8_t expected_update_hash[HASH_LEN] = {0}; + + /* + * If we fail to either setup the SPI bus or read the SPI flash + * then just return...something is wrong in hardware but maybe it's + * temporary. + */ if (spi_setup() != HAL_OK) return; - if (spi_read(0, sizeof(spihdr), (void *)&spihdr) != HAL_OK) + // If the update was requested by the user, then there will be a hash in the first 32 bytes that combines + // the firmware hash with the device hash. + if (spi_read(0, HASH_LEN, (void *)&actual_update_hash) != HAL_OK) return; + // Start reading one page in as there is a 32-byte hash in the first page + if (spi_read(256, sizeof(spihdr), (void *)&spihdr) != HAL_OK) + return; + // ui_show_hex_buffer("SPI Hdr 1", (uint8_t*)&spihdr, 170); - if (!verify_header(&spihdr)) - goto out; + calculate_spi_hdr_hash(&spihdr, spi_hdr_hash, HASH_LEN); - /* Don't allow downgrades */ - if (spihdr.info.timestamp <= internalhdr->info.timestamp) - goto out; - - calculate_spi_hash(&spihdr, fw_hash, sizeof(fw_hash)); + // ui_show_hex_buffer("SPI Hdr Hash", spi_hdr_hash, HASH_LEN); + + calculate_update_hash(spi_hdr_hash, sizeof(spi_hdr_hash), expected_update_hash, sizeof(expected_update_hash)); - if (!verify_signature(&spihdr, fw_hash, sizeof(fw_hash))) + // Ensure that the hashes match! + if (memcmp(expected_update_hash, actual_update_hash, sizeof(expected_update_hash)) != 0) { + // This looks like an unrequested update (i.e., a possible attack) goto out; + } + + /* Verify firmware header in SPI flash and bail if it fails */ + if (!verify_header(&spihdr)) + { + if (ui_show_message("Update Error", "The firmware update you chose has an invalid header and will not be installed.", "SHUTDOWN", "OK", true)){ + goto out; + } else { + display_clean_shutdown(); + } + } + + /* + * If the current firmeware verification passes then compare + * timestamps and don't allow an earlier version. However, if the + * internal firmware header verification fails then proceed with the + * update...maybe the previous update attempt failed because we lost + * power. + * + * We also allow going back and forth between user-signed firmware and Foundation-signed firmware. + */ + if (verify_current_firmware(true) == SEC_TRUE) + { + if ((spihdr.signature.pubkey1 != FW_USER_KEY && internalhdr->signature.pubkey1 != FW_USER_KEY) && + (spihdr.info.timestamp < internalhdr->info.timestamp)) + { + if (ui_show_message("Update Error", "This firmware update is older than the current firmware and will not be installed.", "SHUTDOWN", "OK", true)) + goto out; + else + display_clean_shutdown(); + } + + // Handle the firmware hash update + uint8_t *fwptr = (uint8_t *)internalhdr + FW_HEADER_SIZE; + hash_fw(&internalhdr->info, fwptr, internalhdr->info.fwlength, internal_fw_hash, sizeof(internal_fw_hash)); + hash_board(internal_fw_hash, sizeof(internal_fw_hash), current_board_hash, sizeof(current_board_hash)); + + calculate_spi_hash(&spihdr, spi_fw_hash, sizeof(spi_fw_hash)); + + /* Verify the signature and bail if it fails */ + if (verify_signature(&spihdr, spi_fw_hash, sizeof(spi_fw_hash)) == SEC_FALSE) + { + if (ui_show_message("Update Error", "The firmware update does not appear to be properly signed and will not be installed.\n\nThis can also occur if you lost power during a firmware update.", "SHUTDOWN", "OK", true)) + goto out; + else + display_clean_shutdown(); + } + + /* + * Calculate a new board hash based on the SPI firmware and then + * reprogram the board hash in the SE. If the update fails it + * will be retried until it succeeds or the board is declared dead. + */ + hash_board(spi_fw_hash, sizeof(spi_fw_hash), new_board_hash, sizeof(new_board_hash)); + + #ifdef CONVERSION_BUILD + /* + * Conversion build is temporary and used to get current demo + * boards which have 0's programmed for the board hash to be + * properly programmed with a real board hash. Thereafter they + * will only be able to update via SD card. + * Delete this code once this has been done. + */ + memset(current_board_hash, 0, sizeof(current_board_hash)); + #endif /* CONVERSION_BUILD */ + + rc = se_program_board_hash(current_board_hash, new_board_hash, sizeof(new_board_hash)); + if (rc < 0) { + if (ui_show_message("Update Error", "Unable to update the firmware hash in the Secure Element. Update will continue, but may not be successful.", "SHUTDOWN", "OK", true)){ + // Nothing to do + } else { + display_clean_shutdown(); + + } + } + } + + // Draw the logo and message - progress bar gets drawn and updated periodically in do_update() + show_splash("Updating Firmware..."); rc = do_update(FW_HEADER_SIZE + spihdr.info.fwlength); if (rc < 0) - return; /* Don't erase SPI...maybe it will work next time */ + { + if (ui_show_message("Update Error", "Failed to install the firmware update.", "SHUTDOWN", "RESTART", true)) + passport_reset(); + else + // TODO: Should we have an option here to clear the SPI flash and restart (we could run a verify_current_firmware() first to make sure it's safe to boot there + display_clean_shutdown(); + } out: - spi_write(0, sizeof(zeros), zeros); + clear_update_from_spi_flash(); +} + +secresult is_user_signed_firmware_installed(void) +{ + passport_firmware_header_t *hdr = FW_HDR; + return (hdr->signature.pubkey1 == FW_USER_KEY && hdr->signature.pubkey2 == 0) ? SEC_TRUE : SEC_FALSE; } diff --git a/ports/stm32/boards/Passport/bootloader/update.h b/ports/stm32/boards/Passport/bootloader/update.h index b997c1e..4a11978 100644 --- a/ports/stm32/boards/Passport/bootloader/update.h +++ b/ports/stm32/boards/Passport/bootloader/update.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2018 Coinkite, Inc. +// SPDX-FileCopyrightText: 2018 Coinkite, Inc. // SPDX-License-Identifier: GPL-3.0-only // #pragma once @@ -6,5 +6,5 @@ #include extern void update_firmware(void); -extern bool is_firmware_update_present(void); - +extern secresult is_firmware_update_present(void); +extern secresult is_user_signed_firmware_installed(void); diff --git a/ports/stm32/boards/Passport/bootloader/verify.c b/ports/stm32/boards/Passport/bootloader/verify.c index 19c9e3c..eb0817d 100644 --- a/ports/stm32/boards/Passport/bootloader/verify.c +++ b/ports/stm32/boards/Passport/bootloader/verify.c @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2018 Coinkite, Inc. +// SPDX-FileCopyrightText: 2018 Coinkite, Inc. // SPDX-License-Identifier: GPL-3.0-only // /* @@ -9,40 +9,45 @@ * */ #include +#include #include -#include "utils.h" -#include "sha256.h" +#include "delay.h" #include "firmware-keys.h" #include "hash.h" -#include "delay.h" -#include "uECC.h" #include "se.h" -#include "se-atecc608a.h" +#include "se-config.h" +#include "sha256.h" +#include "uECC.h" +#include "utils.h" -#include "update.h" +#include "se-atecc608a.h" #include "verify.h" -bool verify_header(passport_firmware_header_t *hdr) +secresult verify_header(passport_firmware_header_t *hdr) { if (hdr->info.magic != FW_HEADER_MAGIC) goto fail; if (hdr->info.timestamp == 0) goto fail; if (hdr->info.fwversion[0] == 0x0) goto fail; - if (hdr->info.fwlength == 0x0) goto fail; + if (hdr->info.fwlength < FW_HEADER_SIZE) goto fail; + #ifdef USE_CRYPTO - if (hdr->signature.pubkey1 == 0) goto fail; - if (hdr->signature.pubkey1 > FW_MAX_PUB_KEYS) goto fail; - if (hdr->signature.pubkey2 == 0) goto fail; - if (hdr->signature.pubkey2 > FW_MAX_PUB_KEYS) goto fail; + // if (hdr->signature.pubkey1 == 0) goto fail; + if ((hdr->signature.pubkey1 != FW_USER_KEY) && (hdr->signature.pubkey1 > FW_MAX_PUB_KEYS)) goto fail; + if (hdr->signature.pubkey1 != FW_USER_KEY) + { + // if (hdr->signature.pubkey2 == 0) goto fail; + if (hdr->signature.pubkey2 > FW_MAX_PUB_KEYS) goto fail; + } #endif /* USE_CRYPTO */ - return true; + return SEC_TRUE; fail: - return false; + return SEC_FALSE; } -bool verify_signature( +secresult verify_signature( passport_firmware_header_t *hdr, uint8_t *fw_hash, uint32_t hashlen @@ -51,53 +56,77 @@ bool verify_signature( #ifdef USE_CRYPTO int rc; - rc = uECC_verify(approved_pubkeys[hdr->signature.pubkey1], - fw_hash, hashlen, - hdr->signature.signature1, uECC_secp256k1()); - if (rc == 0) - return false; - - rc = uECC_verify(approved_pubkeys[hdr->signature.pubkey2], - fw_hash, hashlen, - hdr->signature.signature2, uECC_secp256k1()); - if (rc == 0) - return false; - - return true; + if (hdr->signature.pubkey1 == FW_USER_KEY) + { + uint8_t user_public_key[72] = {0}; + + /* + * It looks like the user signed this firmware so, in order to + * validate, we need to get the public key from the SE. + */ + se_pair_unlock(); + rc = se_read_data_slot(KEYNUM_user_fw_pubkey, user_public_key, sizeof(user_public_key)); + if (rc < 0) + return SEC_FALSE; + + rc = uECC_verify(user_public_key, + fw_hash, hashlen, + hdr->signature.signature1, uECC_secp256k1()); + if (rc == 0) + return SEC_FALSE; + } + else + { + rc = uECC_verify(approved_pubkeys[hdr->signature.pubkey1], + fw_hash, hashlen, + hdr->signature.signature1, uECC_secp256k1()); + if (rc == 0) + return SEC_FALSE; + + rc = uECC_verify(approved_pubkeys[hdr->signature.pubkey2], + fw_hash, hashlen, + hdr->signature.signature2, uECC_secp256k1()); + if (rc == 0) + return SEC_FALSE; + } + + return SEC_TRUE; #else - return true; + return SEC_TRUE; #endif /* USE_CRYPTO */ } -bool verify_current_firmware(void) +secresult verify_current_firmware( + bool process_led +) { - int rc; uint8_t fw_hash[HASH_LEN]; - uint8_t board_hash[HASH_LEN]; passport_firmware_header_t *fwhdr = (passport_firmware_header_t *)FW_HDR; uint8_t *fwptr = (uint8_t *)fwhdr + FW_HEADER_SIZE; if (!verify_header(fwhdr)) - goto fail; + return ERR_INVALID_FIRMWARE_HEADER; hash_fw(&fwhdr->info, fwptr, fwhdr->info.fwlength, fw_hash, sizeof(fw_hash)); - if (!verify_signature(fwhdr, fw_hash, sizeof(fw_hash))) - goto fail; + if (verify_signature(fwhdr, fw_hash, sizeof(fw_hash)) == SEC_FALSE) + return ERR_INVALID_FIRMWARE_SIGNATURE; -#ifdef DEMO - memset(board_hash, 0, sizeof(board_hash)); -#else - hash_board(fw_hash, sizeof(fw_hash), board_hash, sizeof(board_hash)); -#endif /* DEMO */ - rc = se_set_gpio_secure(board_hash); - if (rc < 0) - goto fail; +#ifdef PRODUCTION_BUILD + if (process_led) + { + int rc; + uint8_t board_hash[HASH_LEN]; - return true; + hash_board(fw_hash, sizeof(fw_hash), board_hash, sizeof(board_hash)); -fail: - return false; + rc = se_set_gpio_secure(board_hash); + if (rc < 0) + return ERR_UNABLE_TO_UPDATE_FIRMWARE_HASH_IN_SE; + } +#endif /* PRODUCTION_BUILD */ + + return SEC_TRUE; } // EOF diff --git a/ports/stm32/boards/Passport/bootloader/verify.h b/ports/stm32/boards/Passport/bootloader/verify.h index df5bb8f..9dd5edd 100644 --- a/ports/stm32/boards/Passport/bootloader/verify.h +++ b/ports/stm32/boards/Passport/bootloader/verify.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2018 Coinkite, Inc. +// SPDX-FileCopyrightText: 2018 Coinkite, Inc. // SPDX-License-Identifier: GPL-3.0-only // /* @@ -10,9 +10,8 @@ #include #include "fwheader.h" +#include "secresult.h" -extern bool verify_current_firmware(void); -extern bool verify_header(passport_firmware_header_t *hdr); -extern bool verify_signature(passport_firmware_header_t *hdr, uint8_t *fw_hash, uint32_t hashlen); -extern void verify_min_version(uint8_t *min_version); - +extern secresult verify_header(passport_firmware_header_t *hdr); +extern secresult verify_current_firmware(bool process_led); +extern secresult verify_signature(passport_firmware_header_t *hdr, uint8_t *fw_hash, uint32_t hashlen); diff --git a/ports/stm32/boards/Passport/bootloader/version_info.h b/ports/stm32/boards/Passport/bootloader/version_info.h new file mode 100644 index 0000000..d2f4cc8 --- /dev/null +++ b/ports/stm32/boards/Passport/bootloader/version_info.h @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: 2021 Foundation Devices, Inc. +// SPDX-License-Identifier: GPL-3.0-or-later +// + +extern char *build_date; +extern char *build_version; diff --git a/ports/stm32/boards/Passport/busy_bar.c b/ports/stm32/boards/Passport/busy_bar.c new file mode 100644 index 0000000..8b3e585 --- /dev/null +++ b/ports/stm32/boards/Passport/busy_bar.c @@ -0,0 +1,196 @@ +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-License-Identifier: GPL-3.0-or-later +// +// busy_bar.c - Timer and rendering code for busy bar +#include +#include +#include "display.h" +#include "firmware_graphics.h" + +#define BUSY_BAR_HEIGHT 34 + +static TIM_HandleTypeDef htim7; + +#ifdef SINE_WAVE_BUSY_BAR +#define NUM_BUSY_BAR_FRAMES 24 +#define NUM_BUSY_BAR_FRAMES_TO_RENDER 20 +static int busy_bar_frames[NUM_BUSY_BAR_FRAMES] = {6,5,4,3,2,1,0,1,2,3,4,5,6,5,4,3,2,1,0,1,2,3,4,5}; + +#define NUM_BUSY_BAR_IMAGES 7 +static Image* busy_bar_images[NUM_BUSY_BAR_IMAGES] = { + &busybar1_img, + &busybar2_img, + &busybar3_img, + &busybar4_img, + &busybar5_img, + &busybar6_img, + &busybar7_img, +}; + +#define X_OFFSET_PER_IMAGE 20 +#define DIRECTION_RIGHT_TO_LEFT 1 +#define DIRECTION_LEFT_TO_RIGHT 2 + +static float sin_offset = 0.0; +static void busy_bar(void) +{ + int16_t start_y = SCREEN_HEIGHT - BUSY_BAR_HEIGHT; + uint16_t direction = DIRECTION_LEFT_TO_RIGHT; + + // Draw white area for the background since we only draw black pixels below + display_fill_rect(0, start_y, SCREEN_WIDTH, BUSY_BAR_HEIGHT, 0); + + for (int16_t i=0; iwidth, busy_bar_images[image_index]->height, busy_bar_images[image_index]->data, DRAW_MODE_WHITE_ONLY); + } + + display_show_lines(start_y, start_y + SCREEN_HEIGHT - 1); + sin_offset += 1.0; + + // Rotate frame indexes + if (direction == DIRECTION_RIGHT_TO_LEFT) { + int first = busy_bar_frames[0]; + for (int16_t i=0; i0; i--) { + busy_bar_frames[i] = busy_bar_frames[i-1]; + } + busy_bar_frames[0] = last; + } +} +#endif + +#define KNIGHT_RIDER_BUSY_BAR +#ifdef KNIGHT_RIDER_BUSY_BAR + +#define NUM_BUSY_BAR_IMAGES 6 +#define X_OFFSET_PER_IMAGE 23 + +typedef struct _bal_info_t { + Image* image; + int16_t x_pos; + int8_t direction; +} ball_info_t; + +ball_info_t ball_info[NUM_BUSY_BAR_IMAGES] = { +// {&busybar7_img, 0, 1}, + {&busybar6_img, 0, 1}, + {&busybar5_img, 0, 1}, + {&busybar4_img, 0, 1}, + {&busybar3_img, 0, 1}, + {&busybar2_img, 0, 1}, + {&busybar1_img, 0, 1}, +}; + +static bool first_activation = true; + +static void busy_bar_reset_animation(void) { + for (int16_t i=0; iwidth) / 2; + + // Don't draw this the first time we show it on the splash screen -- looks better + if (!first_activation) { + // Draw a black separator line (should be exactly where the footer line is) + display_fill_rect(0, start_y, SCREEN_WIDTH, 1, 1); + } + + // Draw white area for the background since we only draw black pixels below + display_fill_rect(0, start_y + 1, SCREEN_WIDTH, BUSY_BAR_HEIGHT - 1, 0); + + // Vertical offset to center busy bar + int16_t voffset = (BUSY_BAR_HEIGHT / 2) - (ball_info[0].image->height/2); + + for (int16_t i=0; iwidth, ball_info[i].image->height, ball_info[i].image->data, DRAW_MODE_WHITE_ONLY); + + // Move this ball for next time + ball_info[i].x_pos += X_OFFSET_PER_IMAGE * ball_info[i].direction; + if ((ball_info[i].x_pos < 0 && ball_info[i].direction == -1) || + (ball_info[i].x_pos > SCREEN_WIDTH && ball_info[i].direction == 1)) { + ball_info[i].direction = -ball_info[i].direction; + } + } + + int16_t end_y = start_y + BUSY_BAR_HEIGHT - 1; + display_show_lines(start_y, end_y); +} +#endif + +void TIM7_IRQHandler(void) +{ + if (__HAL_TIM_GET_FLAG(&htim7, TIM_FLAG_UPDATE) != RESET) + { + if (__HAL_TIM_GET_ITSTATUS(&htim7, TIM_IT_UPDATE) != RESET) + { + __HAL_TIM_CLEAR_FLAG(&htim7, TIM_FLAG_UPDATE); + busy_bar(); + } + } + return; +} + +void busy_bar_start(void) +{ + busy_bar_reset_animation(); + HAL_NVIC_EnableIRQ(TIM7_IRQn); + HAL_TIM_Base_Start_IT(&htim7); +} + +void busy_bar_stop(void) +{ + HAL_TIM_Base_Stop_IT(&htim7); + HAL_NVIC_DisableIRQ(TIM7_IRQn); + first_activation = false; +} + +void busy_bar_init(void) +{ + TIM_ClockConfigTypeDef sClockSourceConfig = {0}; + TIM_MasterConfigTypeDef sMasterConfig = {0}; + uint16_t prescaler; + uint32_t period; + + __TIM7_CLK_ENABLE(); + + /* Fixed interrupt frequency of 1 Hz */ + prescaler = 24000 - 1; + period = 1000 - 1; + + htim7.Instance = TIM7; + htim7.Init.Prescaler = prescaler; + htim7.Init.CounterMode = TIM_COUNTERMODE_UP; + htim7.Init.Period = period; + htim7.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; + htim7.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE; + HAL_TIM_Base_Init(&htim7); + + sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL; + HAL_TIM_ConfigClockSource(&htim7, &sClockSourceConfig); + + sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET; + sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE; + HAL_TIMEx_MasterConfigSynchronization(&htim7, &sMasterConfig); + + __HAL_TIM_CLEAR_FLAG(&htim7, TIM_SR_UIF); + + HAL_NVIC_SetPriority(TIM7_IRQn, 10, 0); +} diff --git a/ports/stm32/boards/Passport/busy_bar.h b/ports/stm32/boards/Passport/busy_bar.h new file mode 100644 index 0000000..a0745e1 --- /dev/null +++ b/ports/stm32/boards/Passport/busy_bar.h @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-License-Identifier: GPL-3.0-or-later +// +// busy_bar.h - Timer and rendering code for busy bar +#pragma once + +extern void busy_bar_init(void); +extern void busy_bar_start(void); +extern void busy_bar_stop(void); diff --git a/ports/stm32/boards/Passport/bytewords_word_info.c b/ports/stm32/boards/Passport/bytewords_word_info.c new file mode 100644 index 0000000..abe17d1 --- /dev/null +++ b/ports/stm32/boards/Passport/bytewords_word_info.c @@ -0,0 +1,269 @@ +// SPDX-FileCopyrightText: 2021 Foundation Devices, Inc. +// SPDX-License-Identifier: GPL-3.0-or-later +// + +#include + +typedef struct { + uint32_t keypad_digits; + uint16_t offsets; +} word_info_t; + +word_info_t bytewords_word_info[] = { + {2225, 0x4900}, // back + {2243, 0x2800}, // acid + {2253, 0x1900}, // able + {2253, 0x4800}, // bald + {2256, 0x8800}, // calm + {2274, 0x8d00}, // cash + {2276, 0x4900}, // barn + {2287, 0x8300}, // cats + {2358, 0x5800}, // belt + {2382, 0x5000}, // beta + {2427, 0x6300}, // bias + {2433, 0x9600}, // chef + {2489, 0xa200}, // city + {2529, 0xa000}, // claw + {2576, 0x2e00}, // also + {2583, 0x6500}, // blue + {2633, 0xa100}, // code + {2639, 0x6200}, // body + {2652, 0xa800}, // cola + {2665, 0xa900}, // cook + {2678, 0xac00}, // cost + {2724, 0x2900}, // arch + {2724, 0x6000}, // brag + {2739, 0x0500}, // apex + {2739, 0x6400}, // brew + {2782, 0x1400}, // aqua + {2789, 0xa500}, // crux + {2852, 0x5900}, // bulb + {2866, 0x0800}, // atom + {2868, 0x1400}, // aunt + {2875, 0x9a00}, // curl + {2877, 0x9c00}, // cusp + {2899, 0x5f00}, // buzz + {2926, 0xa100}, // cyan + {2929, 0x0200}, // away + {2947, 0x1b00}, // axis + {3224, 0x4900}, // each + {3228, 0x8800}, // fact + {3246, 0x6600}, // echo + {3247, 0x8a00}, // fair + {3275, 0x0900}, // dark + {3279, 0x4e00}, // easy + {3282, 0x0000}, // data + {3297, 0x0b00}, // days + {3343, 0x4100}, // edge + {3354, 0x1a00}, // deli + {3376, 0x9900}, // fern + {3423, 0x2900}, // dice + {3438, 0x2400}, // diet + {3447, 0xa300}, // figs + {3456, 0xa800}, // film + {3474, 0xad00}, // fish + {3499, 0xaf00}, // fizz + {3527, 0xa000}, // flap + {3539, 0xa400}, // flew + {3589, 0xa500}, // flux + {3667, 0x2a00}, // door + {3696, 0x2100}, // down + {3699, 0xa600}, // foxy + {3729, 0x2000}, // draw + {3733, 0xa500}, // free + {3742, 0x4a00}, // epic + {3764, 0xa800}, // frog + {3767, 0x2800}, // drop + {3786, 0x2400}, // drum + {3835, 0x9600}, // fuel + {3836, 0x6500}, // even + {3855, 0x1a00}, // dull + {3863, 0x9400}, // fund + {3889, 0x1200}, // duty + {3926, 0x5000}, // exam + {3937, 0x6700}, // eyes + {3948, 0x5800}, // exit + {4233, 0xa400}, // iced + {4252, 0x0800}, // gala + {4253, 0x4a00}, // half + {4263, 0x0100}, // game + {4264, 0x4400}, // hang + {4273, 0x4800}, // hard + {4295, 0x4100}, // hawk + {4327, 0x1200}, // gear + {4328, 0x5000}, // heat + {4332, 0x8400}, // idea + {4353, 0x8900}, // idle + {4357, 0x5800}, // help + {4367, 0x1300}, // gems + {4438, 0x2800}, // gift + {4444, 0x6100}, // high + {4455, 0x6a00}, // hill + {4475, 0x2a00}, // girl + {4569, 0x2800}, // glow + {4624, 0x9900}, // inch + {4659, 0x6a00}, // holy + {4659, 0x9600}, // inky + {4663, 0x2800}, // good + {4673, 0x6100}, // hope + {4676, 0x6900}, // horn + {4686, 0x9200}, // into + {4729, 0x2200}, // gray + {4746, 0x2800}, // grim + {4747, 0xab00}, // iris + {4766, 0xa900}, // iron + {4836, 0x8400}, // item + {4874, 0x1d00}, // gush + {4878, 0x1900}, // guru + {4887, 0x5300}, // huts + {4976, 0x2a00}, // gyro + {5233, 0x0100}, // jade + {5262, 0x8100}, // lamb + {5282, 0x8800}, // lava + {5299, 0x0f00}, // jazz + {5299, 0x8e00}, // lazy + {5323, 0x9200}, // leaf + {5337, 0x5400}, // keep + {5347, 0x9300}, // legs + {5366, 0x5600}, // keno + {5378, 0x5000}, // kept + {5397, 0x5b00}, // keys + {5425, 0x6900}, // kick + {5427, 0xa200}, // liar + {5456, 0x6900}, // kiln + {5464, 0x6400}, // king + {5466, 0xa900}, // lion + {5467, 0xa000}, // limp + {5478, 0xac00}, // list + {5483, 0x6100}, // kite + {5494, 0x6200}, // kiwi + {5646, 0x2900}, // join + {5646, 0xa200}, // logo + {5658, 0x2800}, // jolt + {5662, 0x5900}, // knob + {5683, 0xa400}, // loud + {5683, 0xa900}, // love + {5695, 0x2200}, // jowl + {5825, 0x9900}, // luck + {5828, 0x9100}, // luau + {5836, 0x1200}, // judo + {5847, 0x1300}, // jugs + {5864, 0x9400}, // lung + {5865, 0x1500}, // junk + {5867, 0x1000}, // jump + {5879, 0x1a00}, // jury + {6239, 0x9600}, // obey + {6245, 0x4a00}, // nail + {6246, 0x0900}, // main + {6263, 0x9900}, // oboe + {6269, 0x0600}, // many + {6284, 0x0100}, // math + {6289, 0x4a00}, // navy + {6293, 0x0d00}, // maze + {6333, 0x5400}, // need + {6366, 0x1200}, // memo + {6368, 0x1500}, // menu + {6369, 0x1800}, // meow + {6397, 0x5300}, // news + {6398, 0x5400}, // next + {6453, 0x2800}, // mild + {6468, 0x2400}, // mint + {6477, 0x2f00}, // miss + {6648, 0x8800}, // omit + {6665, 0x2500}, // monk + {6666, 0x6900}, // noon + {6683, 0x6100}, // note + {6699, 0x9900}, // onyx + {6736, 0x8500}, // open + {6825, 0xa200}, // oval + {6862, 0x5100}, // numb + {6957, 0x8b00}, // owls + {7223, 0x8900}, // race + {7227, 0xe200}, // scar + {7233, 0xc900}, // safe + {7242, 0xc000}, // saga + {7243, 0x0800}, // paid + {7267, 0x8000}, // ramp + {7278, 0x0800}, // part + {7325, 0x1900}, // peck + {7325, 0x9200}, // real + {7336, 0x9200}, // redo + {7387, 0xd300}, // sets + {7424, 0xa900}, // rich + {7455, 0xe900}, // silk + {7529, 0x2200}, // play + {7539, 0xd400}, // skew + {7568, 0xe800}, // slot + {7587, 0x2700}, // plus + {7623, 0xa000}, // road + {7625, 0xa900}, // rock + {7627, 0xe000}, // soap + {7636, 0x2400}, // poem + {7656, 0xea00}, // solo + {7663, 0xaa00}, // roof + {7664, 0xe400}, // song + {7665, 0x2a00}, // pool + {7673, 0x2d00}, // pose + {7823, 0x5000}, // quad + {7829, 0x9600}, // ruby + {7833, 0x1a00}, // puff + {7846, 0x9900}, // ruin + {7849, 0x5b00}, // quiz + {7862, 0x1000}, // puma + {7867, 0x9700}, // runs + {7873, 0xda00}, // surf + {7877, 0x1a00}, // purr + {7878, 0x9c00}, // rust + {7882, 0xc500}, // stub + {7926, 0xc100}, // swan + {8226, 0x0a00}, // taco + {8275, 0x0d00}, // task + {8278, 0x8c00}, // vast + {8294, 0x0600}, // taxi + {8368, 0x1400}, // tent + {8379, 0x9a00}, // very + {8386, 0x9200}, // veto + {8423, 0xa500}, // vibe + {8425, 0xa200}, // vial + {8433, 0x2400}, // tied + {8439, 0xa400}, // view + {8459, 0x4a00}, // ugly + {8463, 0x2100}, // time + {8469, 0x2600}, // tiny + {8472, 0xac00}, // visa + {8636, 0x5200}, // undo + {8643, 0xa800}, // void + {8645, 0x2a00}, // toil + {8648, 0x5800}, // unit + {8662, 0x2100}, // tomb + {8697, 0x2b00}, // toys + {8697, 0xa300}, // vows + {8737, 0x7600}, // user + {8743, 0x6100}, // urge + {8747, 0x2800}, // trip + {8862, 0x1400}, // tuna + {8946, 0x0900}, // twin + {9255, 0x0a00}, // wall + {9263, 0x0400}, // wand + {9265, 0x8500}, // yank + {9276, 0x0800}, // warm + {9277, 0x0c00}, // wasp + {9277, 0xc300}, // zaps + {9283, 0x0900}, // wave + {9296, 0x8100}, // yawn + {9299, 0x0600}, // waxy + {9327, 0x1700}, // webs + {9355, 0x9a00}, // yell + {9376, 0xda00}, // zero + {9378, 0xdc00}, // zest + {9428, 0x1000}, // what + {9436, 0x1500}, // when + {9449, 0x1b00}, // whiz + {9462, 0xe600}, // zinc + {9642, 0xa000}, // yoga + {9653, 0x2a00}, // wolf + {9663, 0xe500}, // zone + {9666, 0xe800}, // zoom + {9675, 0x2900}, // work + {9878, 0x9800} // yurt +}; diff --git a/ports/stm32/boards/Passport/camera-ovm7690.c b/ports/stm32/boards/Passport/camera-ovm7690.c index 0985cc2..0808d2f 100644 --- a/ports/stm32/boards/Passport/camera-ovm7690.c +++ b/ports/stm32/boards/Passport/camera-ovm7690.c @@ -171,9 +171,9 @@ static CAMERA_REG Camera_RegInit[] = { {0x14, 0x29}, /* Max AGC 8x */ {0x13, 0xE7}, /* fast AGC/AEC, AEC step unlimited, banding filter, AEC below banding, AGC auto, AWB auto, exp auto */ - {0x11, 0x00}, /* external clock or internal clock prescalar */ + {0x11, 0x40}, /* external clock or internal clock prescalar */ - {0x0E, 0x03}, /* already specified above */ + {0x0E, 0x00}, /* already specified above */ {0xC8, 0x02}, {0xC9, 0x40}, /* Input Horiz 576 */ @@ -252,7 +252,7 @@ camera_on(void) int rc; uint8_t val; - printf("DRIVER: camera_on()\n"); + // printf("DRIVER: camera_on()\n"); rc = camera_read(0x0E, &val); if (rc < 0) { @@ -279,9 +279,7 @@ camera_off(void) uint8_t val; HAL_StatusTypeDef rc; -printf("DRIVER: camera_off() 1\n"); rc = HAL_DCMI_Stop(&hdcmi); -printf("DRIVER: camera_off() 1\n"); if (rc != HAL_OK) { printf("[%s] HAL_DCMI_Stop() failed\n", __func__); @@ -289,7 +287,6 @@ printf("DRIVER: camera_off() 1\n"); } irc = camera_read(0x0E, &val); -printf("DRIVER: camera_off() 2\n"); if (irc < 0) { printf("[%s] camera_read() failed\n", __func__); @@ -299,14 +296,12 @@ printf("DRIVER: camera_off() 2\n"); /* Put camera into sleep mode */ irc = camera_write(0x0E, val | (1 << 3)); -printf("DRIVER: camera_off() 3\n"); if (irc < 0) { printf("[%s] camera_write() failed\n", __func__); rval = -1; } out: -printf("DRIVER: camera_off() - DONE\n"); return rval; } @@ -335,9 +330,6 @@ int camera_snapshot(void) // uint32_t total_start = HAL_GetTick(); // uint32_t total_end = 0; - /* Clear the buffer */ - memset(camera_frame_buffer, 0, (FRAMEBUF_SIZE * 2)); - /* Clear any current interrupts */ hdcmi.Instance->ICR = DCMI_IT_FRAME | DCMI_IT_OVR | DCMI_IT_ERR | DCMI_IT_VSYNC | DCMI_IT_LINE; @@ -366,6 +358,7 @@ int camera_snapshot(void) } // printf("[%s] frame complete in %d milliseconds\n", __func__, count); } + out: // Need to call this after DMA completes camera_stop_dcmi(); @@ -423,7 +416,7 @@ int camera_init(void) FrameBufAddr = (uint32_t)camera_frame_buffer; - printf("****************************************************************************\n"); + // printf("****************************************************************************\n"); /* * Per STM Appnote AN5020 @@ -498,6 +491,17 @@ int camera_init(void) GPIO_InitStruct.Alternate = GPIO_AF4_I2C1; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); + /* Configure the BUF1_OE and BUF2_OE */ + HAL_GPIO_WritePin(GPIOE, GPIO_PIN_9, 0); + HAL_GPIO_WritePin(GPIOE, GPIO_PIN_10, 0); + + GPIO_InitStruct.Pin = GPIO_PIN_9 | GPIO_PIN_10; + GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; + GPIO_InitStruct.Pull = GPIO_NOPULL; + GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; + GPIO_InitStruct.Alternate = 0; + HAL_GPIO_Init(GPIOE, &GPIO_InitStruct); + /* Configure Timer 3 channel 4 */ __TIM3_CLK_ENABLE(); @@ -541,7 +545,8 @@ int camera_init(void) __HAL_RCC_I2C1_CLK_ENABLE(); hi2c1.Instance = I2C1; - hi2c1.Init.Timing = 0x109095DF; + hi2c1.Init.Timing = 0x00B07FFF; /* 0x00100727 - 300 KHz @ 64 MHz */ + /* 0x00B07FFF - 300 KHz @ 480 MHz */ hi2c1.Init.OwnAddress1 = 0; hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE; @@ -586,6 +591,7 @@ int camera_init(void) HAL_GPIO_WritePin(GPIOE, GPIO_PIN_7, GPIO_PIN_SET); HAL_Delay(20); HAL_GPIO_WritePin(GPIOE, GPIO_PIN_7, GPIO_PIN_RESET); + HAL_Delay(20); /* Configure camera size */ camera_setQVGA(); @@ -595,7 +601,7 @@ int camera_init(void) val &= ~(1 << 7); camera_write(0x6F, val); - printf("CAMERA INIT COMPLETE!\n"); + // printf("CAMERA INIT COMPLETE!\n"); return 0; } diff --git a/ports/stm32/boards/Passport/camera-ovm7690.h b/ports/stm32/boards/Passport/camera-ovm7690.h index 88febc1..7a3f714 100644 --- a/ports/stm32/boards/Passport/camera-ovm7690.h +++ b/ports/stm32/boards/Passport/camera-ovm7690.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: BSD-3-Clause // @@ -44,7 +44,7 @@ #define CAMERA_WIDTH 396 #define CAMERA_HEIGHT 330 #define FRAMEBUF_SIZE (CAMERA_WIDTH * CAMERA_HEIGHT) - +#if 0 /* Not used for now */ /* Camera registers */ #define GAIN 0x00 #define BGAIN 0x01 @@ -190,6 +190,7 @@ #define REGDF 0xDF #define REGE0 0xE0 #define REGE1 0xE1 +#endif extern uint16_t *camera_frame_buffer; diff --git a/ports/stm32/boards/Passport/clang-format.txt b/ports/stm32/boards/Passport/clang-format.txt deleted file mode 100644 index fdf6d78..0000000 --- a/ports/stm32/boards/Passport/clang-format.txt +++ /dev/null @@ -1,4 +0,0 @@ ---- -BreakBeforeBraces: Allman - -... diff --git a/ports/stm32/boards/Passport/backlight.c b/ports/stm32/boards/Passport/common/backlight.c similarity index 72% rename from ports/stm32/boards/Passport/backlight.c rename to ports/stm32/boards/Passport/common/backlight.c index 0169e23..0c527be 100644 --- a/ports/stm32/boards/Passport/backlight.c +++ b/ports/stm32/boards/Passport/common/backlight.c @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // // Backlight driver for LED @@ -23,8 +23,9 @@ void backlight_init(void) __TIM4_CLK_ENABLE(); backlight_timer_handle.Instance = TIM4; + /* Prescale = 10 gives about 331 Hz 200 - 400Hz ideal */ - backlight_timer_handle.Init.Prescaler = 10; + backlight_timer_handle.Init.Prescaler = 10; /* Default for 480 MHz */ backlight_timer_handle.Init.CounterMode = TIM_COUNTERMODE_UP; backlight_timer_handle.Init.Period = 65535; backlight_timer_handle.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; @@ -49,6 +50,14 @@ void backlight_init(void) HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); } +// Only initialize what's necessary for the firmware to takeover control of the backlight since the main +// backlight_init() function was already called by the bootloader. If we call the full init again, the backlight +// turns off for a couple of seconds during startup, which is not good UX. +void backlight_minimal_init(void) +{ + backlight_timer_handle.Instance = TIM4; +} + /* * backlight_intensity() * Adjusts intensity of the backlight when passed a value between 0 and 100. @@ -57,7 +66,9 @@ void backlight_init(void) * Returns: Nothing */ -void backlight_intensity(uint16_t intensity) +void backlight_intensity( + uint16_t intensity +) { if (intensity == 0) { /* Turn backlight timer off */ @@ -70,3 +81,16 @@ void backlight_intensity(uint16_t intensity) *BACKLIGHT_PWM_CCR() = intensity * (BACKLIGHT_PWM_TIM_PERIOD - 1) / 100; } } + +void backlight_adjust( + bool turbo +) +{ + HAL_TIM_PWM_Stop(&backlight_timer_handle, TIM_CHANNEL_3); + if (turbo) + backlight_timer_handle.Init.Prescaler = 10; + else + backlight_timer_handle.Init.Prescaler = 1; + TIM_Base_SetConfig(backlight_timer_handle.Instance, &backlight_timer_handle.Init); + HAL_TIM_PWM_Start(&backlight_timer_handle, TIM_CHANNEL_3); +} diff --git a/ports/stm32/boards/Passport/common/delay.c b/ports/stm32/boards/Passport/common/delay.c index 87fdf18..64490e2 100644 --- a/ports/stm32/boards/Passport/common/delay.c +++ b/ports/stm32/boards/Passport/common/delay.c @@ -1,7 +1,7 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // -// SPDX-FileCopyrightText: 2018 Coinkite, Inc. +// SPDX-FileCopyrightText: 2018 Coinkite, Inc. // SPDX-License-Identifier: GPL-3.0-only // /* diff --git a/ports/stm32/boards/Passport/common/display.c b/ports/stm32/boards/Passport/common/display.c new file mode 100644 index 0000000..868cddf --- /dev/null +++ b/ports/stm32/boards/Passport/common/display.c @@ -0,0 +1,187 @@ +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-License-Identifier: GPL-3.0-or-later +// +// display.c - Display rendering functions for the Passport bootloader +#include + +#include "display.h" +#include "keypad-adp-5587.h" +#include "gpio.h" + +static uint8_t disp_buf[SCREEN_BYTES_PER_LINE * SCREEN_HEIGHT]; + +static uint8_t get_image_pixel(int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t* image, uint8_t default_color) +{ + if (x < 0 || x >= w || y < 0 || y >= h) { + return default_color; + } + + uint16_t w_bytes = (w + 7) / 8; + uint16_t offset = (y * w_bytes) + x / 8; + uint8_t bit = 1 << (7 - x % 8); + + return ((image[offset] & bit) == 0) ? 0 : 1; +} + +static void set_pixel(int16_t x, int16_t y, uint8_t c) +{ + if (x < 0 || x >= SCREEN_WIDTH || y < 0 || y >= SCREEN_HEIGHT) { + return; + } + + uint16_t offset = (y * SCREEN_BYTES_PER_LINE) + x / 8; + uint8_t bit = 1 << (7 - x % 8); + if (c == 1) { + disp_buf[offset] |= bit; + } else { + disp_buf[offset] &= ~bit; + } +} + +uint16_t display_measure_text(char* text, Font* font) +{ + uint16_t width = 0; + uint16_t slen = strlen(text); + for (int i=0; iascent - glyphInfo.h - glyphInfo.y, glyphInfo.w, glyphInfo.h, glyphInfo.bitmap, + invert ? DRAW_MODE_WHITE_ONLY | DRAW_MODE_INVERT : DRAW_MODE_WHITE_ONLY); + x += glyphInfo.advance; + } +} + +uint16_t display_get_char_width(char ch, Font* font) +{ + GlyphInfo glyphInfo; + glyph_lookup(font, ch, &glyphInfo); + return glyphInfo.advance; +} + +void display_rect(int16_t x, int16_t y, int16_t w, int16_t h, u_int8_t color) +{ + // Draw the top and bottom + int16_t y_bottom = y + h - 1; + for (int dx = x; dx < x + w; dx++) { + set_pixel(dx, y, color); + set_pixel(dx, y_bottom, color); + } + + // Draw the sides - repeats the top and bottom pixels to avoid special case + // code for short rectangles + int16_t x_right = x + w - 1; + for (int dy = y; dy < y + w; dy++) { + set_pixel(x, dy, color); + set_pixel(x_right, dy, color); + } +} + +// Very simple and inefficient image drawing, but should be fast enough for our +// limited use. +void display_image(uint16_t x, uint16_t y, uint16_t image_w, uint16_t image_h, uint8_t* image, uint8_t mode) +{ + // Iterate over the image bounds + for (int dy = 0; dy < image_h; dy++) { + for (int dx = 0; dx < image_w; dx++) { + uint8_t color = get_image_pixel(dx, dy, image_w, image_h, image, 0); + if (((mode & DRAW_MODE_BLACK_ONLY) && color == 1) || ((mode & DRAW_MODE_WHITE_ONLY) && color == 0)) { + // Skip this pixel if we are not supposed to draw it + continue; + } + if (mode & DRAW_MODE_INVERT) { + color = !color; + } + + set_pixel(x + dx, y + dy, color); + } + } +} + +// Assumes it's the only thing on these lines, so it does not retain any other +// image that might have been rendered there. +void display_progress_bar(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint8_t percent) +{ + // Clear whole line first + display_fill_rect(0, y, SCREEN_WIDTH-1, h, 0); + + display_fill_rect(x, y, w, h, 1); + display_fill_rect(x + 2, y + 2, w - 4, h - 4, 0); + display_fill_rect(x + 3, y + 3, (w * percent) / 100 - 6, h - 6, 1); +} + +void display_show(void) +{ + // Disable IRQs so keypad events don't interrupt display drawing + __disable_irq(); + lcd_update(disp_buf, true); + __enable_irq(); + +#ifndef DEBUG + // Clear the keypad interrupt so that it will retrigger if it had any events while + // interrupts were disabled, else it will hang the controller since it's waiting + // for the previous interrupt to be acknowledged. + keypad_write(KBD_ADDR, KBD_REG_INT_STAT, 0xFF); +#endif /* DEBUG */ +} + +void display_show_lines(uint16_t y_start, uint16_t y_end) +{ + if (y_start >= SCREEN_HEIGHT) { + return; + } + + if (y_end >= SCREEN_HEIGHT) { + y_end = SCREEN_HEIGHT - 1; + } + + for (uint16_t y=y_start; y<=y_end; y++) { + lcd_prebuffer_line(y, &disp_buf[y * SCREEN_BYTES_PER_LINE], true); + } + + lcd_update_line_range(y_start, y_end); +} + +void display_clear(uint8_t color) +{ + memset(disp_buf, color == 0 ? 0x00 : 0xFF, SCREEN_BYTES_PER_LINE * SCREEN_HEIGHT); +} + +void display_init(bool clear) +{ + lcd_init(clear); +} + +// Clear the memory display and then shutdown +void display_clean_shutdown() +{ + display_clear(0); + display_show(); + passport_shutdown(); +} diff --git a/ports/stm32/boards/Passport/gpio.c b/ports/stm32/boards/Passport/common/gpio.c similarity index 86% rename from ports/stm32/boards/Passport/gpio.c rename to ports/stm32/boards/Passport/common/gpio.c index f5e820b..4b0b397 100644 --- a/ports/stm32/boards/Passport/gpio.c +++ b/ports/stm32/boards/Passport/common/gpio.c @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // @@ -30,7 +30,7 @@ void gpio_init(void) void passport_reset(void) { - HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, 0); + NVIC_SystemReset(); } void passport_shutdown(void) diff --git a/ports/stm32/boards/Passport/common/hash.c b/ports/stm32/boards/Passport/common/hash.c index 4fba244..f1199b3 100644 --- a/ports/stm32/boards/Passport/common/hash.c +++ b/ports/stm32/boards/Passport/common/hash.c @@ -1,7 +1,7 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // -// SPDX-FileCopyrightText: 2018 Coinkite, Inc. +// SPDX-FileCopyrightText: 2018 Coinkite, Inc. // SPDX-License-Identifier: GPL-3.0-only // #include @@ -14,10 +14,33 @@ #include "utils.h" #include "fwheader.h" #include "sha256.h" +#ifndef PASSPORT_COSIGN_TOOL #include "secrets.h" - +#endif #define UID_LEN (96/8) /* 96 bits (Section 61.1 in STMH753 RM) */ +void hash_bl( + uint8_t *bl, + size_t bllen, + uint8_t *hash, + uint8_t hashlen +) +{ + SHA256_CTX ctx; + + sha256_init(&ctx); + + /* Checksum the bootloader */ + sha256_update(&ctx, bl, bllen); + sha256_final(&ctx, hash); + + /* double SHA256 */ + sha256_init(&ctx); + sha256_update(&ctx, hash, hashlen); + sha256_final(&ctx, hash); +} + +// This hash is used for the integrity check of the firmware and is not user-facing void hash_fw( fw_info_t *hdr, uint8_t *fw, @@ -30,7 +53,7 @@ void hash_fw( sha256_init(&ctx); - /* Checksum the header */ + // Checksum the info block too sha256_update(&ctx, (uint8_t *)hdr, sizeof(fw_info_t)); /* Checksum the firmware */ @@ -43,6 +66,32 @@ void hash_fw( sha256_final(&ctx, hash); } + +// User-facing hash includes signatures so user can compare to downloaded file. +// When exclude_hdr is true, only the code part of the firmware is hashed, which +// would allow a user to build the code themselves and compare the hash with what +// is in the Passport. +void hash_fw_user( + uint8_t *fw, + size_t fwlen, + uint8_t *hash, + uint8_t hashlen, + bool exclude_hdr +) +{ + SHA256_CTX ctx; + + // Skip the whole header if requested + if (exclude_hdr) { + fw += FW_HEADER_SIZE; + fwlen -= FW_HEADER_SIZE; + } + + sha256_init(&ctx); + sha256_update(&ctx, fw, fwlen); + sha256_final(&ctx, hash); +} + #ifndef PASSPORT_COSIGN_TOOL void hash_board( uint8_t *fw_hash, @@ -71,4 +120,47 @@ void hash_board( sha256_update(&ctx, hash, hashlen); sha256_final(&ctx, hash); } + +void get_device_hash(uint8_t *hash) +{ + SHA256_CTX ctx; + sha256_init(&ctx); + + /* Add SE serial number */ + sha256_update(&ctx, rom_secrets->se_serial_number, sizeof(rom_secrets->se_serial_number)); + + /* One-time pad */ + sha256_update(&ctx, rom_secrets->otp_key, sizeof(rom_secrets->otp_key)); + + /* Pairing secret */ + sha256_update(&ctx, rom_secrets->pairing_secret, sizeof(rom_secrets->pairing_secret)); + + /* Add unique device ID from MCU */ + sha256_update(&ctx, (uint8_t *)UID_BASE, UID_LEN); + sha256_final(&ctx, hash); + + /* double SHA256 */ + sha256_init(&ctx); + sha256_update(&ctx, hash, 32); + sha256_final(&ctx, hash); +} + +bool get_serial_number( + char *serial_buf, + uint8_t serial_buf_len +) +{ + uint8_t hash[32]; + if (serial_buf_len < 20) { + return false; + } + + get_device_hash(hash); + + // Format as serial number + bytes_to_hex_str(hash, 8, serial_buf, 2, '-'); + + return true; +} + #endif /* PASSPORT_COSIGN_TOOL */ diff --git a/ports/stm32/boards/Passport/keypad-adp-5587.c b/ports/stm32/boards/Passport/common/keypad-adp-5587.c similarity index 81% rename from ports/stm32/boards/Passport/keypad-adp-5587.c rename to ports/stm32/boards/Passport/common/keypad-adp-5587.c index 439a9a6..18bfbe0 100644 --- a/ports/stm32/boards/Passport/keypad-adp-5587.c +++ b/ports/stm32/boards/Passport/common/keypad-adp-5587.c @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // @@ -8,10 +8,12 @@ #include "stm32h7xx_hal.h" #include "stm32h7xx_hal_i2c_ex.h" -#include "extint.h" #include "delay.h" #include "keypad-adp-5587.h" -#include "modfoundation.h" + +#ifndef PASSPORT_BOOTLOADER +#include "extint.h" +#endif /* PASSPORT_BOOTLOADER */ static I2C_HandleTypeDef hi2c; @@ -69,29 +71,31 @@ static int keypad_setup(void) return 0; } -void keypad_ISR() +void keypad_ISR(void) { int rc; uint8_t key = 0; uint8_t key_count = 0; - - printf("keypad_ISR() 1\n"); uint8_t loop_count = 0; + while (loop_count < 10) { rc = keypad_read(KBD_ADDR, KBD_REG_KEY_EVENTA, &key, 1); if (rc < 0) { +#ifndef PASSPORT_BOOTLOADER printf("keypad_ISR() read error\n"); +#endif /* PASSPORT_BOOTLOADER */ break; } if (key == 0) { - printf("keypad_ISR() no key in queue\n"); +#ifndef PASSPORT_BOOTLOADER + // printf("keypad_ISR() no key in queue\n"); +#endif /* PASSPORT_BOOTLOADER */ break; } - ring_buffer_enqueue(&keybuf, key); - printf("key=%d\n", key); + ring_buffer_enqueue(key); key_count++; loop_count++; } @@ -101,9 +105,10 @@ void keypad_ISR() /* Clear the interrrupt on the keypad controller */ rc = keypad_write(KBD_ADDR, KBD_REG_INT_STAT, 0xFF); if (rc < 0) { +#ifndef PASSPORT_BOOTLOADER printf("[%s] I2C problem\n", __func__); +#endif /* PASSPORT_BOOTLOADER */ } - printf("keypad_ISR() 5\n"); } else { @@ -112,13 +117,9 @@ void keypad_ISR() * controller is in a strange state. We'll reset it and reconfigure * it to get it working again. */ - printf("keypad_ISR() 2\n"); keypad_reset(); - printf("keypad_ISR() 3\n"); keypad_setup(); - printf("keypad_ISR() 4\n"); } - printf("keypad_ISR() 6======\n"); } void keypad_init(void) @@ -127,24 +128,18 @@ void keypad_init(void) HAL_StatusTypeDef rc; GPIO_InitTypeDef GPIO_InitStruct = { 0 }; -printf("keypad_init(): 1\n"); // Need to specify the size of the ring buffer 128 for a test - ring_buffer_init(&keybuf); + ring_buffer_init(); -printf("keypad_init(): 2\n"); __HAL_RCC_GPIOE_CLK_ENABLE(); -printf("keypad_init(): 3\n"); GPIO_InitStruct.Pin = GPIO_PIN_2; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; -printf("keypad_init(): 4\n"); HAL_GPIO_Init(GPIOE, &GPIO_InitStruct); -printf("keypad_init(): 5\n"); __HAL_RCC_I2C2_CLK_ENABLE(); -printf("keypad_init(): 6\n"); memset(&GPIO_InitStruct, 0, sizeof(GPIO_InitStruct)); GPIO_InitStruct.Pin = GPIO_PIN_10 | GPIO_PIN_11; @@ -152,21 +147,18 @@ printf("keypad_init(): 6\n"); GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; GPIO_InitStruct.Alternate = GPIO_AF4_I2C2; -printf("keypad_init(): 7\n"); HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); -printf("keypad_init(): 8\n"); /* Configure GPIO pin : PB12 */ memset(&GPIO_InitStruct, 0, sizeof(GPIO_InitStruct)); GPIO_InitStruct.Pin = GPIO_PIN_12; GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING; GPIO_InitStruct.Pull = GPIO_NOPULL; -printf("keypad_init(): 9\n"); HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); -printf("keypad_init(): 10\n"); hi2c.Instance = I2C2; - hi2c.Init.Timing = 0x109095DF; + hi2c.Init.Timing = 0x00B03FDB; /* 0x0010061A - 400 KHz @ 64 MHz */ + /* 0x00B03FDB - 400 KHz @ 480 MHz */ hi2c.Init.OwnAddress1 = 0; hi2c.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; hi2c.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE; @@ -174,28 +166,28 @@ printf("keypad_init(): 10\n"); hi2c.Init.OwnAddress2Masks = I2C_OA2_NOMASK; hi2c.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE; hi2c.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE; -printf("keypad_init(): 11\n"); rc = HAL_I2C_Init(&hi2c); if (rc != HAL_OK) +#ifdef PASSPORT_BOOTLOADER + ; +#else printf("[%s-%d] HAL_I2C_Init failed\n", __func__, __LINE__); -printf("keypad_init(): 12\n"); +#endif /* PASSPORT_BOOTLOADER */ keypad_reset(); -printf("keypad_init(): 13\n"); rcc = keypad_setup(); if (rcc < 0) +#ifdef PASSPORT_BOOTLOADER + ; +#else printf("[%s-%d] keypad_setup() failed\n", __func__, __LINE__); +#endif /* PASSPORT_BOOTLOADER */ /* EXTI interrupt init*/ -printf("keypad_init(): 14\n"); - mp_uint_t irq_state = disable_irq(); -printf("keypad_init(): 15\n"); + __disable_irq(); HAL_NVIC_SetPriority(EXTI15_10_IRQn, 0, 0); -printf("keypad_init(): 16\n"); HAL_NVIC_EnableIRQ(EXTI15_10_IRQn); -printf("keypad_init(): 17\n"); - enable_irq(irq_state); -printf("keypad_init(): 18\n"); + __enable_irq(); } int keypad_write( diff --git a/ports/stm32/boards/Passport/common/lcd-sharp-ls018B7dh02.c b/ports/stm32/boards/Passport/common/lcd-sharp-ls018B7dh02.c index f8699a7..fd847ab 100644 --- a/ports/stm32/boards/Passport/common/lcd-sharp-ls018B7dh02.c +++ b/ports/stm32/boards/Passport/common/lcd-sharp-ls018B7dh02.c @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // // Screen driver for Sharp LS018B7DH02 monochrome display @@ -6,51 +6,16 @@ #include #include -#include "lcd-sharp-ls018B7dh02.h" -#include "mpconfigboard.h" -#include "spi.h" #include "stm32h7xx_hal.h" -Screen screen; -static TIM_HandleTypeDef lcd_refresh_timer_handle; +#include "lcd-sharp-ls018B7dh02.h" -uint8_t busy_pattern[34][288] = { - {0xff, 0xff, 0xc0, 0x00, 0x00, 0xff, 0xff, 0xc0, 0x00, 0x00, 0xff, 0xff, 0xc0, 0x00, 0x00, 0xff, 0xff, 0xc0, 0x00, 0x00, 0xff, 0xff, 0xc0, 0x00, 0x00, 0xff, 0xff, 0xc0, 0x00, 0x00, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, }, - {0x7f, 0xff, 0xe0, 0x00, 0x00, 0x7f, 0xff, 0xe0, 0x00, 0x00, 0x7f, 0xff, 0xe0, 0x00, 0x00, 0x7f, 0xff, 0xe0, 0x00, 0x00, 0x7f, 0xff, 0xe0, 0x00, 0x00, 0x7f, 0xff, 0xe0, 0x00, 0x00, 0x7f, 0xff, 0xe0, 0x00, 0x00, 0x00, }, - {0x3f, 0xff, 0xf0, 0x00, 0x00, 0x3f, 0xff, 0xf0, 0x00, 0x00, 0x3f, 0xff, 0xf0, 0x00, 0x00, 0x3f, 0xff, 0xf0, 0x00, 0x00, 0x3f, 0xff, 0xf0, 0x00, 0x00, 0x3f, 0xff, 0xf0, 0x00, 0x00, 0x3f, 0xff, 0xf0, 0x00, 0x00, 0x00, }, - {0x1f, 0xff, 0xf8, 0x00, 0x00, 0x1f, 0xff, 0xf8, 0x00, 0x00, 0x1f, 0xff, 0xf8, 0x00, 0x00, 0x1f, 0xff, 0xf8, 0x00, 0x00, 0x1f, 0xff, 0xf8, 0x00, 0x00, 0x1f, 0xff, 0xf8, 0x00, 0x00, 0x1f, 0xff, 0xf8, 0x00, 0x00, 0x00, }, - {0x0f, 0xff, 0xfc, 0x00, 0x00, 0x0f, 0xff, 0xfc, 0x00, 0x00, 0x0f, 0xff, 0xfc, 0x00, 0x00, 0x0f, 0xff, 0xfc, 0x00, 0x00, 0x0f, 0xff, 0xfc, 0x00, 0x00, 0x0f, 0xff, 0xfc, 0x00, 0x00, 0x0f, 0xff, 0xfc, 0x00, 0x00, 0x00, }, - {0x07, 0xff, 0xfe, 0x00, 0x00, 0x07, 0xff, 0xfe, 0x00, 0x00, 0x07, 0xff, 0xfe, 0x00, 0x00, 0x07, 0xff, 0xfe, 0x00, 0x00, 0x07, 0xff, 0xfe, 0x00, 0x00, 0x07, 0xff, 0xfe, 0x00, 0x00, 0x07, 0xff, 0xfe, 0x00, 0x00, 0x00, }, - {0x03, 0xff, 0xff, 0x00, 0x00, 0x03, 0xff, 0xff, 0x00, 0x00, 0x03, 0xff, 0xff, 0x00, 0x00, 0x03, 0xff, 0xff, 0x00, 0x00, 0x03, 0xff, 0xff, 0x00, 0x00, 0x03, 0xff, 0xff, 0x00, 0x00, 0x03, 0xff, 0xff, 0x00, 0x00, 0x00, }, - {0x01, 0xff, 0xff, 0x80, 0x00, 0x01, 0xff, 0xff, 0x80, 0x00, 0x01, 0xff, 0xff, 0x80, 0x00, 0x01, 0xff, 0xff, 0x80, 0x00, 0x01, 0xff, 0xff, 0x80, 0x00, 0x01, 0xff, 0xff, 0x80, 0x00, 0x01, 0xff, 0xff, 0x80, 0x00, 0x00, }, - {0x00, 0xff, 0xff, 0xc0, 0x00, 0x00, 0xff, 0xff, 0xc0, 0x00, 0x00, 0xff, 0xff, 0xc0, 0x00, 0x00, 0xff, 0xff, 0xc0, 0x00, 0x00, 0xff, 0xff, 0xc0, 0x00, 0x00, 0xff, 0xff, 0xc0, 0x00, 0x00, 0xff, 0xff, 0xc0, 0x00, 0x00, }, - {0x00, 0x7f, 0xff, 0xe0, 0x00, 0x00, 0x7f, 0xff, 0xe0, 0x00, 0x00, 0x7f, 0xff, 0xe0, 0x00, 0x00, 0x7f, 0xff, 0xe0, 0x00, 0x00, 0x7f, 0xff, 0xe0, 0x00, 0x00, 0x7f, 0xff, 0xe0, 0x00, 0x00, 0x7f, 0xff, 0xe0, 0x00, 0x00, }, - {0x00, 0x3f, 0xff, 0xf0, 0x00, 0x00, 0x3f, 0xff, 0xf0, 0x00, 0x00, 0x3f, 0xff, 0xf0, 0x00, 0x00, 0x3f, 0xff, 0xf0, 0x00, 0x00, 0x3f, 0xff, 0xf0, 0x00, 0x00, 0x3f, 0xff, 0xf0, 0x00, 0x00, 0x3f, 0xff, 0xf0, 0x00, 0x00, }, - {0x00, 0x1f, 0xff, 0xf8, 0x00, 0x00, 0x1f, 0xff, 0xf8, 0x00, 0x00, 0x1f, 0xff, 0xf8, 0x00, 0x00, 0x1f, 0xff, 0xf8, 0x00, 0x00, 0x1f, 0xff, 0xf8, 0x00, 0x00, 0x1f, 0xff, 0xf8, 0x00, 0x00, 0x1f, 0xff, 0xf8, 0x00, 0x00, }, - {0x00, 0x0f, 0xff, 0xfc, 0x00, 0x00, 0x0f, 0xff, 0xfc, 0x00, 0x00, 0x0f, 0xff, 0xfc, 0x00, 0x00, 0x0f, 0xff, 0xfc, 0x00, 0x00, 0x0f, 0xff, 0xfc, 0x00, 0x00, 0x0f, 0xff, 0xfc, 0x00, 0x00, 0x0f, 0xff, 0xfc, 0x00, 0x00, }, - {0x00, 0x07, 0xff, 0xfe, 0x00, 0x00, 0x07, 0xff, 0xfe, 0x00, 0x00, 0x07, 0xff, 0xfe, 0x00, 0x00, 0x07, 0xff, 0xfe, 0x00, 0x00, 0x07, 0xff, 0xfe, 0x00, 0x00, 0x07, 0xff, 0xfe, 0x00, 0x00, 0x07, 0xff, 0xfe, 0x00, 0x00, }, - {0x00, 0x03, 0xff, 0xff, 0x00, 0x00, 0x03, 0xff, 0xff, 0x00, 0x00, 0x03, 0xff, 0xff, 0x00, 0x00, 0x03, 0xff, 0xff, 0x00, 0x00, 0x03, 0xff, 0xff, 0x00, 0x00, 0x03, 0xff, 0xff, 0x00, 0x00, 0x03, 0xff, 0xff, 0x00, 0x00, }, - {0x00, 0x01, 0xff, 0xff, 0x80, 0x00, 0x01, 0xff, 0xff, 0x80, 0x00, 0x01, 0xff, 0xff, 0x80, 0x00, 0x01, 0xff, 0xff, 0x80, 0x00, 0x01, 0xff, 0xff, 0x80, 0x00, 0x01, 0xff, 0xff, 0x80, 0x00, 0x01, 0xff, 0xff, 0x80, 0x00, }, - {0x00, 0x00, 0xff, 0xff, 0xc0, 0x00, 0x00, 0xff, 0xff, 0xc0, 0x00, 0x00, 0xff, 0xff, 0xc0, 0x00, 0x00, 0xff, 0xff, 0xc0, 0x00, 0x00, 0xff, 0xff, 0xc0, 0x00, 0x00, 0xff, 0xff, 0xc0, 0x00, 0x00, 0xff, 0xff, 0xc0, 0x00, }, - {0x00, 0x00, 0xff, 0xff, 0xc0, 0x00, 0x00, 0xff, 0xff, 0xc0, 0x00, 0x00, 0xff, 0xff, 0xc0, 0x00, 0x00, 0xff, 0xff, 0xc0, 0x00, 0x00, 0xff, 0xff, 0xc0, 0x00, 0x00, 0xff, 0xff, 0xc0, 0x00, 0x00, 0xff, 0xff, 0xc0, 0x00, }, - {0x00, 0x01, 0xff, 0xff, 0x80, 0x00, 0x01, 0xff, 0xff, 0x80, 0x00, 0x01, 0xff, 0xff, 0x80, 0x00, 0x01, 0xff, 0xff, 0x80, 0x00, 0x01, 0xff, 0xff, 0x80, 0x00, 0x01, 0xff, 0xff, 0x80, 0x00, 0x01, 0xff, 0xff, 0x80, 0x00, }, - {0x00, 0x03, 0xff, 0xff, 0x00, 0x00, 0x03, 0xff, 0xff, 0x00, 0x00, 0x03, 0xff, 0xff, 0x00, 0x00, 0x03, 0xff, 0xff, 0x00, 0x00, 0x03, 0xff, 0xff, 0x00, 0x00, 0x03, 0xff, 0xff, 0x00, 0x00, 0x03, 0xff, 0xff, 0x00, 0x00, }, - {0x00, 0x07, 0xff, 0xfe, 0x00, 0x00, 0x07, 0xff, 0xfe, 0x00, 0x00, 0x07, 0xff, 0xfe, 0x00, 0x00, 0x07, 0xff, 0xfe, 0x00, 0x00, 0x07, 0xff, 0xfe, 0x00, 0x00, 0x07, 0xff, 0xfe, 0x00, 0x00, 0x07, 0xff, 0xfe, 0x00, 0x00, }, - {0x00, 0x0f, 0xff, 0xfc, 0x00, 0x00, 0x0f, 0xff, 0xfc, 0x00, 0x00, 0x0f, 0xff, 0xfc, 0x00, 0x00, 0x0f, 0xff, 0xfc, 0x00, 0x00, 0x0f, 0xff, 0xfc, 0x00, 0x00, 0x0f, 0xff, 0xfc, 0x00, 0x00, 0x0f, 0xff, 0xfc, 0x00, 0x00, }, - {0x00, 0x1f, 0xff, 0xf8, 0x00, 0x00, 0x1f, 0xff, 0xf8, 0x00, 0x00, 0x1f, 0xff, 0xf8, 0x00, 0x00, 0x1f, 0xff, 0xf8, 0x00, 0x00, 0x1f, 0xff, 0xf8, 0x00, 0x00, 0x1f, 0xff, 0xf8, 0x00, 0x00, 0x1f, 0xff, 0xf8, 0x00, 0x00, }, - {0x00, 0x3f, 0xff, 0xf0, 0x00, 0x00, 0x3f, 0xff, 0xf0, 0x00, 0x00, 0x3f, 0xff, 0xf0, 0x00, 0x00, 0x3f, 0xff, 0xf0, 0x00, 0x00, 0x3f, 0xff, 0xf0, 0x00, 0x00, 0x3f, 0xff, 0xf0, 0x00, 0x00, 0x3f, 0xff, 0xf0, 0x00, 0x00, }, - {0x00, 0x7f, 0xff, 0xe0, 0x00, 0x00, 0x7f, 0xff, 0xe0, 0x00, 0x00, 0x7f, 0xff, 0xe0, 0x00, 0x00, 0x7f, 0xff, 0xe0, 0x00, 0x00, 0x7f, 0xff, 0xe0, 0x00, 0x00, 0x7f, 0xff, 0xe0, 0x00, 0x00, 0x7f, 0xff, 0xe0, 0x00, 0x00, }, - {0x00, 0xff, 0xff, 0xc0, 0x00, 0x00, 0xff, 0xff, 0xc0, 0x00, 0x00, 0xff, 0xff, 0xc0, 0x00, 0x00, 0xff, 0xff, 0xc0, 0x00, 0x00, 0xff, 0xff, 0xc0, 0x00, 0x00, 0xff, 0xff, 0xc0, 0x00, 0x00, 0xff, 0xff, 0xc0, 0x00, 0x00, }, - {0x01, 0xff, 0xff, 0x80, 0x00, 0x01, 0xff, 0xff, 0x80, 0x00, 0x01, 0xff, 0xff, 0x80, 0x00, 0x01, 0xff, 0xff, 0x80, 0x00, 0x01, 0xff, 0xff, 0x80, 0x00, 0x01, 0xff, 0xff, 0x80, 0x00, 0x01, 0xff, 0xff, 0x80, 0x00, 0x00, }, - {0x03, 0xff, 0xff, 0x00, 0x00, 0x03, 0xff, 0xff, 0x00, 0x00, 0x03, 0xff, 0xff, 0x00, 0x00, 0x03, 0xff, 0xff, 0x00, 0x00, 0x03, 0xff, 0xff, 0x00, 0x00, 0x03, 0xff, 0xff, 0x00, 0x00, 0x03, 0xff, 0xff, 0x00, 0x00, 0x00, }, - {0x07, 0xff, 0xfe, 0x00, 0x00, 0x07, 0xff, 0xfe, 0x00, 0x00, 0x07, 0xff, 0xfe, 0x00, 0x00, 0x07, 0xff, 0xfe, 0x00, 0x00, 0x07, 0xff, 0xfe, 0x00, 0x00, 0x07, 0xff, 0xfe, 0x00, 0x00, 0x07, 0xff, 0xfe, 0x00, 0x00, 0x00, }, - {0x0f, 0xff, 0xfc, 0x00, 0x00, 0x0f, 0xff, 0xfc, 0x00, 0x00, 0x0f, 0xff, 0xfc, 0x00, 0x00, 0x0f, 0xff, 0xfc, 0x00, 0x00, 0x0f, 0xff, 0xfc, 0x00, 0x00, 0x0f, 0xff, 0xfc, 0x00, 0x00, 0x0f, 0xff, 0xfc, 0x00, 0x00, 0x00, }, - {0x1f, 0xff, 0xf8, 0x00, 0x00, 0x1f, 0xff, 0xf8, 0x00, 0x00, 0x1f, 0xff, 0xf8, 0x00, 0x00, 0x1f, 0xff, 0xf8, 0x00, 0x00, 0x1f, 0xff, 0xf8, 0x00, 0x00, 0x1f, 0xff, 0xf8, 0x00, 0x00, 0x1f, 0xff, 0xf8, 0x00, 0x00, 0x00, }, - {0x3f, 0xff, 0xf0, 0x00, 0x00, 0x3f, 0xff, 0xf0, 0x00, 0x00, 0x3f, 0xff, 0xf0, 0x00, 0x00, 0x3f, 0xff, 0xf0, 0x00, 0x00, 0x3f, 0xff, 0xf0, 0x00, 0x00, 0x3f, 0xff, 0xf0, 0x00, 0x00, 0x3f, 0xff, 0xf0, 0x00, 0x00, 0x00, }, - {0x7f, 0xff, 0xe0, 0x00, 0x00, 0x7f, 0xff, 0xe0, 0x00, 0x00, 0x7f, 0xff, 0xe0, 0x00, 0x00, 0x7f, 0xff, 0xe0, 0x00, 0x00, 0x7f, 0xff, 0xe0, 0x00, 0x00, 0x7f, 0xff, 0xe0, 0x00, 0x00, 0x7f, 0xff, 0xe0, 0x00, 0x00, 0x00, }, - {0xff, 0xff, 0xc0, 0x00, 0x00, 0xff, 0xff, 0xc0, 0x00, 0x00, 0xff, 0xff, 0xc0, 0x00, 0x00, 0xff, 0xff, 0xc0, 0x00, 0x00, 0xff, 0xff, 0xc0, 0x00, 0x00, 0xff, 0xff, 0xc0, 0x00, 0x00, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, } -}; +#define LCD_NSS_PIN GPIO_PIN_15 // port A +#define LCD_SPI_SCK GPIO_PIN_5 // port A +#define LCD_SPI_MOSI GPIO_PIN_7 // port A +Screen screen; +static TIM_HandleTypeDef lcd_refresh_timer_handle; uint8_t header_lookup[] = { 0x80, 0x00, @@ -360,35 +325,61 @@ uint8_t header_lookup[] = { typedef struct { - mp_obj_base_t base; - - const spi_t* spi; + SPI_HandleTypeDef *spi; int row; int column; } lcd_t; static lcd_t lcd; +static SPI_HandleTypeDef spi_port; void lcd_clear( - bool invert) + bool invert +) { uint8_t invert_mask = invert ? 0x40 : 0x00; uint8_t clear_msg[2] = { 0x20 | invert_mask, 0x00 }; - HAL_SPI_Transmit(lcd.spi->spi, clear_msg, 2, 1000); + HAL_SPI_Transmit(lcd.spi, clear_msg, 2, 1000); } -void lcd_init(void) +void lcd_init(bool clear) { - SPI_InitTypeDef* init; + SPI_InitTypeDef *init; TIM_MasterConfigTypeDef sMasterConfig = { 0 }; TIM_OC_InitTypeDef sConfigOC = { 0 }; GPIO_InitTypeDef GPIO_InitStruct = { 0 }; - lcd.spi = &spi_obj[0]; + __HAL_RCC_GPIOA_CLK_ENABLE(); + __HAL_RCC_GPIOE_CLK_ENABLE(); + __HAL_RCC_SPI1_CLK_ENABLE(); + + GPIO_InitStruct.Pin = LCD_NSS_PIN; + GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; + GPIO_InitStruct.Pull = GPIO_PULLUP; + GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; + GPIO_InitStruct.Alternate = GPIO_AF5_SPI1; + HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); + + GPIO_InitStruct.Pin = LCD_SPI_SCK; + GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; + GPIO_InitStruct.Pull = GPIO_PULLUP; + GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; + GPIO_InitStruct.Alternate = GPIO_AF5_SPI1; + HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); + + GPIO_InitStruct.Pin = LCD_SPI_MOSI; + GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; + GPIO_InitStruct.Pull = GPIO_PULLUP; + GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; + GPIO_InitStruct.Alternate = GPIO_AF5_SPI1; + HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); + + lcd.spi = &spi_port; + lcd.spi->Instance = SPI1; + init = &lcd.spi->Init; // init the SPI bus - init = &lcd.spi->spi->Init; init->Mode = SPI_MODE_MASTER; // @@ -408,11 +399,7 @@ void lcd_init(void) // === These are in the cubeIDE init code but not the MP LCD module make_new init code init->NSSPMode = SPI_NSS_PULSE_ENABLE; -#if defined(MICROPY_PASSPORT_HW_REV1) - init->NSSPolarity = SPI_NSS_POLARITY_LOW; -#elif defined(MICROPY_PASSPORT_HW_REV2) init->NSSPolarity = SPI_NSS_POLARITY_HIGH; -#endif // MICROPY_PASSPORT_HW_REV1 or MICROPY_PASSPORT_HW_REV2 init->FifoThreshold = SPI_FIFO_THRESHOLD_01DATA; init->TxCRCInitializationPattern = SPI_CRC_INITIALIZATION_ALL_ZERO_PATTERN; init->RxCRCInitializationPattern = SPI_CRC_INITIALIZATION_ALL_ZERO_PATTERN; @@ -422,8 +409,7 @@ void lcd_init(void) init->MasterKeepIOState = SPI_MASTER_KEEP_IO_STATE_DISABLE; init->IOSwap = SPI_IO_SWAP_DISABLE; - // Init the SPI bus. Set the enable NSS pin flag. - spi_init(lcd.spi, true); + HAL_SPI_Init(lcd.spi); // Code to configure Timer 1 using code similar to the MP LED module PWM timer code. __TIM1_CLK_ENABLE(); @@ -459,10 +445,9 @@ void lcd_init(void) GPIO_InitStruct.Alternate = GPIO_AF1_TIM1; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); - lcd_clear(false); - - // Enable screen now that it's clear - HAL_GPIO_WritePin(GPIOE, GPIO_PIN_15, 1); + if (clear) { + lcd_clear(false); + } // Start timer to refresh the SRAM inside the LCD HAL_TIM_PWM_Start(&lcd_refresh_timer_handle, TIM_CHANNEL_1); @@ -470,12 +455,15 @@ void lcd_init(void) void lcd_deinit(void) { - spi_deinit(lcd.spi); + __HAL_RCC_SPI1_FORCE_RESET(); + __HAL_RCC_SPI1_RELEASE_RESET(); + __HAL_RCC_SPI1_CLK_DISABLE(); } void lcd_update( uint8_t* screen_data, - bool invert) + bool invert +) { for (int y = 0; y < SCREEN_HEIGHT; y++) { // Use lookup table to set header bytes @@ -503,33 +491,37 @@ void lcd_update( // Write the screen data to the screen all at once -- this is much // faster than separate writes for each line - HAL_SPI_Transmit(lcd.spi->spi, (uint8_t*)&screen, sizeof(screen), 1000); + HAL_SPI_Transmit(lcd.spi, (uint8_t*)&screen, sizeof(screen), 1000); } -int busy_pos = 5; -#define BUSY_BAR_HEIGHT 34 -int cnt = 0; -void lcd_show_busy_bar() { - if (++cnt % 7 != 0) { +// Used to prepare a screen line for updating with lcd_update_line_range() +void lcd_prebuffer_line(uint16_t y, uint8_t* line_data, bool invert) { + if (y >= SCREEN_HEIGHT) { return; } - int busy_bar_start_y = SCREEN_HEIGHT - BUSY_BAR_HEIGHT; - int offset = 0; - for (int y = busy_bar_start_y; y < SCREEN_HEIGHT; y++) { - // Use lookup table to set header bytes - screen.lines[y].header[0] = header_lookup[y * 2]; - screen.lines[y].header[1] = header_lookup[y * 2 + 1]; + screen.lines[y].header[0] = header_lookup[y * 2]; + screen.lines[y].header[1] = header_lookup[y * 2 + 1]; - // Copy data bytes for this line - memcpy(screen.lines[y].pixels, &busy_pattern[y - busy_bar_start_y][busy_pos], SCREEN_BYTES_PER_LINE); - offset++; + if (invert) { + // Invert pixels 16 bits at a time. + // This works because our screen width in bytes is divisible by 2 (but not by 4) + uint16_t* psrc = (uint16_t*)line_data; + for (int i = 0; i < SCREEN_BYTES_PER_LINE / 2; i++) { + screen.lines[y].pixels_u16[i] = ~psrc[i]; + } + } else { + // Copy data bytes for this line + memcpy(screen.lines[y].pixels, line_data, SCREEN_BYTES_PER_LINE); } - busy_pos -= 1; - if (busy_pos < 0) { - busy_pos = 5; +} + +// Update a subset of lines on the LCD +// This is used for updating progress bars and busy bars without need a full screen redraw. +void lcd_update_line_range(uint16_t y_start, uint16_t y_end) { + if (y_start >= SCREEN_HEIGHT || y_end >= SCREEN_HEIGHT) { + return; } - // Write the busy bar data all at once - HAL_SPI_Transmit(lcd.spi->spi, (uint8_t*)&screen.lines[busy_bar_start_y], sizeof(ScreenLine) * BUSY_BAR_HEIGHT, 1000); -} \ No newline at end of file + HAL_SPI_Transmit(lcd.spi, (uint8_t*)&screen.lines[y_start], sizeof(ScreenLine) * (y_end - y_start + 1), 1000); +} diff --git a/ports/stm32/boards/Passport/common/passport_fonts.c b/ports/stm32/boards/Passport/common/passport_fonts.c new file mode 100644 index 0000000..fe3d087 --- /dev/null +++ b/ports/stm32/boards/Passport/common/passport_fonts.c @@ -0,0 +1,845 @@ +// Passport wallet font definitions in C +// Autogenerated by bdf-to-passport.py: DO NOT EDIT +#include "passport_fonts.h" + +// Lookup GlyphInfo for a single codepoint or return None +bool glyph_lookup(Font* font, uint8_t cp, GlyphInfo* glyph_info) { + for (int i=0; inum_codepoint_ranges; i++) { + Codepoints* cp_entry = &font->codepoints[i]; + if (cp < cp_entry->range_start || cp > cp_entry->range_end-1) { + continue; + } + + int offset = cp_entry->bitmap_offsets[cp - cp_entry->range_start]; + + uint8_t bbox_offset = font->bitmaps[offset]; + glyph_info->x = font->bboxes[bbox_offset].x; + glyph_info->y = font->bboxes[bbox_offset].y; + glyph_info->w = font->bboxes[bbox_offset].w; + glyph_info->h = font->bboxes[bbox_offset].h; + glyph_info->advance = font->bboxes[bbox_offset].advance; + + glyph_info->bitmap = font->bitmaps + offset + 1; + + return true; + } + return false; +} + +BBox FontTiny_bboxes[] = { + { 0, 0, 0, 0, 0, 0 }, + { 0, 0, 1, 1, 4, 1 }, + { 1, 0, 2, 11, 4, 11 }, + { 1, 7, 5, 4, 6, 4 }, + { 0, 0, 10, 10, 11, 20 }, + { 1, -2, 7, 15, 9, 15 }, + { 1, 0, 11, 11, 13, 22 }, + { 1, 0, 9, 11, 11, 22 }, + { 1, 7, 2, 4, 3, 4 }, + { 1, -3, 4, 14, 5, 14 }, + { 0, -3, 4, 14, 5, 14 }, + { 0, 4, 7, 7, 6, 7 }, + { 1, 2, 6, 6, 9, 6 }, + { 1, -2, 2, 5, 4, 5 }, + { 1, 3, 4, 2, 6, 2 }, + { 1, 0, 2, 2, 4, 2 }, + { 0, -1, 7, 14, 6, 14 }, + { 1, 0, 8, 11, 10, 11 }, + { 0, 0, 4, 11, 5, 11 }, + { 0, 0, 8, 11, 9, 11 }, + { 0, 0, 10, 11, 10, 22 }, + { 0, 0, 9, 11, 9, 22 }, + { 1, 0, 2, 8, 4, 8 }, + { 1, -2, 2, 10, 4, 10 }, + { 1, 2, 7, 7, 9, 7 }, + { 1, 2, 7, 6, 9, 6 }, + { 1, 0, 7, 11, 9, 11 }, + { 0, -3, 15, 14, 15, 28 }, + { 2, 0, 8, 11, 10, 11 }, + { 0, 0, 6, 11, 7, 11 }, + { 1, 0, 12, 11, 14, 22 }, + { 1, 0, 11, 11, 12, 22 }, + { 1, -2, 11, 13, 12, 26 }, + { 1, 0, 8, 11, 9, 11 }, + { 0, 0, 11, 11, 11, 22 }, + { 1, 0, 15, 11, 17, 22 }, + { -1, -2, 7, 14, 6, 14 }, + { 0, -1, 7, 1, 7, 1 }, + { 2, 9, 3, 2, 9, 2 }, + { 1, 0, 7, 8, 9, 8 }, + { 1, 0, 9, 11, 10, 22 }, + { 0, 0, 8, 8, 9, 8 }, + { 1, 0, 8, 8, 10, 8 }, + { 0, 0, 6, 11, 6, 11 }, + { 0, -3, 9, 11, 10, 22 }, + { -2, -3, 5, 14, 4, 14 }, + { 1, 0, 14, 8, 15, 16 }, + { 1, -3, 9, 11, 10, 22 }, + { 0, -3, 11, 11, 10, 22 }, + { 1, 0, 6, 8, 8, 8 }, + { 1, 0, 6, 10, 7, 10 }, + { 0, 0, 9, 8, 9, 16 }, + { 0, 0, 13, 8, 14, 16 }, + { 0, -3, 9, 11, 9, 22 }, + { 1, -3, 4, 14, 6, 14 }, + { 1, -3, 2, 14, 5, 14 }, + { 1, 3, 7, 4, 9, 4 }, + { 1, 0, 7, 9, 9, 9 }, + { 1, 5, 5, 6, 6, 6 }, + { 4, 9, 3, 2, 9, 2 }, + { 1, -3, 8, 11, 10, 11 }, + { 2, 2, 5, 6, 9, 6 }, +}; + +uint16_t FontTiny_codepoints_0[] = { 1,3,15,20,41,57,80,103,108,123,138,146,153,159,162,165,180,192,204,216,228,251,263,286,309,321,344,353,364,372,379,387,399,428,451,474,497,520,532,544,567,579,591,603,615,627,650,662,685,697,724,736,748,760,783,806,829,852,875,887,902,917,932,939,941,944,953,976,985,1008,1017,1029,1052,1064,1076,1091,1103,1115,1132,1141,1150,1173,1196,1205,1214,1225,1234,1251,1268,1277,1300,1309,1324,1339,1354, }; +uint16_t FontTiny_codepoints_1[] = { 1359,1369,1376,1383,1386, }; +uint16_t FontTiny_codepoints_2[] = { 1398, }; + +Codepoints FontTiny_codepoints[] = { + { 32, 127, FontTiny_codepoints_0 }, + { 177, 182, FontTiny_codepoints_1 }, + { 215, 216, FontTiny_codepoints_2 }, +}; + +uint8_t FontTiny_bitmaps[] = { + 0xAA, // Dummy first entry + + 0x01, // $0020 offset = 1 + 0x00, + + 0x02, // $0021 offset = 3 + 0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0x00,0x00,0xC0,0xC0, + + 0x03, // $0022 offset = 15 + 0xD8,0xD8,0xD8,0xD8, + + 0x04, // $0023 offset = 20 + 0x19,0x80,0x19,0x80,0x7F,0xC0,0x7F,0xC0,0x33,0x00,0x33,0x00,0xFF,0x80,0xFF,0x80,0x66,0x00,0x66,0x00, + + 0x05, // $0024 offset = 41 + 0x10,0x10,0x7C,0xFE,0xD2,0xD0,0xF0,0x7C,0x1E,0x16,0x96,0xFE,0x7C,0x10,0x10, + + 0x06, // $0025 offset = 57 + 0x60,0x80,0x91,0x80,0x93,0x00,0x93,0x00,0x96,0x00,0x64,0xC0,0x0D,0x20,0x19,0x20,0x11,0x20,0x31,0x20,0x60,0xC0, + + 0x07, // $0026 offset = 80 + 0x3C,0x00,0x7E,0x00,0x66,0x00,0x66,0x00,0x3C,0x00,0x79,0x00,0xCD,0x80,0xCD,0x80,0xC7,0x00,0xFF,0x80,0x7D,0x80, + + 0x08, // $0027 offset = 103 + 0xC0,0xC0,0xC0,0xC0, + + 0x09, // $0028 offset = 108 + 0x30,0x60,0x60,0xE0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xE0,0x60,0x60,0x30, + + 0x0a, // $0029 offset = 123 + 0xC0,0x60,0x60,0x70,0x30,0x30,0x30,0x30,0x30,0x30,0x70,0x60,0x60,0xC0, + + 0x0b, // $002A offset = 138 + 0x10,0x54,0x38,0xFE,0x38,0x54,0x10, + + 0x0c, // $002B offset = 146 + 0x30,0x30,0xFC,0xFC,0x30,0x30, + + 0x0d, // $002C offset = 153 + 0xC0,0xC0,0xC0,0x80,0x80, + + 0x0e, // $002D offset = 159 + 0xF0,0xF0, + + 0x0f, // $002E offset = 162 + 0xC0,0xC0, + + 0x10, // $002F offset = 165 + 0x06,0x06,0x0C,0x0C,0x18,0x18,0x18,0x30,0x30,0x30,0x60,0x60,0xC0,0xC0, + + 0x11, // $0030 offset = 180 + 0x3C,0x7E,0xE7,0xC3,0xC3,0xC3,0xC3,0xC3,0xE7,0x7E,0x3C, + + 0x12, // $0031 offset = 192 + 0xF0,0xF0,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30, + + 0x13, // $0032 offset = 204 + 0x3C,0xFE,0x47,0x03,0x07,0x0E,0x1C,0x38,0x70,0xFF,0xFF, + + 0x13, // $0033 offset = 216 + 0xFF,0xFF,0x0E,0x0C,0x1C,0x1E,0x07,0x03,0x47,0xFF,0x7C, + + 0x14, // $0034 offset = 228 + 0x06,0x00,0x0E,0x00,0x0C,0x00,0x18,0x00,0x38,0x00,0x33,0x00,0x73,0x00,0xFF,0xC0,0xFF,0xC0,0x03,0x00,0x03,0x00, + + 0x13, // $0035 offset = 251 + 0x7F,0x7F,0x60,0x60,0x7C,0x7F,0x07,0x03,0x43,0xFF,0x7C, + + 0x15, // $0036 offset = 263 + 0x1F,0x00,0x3F,0x00,0x70,0x00,0x60,0x00,0xEE,0x00,0xFF,0x00,0xF3,0x80,0xE1,0x80,0x73,0x80,0x3F,0x00,0x1E,0x00, + + 0x15, // $0037 offset = 286 + 0xFF,0x80,0xFF,0x80,0xC3,0x00,0x07,0x00,0x06,0x00,0x0E,0x00,0x0E,0x00,0x0C,0x00,0x1C,0x00,0x18,0x00,0x38,0x00, + + 0x11, // $0038 offset = 309 + 0x3C,0x7E,0xC3,0xC3,0x7E,0x7E,0xC3,0xC3,0xC3,0x7E,0x3C, + + 0x15, // $0039 offset = 321 + 0x3C,0x00,0x7E,0x00,0xE7,0x00,0xC3,0x00,0xE7,0x80,0x7F,0x80,0x3B,0x80,0x03,0x00,0x07,0x00,0x7E,0x00,0x7C,0x00, + + 0x16, // $003A offset = 344 + 0xC0,0xC0,0x00,0x00,0x00,0x00,0xC0,0xC0, + + 0x17, // $003B offset = 353 + 0xC0,0xC0,0x00,0x00,0x00,0xC0,0xC0,0xC0,0x80,0x80, + + 0x18, // $003C offset = 364 + 0x06,0x1E,0x78,0xC0,0x78,0x1E,0x06, + + 0x19, // $003D offset = 372 + 0xFE,0xFE,0x00,0x00,0xFE,0xFE, + + 0x18, // $003E offset = 379 + 0xC0,0xF0,0x3C,0x06,0x3C,0xF0,0xC0, + + 0x1a, // $003F offset = 387 + 0x78,0xFC,0xC6,0x06,0x0C,0x18,0x30,0x30,0x00,0x30,0x30, + + 0x1b, // $0040 offset = 399 + 0x0F,0xE0,0x1C,0x70,0x30,0x18,0x63,0x6C,0x67,0xE4,0xCC,0x66,0xCC,0x66,0xCC,0x66,0xCC,0x64,0x6F,0xFC,0x67,0xB8,0x30,0x00,0x1C,0x60,0x0F,0xC0, + + 0x07, // $0041 offset = 428 + 0x08,0x00,0x1C,0x00,0x1C,0x00,0x36,0x00,0x36,0x00,0x63,0x00,0x63,0x00,0x7F,0x00,0xE3,0x80,0xC1,0x80,0xC1,0x80, + + 0x07, // $0042 offset = 451 + 0xFE,0x00,0xFF,0x00,0xC3,0x00,0xC3,0x00,0xFE,0x00,0xFF,0x00,0xC3,0x00,0xC3,0x80,0xC3,0x80,0xFF,0x00,0xFE,0x00, + + 0x07, // $0043 offset = 474 + 0x1F,0x00,0x7F,0x80,0xE1,0x80,0xC0,0x00,0xC0,0x00,0xC0,0x00,0xC0,0x00,0xC0,0x00,0xE1,0x80,0x7F,0x80,0x1F,0x00, + + 0x07, // $0044 offset = 497 + 0xFC,0x00,0xFF,0x00,0xC3,0x80,0xC1,0x80,0xC1,0x80,0xC1,0x80,0xC1,0x80,0xC1,0x80,0xC3,0x80,0xFF,0x00,0xFC,0x00, + + 0x1a, // $0045 offset = 520 + 0xFE,0xFE,0xC0,0xC0,0xFC,0xFC,0xC0,0xC0,0xC0,0xFE,0xFE, + + 0x1a, // $0046 offset = 532 + 0xFE,0xFE,0xC0,0xC0,0xC0,0xFC,0xFC,0xC0,0xC0,0xC0,0xC0, + + 0x07, // $0047 offset = 544 + 0x1F,0x00,0x7F,0x80,0xE1,0x80,0xC0,0x00,0xC0,0x00,0xC3,0x80,0xC3,0x80,0xC1,0x80,0xE1,0x80,0x7F,0x80,0x1F,0x00, + + 0x1c, // $0048 offset = 567 + 0xC3,0xC3,0xC3,0xC3,0xFF,0xFF,0xC3,0xC3,0xC3,0xC3,0xC3, + + 0x02, // $0049 offset = 579 + 0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0, + + 0x1d, // $004A offset = 591 + 0x7C,0x7C,0x0C,0x0C,0x0C,0x0C,0x0C,0x0C,0x4C,0xFC,0x78, + + 0x11, // $004B offset = 603 + 0xC3,0xC7,0xCE,0xDC,0xF0,0xF0,0xF8,0xDC,0xCE,0xC7,0xC3, + + 0x1a, // $004C offset = 615 + 0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xFE,0xFE, + + 0x1e, // $004D offset = 627 + 0xC0,0x30,0xE0,0x70,0xF0,0xF0,0xF0,0xF0,0xF9,0xF0,0xD9,0xB0,0xCF,0x30,0xCF,0x30,0xC6,0x30,0xC6,0x30,0xC0,0x30, + + 0x11, // $004E offset = 650 + 0xC3,0xC3,0xE3,0xF3,0xFB,0xFB,0xDF,0xCF,0xC7,0xC7,0xC3, + + 0x1f, // $004F offset = 662 + 0x1F,0x00,0x3F,0x80,0x60,0xC0,0x60,0xC0,0xC0,0x60,0xC0,0x60,0xC0,0x60,0x60,0xC0,0x60,0xC0,0x3F,0x80,0x1F,0x00, + + 0x11, // $0050 offset = 685 + 0xFC,0xFE,0xC7,0xC3,0xC3,0xC7,0xFE,0xFC,0xC0,0xC0,0xC0, + + 0x20, // $0051 offset = 697 + 0x1F,0x00,0x3F,0x80,0x71,0xC0,0x60,0xC0,0xC0,0x60,0xC0,0x60,0xC0,0x60,0x60,0xC0,0x71,0xC0,0x3F,0x80,0x1E,0x00,0x07,0xE0,0x03,0xC0, + + 0x11, // $0052 offset = 724 + 0xFC,0xFE,0xC7,0xC3,0xC3,0xC7,0xFE,0xFC,0xCE,0xC6,0xC7, + + 0x21, // $0053 offset = 736 + 0x7C,0xFE,0xC2,0xC0,0xE0,0x7C,0x3E,0x03,0x83,0xFE,0x7C, + + 0x11, // $0054 offset = 748 + 0xFF,0xFF,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18, + + 0x07, // $0055 offset = 760 + 0xC1,0x80,0xC1,0x80,0xC1,0x80,0xC1,0x80,0xC1,0x80,0xC1,0x80,0xC1,0x80,0xC1,0x80,0xE3,0x80,0x7F,0x00,0x3E,0x00, + + 0x22, // $0056 offset = 783 + 0xC0,0x60,0xC0,0x60,0x60,0xC0,0x60,0xC0,0x31,0x80,0x31,0x80,0x1B,0x00,0x1B,0x00,0x1B,0x00,0x0E,0x00,0x0E,0x00, + + 0x23, // $0057 offset = 806 + 0xC1,0x06,0xC3,0x86,0xC3,0x86,0xE6,0xCE,0x66,0xCC,0x66,0xCC,0x76,0xDC,0x36,0xD8,0x3C,0x78,0x3C,0x78,0x18,0x30, + + 0x14, // $0058 offset = 829 + 0xE1,0xC0,0x73,0x80,0x33,0x00,0x1F,0x00,0x1E,0x00,0x0C,0x00,0x1E,0x00,0x3F,0x00,0x33,0x00,0x73,0x80,0xE1,0xC0, + + 0x14, // $0059 offset = 852 + 0xE1,0xC0,0x61,0x80,0x73,0x80,0x33,0x00,0x3F,0x00,0x1E,0x00,0x0C,0x00,0x0C,0x00,0x0C,0x00,0x0C,0x00,0x0C,0x00, + + 0x11, // $005A offset = 875 + 0xFF,0xFF,0x06,0x0E,0x1C,0x18,0x38,0x70,0x60,0xFF,0xFF, + + 0x09, // $005B offset = 887 + 0xF0,0xF0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xF0,0xF0, + + 0x24, // $005C offset = 902 + 0xC0,0xC0,0x60,0x60,0x30,0x30,0x30,0x18,0x18,0x18,0x0C,0x0C,0x06,0x06, + + 0x0a, // $005D offset = 917 + 0xF0,0xF0,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0xF0,0xF0, + + 0x19, // $005E offset = 932 + 0x10,0x38,0x6C,0x6C,0xC6,0xC6, + + 0x25, // $005F offset = 939 + 0xFE, + + 0x26, // $0060 offset = 941 + 0xC0,0x60, + + 0x27, // $0061 offset = 944 + 0x7C,0xFE,0x06,0x7E,0xC6,0xC6,0xFE,0x76, + + 0x28, // $0062 offset = 953 + 0xC0,0x00,0xC0,0x00,0xC0,0x00,0xDE,0x00,0xFF,0x00,0xE3,0x00,0xC1,0x80,0xC1,0x80,0xE3,0x00,0xFF,0x00,0xDE,0x00, + + 0x29, // $0063 offset = 976 + 0x1E,0x7F,0x62,0xC0,0xC0,0x62,0x7F,0x1E, + + 0x28, // $0064 offset = 985 + 0x01,0x80,0x01,0x80,0x01,0x80,0x3D,0x80,0x7F,0x80,0x63,0x80,0xC1,0x80,0xC1,0x80,0x63,0x80,0x7F,0x80,0x3D,0x80, + + 0x2a, // $0065 offset = 1008 + 0x3C,0x7E,0xC3,0xC3,0xFE,0xC0,0x7F,0x3E, + + 0x2b, // $0066 offset = 1017 + 0x3C,0x7C,0x60,0xFC,0xFC,0x60,0x60,0x60,0x60,0x60,0x60, + + 0x2c, // $0067 offset = 1029 + 0x3D,0x80,0x7F,0x80,0x63,0x80,0xC1,0x80,0xC1,0x80,0x63,0x80,0x7F,0x80,0x1D,0x80,0x01,0x80,0x7F,0x00,0x3E,0x00, + + 0x11, // $0068 offset = 1052 + 0xC0,0xC0,0xC0,0xDC,0xFE,0xE7,0xC3,0xC3,0xC3,0xC3,0xC3, + + 0x02, // $0069 offset = 1064 + 0xC0,0xC0,0x00,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0, + + 0x2d, // $006A offset = 1076 + 0x18,0x18,0x00,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x98,0xF8,0x70, + + 0x11, // $006B offset = 1091 + 0xC0,0xC0,0xC0,0xC7,0xCE,0xDC,0xF8,0xFC,0xEE,0xC6,0xC7, + + 0x02, // $006C offset = 1103 + 0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0, + + 0x2e, // $006D offset = 1115 + 0xDC,0xF0,0xFF,0xF8,0xE7,0x9C,0xC3,0x0C,0xC3,0x0C,0xC3,0x0C,0xC3,0x0C,0xC3,0x0C, + + 0x2a, // $006E offset = 1132 + 0xDE,0xFF,0xE7,0xC3,0xC3,0xC3,0xC3,0xC3, + + 0x2a, // $006F offset = 1141 + 0x3C,0x7E,0xE7,0xC3,0xC3,0xE7,0x7E,0x3C, + + 0x2f, // $0070 offset = 1150 + 0xDE,0x00,0xFF,0x00,0xE3,0x00,0xC1,0x80,0xC1,0x80,0xE3,0x00,0xFF,0x00,0xDE,0x00,0xC0,0x00,0xC0,0x00,0xC0,0x00, + + 0x30, // $0071 offset = 1173 + 0x3D,0x80,0x7F,0x80,0x63,0x80,0xC1,0x80,0xC1,0x80,0x63,0x80,0x7F,0x80,0x3D,0x80,0x01,0x80,0x01,0xE0,0x01,0xC0, + + 0x31, // $0072 offset = 1196 + 0xD8,0xFC,0xE4,0xC0,0xC0,0xC0,0xC0,0xC0, + + 0x31, // $0073 offset = 1205 + 0x7C,0xFC,0xC0,0xF0,0x7C,0x0C,0xFC,0xF8, + + 0x32, // $0074 offset = 1214 + 0x30,0x30,0xFC,0xFC,0x30,0x30,0x30,0x30,0x3C,0x1C, + + 0x2a, // $0075 offset = 1225 + 0xC3,0xC3,0xC3,0xC3,0xC3,0xE7,0xFF,0x7B, + + 0x33, // $0076 offset = 1234 + 0xC1,0x80,0xE3,0x80,0x63,0x00,0x77,0x00,0x36,0x00,0x3E,0x00,0x1C,0x00,0x1C,0x00, + + 0x34, // $0077 offset = 1251 + 0xC2,0x18,0xC7,0x18,0xC7,0x18,0x67,0x30,0x6D,0xB0,0x3D,0xE0,0x3D,0xE0,0x18,0xC0, + + 0x27, // $0078 offset = 1268 + 0xC6,0x6C,0x7C,0x38,0x38,0x7C,0x6C,0xC6, + + 0x35, // $0079 offset = 1277 + 0xC1,0x80,0xE3,0x00,0x63,0x00,0x76,0x00,0x36,0x00,0x3E,0x00,0x1C,0x00,0x1C,0x00,0x18,0x00,0xF8,0x00,0xF0,0x00, + + 0x31, // $007A offset = 1300 + 0xFC,0xFC,0x18,0x38,0x70,0xE0,0xFC,0xFC, + + 0x36, // $007B offset = 1309 + 0x30,0x70,0x60,0x60,0x60,0x60,0xC0,0xC0,0x60,0x60,0x60,0x60,0x70,0x30, + + 0x37, // $007C offset = 1324 + 0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0, + + 0x36, // $007D offset = 1339 + 0xC0,0xE0,0x60,0x60,0x60,0x60,0x30,0x30,0x60,0x60,0x60,0x60,0xE0,0xC0, + + 0x38, // $007E offset = 1354 + 0x60,0xF2,0x9E,0x0C, + + 0x39, // $00B1 offset = 1359 + 0x30,0x30,0xFE,0xFE,0x30,0x30,0x00,0xFE,0xFE, + + 0x3a, // $00B2 offset = 1369 + 0xF0,0x90,0x10,0x30,0xC0,0xF8, + + 0x3a, // $00B3 offset = 1376 + 0xF0,0x30,0x70,0x18,0x98,0xF0, + + 0x3b, // $00B4 offset = 1383 + 0x60,0xC0, + + 0x3c, // $00B5 offset = 1386 + 0xC3,0xC3,0xC3,0xC3,0xC3,0xE7,0xFF,0xFB,0xC0,0xC0,0xC0, + + 0x3d, // $00D7 offset = 1398 + 0x88,0xD8,0x70,0x70,0xD8,0x88, + + }; + +Font FontTiny = { + 14, + 8, + 14, + 3, + 21, + 32, + 216, + 3, + FontTiny_bboxes, + FontTiny_codepoints, + FontTiny_bitmaps, +}; + + +BBox FontSmall_bboxes[] = { + { 0, 0, 0, 0, 0, 0 }, + { 0, 0, 0, 0, 5, 0 }, + { 0, 0, 6, 13, 20, 13 }, + { 1, 8, 6, 5, 7, 5 }, + { 0, 0, 13, 13, 13, 26 }, + { 1, -2, 10, 17, 12, 34 }, + { 1, 0, 14, 13, 16, 26 }, + { 1, 0, 11, 13, 13, 26 }, + { 1, 8, 2, 5, 4, 5 }, + { 2, -4, 3, 18, 6, 18 }, + { 1, -4, 3, 18, 6, 18 }, + { 1, 8, 5, 6, 8, 6 }, + { 1, 2, 9, 9, 11, 18 }, + { 1, -3, 2, 5, 4, 5 }, + { 1, 4, 5, 2, 7, 2 }, + { 1, 0, 2, 2, 4, 2 }, + { 0, -2, 8, 18, 7, 18 }, + { 1, 0, 10, 13, 13, 26 }, + { 0, 0, 5, 13, 7, 13 }, + { 1, 0, 9, 13, 11, 26 }, + { 0, 0, 10, 13, 11, 26 }, + { 1, 0, 10, 13, 12, 26 }, + { 0, 0, 11, 13, 12, 26 }, + { 1, 0, 2, 10, 4, 10 }, + { 1, -3, 2, 13, 4, 13 }, + { 1, 3, 9, 8, 11, 16 }, + { 1, 4, 9, 5, 11, 10 }, + { 1, -4, 17, 17, 19, 51 }, + { 0, 0, 14, 13, 14, 26 }, + { 2, 0, 11, 13, 14, 26 }, + { 1, 0, 11, 13, 14, 26 }, + { 2, 0, 13, 13, 15, 26 }, + { 2, 0, 9, 13, 13, 26 }, + { 2, 0, 9, 13, 12, 26 }, + { 1, 0, 12, 13, 14, 26 }, + { 2, 0, 11, 13, 15, 26 }, + { 2, 0, 2, 13, 6, 13 }, + { -1, 0, 9, 13, 10, 26 }, + { 2, 0, 11, 13, 13, 26 }, + { 2, 0, 9, 13, 11, 26 }, + { 2, 0, 14, 13, 18, 26 }, + { 1, -2, 14, 15, 16, 30 }, + { 1, 0, 10, 13, 11, 26 }, + { 1, 0, 19, 13, 21, 39 }, + { 0, 0, 12, 13, 12, 26 }, + { 1, 0, 11, 13, 12, 26 }, + { 2, -4, 4, 18, 6, 18 }, + { -1, -2, 8, 18, 7, 18 }, + { 0, -4, 4, 18, 6, 18 }, + { 2, 3, 7, 8, 11, 8 }, + { 0, -1, 9, 1, 9, 2 }, + { 3, 12, 4, 2, 11, 2 }, + { 1, 0, 9, 10, 11, 20 }, + { 2, 0, 10, 14, 13, 28 }, + { 1, 0, 10, 14, 13, 28 }, + { 1, 0, 10, 10, 11, 20 }, + { 0, 0, 8, 14, 7, 14 }, + { 1, -4, 10, 14, 13, 28 }, + { 2, 0, 9, 14, 13, 28 }, + { 2, 0, 2, 14, 5, 14 }, + { -3, -4, 7, 18, 5, 18 }, + { 2, 0, 9, 14, 12, 28 }, + { 2, 0, 16, 10, 20, 20 }, + { 2, 0, 9, 10, 13, 20 }, + { 1, 0, 10, 10, 12, 20 }, + { 2, -4, 10, 14, 13, 28 }, + { 1, -4, 13, 14, 13, 28 }, + { 2, 0, 6, 10, 8, 10 }, + { 1, 0, 8, 10, 9, 10 }, + { 0, 0, 7, 12, 8, 12 }, + { 0, 0, 11, 10, 10, 20 }, + { 0, 0, 17, 10, 17, 30 }, + { 1, 0, 9, 10, 10, 20 }, + { 0, -4, 11, 14, 10, 28 }, + { 1, 0, 8, 10, 10, 10 }, + { 1, -4, 6, 18, 7, 18 }, + { 2, -4, 2, 18, 6, 18 }, + { 0, -4, 6, 18, 7, 18 }, + { 1, 5, 9, 3, 11, 6 }, + { 1, 0, 9, 12, 11, 24 }, + { 1, 6, 6, 8, 8, 8 }, + { 5, 12, 3, 2, 11, 2 }, + { 2, -4, 9, 14, 13, 28 }, + { 2, 3, 7, 7, 11, 7 }, +}; + +uint16_t FontSmall_codepoints_0[] = { 1,2,16,22,49,84,111,138,144,163,182,189,208,214,217,220,239,266,280,307,334,361,388,415,442,469,496,507,521,538,549,566,593,645,672,699,726,753,780,807,834,861,875,902,929,956,983,1010,1037,1064,1095,1122,1149,1176,1203,1230,1270,1297,1324,1351,1370,1389,1408,1417,1420,1423,1444,1473,1494,1523,1544,1559,1588,1617,1632,1651,1680,1695,1716,1737,1758,1787,1816,1827,1838,1851,1872,1893,1924,1945,1974,1985,2004,2023,2042, }; +uint16_t FontSmall_codepoints_1[] = { 2049,2074,2083,2092,2095, }; +uint16_t FontSmall_codepoints_2[] = { 2124, }; + +Codepoints FontSmall_codepoints[] = { + { 32, 127, FontSmall_codepoints_0 }, + { 177, 182, FontSmall_codepoints_1 }, + { 215, 216, FontSmall_codepoints_2 }, +}; + +uint8_t FontSmall_bitmaps[] = { + 0xAA, // Dummy first entry + + 0x01, // $0020 offset = 1 + + + 0x02, // $0021 offset = 2 + 0xFC,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x00,0x00,0x30,0x30, + + 0x03, // $0022 offset = 16 + 0xCC,0xCC,0xCC,0xCC,0xCC, + + 0x04, // $0023 offset = 22 + 0x0C,0x60,0x0C,0x60,0x0C,0x60,0x0C,0x60,0xFF,0xF8,0x18,0xC0,0x18,0xC0,0x18,0xC0,0xFF,0xF0,0x31,0x80,0x31,0x80,0x31,0x80,0x31,0x80, + + 0x05, // $0024 offset = 49 + 0x0C,0x00,0x0C,0x00,0x3F,0x00,0x7F,0x80,0xCC,0x80,0xCC,0x00,0xCC,0x00,0x7C,0x00,0x3F,0x00,0x0F,0x80,0x0C,0xC0,0x0C,0xC0,0x8C,0xC0,0xFF,0x80,0x7F,0x00,0x0C,0x00,0x0C,0x00, + + 0x06, // $0025 offset = 84 + 0x78,0x30,0xCC,0x20,0x84,0x40,0x84,0xC0,0x84,0x80,0xCD,0x00,0x7B,0x78,0x06,0xCC,0x04,0x84,0x0C,0x84,0x18,0x84,0x10,0xCC,0x20,0x78, + + 0x07, // $0026 offset = 111 + 0x1E,0x00,0x33,0x00,0x61,0x00,0x63,0x00,0x33,0x00,0x1C,0x00,0x3C,0x00,0x66,0x20,0xC3,0x60,0xC1,0xC0,0xC0,0xC0,0x7F,0xE0,0x3E,0x20, + + 0x08, // $0027 offset = 138 + 0xC0,0xC0,0xC0,0xC0,0xC0, + + 0x09, // $0028 offset = 144 + 0x20,0x60,0x60,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0x60,0x60,0x20, + + 0x0a, // $0029 offset = 163 + 0x80,0xC0,0xC0,0x60,0x60,0x60,0x60,0x60,0x60,0x60,0x60,0x60,0x60,0x60,0x60,0xC0,0xC0,0x80, + + 0x0b, // $002A offset = 182 + 0x20,0xA8,0x70,0x70,0xA8,0x20, + + 0x0c, // $002B offset = 189 + 0x08,0x00,0x08,0x00,0x08,0x00,0x08,0x00,0xFF,0x80,0x08,0x00,0x08,0x00,0x08,0x00,0x08,0x00, + + 0x0d, // $002C offset = 208 + 0xC0,0xC0,0x40,0xC0,0x80, + + 0x0e, // $002D offset = 214 + 0xF8,0xF8, + + 0x0f, // $002E offset = 217 + 0xC0,0xC0, + + 0x10, // $002F offset = 220 + 0x03,0x03,0x06,0x06,0x06,0x0C,0x0C,0x0C,0x18,0x18,0x18,0x30,0x30,0x60,0x60,0x60,0xC0,0xC0, + + 0x11, // $0030 offset = 239 + 0x3F,0x00,0x7F,0x80,0xE1,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xE1,0xC0,0x7F,0x80,0x3F,0x00, + + 0x12, // $0031 offset = 266 + 0xF8,0xF8,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18, + + 0x13, // $0032 offset = 280 + 0x7E,0x00,0xFF,0x00,0xC3,0x80,0x01,0x80,0x01,0x80,0x03,0x00,0x06,0x00,0x0C,0x00,0x18,0x00,0x30,0x00,0x60,0x00,0xFF,0x80,0xFF,0x80, + + 0x14, // $0033 offset = 307 + 0x7F,0x80,0x7F,0x80,0x03,0x00,0x07,0x00,0x06,0x00,0x0C,0x00,0x0F,0x80,0x01,0x80,0x00,0xC0,0x00,0xC0,0xC1,0xC0,0xFF,0x80,0x3F,0x00, + + 0x07, // $0034 offset = 334 + 0x01,0x80,0x03,0x00,0x06,0x00,0x0C,0x00,0x18,0x00,0x30,0x00,0x61,0x80,0x61,0x80,0xFF,0xE0,0xFF,0xE0,0x01,0x80,0x01,0x80,0x01,0x80, + + 0x14, // $0035 offset = 361 + 0x3F,0x80,0x3F,0x80,0x30,0x00,0x60,0x00,0x60,0x00,0x7F,0x00,0x7F,0x80,0x00,0xC0,0x00,0xC0,0x00,0xC0,0xC0,0xC0,0xFF,0x80,0x3F,0x00, + + 0x15, // $0036 offset = 388 + 0x1F,0x80,0x3F,0x80,0x60,0x00,0x40,0x00,0xC0,0x00,0xDF,0x00,0xFF,0x80,0xE1,0xC0,0xC0,0xC0,0xC0,0xC0,0xE1,0xC0,0x7F,0x80,0x1F,0x00, + + 0x13, // $0037 offset = 415 + 0xFF,0x80,0xFF,0x80,0x81,0x80,0x81,0x80,0x03,0x00,0x03,0x00,0x06,0x00,0x06,0x00,0x0C,0x00,0x0C,0x00,0x18,0x00,0x18,0x00,0x30,0x00, + + 0x15, // $0038 offset = 442 + 0x3F,0x00,0x7F,0x80,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0x61,0x80,0x7F,0x80,0xE1,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0x7F,0x80,0x3F,0x00, + + 0x16, // $0039 offset = 469 + 0x1F,0x00,0x7F,0x80,0xE1,0xC0,0xC0,0xC0,0xC0,0xC0,0xE1,0xE0,0x7F,0xE0,0x1E,0x60,0x00,0x60,0x00,0xC0,0x01,0xC0,0x3F,0x80,0x3E,0x00, + + 0x17, // $003A offset = 496 + 0xC0,0xC0,0x00,0x00,0x00,0x00,0x00,0x00,0xC0,0xC0, + + 0x18, // $003B offset = 507 + 0xC0,0xC0,0x00,0x00,0x00,0x00,0x00,0x00,0xC0,0xC0,0x40,0xC0,0x80, + + 0x19, // $003C offset = 521 + 0x01,0x80,0x0F,0x00,0x3C,0x00,0xE0,0x00,0xE0,0x00,0x3C,0x00,0x0F,0x00,0x01,0x80, + + 0x1a, // $003D offset = 538 + 0xFF,0x80,0x00,0x00,0x00,0x00,0x00,0x00,0xFF,0x80, + + 0x19, // $003E offset = 549 + 0xC0,0x00,0x78,0x00,0x1E,0x00,0x03,0x80,0x03,0x80,0x1E,0x00,0x78,0x00,0xC0,0x00, + + 0x13, // $003F offset = 566 + 0x7E,0x00,0xFF,0x00,0xC3,0x80,0x01,0x80,0x01,0x80,0x03,0x00,0x06,0x00,0x0C,0x00,0x18,0x00,0x18,0x00,0x00,0x00,0x18,0x00,0x18,0x00, + + 0x1b, // $0040 offset = 593 + 0x07,0xF0,0x00,0x1C,0x1C,0x00,0x30,0x06,0x00,0x60,0x03,0x00,0x43,0xE9,0x80,0xC6,0x38,0x80,0x8C,0x18,0x80,0x88,0x08,0x80,0x88,0x08,0x80,0x88,0x08,0x80,0x8C,0x18,0x80,0xC6,0x3D,0x80,0x43,0xE7,0x00,0x60,0x00,0x00,0x30,0x00,0x00,0x1C,0x18,0x00,0x07,0xF0,0x00, + + 0x1c, // $0041 offset = 645 + 0x03,0x00,0x03,0x00,0x07,0x80,0x04,0x80,0x0C,0xC0,0x08,0x40,0x18,0x60,0x18,0x60,0x30,0x30,0x3F,0xF0,0x60,0x18,0x60,0x18,0xC0,0x0C, + + 0x1d, // $0042 offset = 672 + 0xFF,0x80,0xFF,0xC0,0xC0,0xC0,0xC0,0x60,0xC0,0xC0,0xFF,0x80,0xFF,0xC0,0xC0,0xE0,0xC0,0x60,0xC0,0x60,0xC0,0x60,0xFF,0xE0,0xFF,0x80, + + 0x1e, // $0043 offset = 699 + 0x0F,0xC0,0x3F,0xE0,0x70,0x20,0x60,0x00,0xC0,0x00,0xC0,0x00,0xC0,0x00,0xC0,0x00,0xC0,0x00,0x60,0x00,0x70,0x20,0x3F,0xE0,0x0F,0xC0, + + 0x1f, // $0044 offset = 726 + 0xFF,0x00,0xFF,0xC0,0xC0,0xE0,0xC0,0x30,0xC0,0x30,0xC0,0x18,0xC0,0x18,0xC0,0x18,0xC0,0x30,0xC0,0x30,0xC0,0xE0,0xFF,0xC0,0xFF,0x00, + + 0x20, // $0045 offset = 753 + 0xFF,0x80,0xFF,0x80,0xC0,0x00,0xC0,0x00,0xC0,0x00,0xFF,0x00,0xFF,0x00,0xC0,0x00,0xC0,0x00,0xC0,0x00,0xC0,0x00,0xFF,0x80,0xFF,0x80, + + 0x21, // $0046 offset = 780 + 0xFF,0x80,0xFF,0x80,0xC0,0x00,0xC0,0x00,0xC0,0x00,0xC0,0x00,0xFF,0x00,0xFF,0x00,0xC0,0x00,0xC0,0x00,0xC0,0x00,0xC0,0x00,0xC0,0x00, + + 0x22, // $0047 offset = 807 + 0x0F,0xC0,0x3F,0xF0,0x70,0x20,0x60,0x00,0xC0,0x00,0xC0,0x00,0xC0,0xF0,0xC0,0xF0,0xC0,0x30,0x60,0x30,0x70,0x30,0x3F,0xF0,0x0F,0xC0, + + 0x23, // $0048 offset = 834 + 0xC0,0x60,0xC0,0x60,0xC0,0x60,0xC0,0x60,0xC0,0x60,0xFF,0xE0,0xFF,0xE0,0xC0,0x60,0xC0,0x60,0xC0,0x60,0xC0,0x60,0xC0,0x60,0xC0,0x60, + + 0x24, // $0049 offset = 861 + 0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0, + + 0x25, // $004A offset = 875 + 0x3F,0x80,0x3F,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0x01,0x80,0xC1,0x80,0x7F,0x00,0x3E,0x00, + + 0x26, // $004B offset = 902 + 0xC0,0x60,0xC0,0xC0,0xC1,0x80,0xC3,0x00,0xC6,0x00,0xCC,0x00,0xD8,0x00,0xFC,0x00,0xE6,0x00,0xC3,0x00,0xC1,0x80,0xC0,0xC0,0xC0,0x60, + + 0x27, // $004C offset = 929 + 0xC0,0x00,0xC0,0x00,0xC0,0x00,0xC0,0x00,0xC0,0x00,0xC0,0x00,0xC0,0x00,0xC0,0x00,0xC0,0x00,0xC0,0x00,0xC0,0x00,0xFF,0x80,0xFF,0x80, + + 0x28, // $004D offset = 956 + 0xC0,0x0C,0xC0,0x0C,0xE0,0x1C,0xF0,0x3C,0xF0,0x3C,0xD8,0x6C,0xC8,0x4C,0xCC,0xCC,0xC7,0x8C,0xC7,0x8C,0xC3,0x0C,0xC0,0x0C,0xC0,0x0C, + + 0x23, // $004E offset = 983 + 0xC0,0x60,0xE0,0x60,0xF0,0x60,0xF0,0x60,0xD8,0x60,0xCC,0x60,0xCE,0x60,0xC7,0x60,0xC3,0x60,0xC1,0xE0,0xC0,0xE0,0xC0,0xE0,0xC0,0x60, + + 0x06, // $004F offset = 1010 + 0x0F,0xC0,0x3F,0xF0,0x70,0x38,0x60,0x18,0xC0,0x0C,0xC0,0x0C,0xC0,0x0C,0xC0,0x0C,0xC0,0x0C,0x60,0x18,0x70,0x38,0x3F,0xF0,0x0F,0xC0, + + 0x1d, // $0050 offset = 1037 + 0xFF,0x00,0xFF,0x80,0xC0,0xC0,0xC0,0x60,0xC0,0x60,0xC0,0x60,0xC0,0xC0,0xFF,0x80,0xFF,0x00,0xC0,0x00,0xC0,0x00,0xC0,0x00,0xC0,0x00, + + 0x29, // $0051 offset = 1064 + 0x0F,0xC0,0x3F,0xF0,0x70,0x38,0x60,0x18,0xC0,0x0C,0xC0,0x0C,0xC0,0x0C,0xC0,0x0C,0xC0,0x0C,0x60,0x18,0x70,0x38,0x3F,0xF0,0x0F,0xC0,0x01,0xC4,0x00,0xFC, + + 0x1d, // $0052 offset = 1095 + 0xFF,0x00,0xFF,0x80,0xC0,0xC0,0xC0,0x60,0xC0,0x60,0xC0,0x60,0xC0,0xC0,0xFF,0x80,0xFF,0x00,0xC3,0x00,0xC1,0x80,0xC0,0xC0,0xC0,0xC0, + + 0x15, // $0053 offset = 1122 + 0x3F,0x00,0x7F,0x80,0xC0,0xC0,0xC0,0x00,0xC0,0x00,0x60,0x00,0x3E,0x00,0x0F,0x80,0x01,0xC0,0x00,0xC0,0xC0,0xC0,0xFF,0x80,0x3F,0x00, + + 0x2a, // $0054 offset = 1149 + 0xFF,0xC0,0xFF,0xC0,0x0C,0x00,0x0C,0x00,0x0C,0x00,0x0C,0x00,0x0C,0x00,0x0C,0x00,0x0C,0x00,0x0C,0x00,0x0C,0x00,0x0C,0x00,0x0C,0x00, + + 0x23, // $0055 offset = 1176 + 0xC0,0x60,0xC0,0x60,0xC0,0x60,0xC0,0x60,0xC0,0x60,0xC0,0x60,0xC0,0x60,0xC0,0x60,0xC0,0x60,0xC0,0x60,0x60,0xC0,0x7F,0xC0,0x1F,0x00, + + 0x04, // $0056 offset = 1203 + 0xC0,0x18,0xC0,0x18,0x60,0x30,0x60,0x30,0x30,0x60,0x30,0x60,0x18,0xC0,0x18,0xC0,0x0D,0x80,0x0D,0x80,0x07,0x00,0x07,0x00,0x02,0x00, + + 0x2b, // $0057 offset = 1230 + 0xC0,0x40,0x60,0xC0,0xE0,0x60,0xC0,0xE0,0x60,0x61,0xB0,0xC0,0x61,0xB0,0xC0,0x61,0xB0,0xC0,0x33,0x19,0x80,0x33,0x19,0x80,0x33,0x19,0x80,0x1E,0x0F,0x00,0x1E,0x0F,0x00,0x1E,0x07,0x00,0x0C,0x06,0x00, + + 0x07, // $0058 offset = 1270 + 0xC0,0x60,0x60,0xC0,0x31,0x80,0x1B,0x00,0x1B,0x00,0x0E,0x00,0x0E,0x00,0x0E,0x00,0x1B,0x00,0x1B,0x00,0x31,0x80,0x60,0xC0,0xC0,0x60, + + 0x2c, // $0059 offset = 1297 + 0xC0,0x30,0x60,0x60,0x60,0x60,0x30,0xC0,0x19,0x80,0x19,0x80,0x0F,0x00,0x07,0x00,0x06,0x00,0x06,0x00,0x06,0x00,0x06,0x00,0x06,0x00, + + 0x2d, // $005A offset = 1324 + 0xFF,0xE0,0xFF,0xE0,0x01,0xC0,0x01,0x80,0x03,0x00,0x06,0x00,0x0C,0x00,0x18,0x00,0x38,0x00,0x30,0x00,0x60,0x00,0xFF,0xE0,0xFF,0xE0, + + 0x2e, // $005B offset = 1351 + 0xF0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xF0, + + 0x2f, // $005C offset = 1370 + 0xC0,0xC0,0x60,0x60,0x60,0x30,0x30,0x30,0x18,0x18,0x18,0x0C,0x0C,0x06,0x06,0x06,0x03,0x03, + + 0x30, // $005D offset = 1389 + 0xF0,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0xF0, + + 0x31, // $005E offset = 1408 + 0x10,0x38,0x28,0x6C,0x44,0xC6,0x82,0x82, + + 0x32, // $005F offset = 1417 + 0xFF,0x80, + + 0x33, // $0060 offset = 1420 + 0xC0,0x70, + + 0x34, // $0061 offset = 1423 + 0x3E,0x00,0xFF,0x00,0x03,0x80,0x01,0x80,0x7F,0x80,0xE3,0x80,0xC1,0x80,0xC3,0x80,0xE7,0x80,0x7D,0x80, + + 0x35, // $0062 offset = 1444 + 0xC0,0x00,0xC0,0x00,0xC0,0x00,0xC0,0x00,0xDE,0x00,0xFF,0x80,0xC1,0x80,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC1,0x80,0xFF,0x80,0x9E,0x00, + + 0x34, // $0063 offset = 1473 + 0x1E,0x00,0x7F,0x80,0xE1,0x00,0xC0,0x00,0xC0,0x00,0xC0,0x00,0xC0,0x00,0xE1,0x00,0x7F,0x80,0x1E,0x00, + + 0x36, // $0064 offset = 1494 + 0x00,0xC0,0x00,0xC0,0x00,0xC0,0x00,0xC0,0x3E,0xC0,0x7F,0xC0,0xE1,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xE1,0xC0,0x7F,0xC0,0x3E,0xC0, + + 0x37, // $0065 offset = 1523 + 0x3F,0x00,0x7F,0x80,0xC1,0x80,0xC0,0xC0,0xFF,0xC0,0xC0,0x00,0xC0,0x00,0xE0,0x00,0x7F,0x80,0x1F,0x00, + + 0x38, // $0066 offset = 1544 + 0x1E,0x3B,0x30,0x30,0x30,0xFE,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30, + + 0x39, // $0067 offset = 1559 + 0x3E,0xC0,0x7F,0xC0,0xE0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0x61,0xC0,0x3F,0xC0,0x1E,0xC0,0x00,0xC0,0x40,0xC0,0xFF,0x80,0x3F,0x00, + + 0x3a, // $0068 offset = 1588 + 0xC0,0x00,0xC0,0x00,0xC0,0x00,0xC0,0x00,0xDE,0x00,0xFF,0x00,0xC1,0x80,0xC1,0x80,0xC1,0x80,0xC1,0x80,0xC1,0x80,0xC1,0x80,0xC1,0x80,0xC1,0x80, + + 0x3b, // $0069 offset = 1617 + 0xC0,0xC0,0x00,0x00,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0, + + 0x3c, // $006A offset = 1632 + 0x06,0x06,0x00,0x00,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0xC6,0x7C,0x78, + + 0x3d, // $006B offset = 1651 + 0xC0,0x00,0xC0,0x00,0xC0,0x00,0xC0,0x00,0xC3,0x00,0xC6,0x00,0xCC,0x00,0xD8,0x00,0xF0,0x00,0xF8,0x00,0xCC,0x00,0xC6,0x00,0xC3,0x00,0xC1,0x80, + + 0x3b, // $006C offset = 1680 + 0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0, + + 0x3e, // $006D offset = 1695 + 0xDE,0x3C,0xFF,0x7E,0xE1,0xC3,0xC1,0x83,0xC1,0x83,0xC1,0x83,0xC1,0x83,0xC1,0x83,0xC1,0x83,0xC1,0x83, + + 0x3f, // $006E offset = 1716 + 0xDE,0x00,0xFF,0x00,0xC1,0x80,0xC1,0x80,0xC1,0x80,0xC1,0x80,0xC1,0x80,0xC1,0x80,0xC1,0x80,0xC1,0x80, + + 0x40, // $006F offset = 1737 + 0x1E,0x00,0x7F,0x80,0xE1,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xE1,0xC0,0x7F,0x80,0x1E,0x00, + + 0x41, // $0070 offset = 1758 + 0xDE,0x00,0xFF,0x80,0xC1,0x80,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC1,0x80,0xFF,0x80,0xDE,0x00,0xC0,0x00,0xC0,0x00,0xC0,0x00,0xC0,0x00, + + 0x42, // $0071 offset = 1787 + 0x3E,0xC0,0x7F,0xC0,0xE1,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xE1,0xC0,0x7F,0xC0,0x3E,0xC0,0x00,0xC0,0x00,0xC0,0x00,0xF8,0x00,0xE0, + + 0x43, // $0072 offset = 1816 + 0xD8,0xFC,0xE4,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0, + + 0x44, // $0073 offset = 1827 + 0x3E,0x7F,0xC0,0xC0,0x70,0x1E,0x03,0x03,0xFE,0x7C, + + 0x45, // $0074 offset = 1838 + 0x30,0x30,0xFE,0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x3E,0x1E, + + 0x3f, // $0075 offset = 1851 + 0xC1,0x80,0xC1,0x80,0xC1,0x80,0xC1,0x80,0xC1,0x80,0xC1,0x80,0xC1,0x80,0xE3,0x80,0xFF,0x80,0x3D,0x80, + + 0x46, // $0076 offset = 1872 + 0xC0,0x60,0x60,0xC0,0x60,0xC0,0x31,0x80,0x31,0x80,0x1B,0x00,0x1B,0x00,0x0E,0x00,0x0E,0x00,0x04,0x00, + + 0x47, // $0077 offset = 1893 + 0xC0,0x81,0x80,0x41,0xC1,0x00,0x61,0xC3,0x00,0x63,0x63,0x00,0x23,0x62,0x00,0x32,0x26,0x00,0x16,0x34,0x00,0x1C,0x3C,0x00,0x1C,0x1C,0x00,0x0C,0x18,0x00, + + 0x48, // $0078 offset = 1924 + 0xC1,0x80,0x63,0x00,0x36,0x00,0x1C,0x00,0x1C,0x00,0x1C,0x00,0x1C,0x00,0x36,0x00,0x63,0x00,0xC1,0x80, + + 0x49, // $0079 offset = 1945 + 0xC0,0x60,0x60,0xC0,0x60,0xC0,0x60,0x80,0x31,0x80,0x33,0x00,0x1B,0x00,0x1A,0x00,0x0E,0x00,0x0C,0x00,0x0C,0x00,0x0C,0x00,0xF8,0x00,0xF0,0x00, + + 0x4a, // $007A offset = 1974 + 0xFF,0x03,0x06,0x0C,0x18,0x30,0x30,0x60,0xC0,0xFF, + + 0x4b, // $007B offset = 1985 + 0x1C,0x38,0x30,0x30,0x30,0x30,0x30,0x70,0xE0,0x70,0x30,0x30,0x30,0x30,0x30,0x30,0x38,0x1C, + + 0x4c, // $007C offset = 2004 + 0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0, + + 0x4d, // $007D offset = 2023 + 0xE0,0x70,0x30,0x30,0x30,0x30,0x30,0x38,0x1C,0x38,0x30,0x30,0x30,0x30,0x30,0x30,0x70,0xE0, + + 0x4e, // $007E offset = 2042 + 0x78,0x80,0xCD,0x80,0x87,0x00, + + 0x4f, // $00B1 offset = 2049 + 0x08,0x00,0x08,0x00,0x08,0x00,0x08,0x00,0xFF,0x80,0x08,0x00,0x08,0x00,0x08,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xFF,0x80, + + 0x50, // $00B2 offset = 2074 + 0xF8,0x8C,0x0C,0x08,0x18,0x20,0x40,0xFC, + + 0x50, // $00B3 offset = 2083 + 0xFC,0x08,0x10,0x38,0x0C,0x04,0x8C,0xF8, + + 0x51, // $00B4 offset = 2092 + 0x60,0xC0, + + 0x52, // $00B5 offset = 2095 + 0xC1,0x80,0xC1,0x80,0xC1,0x80,0xC1,0x80,0xC1,0x80,0xC1,0x80,0xC1,0x80,0xC3,0x80,0xFF,0x80,0xFD,0x80,0xC0,0x00,0xC0,0x00,0xC0,0x00,0xC0,0x00, + + 0x53, // $00D7 offset = 2124 + 0x84,0xCE,0x7C,0x38,0x78,0xEC,0xC4, + + }; + +Font FontSmall = { + 18, + 10, + 18, + 4, + 28, + 32, + 216, + 3, + FontSmall_bboxes, + FontSmall_codepoints, + FontSmall_bitmaps, +}; + diff --git a/ports/stm32/boards/Passport/common/pprng.c b/ports/stm32/boards/Passport/common/pprng.c index 33eac32..7991ba3 100644 --- a/ports/stm32/boards/Passport/common/pprng.c +++ b/ports/stm32/boards/Passport/common/pprng.c @@ -1,7 +1,7 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // -// SPDX-FileCopyrightText: 2018 Coinkite, Inc. +// SPDX-FileCopyrightText: 2018 Coinkite, Inc. // SPDX-License-Identifier: GPL-3.0-only // /* diff --git a/ports/stm32/boards/Passport/common/ring_buffer.c b/ports/stm32/boards/Passport/common/ring_buffer.c new file mode 100644 index 0000000..16b307e --- /dev/null +++ b/ports/stm32/boards/Passport/common/ring_buffer.c @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-License-Identifier: GPL-3.0-or-later +// +#include + +#include "ring_buffer.h" + +static ring_buffer_t keybuf; + +/** + * Code adapted from https://github.com/AndersKaloer/Ring-Buffer + */ + +int ring_buffer_init(void) +{ + keybuf.size = MAX_RING_BUFFER_SIZE; + keybuf.size_plus1 = MAX_RING_BUFFER_SIZE + 1; + keybuf.head_index = 0; + keybuf.tail_index = 0; + return 0; +} + +void ring_buffer_enqueue(uint8_t data) +{ + if (ring_buffer_is_full()) { + keybuf.tail_index = ((keybuf.tail_index + 1) % keybuf.size_plus1); + } + + keybuf.buffer[keybuf.head_index] = data; + keybuf.head_index = ((keybuf.head_index + 1) % keybuf.size_plus1); +} + +uint8_t ring_buffer_dequeue(uint8_t* data) +{ + if (ring_buffer_is_empty()) { + return 0; + } + + *data = keybuf.buffer[keybuf.tail_index]; + keybuf.tail_index = ((keybuf.tail_index + 1) % keybuf.size_plus1); + return 1; +} + +uint8_t ring_buffer_peek(uint8_t* data, ring_buffer_size_t index) +{ + if (index >= ring_buffer_num_items()) { + return 0; + } + + ring_buffer_size_t data_index = ((keybuf.tail_index + index) % keybuf.size_plus1); + *data = keybuf.buffer[data_index]; + return 1; +} + +uint8_t ring_buffer_is_empty(void) +{ + uint8_t result = (keybuf.head_index == keybuf.tail_index); + return result; +} + +uint8_t ring_buffer_is_full(void) +{ + uint8_t num_items = ring_buffer_num_items(); + uint8_t result = num_items == keybuf.size; + return result; +} + +ring_buffer_size_t ring_buffer_num_items(void) +{ + uint8_t result = (keybuf.head_index + keybuf.size_plus1 - keybuf.tail_index) % keybuf.size_plus1; + return result; +} diff --git a/ports/stm32/boards/Passport/common/se.c b/ports/stm32/boards/Passport/common/se.c index 91e2c12..3555698 100644 --- a/ports/stm32/boards/Passport/common/se.c +++ b/ports/stm32/boards/Passport/common/se.c @@ -1,9 +1,10 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // -// SPDX-FileCopyrightText: 2018 Coinkite, Inc. +// SPDX-FileCopyrightText: 2018 Coinkite, Inc. // SPDX-License-Identifier: GPL-3.0-only // +#include #include #include #include @@ -21,11 +22,11 @@ #include "utils.h" #ifndef PASSPORT_BOOTLOADER -#include "lcd-sharp-ls018B7dh02.h" +#include "display.h" #endif /* PASSPORT_BOOTLOADER */ /* - * This bit should be define in the STM32H7 header files but it is not... + * This bit should be defined in the STM32H7 header files but it is not... * somehow was missed. It is a valid bit in the interrupt status register * so we'll define it here so as not to mess with the micropython HAL * installation. @@ -35,8 +36,24 @@ // "one wire" is on PA0 aka. UART4 #define MY_UART UART4 +/* SE error codes */ +#define SE_SUCCESS 0x00 +#define SE_CHECKMAC_MISCOMPARE 0x01 +#define SE_PARSE_ERROR 0x03 +#define SE_ECC_FAULT 0x05 +#define SE_SELF_TEST_ERROR 0x07 +#define SE_EXECUTION_ERROR 0x0F +#define SE_WAKE_ACK 0x11 +#define SE_WATCHDOG_EXPIRING 0xEE +#define SE_COMMS_ERROR 0xFF + +/* SE extended error codes */ +#define SE_EX_RETRY_OUT 0xE0 + #define STATS(x) +static uint8_t last_error; + uint32_t crc_errors; uint32_t not_ready_n; uint32_t short_error; @@ -66,6 +83,38 @@ typedef enum { IOFLAG_SLEEP = 0xCC, } ioflag_t; +#ifndef PASSPORT_BOOTLOADER +static char se_error[23]; +static char *error_to_str(uint8_t error) +{ + switch (error) + { + case SE_SUCCESS: strcpy(se_error, "SE_SUCCESS"); break; + case SE_CHECKMAC_MISCOMPARE: strcpy(se_error, "SE_CHECKMAC_MISCOMPARE"); break; + case SE_PARSE_ERROR: strcpy(se_error, "SE_PARSE_ERROR"); break; + case SE_ECC_FAULT: strcpy(se_error, "SE_ECC_FAULT"); break; + case SE_SELF_TEST_ERROR: strcpy(se_error, "SE_SELF_TEST_ERROR"); break; + case SE_EXECUTION_ERROR: strcpy(se_error, "SE_EXECUTION_ERROR"); break; + case SE_WAKE_ACK: strcpy(se_error, "SE_WAKE_ACK"); break; + case SE_WATCHDOG_EXPIRING: strcpy(se_error, "SE_WATCHDOG_EXPIRING"); break; + case SE_COMMS_ERROR: strcpy(se_error, "SE_COMMS_ERROR"); break; + case SE_EX_RETRY_OUT: strcpy(se_error, "SE_EX_RETRY_OUT"); break; + default: strcpy(se_error, "unknown error"); break; + } + return se_error; +} +#endif /* PASSPORT_BOOTLOADER */ + +uint8_t se_show_error(void) +{ + uint8_t error = last_error; +#ifndef PASSPORT_BOOTLOADER + printf("[%s] last SE error: %s, (%02X)\n", __func__, error_to_str(last_error), last_error); +#endif /* PASSPORT_BOOTLOADER */ + last_error = 0; + return error; +} + static inline void _send_byte(uint8_t ch) { // reset timeout timer (Systick) @@ -135,11 +184,8 @@ static inline int _read_byte(void) // "fast" timeout reached, clear flag ++rtof; MY_UART->ICR = USART_ICR_RTOCF; - return -1; } -#ifdef FIXME - INCONSISTENT("rxf"); -#endif + return -1; } @@ -271,9 +317,6 @@ void se_write(seopcode_t opcode, uint8_t p1, uint16_t p2, uint8_t *data, uint8_t .p2_lsb = p2 & 0xff, .p2_msb = (p2 >> 8) & 0xff, }; -#ifdef FIXME - STATIC_ASSERT(sizeof(known) == 6); -#endif STATS(last_op = opcode); STATS(last_p1 = p1); STATS(last_p2 = p2); @@ -296,16 +339,12 @@ void se_write(seopcode_t opcode, uint8_t p1, uint16_t p2, uint8_t *data, uint8_t // insert a variable-length body area (sometimes) if (data_len) { _send_serialized(data, data_len); - + se_crc16_chain(data_len, data, crc); } // send final CRC bytes _send_serialized(crc, 2); - -#ifndef PASSPORT_BOOTLOADER - lcd_show_busy_bar(); -#endif /* PASSPORT_BOOTLOADER */ } int se_read(uint8_t *data, uint8_t len) @@ -342,12 +381,14 @@ int se_read(uint8_t *data, uint8_t len) len_error++; if (resp_len == 4) { /* Error code returned */ - ERRV(tmp[1], "ae errcode"); + ERRV(tmp[1], "se errcode"); len_error_two++; if (tmp[1] == 0xEE) wdgtimeout++; - return -1; + + last_error = tmp[1]; + goto out; } ERRV(tmp[0], "wr len"); goto try_again; @@ -356,13 +397,14 @@ int se_read(uint8_t *data, uint8_t len) if (!check_crc(tmp, actual)) { ERR("bad crc"); crc_errors++; + last_error = SE_COMMS_ERROR; goto try_again; } } memcpy(data, tmp + 1, actual - 3); - /* + /* * Pause the watchdog in case there's more to do * NOTE: Requires a wake commmand to resume! */ @@ -374,6 +416,10 @@ try_again: ln_retry++; } retry_out++; + last_error = SE_EX_RETRY_OUT; + +out: + se_show_error(); return -1; } @@ -388,6 +434,51 @@ int se_read1(void) return data; } +int se_read_data_slot(int slot_num, uint8_t *data, int len) +{ + int rc; + int rval = 0; +#ifdef FIXME + ASSERT((len == 4) || (len == 32) || (len == 72)); +#endif + // zone => data + // only reading first block of 32 bytes. ignore the rest + se_write(OP_Read, (len == 4 ? 0x00 : 0x80) | 2, (slot_num<<3), NULL, 0); + rc = se_read(data, (len == 4) ? 4 : 32); + if (rc < 0) + { + rval = -1; + goto out; + } + + if (len == 72) { + // read second block + se_write(OP_Read, 0x82, (1<<8) | (slot_num<<3), NULL, 0); + rc = se_read(data+32, 32); + if (rc < 0) + { + rval = -1; + goto out; + } + + // read third block, but only using part of it + uint8_t tmp[32]; + se_write(OP_Read, 0x82, (2<<8) | (slot_num<<3), NULL, 0); + rc = se_read(tmp, 32); + if (rc < 0) + { + rval = -1; + goto out; + } + + memcpy(data+64, tmp, 72-64); + } + +out: + se_sleep(); + return rval; +} + void se_crc16_chain(uint8_t length, const uint8_t *data, uint8_t crc[2]) { uint8_t counter; @@ -433,7 +524,7 @@ int se_wake(void) #ifdef PASSPORT_BOOTLOADER delay_us(2500); #else - delay_us(100); + delay_us(1250); #endif return 0; @@ -571,7 +662,7 @@ bool se_is_correct_tempkey( uint8_t resp[32]; int rc; - se_write(OP_MAC, mode, KEYNUM_pairing, NULL, 0); + se_write(OP_MAC, mode, KEYNUM_pairing_secret, NULL, 0); rc = se_read(resp, 32); se_sleep(); if (rc < 0) @@ -584,7 +675,7 @@ bool se_is_correct_tempkey( sha256_update(&ctx, rom_secrets->pairing_secret, 32); sha256_update(&ctx, expected_tempkey, 32); - const uint8_t fixed[16] = { OP_MAC, mode, KEYNUM_pairing, 0x0, + const uint8_t fixed[16] = { OP_MAC, mode, KEYNUM_pairing_secret, 0x0, 0,0,0,0, 0,0,0,0, // eight zeros 0,0,0, // three zeros 0xEE }; @@ -606,7 +697,7 @@ int se_pair_unlock() int rc; int attempts = 3; for (int i = 0; i < attempts; i++) { - rc = se_checkmac(KEYNUM_pairing, rom_secrets->pairing_secret); + rc = se_checkmac(KEYNUM_pairing_secret, rom_secrets->pairing_secret); if (rc == 0) return 0; } @@ -673,7 +764,7 @@ int se_checkmac( sha256_final(&ctx, req.resp); memcpy(req.od, od, 13); - // Give our answer to the chip. The 0x01 means that TempKey holds + // Give our answer to the chip. The 0x01 means that TempKey holds // the second 32 byte value. First 32 byte value is in key slot 1 (pairing secret). se_write(OP_CheckMac, 0x01, keynum, (uint8_t *)&req, sizeof(req)); rc = se_read1(); @@ -715,7 +806,72 @@ int se_checkmac_hard( return 0; } -int se_encrypted_write32( +static int se_encrypted_read32( + int data_slot, + int blk, + int read_kn, + const uint8_t *read_key, + uint8_t *data +) +{ + int rc; + uint8_t digest[32]; + + rc = se_pair_unlock(); + if (rc < 0) + return -1; + + rc = se_gendig_slot(read_kn, read_key, digest); + if (rc < 0) + return -1; + + // read nth 32-byte "block" + se_write(OP_Read, 0x82, (blk << 8) | (data_slot<<3), NULL, 0); + rc = se_read(data, 32); + se_sleep(); + if (rc < 0) + return -1; + + xor_mixin(data, digest, 32); + + return 0; +} + +int se_encrypted_read( + int data_slot, + int read_kn, + const uint8_t *read_key, + uint8_t *data, + int len +) +{ + int rc; +#ifdef FIXME + // not clear if chip supports 4-byte encrypted reads + ASSERT((len == 32) || (len == 72)); +#endif + rc = se_encrypted_read32(data_slot, 0, read_kn, read_key, data); + if (rc < 0) + return -1; + + if (len == 32) + return 0; + + rc = se_encrypted_read32(data_slot, 1, read_kn, read_key, data+32); + if (rc < 0) + return -1; + + uint8_t tmp[32]; + rc = se_encrypted_read32(data_slot, 2, read_kn, read_key, tmp); + if (rc < 0) + return -1; + + memcpy(data+64, tmp, 72-64); + + return 0; +} + +static int se_encrypted_write32( int data_slot, int blk, int write_kn, @@ -882,3 +1038,112 @@ void se_setup(void) // finally enable UART MY_UART->CR1 |= USART_CR1_UE; } + +// Just read a one-way counter. +// +int se_get_counter(uint32_t *result, uint8_t counter_number) +{ + int rc; + + se_write(OP_Counter, 0x0, counter_number, NULL, 0); + rc = se_read((uint8_t *)result, 4); + se_sleep(); + if (rc < 0) + return -1; + + // IMPORTANT: Always verify the counter's value because otherwise + // nothing prevents an active MitM changing the value that we think + // we just read. + uint8_t digest[32]; + rc = se_gendig_counter(counter_number, *result, digest); + if (rc < 0) + return -1; + + if (!se_is_correct_tempkey(digest)) + return -1; + + return 0; +} + +// Add-to and return a one-way counter's value. Have to go up in +// single-unit steps, but can we loop. +// +int se_add_counter(uint32_t *result, uint8_t counter_number, int incr) +{ + int rc; + int rval = 0; + + for (int i = 0; i < incr; i++) { + se_write(OP_Counter, 0x1, counter_number, NULL, 0); + rc = se_read((uint8_t *)result, 4); + if (rc < 0) + { + rval = -1; + goto out; + } + } + + // IMPORTANT: Always verify the counter's value because otherwise + // nothing prevents an active MitM changing the value that we think + // we just read. They could also stop us from incrementing the counter. + + uint8_t digest[32]; + rc = se_gendig_counter(counter_number, *result, digest); + if (rc < 0) + { + rval = -1; + goto out; + } + + if (!se_is_correct_tempkey(digest)) + rval = -1; + +out: + se_sleep(); + return rval; +} + +// Construct a digest over one of the two counters. Track what we think +// the digest should be, and ask the chip to do the same. Verify we match +// using MAC command (done elsewhere). +// +int se_gendig_counter(int counter_num, const uint32_t expected_value, uint8_t digest[32]) +{ + int rc; + uint8_t num_in[20], tempkey[32]; + + rng_buffer(num_in, sizeof(num_in)); + + rc = se_pick_nonce(num_in, tempkey); + if (rc < 0) + return -1; + + //using Zone=4="Counter" => "KeyID specifies the monotonic counter ID" + se_write(OP_GenDig, 0x4, counter_num, NULL, 0); + rc = se_read1(); + se_sleep(); + if (rc != 0) + return -1; + + // we now have to match the digesting (hashing) that has happened on + // the chip. No feedback at this point if it's right tho. + // + // msg = hkey + b'\x15\x02' + ustruct.pack(" +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // -// SPDX-FileCopyrightText: 2018 Coinkite, Inc. +// SPDX-FileCopyrightText: 2018 Coinkite, Inc. // SPDX-License-Identifier: GPL-3.0-only // /* diff --git a/ports/stm32/boards/Passport/common/utils.c b/ports/stm32/boards/Passport/common/utils.c index e01d897..ed04449 100644 --- a/ports/stm32/boards/Passport/common/utils.c +++ b/ports/stm32/boards/Passport/common/utils.c @@ -1,13 +1,14 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // -// SPDX-FileCopyrightText: 2018 Coinkite, Inc. +// SPDX-FileCopyrightText: 2018 Coinkite, Inc. // SPDX-License-Identifier: GPL-3.0-only // /* * (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard * and is covered by GPLv3 license found in COPYING. */ +#include #include "utils.h" // Return T if all bytes are 0xFF @@ -75,3 +76,94 @@ void xor_mixin(uint8_t *acc, uint8_t *more, int len) *(acc) ^= *(more); } } + + +char hex_map[16] = {'0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', }; + +void to_hex(char* buf, uint8_t value) { + buf[0] = hex_map[value >> 4]; + buf[1] = hex_map[value & 0xF]; + buf[2]=0; +} + +// Assumes str is big enough to hold len*2 + 1 bytes +void bytes_to_hex_str(uint8_t* bytes, uint32_t len, char* str, uint32_t split_every, char split_char) { + for (uint32_t i=0; i max_diff) { + max_diff = -diff; + } + } + + bool sentinel_overwritten = !check_stack_sentinel(); + + // Only print if there is a problem + if (print) { + printf("%s: (sp=0x%08lx, Diff=%ld, Max Diff=%ld : %s, %s)\n", + msg, + sp, + diff, + max_diff, + sp <= MIN_SP ? "BLOWN!" : "OK", + sentinel_overwritten ? "SENTINEL OVERWRITTEN!" : "OK"); + } + + return !sentinel_overwritten; +} + +#endif /* PASSPORT_BOOTLOADER */ diff --git a/ports/stm32/boards/Passport/debug-utils.c b/ports/stm32/boards/Passport/debug-utils.c index 3946686..025cca1 100644 --- a/ports/stm32/boards/Passport/debug-utils.c +++ b/ports/stm32/boards/Passport/debug-utils.c @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // // Utility functions diff --git a/ports/stm32/boards/Passport/debug-utils.h b/ports/stm32/boards/Passport/debug-utils.h index c6d0f43..5ee1172 100644 --- a/ports/stm32/boards/Passport/debug-utils.h +++ b/ports/stm32/boards/Passport/debug-utils.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // // Utility functions diff --git a/ports/stm32/boards/Passport/dispatch.c b/ports/stm32/boards/Passport/dispatch.c index 21085cc..f184659 100644 --- a/ports/stm32/boards/Passport/dispatch.c +++ b/ports/stm32/boards/Passport/dispatch.c @@ -1,7 +1,7 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // -// SPDX-FileCopyrightText: 2018 Coinkite, Inc. +// SPDX-FileCopyrightText: 2018 Coinkite, Inc. // SPDX-License-Identifier: GPL-3.0-only // /* @@ -44,78 +44,6 @@ static inline void memset4(uint32_t *dest, uint32_t value, uint32_t byte_len) } } -// wipe_all_sram() -// -static void wipe_all_sram(void) -{ -#ifndef FIXME - return; -#else - const uint32_t noise = 0xdeadbeef; - - // wipe all of SRAM (except our own memory, which was already wiped) - memset4((void *)D1_AXISRAM_BASE, noise, D1_AXISRAM_SIZE_MAX); - memset4((void *)SRAM2_BASE, noise, SRAM2_SIZE - BL_SRAM_SIZE); -#endif /* FIXME */ -} - -// fatal_error(const char *msg) -// - void -fatal_error(const char *msgvoid) -{ -#ifdef FIXME - oled_setup(); - oled_show(screen_fatal); -#endif - // Maybe should do a reset after a delay, like with - // the watchdog timer or something. - LOCKUP_FOREVER(); -} - -// fatal_mitm() -// -void fatal_mitm(void) -{ -#ifdef FIXME - oled_setup(); - oled_show(screen_mitm); -#endif -#ifdef RELEASE - wipe_all_sram(); -#endif - printf("====================================!\n"); - printf("FATAL MITM ATTACK! LOOPING FOREVER!\n"); - printf("====================================!\n"); - LOCKUP_FOREVER(); -} - -static int good_addr(const uint8_t *b, int minlen, int len, bool readonly) -{ - uint32_t x = (uint32_t)b; - - if (minlen) { - if (!b) return EFAULT; // gave no buffer - if (len < minlen) return ERANGE; // too small - } - - if ((x >= D1_AXISRAM_BASE) && ((x - D1_AXISRAM_BASE) < D1_AXISRAM_SIZE_MAX)) { - // inside SRAM1, okay - return 0; - } - - if (!readonly) { - return EPERM; - } -#ifdef FIXME - if ((x >= FIRMWARE_START) && (x - FIRMWARE_START) < FW_MAX_LENGTH) { - // inside flash of main firmware (happens for QSTR's) - return 0; - } -#endif /* FIXME */ - return EACCES; -} - // se_dispatch() // // A C-runtime compatible env. is running, so do some work. @@ -136,7 +64,7 @@ int se_dispatch( // - range check pointers so we aren't tricked into revealing our secrets // - check buf_io points to main SRAM, and not into us! // - range check len_in tightly - // - calling convention only gives me enough for 4 args to this function, so + // - calling convention only gives me enough for 4 args to this function, so // using read/write in place. // - use arg2 use when a simple number is needed; never a pointer! // - mpy may provide a pointer to flash if we give it a qstr or small value, and if @@ -148,149 +76,33 @@ int se_dispatch( } // Use these macros -#define REQUIRE_IN_ONLY(x) if ((rv = good_addr(buf_io, (x), len_in, true))) { goto fail; } -#define REQUIRE_OUT(x) if ((rv = good_addr(buf_io, (x), len_in, false))) { goto fail; } - - printf("se_dispatch() method_num=%d\n", method_num); - switch(method_num) { - case CMD_GET_BOOTLOADER_VERSION: { - REQUIRE_OUT(64); - - // Return my version string - memset(buf_io, 0, len_in); -#ifdef FIXME - strlcpy((char *)buf_io, version_string, len_in); -#else - memcpy(buf_io, version_string, len_in); -#endif - rv = strlen(version_string); - - break; - } -#ifdef FIXME - case CMD_GET_FIRMWARE_HASH: { - // Perform SHA256 over ourselves, with 32-bits of salt, to imply we - // haven't stored valid responses. - REQUIRE_OUT(32); - - SHA256_CTX ctx; - sha256_init(&ctx); - sha256_update(&ctx, (void *)&arg2, 4); - sha256_update(&ctx, (void *)BL_FLASH_BASE, BL_FLASH_SIZE); - sha256_final(&ctx, buf_io); - - break; - } -#endif /* FIXME */ -#ifdef FIXME - case CMD_UPGRADE_FIRMWARE: { - const uint8_t *scr; - bool secure = flash_is_security_level2(); - - // Go into DFU mode. It's a one-way trip. - // Also used to show some "fatal" screens w/ memory wipe. +#define REQUIRE_OUT(x) if (len_in < x) { goto fail; } - switch (arg2) { - default: - case 0: // TODO: define constants once these are understood - // enter DFU for firmware upgrades - if (secure) { - // we cannot support DFU in secure mode anymore - rv = EPERM; - goto fail; - } - scr = screen_dfu; - break; - case 1: - // in case some way for Micropython to detect it. - scr = screen_downgrade; - break; - case 2: - scr = screen_blankish; - break; - case 3: - scr = screen_brick; - secure = true; // no point going into DFU, if even possible - break; - } + // printf("se_dispatch() method_num=%d\n", method_num); - oled_setup(); - oled_show(scr); + // Random small delay to make cold-boot stepping attacks harder: 0 - 10,000us + uint32_t us_to_delay = rng_sample() % 10000; + delay_us(us_to_delay); - wipe_all_sram(); - - if (secure) { - // just die with that message shown; can't start DFU - LOCKUP_FOREVER(); - } else { - // Cannot just call enter_dfu() because it doesn't work well - // once Micropython has configured so much stuff in the chip. - - // Leave a reminder to ourselves - memcpy(dfu_flag->magic, REBOOT_TO_DFU, sizeof(dfu_flag->magic)); - dfu_flag->screen = scr; - - // reset system - NVIC_SystemReset(); - - // NOT-REACHED - } - break; - } -#endif /* FIXME */ - case CMD_RESET: - // logout: wipe all of memory and lock up. Must powercycle to recover. - switch (arg2) { - case 0: - case 2: -#ifdef FIXME - oled_show(screen_logout); -#endif /* FIXME */ - break; - case 1: - // leave screen untouched - break; - } - - wipe_all_sram(); - - if (arg2 == 2) { - // need some time to show OLED contents - delay_ms(100); - - // reboot so we can "login" again - NVIC_SystemReset(); - - // NOT-REACHED (but ok if it does) - } - - // wait for an interrupt which will never happen (ie. sleep) - LOCKUP_FOREVER() - break; - - case CMD_IS_BRICKED: - // Are we a brick? - // if the pairing secret doesn't work anymore, that - // means we've been bricked. - // TODO: also report hardware issue, and non-configured states - se_setup(); + switch(method_num) { + case CMD_IS_BRICKED: + // If the pairing secret doesn't work anymore, that means we've been bricked. rv = (se_pair_unlock() != 0); break; case CMD_READ_SE_SLOT: { - // Read a dataslot directly. Will fail on + // Read a dataslot directly. Will fail on // encrypted slots. if (len_in != 4 && len_in != 32 && len_in != 72) { rv = ERANGE; } else { REQUIRE_OUT(4); - se_setup(); if (se_read_data_slot(arg2 & 0xf, buf_io, len_in)) { rv = EIO; } } - + break; } @@ -339,15 +151,11 @@ int se_dispatch( case PIN_GET_SECRET: rv = pin_fetch_secret(args); break; - case PIN_LONG_SECRET: - rv = pin_long_secret(args); - break; default: rv = ENOENT; break; } - break; } @@ -355,11 +163,10 @@ int se_dispatch( // Read out entire config dataspace REQUIRE_OUT(128); - se_setup(); rv = se_config_read(buf_io); if(rv) { rv = EIO; - } + } break; default: @@ -370,11 +177,9 @@ int se_dispatch( #undef REQUIRE_OUT fail: - // Precaution: we don't want to leave ATECC508A authorized for any specific keys, // perhaps due to an error path we didn't see. Always reset the chip. se_reset_chip(); return rv; } - diff --git a/ports/stm32/boards/Passport/dispatch.h b/ports/stm32/boards/Passport/dispatch.h index b1909fc..a22077e 100644 --- a/ports/stm32/boards/Passport/dispatch.h +++ b/ports/stm32/boards/Passport/dispatch.h @@ -1,7 +1,7 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // -// SPDX-FileCopyrightText: 2018 Coinkite, Inc. +// SPDX-FileCopyrightText: 2018 Coinkite, Inc. // SPDX-License-Identifier: GPL-3.0-only // /* @@ -10,47 +10,24 @@ */ #pragma once -// Go into DFU mode, and certainly clear things. -extern void enter_dfu(void) __attribute__((noreturn)); - // Start DFU, or return doing nothing if chip is secure (no DFU possible). extern void dfu_by_request(void); /* Temporary declaration for unit-testing */ extern int se_dispatch(int method_num, uint8_t *buf_io, int len_in, uint32_t arg2, uint32_t incoming_sp, uint32_t incoming_lr); -#define CMD_GET_BOOTLOADER_VERSION 0 -#define CMD_GET_FIRMWARE_HASH 1 -#define CMD_UPGRADE_FIRMWARE 2 // TODO: What is this actually for? -#define CMD_RESET 3 -#define CMD_LED_CONTROL 4 #define CMD_IS_BRICKED 5 #define CMD_READ_SE_SLOT 15 #define CMD_GET_ANTI_PHISHING_WORDS 16 #define CMD_GET_RANDOM_BYTES 17 #define CMD_PIN_CONTROL 18 #define CMD_GET_SE_CONFIG 20 -#define CMD_FIRMWARE_CONTROL 21 -#define CMD_GET_SUPPLY_CHAIN_VALIDATION_WORDS 22 -#define CMD_FACTORY_SETUP -1 - +#define CMD_GET_SUPPLY_CHAIN_VALIDATION_WORDS 21 -// Subcommands for CMD_LED_CONTROL -#define LED_READ 0 -#define LED_SET_RED 1 -#define LED_SET_GREEN 2 -#define LED_ATTEMPT_TO_SET_GREEN 3 // Subcommands for CMD_PIN_CONTROL #define PIN_SETUP 0 #define PIN_ATTEMPT 1 #define PIN_CHANGE 2 #define PIN_GET_SECRET 3 -#define PIN_GREENLIGHT_FIRMWARE 4 -#define PIN_LONG_SECRET 5 -// Subcommands for CMD_FIRMWARE_CONTROL -#define GET_MIN_FIRMWARE_VERSION 0 -#define GET_IS_FIRMWARE_DOWNGRADE 1 // May not be used -#define UPDATE_HIGH_WATERMARK 2 -#define GET_HIGH_WATERMARK 3 // May not be used diff --git a/ports/stm32/boards/Passport/docs/generic-wallet-export.md b/ports/stm32/boards/Passport/docs/generic-wallet-export.md new file mode 100644 index 0000000..c803636 --- /dev/null +++ b/ports/stm32/boards/Passport/docs/generic-wallet-export.md @@ -0,0 +1,61 @@ +# Export wallet file format (Generic JSON) + +Passport can export data intended for various desktop and mobile +wallet systems, but it also supports a file format for general purpose +exports. + +It contains master XPUB, XFP for that, and derived values for the top hardened +position of BIP44, BIP84 and BIP49. + +# Example JSON file + +Here is an example: + +```javascript +{ + "chain": "XTN", + "xfp": "0F056943", + "xpub": "tpubD6NzVbkrYhZ4XzL5Dhayo67Gorv1YMS7j8pRUvVMd5odC2LBPLAygka9p7748JtSq82FNGPppFEz5xxZUdasBRCqJqXvUHq6xpnsMcYJzeh", + "account": 123, + "bip44": { + "deriv": "m/44'/1'/123'", + "first": "n44vs1Rv7T8SANrg2PFGQhzVkhr5Q6jMMD", + "name": "p2pkh", + "xfp": "B7908B26", + "xpub": "tpubDCiHGUNYdRRGoSH22j8YnruUKgguCK1CC2NFQUf9PApeZh8ewAJJWGMUrhggDNK73iCTanWXv1RN5FYemUH8UrVUBjqDb8WF2VoKmDh9UTo" + }, + "bip49": { + "_pub": "upub5DMRSsh6mNak9KbcVjJ7xAgHJvbE3Nx22CBTier5C35kv8j7g2q58ywxskBe6JCcAE2VH86CE2aL4MifJyKbRw8Gj9ay7SWvUBkp2DJ7y52", + "deriv": "m/49'/1'/123'", + "first": "2N87V39riUUCd4vmXfDjMWAu9gUCiBji5jB", + "name": "p2wpkh-p2sh", + "xfp": "CEE1D809", + "xpub": "tpubDCDqt7XXvhAdy1MpSze5nMJA9x8DrdRaKALRRPasfxyHpiqWWEAr9cbDBQ9BcX7cB3up98Pk97U2QQ3xrvQsi5dNPmRYYhdcsKY9wwEY87T" + }, + "bip84": { + "_pub": "vpub5Y5a91QvDT45EnXQaKeuvJupVvX8f9BiywDcadSTtaeJ1VgJPPXMitnYsqd9k7GnEqh44FKJ5McJfu6KrihFXhAmvSWgm7BAVVK8Gupu4fL", + "deriv": "m/84'/1'/123'", + "first": "tb1qc58ys2dphtphg6yuugdf3d0kufmk0tye044g3l", + "name": "p2wpkh", + "xfp": "78CF94E5", + "xpub": "tpubDC7jGaaSE66VDB6VhEDFYQSCAyugXmfnMnrMVyHNzW9wryyTxvha7TmfAHd7GRXrr2TaAn2HXn9T8ep4gyNX1bzGiieqcTUNcu2poyntrET" + } +} +``` + +## Notes + +1. The `first` address is formed by added `/0/0` onto the given derivation, and is assumed +to be the first (non-change) receive address for the wallet. + +2. The user may specify any value (up to 9999) for the account number, and it's meant to +segregate funds into sub-wallets. Don't assume it's zero. + +3. When making your PSBT files to spend these amounts, remember that the XFP of the master +(`0F056943` in this example) is is the root of the subkey paths found in the file, and +you must include the full derivation path from master. So based on this example, +to spend a UTXO on `tb1qc58ys2dphtphg6yuugdf3d0kufmk0tye044g3l`, the input section +of your PSBT would need to specify `(m=0F056943)/84'/1'/123'/0/0`. + +4. The `_pub` value is the [SLIP-132](https://github.com/satoshilabs/slips/blob/master/slip-0132.md) style "ypub/zpub/etc" which some systems might want. It implies +a specific address format. diff --git a/ports/stm32/boards/Passport/factory/test_ocd.py b/ports/stm32/boards/Passport/factory/test_ocd.py deleted file mode 100644 index f402965..0000000 --- a/ports/stm32/boards/Passport/factory/test_ocd.py +++ /dev/null @@ -1,71 +0,0 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. -# SPDX-License-Identifier: GPL-3.0-or-later -# -import telnetlib - -HOST = "localhost" - -tn = telnetlib.Telnet(HOST, 4444) - -print("1") -result = tn.expect(['>'], 10)[2] -print("2: result={}".format(result)) -tn.write(b"reset halt\r") -print("3") -result = tn.expect(['>'], 10)[2] -print("4: result={}".format(result)) -tn.write(b"flash write_image erase build-Passport/firmware0.bin 0x8000000\r") -print("5") -result = tn.expect(['>'], 10)[2] -print("6: result={}".format(result)) - -tn.write(b"flash write_image erase build-Passport/firmware1.bin 0x8040000\r") -print("7") -result = tn.expect(['>'], 100)[2] - -# TODO: This should not need to be here - someone we have to expect the prompt twice. -# TODO: Is it an extra echo character? Or is expect() not consuming the input? Do we also need to read the input? -result = tn.expect(['>'], 100)[2] -print("8: result = {}".format(result)) -tn.write(b"reset\r") -print("9") -result = tn.expect(['>'], 10)[2] -result = tn.expect(['>'], 10)[2] -print("10: result = {}".format(result)) -tn.write(b"mdb 0x081e0000 20\r") -print("11") -result = tn.expect(["0x081e0000: (.*)\r"]) -print("12: result = {}".format(result)) -print('Memory at 0x81e0000 = {}'.format(result[1].group(1))) - - -# In order to readout the public key generated for supply chain validation: -# -# - The initialization code must generate the public key for the device and -# store it at a known address in RAM. -# - The MPU should NOT be configured on initial boot (if SE is blank) -# - The Python script should issue a command like 'mdb 0x01234567 32' and save the -# result somewhere (probably POST to a server with a long cookie for auth). - - -# At a high level, this script will: -# -# - reset halt -# - Flash the firmware to the device -# - reset -# - Wait x seconds for the basic config to complete -# - Read any necessary information from known SRAM locations -# - Post information to our server -# - reset -# -# Disconnect from telnet -# -# Use some other lib to connect to the GPIO board and start factory test -# -# Numato 32 channel GPIO board over USB serial: -# -# - https://numato.com/product/32-channel-usb-gpio-module-with-analog-inputs/ -# - https://github.com/numato/samplecode/blob/master/RelayAndGPIOModules/USBRelayAndGPIOModules/python/usbgpio16_32/gpioread.py -# - https://github.com/numato/samplecode/blob/master/RelayAndGPIOModules/USBRelayAndGPIOModules/python/usbgpio16_32/gpiowrite.py -# - https://github.com/numato/samplecode/blob/master/RelayAndGPIOModules/USBRelayAndGPIOModules/python/usbgpio16_32/analogread.py - diff --git a/ports/stm32/boards/Passport/firmware_graphics.c b/ports/stm32/boards/Passport/firmware_graphics.c new file mode 100644 index 0000000..bacff55 --- /dev/null +++ b/ports/stm32/boards/Passport/firmware_graphics.c @@ -0,0 +1,142 @@ +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-License-Identifier: GPL-3.0-or-later +// +// +// Autogenerated - Do not edit! +// + +#include "firmware_graphics.h" + +uint8_t busybar1_data[] = { + 0x00, 0x00, + 0x00, 0x00, + 0x00, 0x00, + 0x00, 0x00, + 0x00, 0x00, + 0x03, 0x80, + 0x07, 0xc0, + 0x07, 0xc0, + 0x07, 0xc0, + 0x03, 0x80, + 0x00, 0x00, + 0x00, 0x00, + 0x00, 0x00, + 0x00, 0x00, + 0x00, 0x00, +}; +Image busybar1_img = { 15, 15, 2, busybar1_data }; + +uint8_t busybar2_data[] = { + 0x00, 0x00, + 0x00, 0x00, + 0x00, 0x00, + 0x00, 0x00, + 0x03, 0x80, + 0x07, 0xc0, + 0x0f, 0xe0, + 0x0f, 0xe0, + 0x0f, 0xe0, + 0x07, 0xc0, + 0x03, 0x80, + 0x00, 0x00, + 0x00, 0x00, + 0x00, 0x00, + 0x00, 0x00, +}; +Image busybar2_img = { 15, 15, 2, busybar2_data }; + +uint8_t busybar3_data[] = { + 0x00, 0x00, + 0x00, 0x00, + 0x00, 0x00, + 0x03, 0x80, + 0x07, 0xc0, + 0x0f, 0xe0, + 0x1f, 0xf0, + 0x1f, 0xf0, + 0x1f, 0xf0, + 0x0f, 0xe0, + 0x07, 0xc0, + 0x03, 0x80, + 0x00, 0x00, + 0x00, 0x00, + 0x00, 0x00, +}; +Image busybar3_img = { 15, 15, 2, busybar3_data }; + +uint8_t busybar4_data[] = { + 0x00, 0x00, + 0x00, 0x00, + 0x03, 0x80, + 0x0f, 0xe0, + 0x1f, 0xf0, + 0x1f, 0xf0, + 0x3f, 0xf8, + 0x3f, 0xf8, + 0x3f, 0xf8, + 0x1f, 0xf0, + 0x1f, 0xf0, + 0x0f, 0xe0, + 0x03, 0x80, + 0x00, 0x00, + 0x00, 0x00, +}; +Image busybar4_img = { 15, 15, 2, busybar4_data }; + +uint8_t busybar5_data[] = { + 0x00, 0x00, + 0x00, 0x00, + 0x07, 0xc0, + 0x1f, 0xf0, + 0x1f, 0xf0, + 0x3f, 0xf8, + 0x3f, 0xf8, + 0x3f, 0xf8, + 0x3f, 0xf8, + 0x3f, 0xf8, + 0x1f, 0xf0, + 0x1f, 0xf0, + 0x07, 0xc0, + 0x00, 0x00, + 0x00, 0x00, +}; +Image busybar5_img = { 15, 15, 2, busybar5_data }; + +uint8_t busybar6_data[] = { + 0x00, 0x00, + 0x0f, 0xe0, + 0x1f, 0xf0, + 0x3f, 0xf8, + 0x7f, 0xfc, + 0x7f, 0xfc, + 0x7f, 0xfc, + 0x7f, 0xfc, + 0x7f, 0xfc, + 0x7f, 0xfc, + 0x7f, 0xfc, + 0x3f, 0xf8, + 0x1f, 0xf0, + 0x0f, 0xe0, + 0x00, 0x00, +}; +Image busybar6_img = { 15, 15, 2, busybar6_data }; + +uint8_t busybar7_data[] = { + 0x0f, 0xe0, + 0x1f, 0xf0, + 0x3f, 0xf8, + 0x7f, 0xfc, + 0xff, 0xfe, + 0xff, 0xfe, + 0xff, 0xfe, + 0xff, 0xfe, + 0xff, 0xfe, + 0xff, 0xfe, + 0xff, 0xfe, + 0x7f, 0xfc, + 0x3f, 0xf8, + 0x1f, 0xf0, + 0x0f, 0xe0, +}; +Image busybar7_img = { 15, 15, 2, busybar7_data }; + diff --git a/ports/stm32/boards/Passport/firmware_graphics.h b/ports/stm32/boards/Passport/firmware_graphics.h new file mode 100644 index 0000000..6e55f64 --- /dev/null +++ b/ports/stm32/boards/Passport/firmware_graphics.h @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-License-Identifier: GPL-3.0-or-later +// +// +// Autogenerated - Do not edit! +// + +#include + +typedef struct _Image { + int16_t width; + int16_t height; + int16_t byte_width; + uint8_t* data; +} Image; + +extern Image busybar1_img; +extern Image busybar2_img; +extern Image busybar3_img; +extern Image busybar4_img; +extern Image busybar5_img; +extern Image busybar6_img; +extern Image busybar7_img; diff --git a/ports/stm32/boards/Passport/frequency.c b/ports/stm32/boards/Passport/frequency.c new file mode 100644 index 0000000..560f44f --- /dev/null +++ b/ports/stm32/boards/Passport/frequency.c @@ -0,0 +1,159 @@ +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-License-Identifier: GPL-3.0-or-later +// + +#include + +#include "stm32h7xx_hal.h" + +#include "py/mperrno.h" +#include "py/mpstate.h" +#include "py/mphal.h" + +#include "uart.h" + +#include "backlight.h" +#include "frequency.h" +#include "se.h" + +#define LOW_FREQUENCY 64000000 +// #define HIGH_FREQUENCY 240000000 +#define HIGH_FREQUENCY 480000000 + +static uint8_t rxbuf[260]; +static pyb_uart_obj_t pyb_uart_repl_obj; + +void frequency_update_console_uart(void) +{ + pyb_uart_repl_obj.base.type = &pyb_uart_type; + pyb_uart_repl_obj.uart_id = MICROPY_HW_UART_REPL; + pyb_uart_repl_obj.is_static = true; + pyb_uart_repl_obj.timeout = 0; + pyb_uart_repl_obj.timeout_char = 2; + uart_init(&pyb_uart_repl_obj, MICROPY_HW_UART_REPL_BAUD, UART_WORDLENGTH_8B, UART_PARITY_NONE, UART_STOPBITS_1, 0); + uart_set_rxbuf(&pyb_uart_repl_obj, sizeof(rxbuf), rxbuf); + MP_STATE_PORT(pyb_stdio_uart) = &pyb_uart_repl_obj; +} + +void frequency_turbo( + bool enable +) +{ + HAL_StatusTypeDef rc; + RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; + RCC_OscInitTypeDef RCC_OscInitStruct = {0}; + RCC_PeriphCLKInitTypeDef PeriphClkInitStruct = {0}; + + // printf("[%s] %s\n", __func__, enable ? "true":"false"); + + // HACK: TEMP - always be in high speed mode + enable = true; + + if ((!enable && (SystemCoreClock == LOW_FREQUENCY)) || + (enable && (SystemCoreClock == HIGH_FREQUENCY))) + return; /* Already at requested frequency...nothing to do */ + + RCC->CR |= RCC_CR_HSION; + + /* Wait till HSI is ready */ + while (!(RCC->CR & RCC_CR_HSIRDY)); + + /* Select HSI clock as main clock */ + RCC->CFGR = (RCC->CFGR & ~(RCC_CFGR_SW)) | RCC_CFGR_SW_HSI; + + /* Reconfigure the clocks based on enable flag: + * 64 MHz core clock if false + * 480 MHz core clock if true + */ + RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE | RCC_OSCILLATORTYPE_HSI48; + RCC_OscInitStruct.HSEState = RCC_HSE_ON; + RCC_OscInitStruct.HSIState = RCC_HSI_OFF; + RCC_OscInitStruct.CSIState = RCC_CSI_OFF; + RCC_OscInitStruct.LSEState = RCC_LSE_OFF; + RCC_OscInitStruct.HSI48State = RCC_HSI48_ON; + RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; + RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; + RCC_OscInitStruct.PLL.PLLRGE = RCC_PLL1VCIRANGE_1; + RCC_OscInitStruct.PLL.PLLVCOSEL = RCC_PLL1VCOWIDE; + RCC_OscInitStruct.PLL.PLLFRACN = 0; + + RCC_ClkInitStruct.ClockType = (RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2); + RCC_ClkInitStruct.ClockType |= (RCC_CLOCKTYPE_D3PCLK1 | RCC_CLOCKTYPE_D1PCLK1); + RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; + RCC_ClkInitStruct.SYSCLKDivider = RCC_SYSCLK_DIV1; + RCC_ClkInitStruct.AHBCLKDivider = RCC_HCLK_DIV2; + RCC_ClkInitStruct.APB3CLKDivider = RCC_APB3_DIV2; + RCC_ClkInitStruct.APB1CLKDivider = RCC_APB1_DIV2; + RCC_ClkInitStruct.APB2CLKDivider = RCC_APB2_DIV2; + RCC_ClkInitStruct.APB4CLKDivider = RCC_APB4_DIV2; + + PeriphClkInitStruct.PeriphClockSelection = RCC_PERIPHCLK_RTC|RCC_PERIPHCLK_USART2 + |RCC_PERIPHCLK_RNG|RCC_PERIPHCLK_SPI4 + |RCC_PERIPHCLK_SPI1|RCC_PERIPHCLK_SPI2 + |RCC_PERIPHCLK_SDMMC|RCC_PERIPHCLK_I2C2 + |RCC_PERIPHCLK_ADC|RCC_PERIPHCLK_I2C1 + |RCC_PERIPHCLK_I2C4; + PeriphClkInitStruct.PLL2.PLL2M = 1; + PeriphClkInitStruct.PLL2.PLL2N = 18; + PeriphClkInitStruct.PLL2.PLL2P = 1; + PeriphClkInitStruct.PLL2.PLL2Q = 2; + PeriphClkInitStruct.PLL2.PLL2R = 2; + PeriphClkInitStruct.PLL2.PLL2RGE = RCC_PLL2VCIRANGE_3; + PeriphClkInitStruct.PLL2.PLL2VCOSEL = RCC_PLL2VCOMEDIUM; + PeriphClkInitStruct.PLL2.PLL2FRACN = 6144; + PeriphClkInitStruct.SdmmcClockSelection = RCC_SDMMCCLKSOURCE_PLL; + PeriphClkInitStruct.Spi123ClockSelection = RCC_SPI123CLKSOURCE_PLL; + PeriphClkInitStruct.Spi45ClockSelection = RCC_SPI45CLKSOURCE_D2PCLK1; + PeriphClkInitStruct.Usart234578ClockSelection = RCC_USART234578CLKSOURCE_D2PCLK1; + PeriphClkInitStruct.RngClockSelection = RCC_RNGCLKSOURCE_HSI48; + PeriphClkInitStruct.I2c123ClockSelection = RCC_I2C123CLKSOURCE_D2PCLK1; + PeriphClkInitStruct.I2c4ClockSelection = RCC_I2C4CLKSOURCE_D3PCLK1; + PeriphClkInitStruct.AdcClockSelection = RCC_ADCCLKSOURCE_PLL2; + PeriphClkInitStruct.RTCClockSelection = RCC_RTCCLKSOURCE_LSI; + + if (!enable) + { + RCC_OscInitStruct.PLL.PLLM = 1; + RCC_OscInitStruct.PLL.PLLN = 32; + RCC_OscInitStruct.PLL.PLLP = 2; + RCC_OscInitStruct.PLL.PLLQ = 32; + RCC_OscInitStruct.PLL.PLLR = 2; + RCC_ClkInitStruct.SYSCLKDivider = RCC_SYSCLK_DIV2; + } + else + { + RCC_OscInitStruct.PLL.PLLM = 1; + RCC_OscInitStruct.PLL.PLLN = 120; + RCC_OscInitStruct.PLL.PLLP = 2; + RCC_OscInitStruct.PLL.PLLQ = 120; + RCC_OscInitStruct.PLL.PLLR = 2; + // RCC_OscInitStruct.PLL.PLLM = 1; + // RCC_OscInitStruct.PLL.PLLN = 60; // TODO: clock tree + // RCC_OscInitStruct.PLL.PLLP = 2; + // RCC_OscInitStruct.PLL.PLLQ = 60; + // RCC_OscInitStruct.PLL.PLLR = 2; + } + + rc = HAL_RCC_OscConfig(&RCC_OscInitStruct); + if (rc != HAL_OK) + printf("[%s] HAL_RCC_OscConfig failed\n", __func__); + + rc = HAL_RCCEx_PeriphCLKConfig(&PeriphClkInitStruct); + if (rc != HAL_OK) + printf("[%s] HAL_RCCEx_PeriphCLKConfig failed\n", __func__); + + rc = HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4); + if (rc != HAL_OK) + printf("[%s] HAL_RCC_ClockConfig failed\n", __func__); + + /* Adjust the backlight PWM based on the new frequency */ + backlight_adjust(enable); + + /* Re-initialize the console UART based on the new frequency */ + frequency_update_console_uart(); + + /* Re-initialize the SE UART based on the new frequency */ + se_setup(); + + //printf("%lu, %lu, %lu, %lu, %lu\n", HAL_RCC_GetSysClockFreq(), SystemCoreClock, HAL_RCC_GetHCLKFreq(), HAL_RCC_GetPCLK1Freq(), HAL_RCC_GetPCLK2Freq()); +} diff --git a/ports/stm32/boards/Passport/frequency.h b/ports/stm32/boards/Passport/frequency.h new file mode 100644 index 0000000..a52e072 --- /dev/null +++ b/ports/stm32/boards/Passport/frequency.h @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-License-Identifier: GPL-3.0-or-later +// + +#ifndef __FREQUENCY_H__ +#define __FREQUENCY_H__ + +#include + +extern void frequency_turbo(bool enable); +extern void frequency_update_console_uart(void); + +#endif // __FREQUENCY_H__ diff --git a/ports/stm32/boards/Passport/graphics/README b/ports/stm32/boards/Passport/graphics/README new file mode 100644 index 0000000..ee209d5 --- /dev/null +++ b/ports/stm32/boards/Passport/graphics/README @@ -0,0 +1,5 @@ +NOTE: The following website is able to convert PNGs to true two-color images: + +https://manytools.org/image/colorize-filter/ + +The py/build.py script requires exactly two color entries or it will fail. diff --git a/ports/stm32/boards/Passport/graphics/c/.gitignore b/ports/stm32/boards/Passport/graphics/c/.gitignore new file mode 100644 index 0000000..ba61e5e --- /dev/null +++ b/ports/stm32/boards/Passport/graphics/c/.gitignore @@ -0,0 +1,5 @@ +bootloader_graphics.c +bootloader_graphics.h +firmware_graphics.c +firmware_graphics.h + diff --git a/ports/stm32/boards/Passport/graphics/c/Makefile b/ports/stm32/boards/Passport/graphics/c/Makefile new file mode 100644 index 0000000..a089b97 --- /dev/null +++ b/ports/stm32/boards/Passport/graphics/c/Makefile @@ -0,0 +1,24 @@ +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# SPDX-FileCopyrightText: 2018 Coinkite, Inc. +# SPDX-License-Identifier: GPL-3.0-only +# +# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard +# and is covered by GPLv3 license found in COPYING. + +all: bootloader_graphics.c firmware_graphics.c + +BOOTLOADER_SOURCES = splash.png + +bootloader_graphics.c: Makefile $(BOOTLOADER_SOURCES) cbuild.py + python3 cbuild.py bootloader_graphics $(BOOTLOADER_SOURCES) + +FIRMWARE_SOURCES = $(wildcard busybar*.png) + +firmware_graphics.c: Makefile $(FIRMWARE_SOURCES) cbuild.py + python3 cbuild.py firmware_graphics $(FIRMWARE_SOURCES) + + +up: all + (cd ../shared; make up) diff --git a/ports/stm32/boards/Passport/graphics/c/busybar1.png b/ports/stm32/boards/Passport/graphics/c/busybar1.png new file mode 100644 index 0000000000000000000000000000000000000000..2875fd46fb243659ef4864a7b6d7a14a7ebb4a39 GIT binary patch literal 108 zcmeAS@N?(olHy`uVBq!ia0vp^{2#-1*YAsQ2t|NQ@N&wR0=vtntp zmnru$0cB<7mPSd27iov5F4}mqkTZ4F#JgQD^NtA$PG`8cf?x7Zww^AIAsQ2t|NQ@N&wR0=vtp@9 z*x4q{BPUL9BqmgG-p$z&WMU?Eb;hL(r4nvopZtZL3ugD~^sby)*m2B;VSl!$UY;(FAsQ2t|NQ@N&wR0=vtp^r z>a>F*Nhv8U1_rX7d1f)1QqtX7KA9$hYBpYmM>8eWIIiwk#2Gn|yV99g^@8qmB c8IzYR41Mm>k{4ESfedHxboFyt=akR{0LU99L;wH) literal 0 HcmV?d00001 diff --git a/ports/stm32/boards/Passport/graphics/c/busybar4.png b/ports/stm32/boards/Passport/graphics/c/busybar4.png new file mode 100644 index 0000000000000000000000000000000000000000..559853e54f0a53c6c10ca5ce6af69747938b8bad GIT binary patch literal 136 zcmeAS@N?(olHy`uVBq!ia0vp^{2{+=$5AsQ3sPCUrPpupig_sjqN zIqeQU-dE-(*FVdQ&MBb@0K>H|?EnA( literal 0 HcmV?d00001 diff --git a/ports/stm32/boards/Passport/graphics/c/busybar5.png b/ports/stm32/boards/Passport/graphics/c/busybar5.png new file mode 100644 index 0000000000000000000000000000000000000000..5eacce574ab353b635d4622085c328f4c1cd14bf GIT binary patch literal 125 zcmeAS@N?(olHy`uVBq!ia0vp^{2E}kxqAsQ2t|NQ@N&wR0=vtlU^ z506epjhmZWir%cVJq32NtkzU{6mtqYHcw1FX;l4T#WDHCDQybMRXKu^(~mG*ieX~l XJ}50%rn0COXdZ*7tDnm{r-UW|L1QKd literal 0 HcmV?d00001 diff --git a/ports/stm32/boards/Passport/graphics/c/busybar6.png b/ports/stm32/boards/Passport/graphics/c/busybar6.png new file mode 100644 index 0000000000000000000000000000000000000000..4be729ab3dce18b2b6ccc64490518db3c5ec3ef2 GIT binary patch literal 126 zcmeAS@N?(olHy`uVBq!ia0vp^{2uAVNAAsQ2(PBP?UP~c$Bef)oa z4Eyvh!NP6P4;6SBCq+fx}J~S44$rjF6*2UngFC#Djxs< literal 0 HcmV?d00001 diff --git a/ports/stm32/boards/Passport/graphics/c/busybar7.png b/ports/stm32/boards/Passport/graphics/c/busybar7.png new file mode 100644 index 0000000000000000000000000000000000000000..9a6112d7cb5e553f7a36cdbf884b8df87e66bc6b GIT binary patch literal 120 zcmeAS@N?(olHy`uVBq!ia0vp^{2_MR?|AsQ3kPB7$RFyLUG{OAAq zG7a5D8fEW$PkK7clH^mF_@SxF^vSEMH-0UgxAW)9%3s^t*l%A literal 0 HcmV?d00001 diff --git a/ports/stm32/boards/Passport/graphics/cbuild.py b/ports/stm32/boards/Passport/graphics/c/cbuild.py similarity index 58% rename from ports/stm32/boards/Passport/graphics/cbuild.py rename to ports/stm32/boards/Passport/graphics/c/cbuild.py index ceccd8e..fee825b 100755 --- a/ports/stm32/boards/Passport/graphics/cbuild.py +++ b/ports/stm32/boards/Passport/graphics/c/cbuild.py @@ -37,7 +37,7 @@ def read_img(fn): # fix colour issues: assume minority colour is white (1) histo = img.histogram() assert len(histo) == 256, repr(histo) - assert len(set(histo)) == 3, "Too many colours: "+repr(histo) + assert len(set(histo)) == 3, "Too many colors: "+repr(histo) # if histo[-1] > histo[0]: img = ImageOps.invert(img) @@ -61,26 +61,56 @@ def crunch(n): return a[0] -def doit(outfname, fnames): +def gen_header(outfile, fnames): assert fnames, "need some files" - fp = open(outfname, 'wt') + fp = open('{}.h'.format(outfile_prefix), 'wt') fp.write("""\ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. -# SPDX-License-Identifier: GPL-3.0-or-later -# -# SPDX-FileCopyrightText: 2018 Coinkite, Inc. -# SPDX-License-Identifier: GPL-3.0-only -# -# autogenerated; don't edit -# -class Graphics: - # (w,h, w_bytes, wbits, data) +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-License-Identifier: GPL-3.0-or-later +// +// +// Autogenerated - Do not edit! +// + +#include + +typedef struct _Image { + int16_t width; + int16_t height; + int16_t byte_width; + uint8_t* data; +} Image; """) + from io import StringIO + for fn in fnames: + varname = fn.split('.')[0].replace('-', '_') + + fp.write("extern Image {}_img;\n".format(varname)) + + +def gen_source(outfile_prefix, fnames): + + assert fnames, "need some files" + + fp = open('{}.c'.format(outfile_prefix), 'wt') + + fp.write("""\ +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-License-Identifier: GPL-3.0-or-later +// +// +// Autogenerated - Do not edit! +// + +#include "{}.h" + +""".format(outfile_prefix)) + from io import StringIO for fn in fnames: if fn.endswith('.txt'): @@ -100,29 +130,23 @@ class Graphics: print('w={}, h={} len(raw)={}'.format(w, h, len(raw))) str.write('{') str.write(os.linesep) + w_bytes = (w + 7) // 8 for y in range(h): - str.write('{') - for x in range(w//8): - b = raw[(y * w//8) + x] + str.write(' ') + for x in range(w_bytes): + b = raw[(y * w_bytes) + x] str.write('0x{:02x}{} '.format(b, ',' if x < w-1 else '')) - str.write('},\\n') - str.write('}\\n') - - wbits, comp = crunch(raw) + str.write('\n') + str.write('};') - if 0: - # is compression better? - is_comp = len(comp)+8 < len(raw) - else: - # disable; taking too much runtime memory - is_comp = False - - print(" %s = (%d, %d, %d, %s, %r)\n" % (varname, w, h, ((w+7)//8), - wbits if is_comp else 0, str.getvalue() if not is_comp else comp), file=fp) + fp.write("uint8_t {}_data[] = {}\n".format(varname, str.getvalue())) + fp.write("Image {}_img = {{ {}, {}, {}, {}_data }};\n\n".format(varname, w, h, (w+7)//8, varname)) print("done: '%s' (%d x %d)" % (varname, w, h)) - fp.write("\n# EOF\n") - if 1: - doit('graphics.py', sys.argv[1:]) + outfile_prefix = sys.argv[1] + sources = sys.argv[2:].copy() + sources.sort() + gen_header(outfile_prefix, sources) + gen_source(outfile_prefix, sources) diff --git a/ports/stm32/boards/Passport/graphics/splash.png b/ports/stm32/boards/Passport/graphics/c/splash.png similarity index 100% rename from ports/stm32/boards/Passport/graphics/splash.png rename to ports/stm32/boards/Passport/graphics/c/splash.png diff --git a/ports/stm32/boards/Passport/graphics/graphics.py b/ports/stm32/boards/Passport/graphics/graphics.py deleted file mode 100644 index 2e71423..0000000 --- a/ports/stm32/boards/Passport/graphics/graphics.py +++ /dev/null @@ -1,67 +0,0 @@ -# autogenerated; don't edit -# -class Graphics: - # (w,h, w_bytes, wbits, data) - - scroll = (3, 61, 1, 0, b'@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@@\xe0@') - - scrollbar = (5, 276, 1, 0, b'\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P') - - space = (13, 3, 2, 0, b'\x80\x08\x80\x08\xff\xf8') - - sm_box = (11, 17, 2, 0, b'\xe4\xe0\x80 \x80 \x80 \x00\x00\x00\x00\x80 \x00\x00\x00\x00\x00\x00\x80 \x00\x00\x00\x00\x80 \x80 \x80 \xe4\xe0') - - spin = (25, 40, 4, 0, b'\x00\x08\x00\x00\x00\x1c\x00\x00\x00>\x00\x00\x00\x7f\x00\x00\x00\xff\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\x80\x80\x00\x00\x80\x80\x00\x00\x80\x80\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x00\x00\x80\x80\x00\x00\x80\x80\x00\x00\x80\xff\xff\xff\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\x80\x00\x00\x7f\x00\x00\x00>\x00\x00\x00\x1c\x00\x00\x00\x08\x00\x00') - - arrow_down = (7, 11, 1, 0, b'\x10\x10\x10\x10\x10\x10\x10\xfe|8\x10') - - more_right = (9, 17, 2, 0, b'\x80\x00\xc0\x00\xe0\x00\xf0\x00\xf8\x00\xfc\x00\xfe\x00\xff\x00\xff\x80\xff\x00\xfe\x00\xfc\x00\xf8\x00\xf0\x00\xe0\x00\xc0\x00\x80\x00') - - arrow_up = (7, 11, 1, 0, b'\x108|\xfe\x10\x10\x10\x10\x10\x10\x10') - - selected = (15, 12, 2, 0, b'\x00\x00\x00\x00\x00\x06\x00\x0c\x00\x18\x0000`\x18\xc0\r\x80\x07\x00\x02\x00\x00\x00') - - more_left = (9, 17, 2, 0, b'\x00\x80\x01\x80\x03\x80\x07\x80\x0f\x80\x1f\x80?\x80\x7f\x80\xff\x80\x7f\x80?\x80\x1f\x80\x0f\x80\x07\x80\x03\x80\x01\x80\x00\x80') - - tetris_pattern_1 = (12, 12, 2, 0, b':\xc0z\xe0\xc0p\xef\xb0\xef\xb0\xe0p\xef\xb0\xef\xb0\xef\xb0\xc0pz\xe0:\xc0') - - tetris_pattern_6 = (12, 12, 2, 0, b':\xc0z\xe0\xc0p\xef\xb0\xef\xb0\xe0p\xef\xb0\xef\xb0\xef\xb0\xc0pz\xe0:\xc0') - - wedge = (11, 11, 2, 0, b'\xc0\x00\xf0\x00|\x00\x1f\x00\x07\xc0\x01\xe0\x07\xc0\x1f\x00|\x00\xf0\x00\xc0\x00') - - fruit = (10, 10, 2, 0, b'\x07\x80\x04\xc0\x1f\x00?\x80\x7f\xc0\x7f@\x7f@~\xc09\x80\x1f\x00') - - tetris_pattern_3 = (12, 12, 2, 0, b':\xc0z\xe0\xc0p\xef\xb0\xef\xb0\xe0p\xef\xb0\xef\xb0\xef\xb0\xc0pz\xe0:\xc0') - - splash = (172, 129, 22, 0, b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1e\x00\x0f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1e\x00\x0f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>\x00\x0f\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00~\x00\x0f\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00~\x00\x0f\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfe\x00\x0f\xe0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfe\x00\x0f\xe0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xfe\x00\x0f\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xfe\x00\x0f\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\xfe\x00\x0f\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xfe\x00\x0f\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xfe\x00\x0f\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\xfe\x00\x0f\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\xfe\x00\x0f\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\xfe\x00\x0f\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00?\xfe\x00\x0f\xff\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00?\xfe\x00\x0f\xff\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00`\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xe0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xe0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00?\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xff\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xff\xe0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xff\xe0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xff\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xff\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\xff\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xff\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\xff\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xff\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xff\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xff\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xff\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xff\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xff\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\xff\xc0\x00?\xfc\x00\x07\xff\x80\x00\x7f\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00?\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\x00\x00\x00\x00\x00\x7f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xc0\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xe0\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xe0\x00\x00\x00\x00\x00\x01\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xf0\x00\x00\x00\x00\x00\x01\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xf0\x00\x00\x00\x00\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xf8\x00\x00\x00\x00\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xf8\x00\x00\x00\x00\x00\x07\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfc\x00\x00\x00\x00\x00\x0f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x0f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x1f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\x80\x0f\x80\x0e\x0e\x01\xc0p\x1f\xe0\x00\x10\x03\xff\x80p\x00\xf8\x01\xc0p\xff\x80\x1f\xc0\x0e\x0e\x01\xc0p\x1f\xf0\x008\x03\xff\x80p\x01\xfc\x01\xc0p\xff\x80?\xe0\x0e\x0e\x01\xe0p\x1f\xf8\x008\x03\xff\x80p\x03\xfe\x01\xe0p\xe0\x00x\xf0\x0e\x0e\x01\xf0p\x1c<\x00|\x008\x00p\x07\x8f\x01\xf0p\xe0\x00\xf0x\x0e\x0e\x01\xf8p\x1c\x1e\x00|\x008\x00p\x0f\x07\x81\xf8p\xfe\x00\xe08\x0e\x0e\x01\xfcp\x1c\x0e\x00\xfe\x008\x00p\x0e\x03\x81\xfcp\xfe\x00\xe08\x0e\x0e\x01\xdep\x1c\x0e\x00\xee\x008\x00p\x0e\x03\x81\xdep\xfe\x00\xe08\x0e\x0e\x01\xcfp\x1c\x0e\x01\xef\x008\x00p\x0e\x03\x81\xcfp\xe0\x00\xf0x\x0e\x0e\x01\xc7\xf0\x1c\x1e\x01\xc7\x008\x00p\x0f\x07\x81\xc7\xf0\xe0\x00x\xf0\x0f\x1e\x01\xc3\xf0\x1c<\x03\xc7\x808\x00p\x07\x8f\x01\xc3\xf0\xe0\x00?\xe0\x07\xfc\x01\xc1\xf0\x1f\xf8\x03\x83\x808\x00p\x03\xfe\x01\xc1\xf0\xe0\x00\x1f\xc0\x07\xfc\x01\xc0\xf0\x1f\xf0\x07\x83\xc08\x00p\x01\xfc\x01\xc0\xf0\xe0\x00\x0f\x80\x03\xf8\x01\xc0p\x1f\xe0\x07\x01\xc08\x00p\x00\xf8\x01\xc0p') - - tetris_pattern_0 = (12, 12, 2, 0, b':\xc0z\xe0\xc0p\xef\xb0\xef\xb0\xe0p\xef\xb0\xef\xb0\xef\xb0\xc0pz\xe0:\xc0') - - battery_100 = (29, 15, 4, 0, b'\xff\xff\xff\xc0\xff\xff\xff\xc0\xc0\x00\x00\xc0\xc0\x00\x00\xc0\xces\x9c\xf8\xces\x9c\xf8\xces\x9c\x18\xces\x9c\x18\xces\x9c\x18\xces\x9c\xf8\xces\x9c\xf8\xc0\x00\x00\xc0\xc0\x00\x00\xc0\xff\xff\xff\xc0\xff\xff\xff\xc0') - - wordmark = (172, 13, 22, 0, b'\xff\x80\x0f\x80\x0e\x0e\x01\xc0p\x1f\xe0\x00\x10\x03\xff\x80p\x00\xf8\x01\xc0p\xff\x80\x1f\xc0\x0e\x0e\x01\xc0p\x1f\xf0\x008\x03\xff\x80p\x01\xfc\x01\xc0p\xff\x80?\xe0\x0e\x0e\x01\xe0p\x1f\xf8\x008\x03\xff\x80p\x03\xfe\x01\xe0p\xe0\x00x\xf0\x0e\x0e\x01\xf0p\x1c<\x00|\x008\x00p\x07\x8f\x01\xf0p\xe0\x00\xf0x\x0e\x0e\x01\xf8p\x1c\x1e\x00|\x008\x00p\x0f\x07\x81\xf8p\xfe\x00\xe08\x0e\x0e\x01\xfcp\x1c\x0e\x00\xfe\x008\x00p\x0e\x03\x81\xfcp\xfe\x00\xe08\x0e\x0e\x01\xdep\x1c\x0e\x00\xee\x008\x00p\x0e\x03\x81\xdep\xfe\x00\xe08\x0e\x0e\x01\xcfp\x1c\x0e\x01\xef\x008\x00p\x0e\x03\x81\xcfp\xe0\x00\xf0x\x0e\x0e\x01\xc7\xf0\x1c\x1e\x01\xc7\x008\x00p\x0f\x07\x81\xc7\xf0\xe0\x00x\xf0\x0f\x1e\x01\xc3\xf0\x1c<\x03\xc7\x808\x00p\x07\x8f\x01\xc3\xf0\xe0\x00?\xe0\x07\xfc\x01\xc1\xf0\x1f\xf8\x03\x83\x808\x00p\x03\xfe\x01\xc1\xf0\xe0\x00\x1f\xc0\x07\xfc\x01\xc0\xf0\x1f\xf0\x07\x83\xc08\x00p\x01\xfc\x01\xc0\xf0\xe0\x00\x0f\x80\x03\xf8\x01\xc0p\x1f\xe0\x07\x01\xc08\x00p\x00\xf8\x01\xc0p') - - box = (30, 43, 4, 0, b'?\xff\xff\xf0\x7f\xff\xff\xf8\xe0\x00\x00\x1c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xe0\x00\x00\x1c\x7f\xff\xff\xf8?\xff\xff\xf0') - - tetris_pattern_4 = (12, 12, 2, 0, b':\xc0z\xe0\xc0p\xef\xb0\xef\xb0\xe0p\xef\xb0\xef\xb0\xef\xb0\xc0pz\xe0:\xc0') - - battery_50 = (29, 15, 4, 0, b'\xff\xff\xff\xc0\xff\xff\xff\xc0\xc0\x00\x00\xc0\xc0\x00\x00\xc0\xcep\x00\xf8\xcep\x00\xf8\xcep\x00\x18\xcep\x00\x18\xcep\x00\x18\xcep\x00\xf8\xcep\x00\xf8\xc0\x00\x00\xc0\xc0\x00\x00\xc0\xff\xff\xff\xc0\xff\xff\xff\xc0') - - tetris_pattern_2 = (12, 12, 2, 0, b':\xc0z\xe0\xc0p\xef\xb0\xef\xb0\xe0p\xef\xb0\xef\xb0\xef\xb0\xc0pz\xe0:\xc0') - - battery_25 = (29, 15, 4, 0, b'\xff\xff\xff\xc0\xff\xff\xff\xc0\xc0\x00\x00\xc0\xc0\x00\x00\xc0\xce\x00\x00\xf8\xce\x00\x00\xf8\xce\x00\x00\x18\xce\x00\x00\x18\xce\x00\x00\x18\xce\x00\x00\xf8\xce\x00\x00\xf8\xc0\x00\x00\xc0\xc0\x00\x00\xc0\xff\xff\xff\xc0\xff\xff\xff\xc0') - - battery_low = (29, 15, 4, 0, b'\xff\xff\xff\xc0\xff\xff\xff\xc0\xc0\x00\x00\xc0\xc0\x00\x00\xc0\xc8\x00\x00\xf8\xc8\x00\x00\xf8\xc8\x00\x00\x18\xc8\x00\x00\x18\xc8\x00\x00\x18\xc8\x00\x00\xf8\xc8\x00\x00\xf8\xc0\x00\x00\xc0\xc0\x00\x00\xc0\xff\xff\xff\xc0\xff\xff\xff\xc0') - - xbox = (30, 43, 4, 0, b'?\xff\xff\xf0\x7f\xff\xff\xf8\xe0\x00\x00\x1c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x07\x80\x0c\xc0\x1f\xe0\x0c\xc0?\xf0\x0c\xc0\x7f\xf8\x0c\xc0\x7f\xf8\x0c\xc0\xff\xfc\x0c\xc0\xff\xfc\x0c\xc0\xff\xfc\x0c\xc0\xff\xfc\x0c\xc0\x7f\xf8\x0c\xc0\x7f\xf8\x0c\xc0?\xf0\x0c\xc0\x1f\xe0\x0c\xc0\x07\x80\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xe0\x00\x00\x1c\x7f\xff\xff\xf8?\xff\xff\xf0') - - tbox = (30, 43, 4, 0, b'?\xff\xff\xf0\x7f\xff\xff\xf8\xe0\x00\x00\x1c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x07\x80\x0c\xc0\x0f\xc0\x0c\xc0\x1f\xe0\x0c\xc0?\xf0\x0c\xc0?\xf0\x0c\xc0?\xf0\x0c\xc0?\xf0\x0c\xc0\x1f\xe0\x0c\xc0\x0f\xc0\x0c\xc0\x07\x80\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xe0\x00\x00\x1c\x7f\xff\xff\xf8?\xff\xff\xf0') - - passport = (124, 12, 16, 0, b'\xfc\x00\x10\x00\x1e\x00\x1e\x00~\x00\x0f\x80\x0f\xc0\x0f\xf0\xfe\x008\x00?\x00?\x00\x7f\x00\x1f\xc0\x0f\xe0\x0f\xf0\xc7\x008\x00s\x00s\x00c\x808\xe0\x0cp\x01\x80\xc3\x00|\x00`\x00`\x00a\x80pp\x0c0\x01\x80\xc3\x00l\x00p\x00p\x00a\x80`0\x0c0\x01\x80\xc7\x00\xee\x00<\x00<\x00c\x80`0\x0cp\x01\x80\xfe\x00\xc6\x00\x1e\x00\x1e\x00\x7f\x00`0\x0f\xe0\x01\x80\xfc\x01\xc7\x00\x07\x00\x07\x00~\x00`0\x0f\xc0\x01\x80\xc0\x01\x83\x00\x03\x00\x03\x00`\x00pp\x0c\xc0\x01\x80\xc0\x03\x83\x80g\x00g\x00`\x008\xe0\x0c\xe0\x01\x80\xc0\x03\x01\x80~\x00~\x00`\x00\x1f\xc0\x0cp\x01\x80\xc0\x07\x01\xc0<\x00<\x00`\x00\x0f\x80\x0c0\x01\x80') - - battery_75 = (29, 15, 4, 0, b'\xff\xff\xff\xc0\xff\xff\xff\xc0\xc0\x00\x00\xc0\xc0\x00\x00\xc0\xces\x80\xf8\xces\x80\xf8\xces\x80\x18\xces\x80\x18\xces\x80\x18\xces\x80\xf8\xces\x80\xf8\xc0\x00\x00\xc0\xc0\x00\x00\xc0\xff\xff\xff\xc0\xff\xff\xff\xc0') - - tetris_pattern_5 = (12, 12, 2, 0, b':\xc0z\xe0\xc0p\xef\xb0\xef\xb0\xe0p\xef\xb0\xef\xb0\xef\xb0\xc0pz\xe0:\xc0') - - -# EOF diff --git a/ports/stm32/boards/Passport/graphics/loading1.png b/ports/stm32/boards/Passport/graphics/loading1.png deleted file mode 100644 index 90d3d3a518467ea0ba5d12e4edc5a431b7c9a7ee..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 407 zcmeAS@N?(olHy`uVBq!ia0y~yU{nCIl{lDyWI^2I44^=qr;B4q#hkadH}W+bh&W$# z|M!3XO9$6XkA@{5FZ`%#4+btjoqsYRx@+&*DEM&-yP1(y&23fXXaiGW_)Mno(*R7vx&(8&E_kg z&!@u!v{&|>8IMlddnWPn9k{t-<>_+6LsLI=+1EF&zT^VVeDyFne^73(B(g~7O1dQ`e@_M ztqnkzZGXaCoDEd*@ZC?T8PVK8k9?nT{=gle=FlCH_n>m~CLdZ4(iQFVTtEB&ZJ +# SPDX-License-Identifier: GPL-3.0-or-later +# +# SPDX-FileCopyrightText: 2018 Coinkite, Inc. +# SPDX-License-Identifier: GPL-3.0-only +# # (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard # and is covered by GPLv3 license found in COPYING. diff --git a/ports/stm32/boards/Passport/graphics/README.md b/ports/stm32/boards/Passport/graphics/py/README.md similarity index 100% rename from ports/stm32/boards/Passport/graphics/README.md rename to ports/stm32/boards/Passport/graphics/py/README.md diff --git a/ports/stm32/boards/Passport/graphics/arrow_down.txt b/ports/stm32/boards/Passport/graphics/py/arrow_down.txt similarity index 100% rename from ports/stm32/boards/Passport/graphics/arrow_down.txt rename to ports/stm32/boards/Passport/graphics/py/arrow_down.txt diff --git a/ports/stm32/boards/Passport/graphics/arrow_up.txt b/ports/stm32/boards/Passport/graphics/py/arrow_up.txt similarity index 100% rename from ports/stm32/boards/Passport/graphics/arrow_up.txt rename to ports/stm32/boards/Passport/graphics/py/arrow_up.txt diff --git a/ports/stm32/boards/Passport/graphics/battery_100.png b/ports/stm32/boards/Passport/graphics/py/battery_100.png similarity index 100% rename from ports/stm32/boards/Passport/graphics/battery_100.png rename to ports/stm32/boards/Passport/graphics/py/battery_100.png diff --git a/ports/stm32/boards/Passport/graphics/battery_25.png b/ports/stm32/boards/Passport/graphics/py/battery_25.png similarity index 100% rename from ports/stm32/boards/Passport/graphics/battery_25.png rename to ports/stm32/boards/Passport/graphics/py/battery_25.png diff --git a/ports/stm32/boards/Passport/graphics/battery_50.png b/ports/stm32/boards/Passport/graphics/py/battery_50.png similarity index 100% rename from ports/stm32/boards/Passport/graphics/battery_50.png rename to ports/stm32/boards/Passport/graphics/py/battery_50.png diff --git a/ports/stm32/boards/Passport/graphics/battery_75.png b/ports/stm32/boards/Passport/graphics/py/battery_75.png similarity index 100% rename from ports/stm32/boards/Passport/graphics/battery_75.png rename to ports/stm32/boards/Passport/graphics/py/battery_75.png diff --git a/ports/stm32/boards/Passport/graphics/battery_low.png b/ports/stm32/boards/Passport/graphics/py/battery_low.png similarity index 100% rename from ports/stm32/boards/Passport/graphics/battery_low.png rename to ports/stm32/boards/Passport/graphics/py/battery_low.png diff --git a/ports/stm32/boards/Passport/graphics/build.py b/ports/stm32/boards/Passport/graphics/py/build.py similarity index 89% rename from ports/stm32/boards/Passport/graphics/build.py rename to ports/stm32/boards/Passport/graphics/py/build.py index eddf6f1..59b5dfc 100755 --- a/ports/stm32/boards/Passport/graphics/build.py +++ b/ports/stm32/boards/Passport/graphics/py/build.py @@ -10,7 +10,7 @@ import zlib def read_text(fname): w = 0 - rows = [] + rows = [] Z = b'\0' F = b'\xff' @@ -22,7 +22,7 @@ def read_text(fname): w = max(w, len(r)) rows.append(r) - assert 1 <= w < 220, w + assert 1 <= w < 230, w raw = b'' for r in rows: @@ -34,13 +34,13 @@ def read_text(fname): def read_img(fn): img = Image.open(fn) w,h = img.size - assert 1 <= w < 220, w + assert 1 <= w < 230, w img = img.convert('L') # fix colour issues: assume minority colour is white (1) histo = img.histogram() assert len(histo) == 256, repr(histo) - assert len(set(histo)) == 3, "Too many colours: "+repr(histo) + #assert len(set(histo)) == 3, "Too many colors: "+repr(histo) # if histo[-1] > histo[0]: img = ImageOps.invert(img) @@ -62,7 +62,7 @@ def crunch(n): #print(' / '.join("%d => %d" % (wb,len(d)) for wb,d in a)) return a[0] - + def doit(outfname, fnames): @@ -71,10 +71,10 @@ def doit(outfname, fnames): fp = open(outfname, 'wt') fp.write("""\ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2021 Foundation Devices, Inc. # SPDX-License-Identifier: GPL-3.0-or-later # -# SPDX-FileCopyrightText: 2018 Coinkite, Inc. +# SPDX-FileCopyrightText: 2018 Coinkite, Inc. # SPDX-License-Identifier: GPL-3.0-only # # autogenerated; don't edit diff --git a/ports/stm32/boards/Passport/graphics/cylon.py b/ports/stm32/boards/Passport/graphics/py/cylon.py similarity index 91% rename from ports/stm32/boards/Passport/graphics/cylon.py rename to ports/stm32/boards/Passport/graphics/py/cylon.py index 13a4a67..3d14724 100644 --- a/ports/stm32/boards/Passport/graphics/cylon.py +++ b/ports/stm32/boards/Passport/graphics/py/cylon.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2018 Coinkite, Inc. +# SPDX-FileCopyrightText: 2018 Coinkite, Inc. # SPDX-License-Identifier: GPL-3.0-only # #!/usr/bin/env python3 diff --git a/ports/stm32/boards/Passport/graphics/py/fcc-ce-logos.jpg b/ports/stm32/boards/Passport/graphics/py/fcc-ce-logos.jpg new file mode 100644 index 0000000000000000000000000000000000000000..925401bea41f732214826f61cadca0a38af50341 GIT binary patch literal 3294 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 JPDHLkV1kk8B7pz^ literal 0 HcmV?d00001 diff --git a/ports/stm32/boards/Passport/graphics/fruit.png b/ports/stm32/boards/Passport/graphics/py/fruit.png similarity index 100% rename from ports/stm32/boards/Passport/graphics/fruit.png rename to ports/stm32/boards/Passport/graphics/py/fruit.png diff --git a/ports/stm32/boards/Passport/graphics/py/graphics.py b/ports/stm32/boards/Passport/graphics/py/graphics.py new file mode 100644 index 0000000..dfe35b3 --- /dev/null +++ b/ports/stm32/boards/Passport/graphics/py/graphics.py @@ -0,0 +1,87 @@ +# SPDX-FileCopyrightText: 2021 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# SPDX-FileCopyrightText: 2018 Coinkite, Inc. +# SPDX-License-Identifier: GPL-3.0-only +# +# autogenerated; don't edit +# +class Graphics: + # (w,h, w_bytes, wbits, data) + + scroll = (3, 61, 1, 0, b'@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@@\xe0@') + + scrollbar = (8, 276, 1, 0, b'\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU') + + space = (13, 3, 2, 0, b'\x80\x08\x80\x08\xff\xf8') + + sm_box = (11, 17, 2, 0, b'\xe4\xe0\x80 \x80 \x80 \x00\x00\x00\x00\x80 \x00\x00\x00\x00\x00\x00\x80 \x00\x00\x00\x00\x80 \x80 \x80 \xe4\xe0') + + spin = (25, 40, 4, 0, b'\x00\x08\x00\x00\x00\x1c\x00\x00\x00>\x00\x00\x00\x7f\x00\x00\x00\xff\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\x80\x80\x00\x00\x80\x80\x00\x00\x80\x80\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x00\x00\x80\x80\x00\x00\x80\x80\x00\x00\x80\xff\xff\xff\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\x80\x00\x00\x7f\x00\x00\x00>\x00\x00\x00\x1c\x00\x00\x00\x08\x00\x00') + + arrow_down = (7, 11, 1, 0, b'\x10\x10\x10\x10\x10\x10\x10\xfe|8\x10') + + more_right = (9, 17, 2, 0, b'\x80\x00\xc0\x00\xe0\x00\xf0\x00\xf8\x00\xfc\x00\xfe\x00\xff\x00\xff\x80\xff\x00\xfe\x00\xfc\x00\xf8\x00\xf0\x00\xe0\x00\xc0\x00\x80\x00') + + arrow_up = (7, 11, 1, 0, b'\x108|\xfe\x10\x10\x10\x10\x10\x10\x10') + + more_left = (9, 17, 2, 0, b'\x00\x80\x01\x80\x03\x80\x07\x80\x0f\x80\x1f\x80?\x80\x7f\x80\xff\x80\x7f\x80?\x80\x1f\x80\x0f\x80\x07\x80\x03\x80\x01\x80\x00\x80') + + fruit = (10, 10, 2, 0, b'\x07\x80\x04\xc0\x1f\x00?\x80\x7f\xc0\x7f@\x7f@~\xc09\x80\x1f\x00') + + tetris_pattern_1 = (12, 12, 2, 0, b':\xc0z\xe0\xc0p\xef\xb0\xef\xb0\xe0p\xef\xb0\xef\xb0\xef\xb0\xc0pz\xe0:\xc0') + + tetris_pattern_6 = (12, 12, 2, 0, b':\xc0z\xe0\xc0p\xef\xb0\xef\xb0\xe0p\xef\xb0\xef\xb0\xef\xb0\xc0pz\xe0:\xc0') + + pw_pressed_box_sm = (14, 20, 2, 0, b'?\xf0\x7f\xf8\xe0\x1c\xc0\x0c\xc0\x0c\xc0\x0c\xc0\x0c\xc0\x0c\xc3\x0c\xc7\x8c\xc7\x8c\xc3\x0c\xc0\x0c\xc0\x0c\xc0\x0c\xc0\x0c\xc0\x0c\xe0\x1c\x7f\xf8?\xf0') + + wedge = (11, 11, 2, 0, b'\xc0\x00\xf0\x00|\x00\x1f\x00\x07\xc0\x01\xe0\x07\xc0\x1f\x00|\x00\xf0\x00\xc0\x00') + + x = (15, 15, 2, 0, b'\x00\x00`\x0cp\x1c88\x1cp\x0e\xe0\x07\xc0\x03\x80\x07\xc0\x0e\xe0\x1cp88p\x1c`\x0c\x00\x00') + + pw_filled_box_sm = (14, 20, 2, 0, b'?\xf0\x7f\xf8\xe0\x1c\xc0\x0c\xc0\x0c\xc0\x0c\xc7\x8c\xcf\xcc\xdf\xec\xdf\xec\xdf\xec\xdf\xec\xcf\xcc\xc7\x8c\xc0\x0c\xc0\x0c\xc0\x0c\xe0\x1c\x7f\xf8?\xf0') + + tetris_pattern_3 = (12, 12, 2, 0, b':\xc0z\xe0\xc0p\xef\xb0\xef\xb0\xe0p\xef\xb0\xef\xb0\xef\xb0\xc0pz\xe0:\xc0') + + splash = (172, 129, 22, 0, b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1e\x00\x0f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1e\x00\x0f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>\x00\x0f\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00~\x00\x0f\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00~\x00\x0f\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfe\x00\x0f\xe0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfe\x00\x0f\xe0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xfe\x00\x0f\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xfe\x00\x0f\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\xfe\x00\x0f\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xfe\x00\x0f\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xfe\x00\x0f\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\xfe\x00\x0f\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\xfe\x00\x0f\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\xfe\x00\x0f\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00?\xfe\x00\x0f\xff\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00?\xfe\x00\x0f\xff\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00`\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xe0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xe0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00?\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xff\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xff\xe0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xff\xe0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xff\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xff\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\xff\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xff\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\xff\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xff\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xff\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xff\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xff\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xff\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xff\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\xff\xc0\x00?\xfc\x00\x07\xff\x80\x00\x7f\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00?\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\x00\x00\x00\x00\x00\x7f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xc0\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xe0\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xe0\x00\x00\x00\x00\x00\x01\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xf0\x00\x00\x00\x00\x00\x01\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xf0\x00\x00\x00\x00\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xf8\x00\x00\x00\x00\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xf8\x00\x00\x00\x00\x00\x07\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfc\x00\x00\x00\x00\x00\x0f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x0f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x1f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\x80\x0f\x80\x0e\x0e\x01\xc0p\x1f\xe0\x00\x10\x03\xff\x80p\x00\xf8\x01\xc0p\xff\x80\x1f\xc0\x0e\x0e\x01\xc0p\x1f\xf0\x008\x03\xff\x80p\x01\xfc\x01\xc0p\xff\x80?\xe0\x0e\x0e\x01\xe0p\x1f\xf8\x008\x03\xff\x80p\x03\xfe\x01\xe0p\xe0\x00x\xf0\x0e\x0e\x01\xf0p\x1c<\x00|\x008\x00p\x07\x8f\x01\xf0p\xe0\x00\xf0x\x0e\x0e\x01\xf8p\x1c\x1e\x00|\x008\x00p\x0f\x07\x81\xf8p\xfe\x00\xe08\x0e\x0e\x01\xfcp\x1c\x0e\x00\xfe\x008\x00p\x0e\x03\x81\xfcp\xfe\x00\xe08\x0e\x0e\x01\xdep\x1c\x0e\x00\xee\x008\x00p\x0e\x03\x81\xdep\xfe\x00\xe08\x0e\x0e\x01\xcfp\x1c\x0e\x01\xef\x008\x00p\x0e\x03\x81\xcfp\xe0\x00\xf0x\x0e\x0e\x01\xc7\xf0\x1c\x1e\x01\xc7\x008\x00p\x0f\x07\x81\xc7\xf0\xe0\x00x\xf0\x0f\x1e\x01\xc3\xf0\x1c<\x03\xc7\x808\x00p\x07\x8f\x01\xc3\xf0\xe0\x00?\xe0\x07\xfc\x01\xc1\xf0\x1f\xf8\x03\x83\x808\x00p\x03\xfe\x01\xc1\xf0\xe0\x00\x1f\xc0\x07\xfc\x01\xc0\xf0\x1f\xf0\x07\x83\xc08\x00p\x01\xfc\x01\xc0\xf0\xe0\x00\x0f\x80\x03\xf8\x01\xc0p\x1f\xe0\x07\x01\xc08\x00p\x00\xf8\x01\xc0p') + + battery_100 = (29, 15, 4, 0, b'\xff\xff\xff\xc0\xff\xff\xff\xc0\xc0\x00\x00\xc0\xc0\x00\x00\xc0\xces\x9c\xf8\xces\x9c\xf8\xces\x9c\x18\xces\x9c\x18\xces\x9c\x18\xces\x9c\xf8\xces\x9c\xf8\xc0\x00\x00\xc0\xc0\x00\x00\xc0\xff\xff\xff\xc0\xff\xff\xff\xc0') + + wordmark = (172, 13, 22, 0, b'\xff\x80\x0f\x80\x0e\x0e\x01\xc0p\x1f\xe0\x00\x10\x03\xff\x80p\x00\xf8\x01\xc0p\xff\x80\x1f\xc0\x0e\x0e\x01\xc0p\x1f\xf0\x008\x03\xff\x80p\x01\xfc\x01\xc0p\xff\x80?\xe0\x0e\x0e\x01\xe0p\x1f\xf8\x008\x03\xff\x80p\x03\xfe\x01\xe0p\xe0\x00x\xf0\x0e\x0e\x01\xf0p\x1c<\x00|\x008\x00p\x07\x8f\x01\xf0p\xe0\x00\xf0x\x0e\x0e\x01\xf8p\x1c\x1e\x00|\x008\x00p\x0f\x07\x81\xf8p\xfe\x00\xe08\x0e\x0e\x01\xfcp\x1c\x0e\x00\xfe\x008\x00p\x0e\x03\x81\xfcp\xfe\x00\xe08\x0e\x0e\x01\xdep\x1c\x0e\x00\xee\x008\x00p\x0e\x03\x81\xdep\xfe\x00\xe08\x0e\x0e\x01\xcfp\x1c\x0e\x01\xef\x008\x00p\x0e\x03\x81\xcfp\xe0\x00\xf0x\x0e\x0e\x01\xc7\xf0\x1c\x1e\x01\xc7\x008\x00p\x0f\x07\x81\xc7\xf0\xe0\x00x\xf0\x0f\x1e\x01\xc3\xf0\x1c<\x03\xc7\x808\x00p\x07\x8f\x01\xc3\xf0\xe0\x00?\xe0\x07\xfc\x01\xc1\xf0\x1f\xf8\x03\x83\x808\x00p\x03\xfe\x01\xc1\xf0\xe0\x00\x1f\xc0\x07\xfc\x01\xc0\xf0\x1f\xf0\x07\x83\xc08\x00p\x01\xfc\x01\xc0\xf0\xe0\x00\x0f\x80\x03\xf8\x01\xc0p\x1f\xe0\x07\x01\xc08\x00p\x00\xf8\x01\xc0p') + + fcc_ce_logos = (157, 40, 20, 0, b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00?\x00\x00\x01\x80\x00\xf8\x00\x18\x00\x00\x01\xfc\x00\x00\x0f\xf0\x7f\xff\x01\xff\xc0\x00\x00\xc0\x0f\xff\x000\x00\x00\x0f\xfc\x00\x00?\xf0\x7f\xfe\x07\xff\xf8\x00\x00`?\x8f\xc0`\x00\x00?\xfc\x00\x00\x7f\xf0\x7f\xfc\x0f\xff\xfe\x00\x000p\xf8\xf0\xc0\x00\x00\xff\xfc\x00\x01\xff\xf0\x7f\xf8?\xc0\xff\x00\x00\x18\xc0\x009\x80\x00\x01\xff\xfc\x00\x03\xff\xf0x\x00~\x00?\x80\x00\r\xff\xff\xfb\x00\x00\x03\xff\x80\x00\x07\xfe\x00x\x00\xfc\x00\x0f\xc0\x00\x06\xff\xff\xf6\x00\x00\x07\xfe\x00\x00\x0f\xf8\x00x\x01\xf0\x00\x03\xe0\x00\x03`\x00l\x00\x00\x07\xf8\x00\x00\x1f\xe0\x00x\x03\xe0?\x01\xc0\x00\x01\xe0\x00|\x00\x00\x0f\xe0\x00\x00?\xc0\x00x\x03\xc0\xff\xc0\x80\x00\x00\xe0\x00l\x00\x00\x1f\xc0\x00\x00?\x80\x00x\x07\x83\xff\xf0\x00\x00\x00p\x00\xfc\x00\x00\x1f\x80\x00\x00?\x00\x00x\x07\x83\xff\xf8\x00\x00\x000\xf9\xf8\x00\x00\x1f\x80\x00\x00~\x00\x00x\x0f\x0f\xc0\xf0\x00\x00\x008\xfb\xc0\x00\x00?\x00\x00\x00~\x00\x00x\x0f\x0f\x80`\x00\x00\x00<\x06\xc0\x00\x00?\x00\x00\x00~\x00\x00x\x0f\x1f\x00\x00\x00\x00\x006\x0c\xc0\x00\x00~\x00\x00\x00\xfc\x00\x00x\x1e\x1e\x00\x00\x00\x00\x003\x18\xc0\x00\x00~\x00\x00\x00\xff\xff\x00\x7f\xfe\x1e\x00\x00\x00\x00\x001\xb0\xc0\x00\x00~\x00\x00\x00\xff\xff\x00\x7f\xfe\x1c\x00\x00\x00\x00\x000\xe0\xc0\x00\x00~\x00\x00\x00\xff\xff\x00\x7f\xfe\x1c\x00\x00\x00\x00\x000\xe0\xc0\x00\x00~\x00\x00\x00\xff\xff\x00\x7f\xfe\x1e\x00\x00\x00\x00\x001\xb0\xc0\x00\x00~\x00\x00\x00\xff\xff\x00x\x1e\x1e\x00\x00\x00\x00\x003\x18\xc0\x00\x00~\x00\x00\x00\xff\xff\x00x\x0f\x0f\x00\x00\x00\x00\x00\x1e\x0c\xc0\x00\x00~\x00\x00\x00\xfc\x00\x00x\x0f\x0f\x80p\x00\x00\x00\x1c\x07\x80\x00\x00?\x00\x00\x00~\x00\x00x\x0f\x07\xc0\xf8\x00\x00\x00\x18\x03\x80\x00\x00?\x00\x00\x00~\x00\x00x\x07\x87\xff\xf0\x00\x00\x008\x01\xc0\x00\x00\x1f\x80\x00\x00~\x00\x00x\x07\x83\xff\xf0\x00\x00\x00x\x01\xe0\x00\x00\x1f\x80\x00\x00?\x00\x00x\x03\xc0\xff\xc0\xc0\x00\x00\xd8\x01\xb0\x00\x00\x1f\xc0\x00\x00?\x80\x00x\x03\xe0?\x01\xe0\x00\x01\x98\x03\xd8\x00\x00\x0f\xe0\x00\x00?\xc0\x00x\x01\xf0\x00\x03\xe0\x00\x03\x18\x06l\x00\x00\x07\xf8\x00\x00\x1f\xe0\x00x\x00\xf8\x00\x0f\xc0\x00\x06\x18\x05\xa6\x00\x00\x07\xfe\x00\x00\x0f\xf8\x00x\x00\xfe\x00?\x80\x00\x0c\x1f\xfd\xa3\x00\x00\x03\xff\x80\x00\x07\xfe\x00x\x00?\xc0\xff\x00\x00\x18\x0e\x06a\x80\x00\x01\xff\xfc\x00\x03\xff\xf0x\x00\x1f\xff\xfe\x00\x000\x0e\x03\xc0\xc0\x00\x00\xff\xfc\x00\x01\xff\xf0x\x00\x07\xff\xf8\x00\x00`\x00\x00\x00`\x00\x00?\xfc\x00\x00\x7f\xf0\x00\x00\x01\xff\xc0\x00\x00\xc0\x00\x00\x000\x00\x00\x0f\xfc\x00\x00?\xf0\x00\x00\x00?\x00\x00\x00\x80\x00\x00\x00\x10\x00\x00\x01\xfc\x00\x00\x0f\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') + + selected = (15, 15, 2, 0, b'\x00\x0c\x00\x1c\x008\x000\x00p\x00`\x00\xe0`\xc0q\xc09\x80\x1f\x80\x0f\x00\x07\x00\x02\x00\x00\x00') + + pw_empty_box_sm = (14, 20, 2, 0, b'?\xf0\x7f\xf8\xe0\x1c\xc0\x0c\xc0\x0c\xc0\x0c\xc0\x0c\xc0\x0c\xc0\x0c\xc0\x0c\xc0\x0c\xc0\x0c\xc0\x0c\xc0\x0c\xc0\x0c\xc0\x0c\xc0\x0c\xe0\x1c\x7f\xf8?\xf0') + + tetris_pattern_4 = (12, 12, 2, 0, b':\xc0z\xe0\xc0p\xef\xb0\xef\xb0\xe0p\xef\xb0\xef\xb0\xef\xb0\xc0pz\xe0:\xc0') + + pw_pressed_box_lg = (30, 43, 4, 0, b'?\xff\xff\xf0\x7f\xff\xff\xf8\xe0\x00\x00\x1c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x07\x80\x0c\xc0\x0f\xc0\x0c\xc0\x1f\xe0\x0c\xc0?\xf0\x0c\xc0?\xf0\x0c\xc0?\xf0\x0c\xc0?\xf0\x0c\xc0\x1f\xe0\x0c\xc0\x0f\xc0\x0c\xc0\x07\x80\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xe0\x00\x00\x1c\x7f\xff\xff\xf8?\xff\xff\xf0') + + battery_50 = (29, 15, 4, 0, b'\xff\xff\xff\xc0\xff\xff\xff\xc0\xc0\x00\x00\xc0\xc0\x00\x00\xc0\xcep\x00\xf8\xcep\x00\xf8\xcep\x00\x18\xcep\x00\x18\xcep\x00\x18\xcep\x00\xf8\xcep\x00\xf8\xc0\x00\x00\xc0\xc0\x00\x00\xc0\xff\xff\xff\xc0\xff\xff\xff\xc0') + + pw_filled_box_lg = (30, 43, 4, 0, b'?\xff\xff\xf0\x7f\xff\xff\xf8\xe0\x00\x00\x1c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x07\x80\x0c\xc0\x1f\xe0\x0c\xc0?\xf0\x0c\xc0\x7f\xf8\x0c\xc0\x7f\xf8\x0c\xc0\xff\xfc\x0c\xc0\xff\xfc\x0c\xc0\xff\xfc\x0c\xc0\xff\xfc\x0c\xc0\x7f\xf8\x0c\xc0\x7f\xf8\x0c\xc0?\xf0\x0c\xc0\x1f\xe0\x0c\xc0\x07\x80\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xe0\x00\x00\x1c\x7f\xff\xff\xf8?\xff\xff\xf0') + + tetris_pattern_2 = (12, 12, 2, 0, b':\xc0z\xe0\xc0p\xef\xb0\xef\xb0\xe0p\xef\xb0\xef\xb0\xef\xb0\xc0pz\xe0:\xc0') + + battery_25 = (29, 15, 4, 0, b'\xff\xff\xff\xc0\xff\xff\xff\xc0\xc0\x00\x00\xc0\xc0\x00\x00\xc0\xce\x00\x00\xf8\xce\x00\x00\xf8\xce\x00\x00\x18\xce\x00\x00\x18\xce\x00\x00\x18\xce\x00\x00\xf8\xce\x00\x00\xf8\xc0\x00\x00\xc0\xc0\x00\x00\xc0\xff\xff\xff\xc0\xff\xff\xff\xc0') + + pw_empty_box_lg = (30, 43, 4, 0, b'?\xff\xff\xf0\x7f\xff\xff\xf8\xe0\x00\x00\x1c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xe0\x00\x00\x1c\x7f\xff\xff\xf8?\xff\xff\xf0') + + tetris_pattern_0 = (12, 12, 2, 0, b':\xc0z\xe0\xc0p\xef\xb0\xef\xb0\xe0p\xef\xb0\xef\xb0\xef\xb0\xc0pz\xe0:\xc0') + + battery_low = (29, 15, 4, 0, b'\xff\xff\xff\xc0\xff\xff\xff\xc0\xc0\x00\x00\xc0\xc0\x00\x00\xc0\xc8\x00\x00\xf8\xc8\x00\x00\xf8\xc8\x00\x00\x18\xc8\x00\x00\x18\xc8\x00\x00\x18\xc8\x00\x00\xf8\xc8\x00\x00\xf8\xc0\x00\x00\xc0\xc0\x00\x00\xc0\xff\xff\xff\xc0\xff\xff\xff\xc0') + + passphrase_icon = (21, 21, 3, 0, b'\x00p\x00\x07\xff\x00?\xff\xe0?\xff\xe0>\x03\xe0>\x01\xe0>y\xe0>y\xe0>y\xe0>y\xe0>\x01\xe0>\x03\xe0>\x7f\xe0>\x7f\xe0\x1e\x7f\xc0\x1e\x7f\xc0\x0f\xff\x80\x07\xff\x00\x03\xfe\x00\x01\xfc\x00\x00p\x00') + + passport = (124, 12, 16, 0, b'\xfc\x00\x10\x00\x1e\x00\x1e\x00~\x00\x0f\x80\x0f\xc0\x0f\xf0\xfe\x008\x00?\x00?\x00\x7f\x00\x1f\xc0\x0f\xe0\x0f\xf0\xc7\x008\x00s\x00s\x00c\x808\xe0\x0cp\x01\x80\xc3\x00|\x00`\x00`\x00a\x80pp\x0c0\x01\x80\xc3\x00l\x00p\x00p\x00a\x80`0\x0c0\x01\x80\xc7\x00\xee\x00<\x00<\x00c\x80`0\x0cp\x01\x80\xfe\x00\xc6\x00\x1e\x00\x1e\x00\x7f\x00`0\x0f\xe0\x01\x80\xfc\x01\xc7\x00\x07\x00\x07\x00~\x00`0\x0f\xc0\x01\x80\xc0\x01\x83\x00\x03\x00\x03\x00`\x00pp\x0c\xc0\x01\x80\xc0\x03\x83\x80g\x00g\x00`\x008\xe0\x0c\xe0\x01\x80\xc0\x03\x01\x80~\x00~\x00`\x00\x1f\xc0\x0cp\x01\x80\xc0\x07\x01\xc0<\x00<\x00`\x00\x0f\x80\x0c0\x01\x80') + + ie_logo = (150, 148, 19, 0, b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\xff\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xff\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xff\xff\xff\xff\xe0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\xff\xff\xff\xff\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xff\xff\xff\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\xff\xff\xff\xff\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00?\xff\xff\xff\xf8\x1f\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xff\xff\xff\xff\x00\x01\xff\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xff\xff\xff\xff\xf0\x00\x00\x7f\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x0f\xff\xff\xff\xff\xff\x80\x00\x00\x1f\xc0\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xfe\x00\x00\x00\x0f\xc0\x00\x00\x00\x00\x00\x00\x00\x0f\xff\xff\xff\xff\xff\xf0\x00\x00\x00\x0f\xe0\x00\x00\x00\x00\x00\x00\x00?\xff\xff\xff\xff\xff\xf0\x00\x00\x00\x03\xe0\x00\x00\x00\x00\x00\x00\x01\xff\xff\xff\xff\xff\xff\xfc\x00\x00\x00\x03\xe0\x00\x00\x00\x00\x00\x00\x07\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x01\xe0\x00\x00\x00\x00\x00\x00\x0f\xff\xff\xff\xff\xff\xff\xff\xc0\x00\x00\x01\xe0\x00\x00\x00\x00\x00\x00\x7f\xff\xff\xff\xff\xff\xff\xff\xf0\x00\x00\x01\xf0\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xf8\x00\x00\x00\xf0\x00\x00\x00\x00\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xfe\x00\x00\x00\xf0\x00\x00\x00\x00\x00\x07\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\x00\xf0\x00\x00\x00\x00\x00\x1f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xc0\x00\x00p\x00\x00\x00\x00\x00?\xff\xff\xff\xff\xff\xff\xff\xff\xff\xe0\x00\x00p\x00\x00\x00\x00\x00\x7f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xf0\x00\x00p\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfc\x00\x00\xf0\x00\x00\x00\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\x00\x00\xe0\x00\x00\x00\x00\x07\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\xe0\x00\x00\x00\x00\x0f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\xe0\x00\x00\x00\x00\x1f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xc0\x00\xe0\x00\x00\x00\x00?\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xe0\x00\xe0\x00\x00\x00\x00\x7f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xf0\x01\xc0\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xf8\x01\xc0\x00\x00\x00\x01\xff\xff\xef\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfc\x01\xc0\x00\x00\x00\x01\xff\xff\x1f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\x03\x80\x00\x00\x00\x03\xff\xfe\x7f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\x03\x80\x00\x00\x00\x07\xff\xf8\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x03\x80\x00\x00\x00\x0f\xff\xf1\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x87\x00\x00\x00\x00\x1f\xff\xe3\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xc7\x00\x00\x00\x00\x1f\xff\x87\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xc7\x00\x00\x00\x00?\xff\x0f\xff\xff\xff\xf0\x00?\xff\xff\xff\xff\xff\xef\x00\x00\x00\x00?\xfc\x1f\xff\xff\xff\x80\x00\x03\xff\xff\xff\xff\xff\xee\x00\x00\x00\x00\x7f\xf8?\xff\xff\xfc\x00\x00\x00\x7f\xff\xff\xff\xff\xfc\x00\x00\x00\x00\xff\xf0?\xff\xff\xf0\x00\x00\x00?\xff\xff\xff\xff\xfc\x00\x00\x00\x00\xff\xc0\x7f\xff\xff\xe0\x00\x00\x00\x0f\xff\xff\xff\xff\xfc\x00\x00\x00\x01\xff\x80\xff\xff\xff\x80\x00\x00\x00\x03\xff\xff\xff\xff\xfc\x00\x00\x00\x01\xff\x01\xff\xff\xff\x00\x00\x00\x00\x01\xff\xff\xff\xff\xfe\x00\x00\x00\x03\xfe\x03\xff\xff\xfc\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x03\xfc\x07\xff\xff\xf8\x00\x00\x00\x00\x00\x7f\xff\xff\xff\xfe\x00\x00\x00\x07\xf8\x0f\xff\xff\xf8\x00\x00\x00\x00\x00?\xff\xff\xff\xff\x00\x00\x00\x07\xf0\x1f\xff\xff\xf0\x00\x00\x00\x00\x00\x1f\xff\xff\xff\xff\x00\x00\x00\x0f\xe0\x1f\xff\xff\xe0\x00\x00\x00\x00\x00\x0f\xff\xff\xff\xff\x80\x00\x00\x0f\xc0?\xff\xff\xc0\x00\x00\x00\x00\x00\x07\xff\xff\xff\xff\x80\x00\x00\x0f\x80\x7f\xff\xff\x80\x00\x00\x00\x00\x00\x07\xff\xff\xff\xff\xc0\x00\x00\x1f\x00\xff\xff\xff\x80\x00\x00\x00\x00\x00\x03\xff\xff\xff\xff\xc0\x00\x00\x1e\x01\xff\xff\xff\x00\x00\x00\x00\x00\x00\x01\xff\xff\xff\xff\xc0\x00\x00<\x01\xff\xff\xff\x00\x00\x00\x00\x00\x00\x01\xff\xff\xff\xff\xe0\x00\x008\x03\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x01\xff\xff\xff\xff\xe0\x00\x000\x07\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xe0\x00\x00 \x0f\xff\xff\xfc\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xf0\x00\x00`\x0f\xff\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x7f\xff\xff\xff\xf0\x00\x00@\x1f\xff\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x7f\xff\xff\xff\xf0\x00\x00\x00?\xff\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x7f\xff\xff\xff\xf0\x00\x00\x00\x7f\xff\xff\xf8\x00\x00\x00\x00\x00\x00\x00\x7f\xff\xff\xff\xf8\x00\x00\x00\x7f\xff\xff\xf8\x00\x00\x00\x00\x00\x00\x00\x7f\xff\xff\xff\xf8\x00\x00\x00\xff\xff\xff\xf8\x00\x00\x00\x00\x00\x00\x00?\xff\xff\xff\xf8\x00\x00\x01\xff\xff\xff\xf8\x00\x00\x00\x00\x00\x00\x00?\xff\xff\xff\xf8\x00\x00\x01\xff\xff\xff\xf8\x00\x00\x00\x00\x00\x00\x00?\xff\xff\xff\xf8\x00\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfc\x00\x00\x07\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfc\x00\x00\x07\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfc\x00\x00\x0f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfc\x00\x00\x1f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfc\x00\x00\x1f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfc\x00\x00?\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfc\x00\x00?\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfc\x00\x00\x7f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfc\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfc\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfc\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfc\x00\x01\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfc\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfc\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfc\x00\x07\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfc\x00\x0f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfc\x00\x0f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfc\x00\x1f\xff\xff\xff\xff\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\xff\xff\xff\xff\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00?\xff\xff\xff\xff\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00?\xff\xff\xff\xff\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xff\xff\xff\xff\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xff\xff\xff\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xff\xff\xff\xff\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xff\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\xff\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\xff\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xff\xff\xff\xff\xff\xff\x80\x00\x00\x00\x00\x00\x01\xff\xff\xff\xff\xc0\x0f\xff\xcf\xff\xff\xff\xff\xc0\x00\x00\x00\x00\x00\x03\xff\xff\xff\xff\xc0\x0f\xff\xcf\xff\xff\xff\xff\xe0\x00\x00\x00\x00\x00\x07\xff\xff\xff\xff\x80\x0f\xff\x8f\xff\xff\xff\xff\xe0\x00\x00\x00\x00\x00\x0f\xff\xff\xff\xff\x80\x1f\xff\x87\xff\xff\xff\xff\xf0\x00\x00\x00\x00\x00\x0f\xff\xff\xff\xff\x00\x1f\xff\x07\xff\xff\xff\xff\xf8\x00\x00\x00\x00\x00\x1f\xff\xff\xff\xff\x00\x1f\xff\x03\xff\xff\xff\xff\xfc\x00\x00\x00\x00\x00?\xff\xff\xff\xfe\x00\x1f\xff\x03\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00?\xfe\x01\xff\xff\xff\xff\xff\x00\x00\x00\x00\x01\xff\xff\xff\xff\xfe\x00?\xfe\x01\xff\xff\xff\xff\xff\xc0\x00\x00\x00\x03\xff\xff\xff\xff\xfc\x00?\xfc\x00\xff\xff\xff\xff\xff\xe0\x00\x00\x00\x0f\xff\xff\xff\xff\xf8\x00?\xfc\x00\xff\xff\xff\xff\xff\xf8\x00\x00\x00\x1f\xff\xff\xff\xff\xf8\x00?\xfc\x00\x7f\xff\xff\xff\xff\xfe\x00\x00\x00\x7f\xff\xff\xff\xff\xf0\x00\x7f\xf8\x00?\xff\xff\xff\xff\xff\xc0\x00\x03\xff\xff\xff\xff\xff\xf0\x00\x7f\xf8\x00?\xff\xff\xff\xff\xff\xfc\x00?\xff\xff\xff\xff\xff\xe0\x00\x7f\xf8\x00\x1f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xc0\x00\x7f\xf8\x00\x0f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xc0\x00\xff\xf0\x00\x0f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\xff\xf0\x00\x07\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\xff\xf0\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\x00\x00\xff\xf0\x00\x01\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\x00\x00\xff\xe0\x00\x01\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfc\x00\x00\xff\xe0\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xf8\x00\x00\xff\xe0\x00\x00\x7f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xf0\x00\x00\xff\xe0\x00\x00?\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xe0\x00\x00\xff\xe0\x00\x00\x1f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xc0\x00\x00\xff\xe0\x00\x00\x0f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\x00\xff\xe0\x00\x00\x07\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\xff\xe0\x00\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\x00\x00\x00\xff\xe0\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfc\x00\x00\x00\xff\xe0\x00\x00\x00\x7f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xf8\x00\x00\x00\xff\xe0\x00\x00\x00?\xff\xff\xff\xff\xff\xff\xff\xff\xff\xe0\x00\x00\x00\xff\xe0\x00\x00\x00\x1f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xc0\x00\x00\x00\x7f\xe0\x00\x00\x00\x07\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x7f\xf0\x00\x00\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x7f\xf0\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xf8\x00\x00\x00\x00?\xf8\x00\x00\x00\x01\xff\xff\xff\xff\xff\xff\xff\xff\xf0\x00\x00\x00\x00?\xf8\x00\x00\x00\x07\xff\xff\xff\xff\xff\xff\xff\xff\xc0\x00\x00\x00\x00?\xfe\x00\x00\x00\x1f\xf7\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x1f\xff\x00\x00\x00\x7f\xc0\xff\xff\xff\xff\xff\xff\xfc\x00\x00\x00\x00\x00\x0f\xff\x80\x00\x07\xff\x00?\xff\xff\xff\xff\xff\xe0\x00\x00\x00\x00\x00\x0f\xff\xe0\x00\x1f\xfc\x00\x07\xff\xff\xff\xff\xff\x80\x00\x00\x00\x00\x00\x07\xff\xff\x83\xff\xe0\x00\x01\xff\xff\xff\xff\xf8\x00\x00\x00\x00\x00\x00\x03\xff\xff\xff\xff\x80\x00\x00\x0f\xff\xff\xff\xc0\x00\x00\x00\x00\x00\x00\x01\xff\xff\xff\xfc\x00\x00\x00\x00\xff\xff\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xff\xff\xe0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') + + battery_75 = (29, 15, 4, 0, b'\xff\xff\xff\xc0\xff\xff\xff\xc0\xc0\x00\x00\xc0\xc0\x00\x00\xc0\xces\x80\xf8\xces\x80\xf8\xces\x80\x18\xces\x80\x18\xces\x80\x18\xces\x80\xf8\xces\x80\xf8\xc0\x00\x00\xc0\xc0\x00\x00\xc0\xff\xff\xff\xc0\xff\xff\xff\xc0') + + tetris_pattern_5 = (12, 12, 2, 0, b':\xc0z\xe0\xc0p\xef\xb0\xef\xb0\xe0p\xef\xb0\xef\xb0\xef\xb0\xc0pz\xe0:\xc0') + + +# EOF diff --git a/ports/stm32/boards/Passport/graphics/py/ie_logo.py b/ports/stm32/boards/Passport/graphics/py/ie_logo.py new file mode 100644 index 0000000000000000000000000000000000000000..dd53203de30002de500758e92c0cd737555cdab5 GIT binary patch literal 2707 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 N002ovPDHLkV1fnZ7VQ84 literal 0 HcmV?d00001 diff --git a/ports/stm32/boards/Passport/graphics/more_left.txt b/ports/stm32/boards/Passport/graphics/py/more_left.txt similarity index 100% rename from ports/stm32/boards/Passport/graphics/more_left.txt rename to ports/stm32/boards/Passport/graphics/py/more_left.txt diff --git a/ports/stm32/boards/Passport/graphics/more_right.txt b/ports/stm32/boards/Passport/graphics/py/more_right.txt similarity index 100% rename from ports/stm32/boards/Passport/graphics/more_right.txt rename to ports/stm32/boards/Passport/graphics/py/more_right.txt diff --git a/ports/stm32/boards/Passport/graphics/py/passphrase_icon.png b/ports/stm32/boards/Passport/graphics/py/passphrase_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..ae216a1f9864cb6f2cc8f1bed84f0d37c83c129d GIT binary patch literal 175 zcmeAS@N?(olHy`uVBq!ia0vp^q9Dw{1SHi;jSd2-GEW!B5R2ZUA&z_p6gZ@h{jZ-X z>(g^sEYRoLWVTyf9g9}+H+F1kot~UD_i)1J+jXAFPN~z=0%UWUD~|EjYjpihcfQt? z^SA!<-d}Um%OCOo7T^1y`=6YB(5sGl^S}N-lV8B)?v>ccar4u3p<-!m6rn<>6dwn9(V76V57=3 zt7{C%t}%kzT@j20lwZPc$WXUKS!@xuow^!iD#H$ z+QjR`Q=7KId7oHdWmzRYCmuEFg2azpS3G{>TyiP=*vr7r#Xxi7O)5jA{5k6K@~O4u zx=P;w2ZunsMA>UT@9ydB?cX!4{(b;C=5mk=;-!m6rn<>6dwn9(V76V57=3 zt7{C%t}%kzT@j20lwZPc$WXUKS!@xuow^!iD#H$ z+QjR`Q=7KId7oHdWmzRYCmuEFg2azpS3G{>TyiP=*vr7r#Xxi7O)5jA{5k6K@~O4u zx=P;w2ZunsMA>UT@9ydB?cX!4{(b;C=5mk=;-!m6rn<>6dwn9(V76V57=3 zt7{C%t}%kzT@j20lwZPc$WXUKS!@xuow^!iD#H$ z+QjR`Q=7KId7oHdWmzRYCmuEFg2azpS3G{>TyiP=*vr7r#Xxi7O)5jA{5k6K@~O4u zx=P;w2ZunsMA>UT@9ydB?cX!4{(b;C=5mk=;FzlZ1%{g6Xj2pDjRC6Q rX!?r3vvr*4HOs;gE}V9mp&hsZtbHmGlaz2u00000NkvXXu0mjf5W@2A literal 0 HcmV?d00001 diff --git a/ports/stm32/boards/Passport/graphics/scroll.txt b/ports/stm32/boards/Passport/graphics/py/scroll.txt similarity index 100% rename from ports/stm32/boards/Passport/graphics/scroll.txt rename to ports/stm32/boards/Passport/graphics/py/scroll.txt diff --git a/ports/stm32/boards/Passport/graphics/py/scrollbar.txt b/ports/stm32/boards/Passport/graphics/py/scrollbar.txt new file mode 100644 index 0000000..1ac799f --- /dev/null +++ b/ports/stm32/boards/Passport/graphics/py/scrollbar.txt @@ -0,0 +1,276 @@ +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x +x x x x + x x x x diff --git a/ports/stm32/boards/Passport/graphics/py/selected.png b/ports/stm32/boards/Passport/graphics/py/selected.png new file mode 100644 index 0000000000000000000000000000000000000000..bb4e587e79458d91c8851a912e120902b0c2577b GIT binary patch literal 148 zcmeAS@N?(olHy`uVBq!ia0vp^{2(Vi}jAsQ2VCpmI47;-T8{{LV6 z?SfL?>fX4yJdCWK!OREFc>EHGRes?R&ia-wzopr0PPqwc>n+a literal 0 HcmV?d00001 diff --git a/ports/stm32/boards/Passport/graphics/sm_box.txt b/ports/stm32/boards/Passport/graphics/py/sm_box.txt similarity index 100% rename from ports/stm32/boards/Passport/graphics/sm_box.txt rename to ports/stm32/boards/Passport/graphics/py/sm_box.txt diff --git a/ports/stm32/boards/Passport/graphics/space.txt b/ports/stm32/boards/Passport/graphics/py/space.txt similarity index 100% rename from ports/stm32/boards/Passport/graphics/space.txt rename to ports/stm32/boards/Passport/graphics/py/space.txt diff --git a/ports/stm32/boards/Passport/graphics/spin.txt b/ports/stm32/boards/Passport/graphics/py/spin.txt similarity index 100% rename from ports/stm32/boards/Passport/graphics/spin.txt rename to ports/stm32/boards/Passport/graphics/py/spin.txt diff --git a/ports/stm32/boards/Passport/graphics/py/splash.png b/ports/stm32/boards/Passport/graphics/py/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..96f36d9137d7dc582ca2fd7a7aec33e0fe1a5357 GIT binary patch literal 919 zcmeAS@N?(olHy`uVBq!ia0vp^Yk;_sg9%7pN<5a%z`z{m>EaktG3V`^+mjX>@UZf) z{QEyXO+M&ofQm$Xi|Ukj$9vWnMYl<&Pb-c6%B*u*X=TwS-&WxdpX3hk-0QYleogo% zC+7m6I!-sUFa0Xbe}oRGo>Y~%e`@~(9wYCBt#vPLPv|g)ObTPV{dTssG!srYbg_an|5Ezk!ii}Of|`r2@GtGOb$ROAk;8{p@l_3g+Y;#gVVu-!HEbVL)D0B zs%Opjm?`;d&0$GYPIWqSvvo4VGhy$h8PZ3mH5B)#GVT!YXljUlqPFO)Nf*n-M@8lf zpGmti6{xhZEZla~Snwt6Vp-B?r1w!#m_djEl>DYIpscyvj-rRg!l1R%uaJC|4*E!@aMOMTWhB zh;UqTsdvih-)imLx?i92tDd)8uM_a|GV|K)nbV#=VmkNq>%4t8r&h0&39>YrcK!6c z(uZp@x4b{}PI!;zv$;ZZzSfq{$hln}e?`qLDNgV6_y3%)EVV-zExa9NQ`UpXKKgf$i7#U2yYV+*f(&Nb|C; z_i}e9A51uN{?WBLoadyhsw;k;iafe6u_VCw)~5q$YRr>oh0O?% zJvnRJ*0*L`+1gClJ%_bz;qN(4&y5|=?*7EWwp%+y{y%SzWt*XW$jf?zH`TF*+n>Md i+LwQ5dhBeU|MqjAsGP{35vmBxa15TVelF{r5}E*A%X-ED literal 0 HcmV?d00001 diff --git a/ports/stm32/boards/Passport/graphics/tetris_pattern_0.png b/ports/stm32/boards/Passport/graphics/py/tetris_pattern_0.png similarity index 100% rename from ports/stm32/boards/Passport/graphics/tetris_pattern_0.png rename to ports/stm32/boards/Passport/graphics/py/tetris_pattern_0.png diff --git a/ports/stm32/boards/Passport/graphics/tetris_pattern_1.png b/ports/stm32/boards/Passport/graphics/py/tetris_pattern_1.png similarity index 100% rename from ports/stm32/boards/Passport/graphics/tetris_pattern_1.png rename to ports/stm32/boards/Passport/graphics/py/tetris_pattern_1.png diff --git a/ports/stm32/boards/Passport/graphics/tetris_pattern_2.png b/ports/stm32/boards/Passport/graphics/py/tetris_pattern_2.png similarity index 100% rename from ports/stm32/boards/Passport/graphics/tetris_pattern_2.png rename to ports/stm32/boards/Passport/graphics/py/tetris_pattern_2.png diff --git a/ports/stm32/boards/Passport/graphics/tetris_pattern_3.png b/ports/stm32/boards/Passport/graphics/py/tetris_pattern_3.png similarity index 100% rename from ports/stm32/boards/Passport/graphics/tetris_pattern_3.png rename to ports/stm32/boards/Passport/graphics/py/tetris_pattern_3.png diff --git a/ports/stm32/boards/Passport/graphics/tetris_pattern_4.png b/ports/stm32/boards/Passport/graphics/py/tetris_pattern_4.png similarity index 100% rename from ports/stm32/boards/Passport/graphics/tetris_pattern_4.png rename to ports/stm32/boards/Passport/graphics/py/tetris_pattern_4.png diff --git a/ports/stm32/boards/Passport/graphics/tetris_pattern_5.png b/ports/stm32/boards/Passport/graphics/py/tetris_pattern_5.png similarity index 100% rename from ports/stm32/boards/Passport/graphics/tetris_pattern_5.png rename to ports/stm32/boards/Passport/graphics/py/tetris_pattern_5.png diff --git a/ports/stm32/boards/Passport/graphics/tetris_pattern_6.png b/ports/stm32/boards/Passport/graphics/py/tetris_pattern_6.png similarity index 100% rename from ports/stm32/boards/Passport/graphics/tetris_pattern_6.png rename to ports/stm32/boards/Passport/graphics/py/tetris_pattern_6.png diff --git a/ports/stm32/boards/Passport/graphics/wedge.png b/ports/stm32/boards/Passport/graphics/py/wedge.png similarity index 100% rename from ports/stm32/boards/Passport/graphics/wedge.png rename to ports/stm32/boards/Passport/graphics/py/wedge.png diff --git a/ports/stm32/boards/Passport/graphics/wordmark.png b/ports/stm32/boards/Passport/graphics/py/wordmark.png similarity index 100% rename from ports/stm32/boards/Passport/graphics/wordmark.png rename to ports/stm32/boards/Passport/graphics/py/wordmark.png diff --git a/ports/stm32/boards/Passport/graphics/py/x.png b/ports/stm32/boards/Passport/graphics/py/x.png new file mode 100644 index 0000000000000000000000000000000000000000..18313b818acb4a1e9bd00615d6c2e8b7e9dc959d GIT binary patch literal 156 zcmeAS@N?(olHy`uVBq!ia0vp^{2$(}BbAsQ2VCpdC37;-QR|No!< zJwQV$IWm8(vCyG2N8WQXsCe2f;CA_Y>q6P@wwZ_iTKZgR=V;odqU0mOuvC~+`L@IS zUGtu5 #include #include +// #include #include "quirc_internal.h" +#include "utils.h" + #pragma GCC diagnostic ignored "-Wdouble-promotion" /************************************************************************ @@ -129,54 +132,70 @@ static void perspective_unmap(const double *c, * Span-based floodfill routine */ -#define FLOOD_FILL_MAX_DEPTH 4096 +// FOUNDATION: Added this code to restrict max stack depth +#define FLOOD_FILL_MAX_DEPTH 720 +static int max_depth = 0; typedef void (*span_func_t)(void *user_data, int y, int left, int right); -static void flood_fill_seed(struct quirc *q, int x, int y, int from, int to, - span_func_t func, void *user_data, - int depth) +typedef struct _flood_fill_context { + struct quirc *q; + span_func_t func; + void *user_data; +} flood_fill_context; + +static void flood_fill_seed(flood_fill_context *context, short x, short y, short from, short to, + short depth) { - int left = x; - int right = x; - int i; - quirc_pixel_t *row = q->pixels + y * q->w; + short left = x; + short right = x; + short i; - if (depth >= FLOOD_FILL_MAX_DEPTH) + quirc_pixel_t *row = context->q->pixels + y * context->q->w; + + // if (!check_stack_sentinel()) { + // printf("flood_fill_seed: depth=%d max_depth=%d\n", depth, max_depth); + // return; + // } + + if (depth >= FLOOD_FILL_MAX_DEPTH) { return; + } + + if (depth > max_depth) { + max_depth = depth; + } while (left > 0 && row[left - 1] == from) left--; - while (right < q->w - 1 && row[right + 1] == from) + while (right < context->q->w - 1 && row[right + 1] == from) right++; /* Fill the extent */ for (i = left; i <= right; i++) row[i] = to; - if (func) - func(user_data, y, left, right); + if (context->func) + context->func(context->user_data, y, left, right); /* Seed new flood-fills */ if (y > 0) { - row = q->pixels + (y - 1) * q->w; + row = context->q->pixels + (y - 1) * context->q->w; for (i = left; i <= right; i++) if (row[i] == from) - flood_fill_seed(q, i, y - 1, from, to, - func, user_data, depth + 1); + flood_fill_seed(context, i, y - 1, from, to, depth + 1); } - if (y < q->h - 1) + if (y < context->q->h - 1) { - row = q->pixels + (y + 1) * q->w; + row = context->q->pixels + (y + 1) * context->q->w; for (i = left; i <= right; i++) if (row[i] == from) - flood_fill_seed(q, i, y + 1, from, to, - func, user_data, depth + 1); + flood_fill_seed(context, i, y + 1, from, to, depth + 1); } } @@ -273,7 +292,12 @@ static int region_code(struct quirc *q, int x, int y) box->seed.y = y; box->capstone = -1; - flood_fill_seed(q, x, y, pixel, region, area_count, box, 0); + flood_fill_context context; + context.q = q; + context.func = area_count; + context.user_data = box; + + flood_fill_seed(&context, x, y, pixel, region, 0); return region; } @@ -347,9 +371,14 @@ static void find_region_corners(struct quirc *q, memcpy(&psd.ref, ref, sizeof(psd.ref)); psd.scores[0] = -1; - flood_fill_seed(q, region->seed.x, region->seed.y, - rcode, QUIRC_PIXEL_BLACK, - find_one_corner, &psd, 0); + + flood_fill_context context; + context.q = q; + context.func = find_one_corner; + context.user_data = &psd; + + flood_fill_seed(&context, region->seed.x, region->seed.y, + rcode, QUIRC_PIXEL_BLACK, 0); psd.ref.x = psd.corners[0].x - psd.ref.x; psd.ref.y = psd.corners[0].y - psd.ref.y; @@ -365,9 +394,9 @@ static void find_region_corners(struct quirc *q, psd.scores[1] = i; psd.scores[3] = -i; - flood_fill_seed(q, region->seed.x, region->seed.y, - QUIRC_PIXEL_BLACK, rcode, - find_other_corners, &psd, 0); + context.func = find_other_corners; + flood_fill_seed(&context, region->seed.x, region->seed.y, + QUIRC_PIXEL_BLACK, rcode, 0); } static void record_capstone(struct quirc *q, int ring, int stone) @@ -471,8 +500,9 @@ static void finder_scan(struct quirc *q, int y) pb[i] > check[i] * avg + err) ok = 0; - if (ok) + if (ok) { test_capstone(q, x, y, pb); + } } } @@ -1014,12 +1044,18 @@ static void record_qr_grid(struct quirc *q, int a, int b, int c) psd.scores[0] = -hd.y * qr->align.x + hd.x * qr->align.y; - flood_fill_seed(q, reg->seed.x, reg->seed.y, - qr->align_region, QUIRC_PIXEL_BLACK, - NULL, NULL, 0); - flood_fill_seed(q, reg->seed.x, reg->seed.y, - QUIRC_PIXEL_BLACK, qr->align_region, - find_leftmost_to_line, &psd, 0); + flood_fill_context context; + context.q = q; + context.func = NULL; + context.user_data = NULL; + + flood_fill_seed(&context, reg->seed.x, reg->seed.y, + qr->align_region, QUIRC_PIXEL_BLACK, 0); + + context.func = find_leftmost_to_line; + context.user_data = &psd; + flood_fill_seed(&context, reg->seed.x, reg->seed.y, + QUIRC_PIXEL_BLACK, qr->align_region, 0); } } @@ -1170,8 +1206,9 @@ void quirc_end(struct quirc *q) uint8_t threshold = otsu(q); pixels_setup(q, threshold); - for (i = 0; i < q->h; i++) + for (i = 0; i < q->h; i++) { finder_scan(q, i); + } for (i = 0; i < q->num_capstones; i++) test_grouping(q, i); diff --git a/ports/stm32/boards/Passport/image_conversion.c b/ports/stm32/boards/Passport/image_conversion.c index 49aad85..984dee7 100644 --- a/ports/stm32/boards/Passport/image_conversion.c +++ b/ports/stm32/boards/Passport/image_conversion.c @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // diff --git a/ports/stm32/boards/Passport/image_conversion.h b/ports/stm32/boards/Passport/image_conversion.h index 8485d21..5008d34 100644 --- a/ports/stm32/boards/Passport/image_conversion.h +++ b/ports/stm32/boards/Passport/image_conversion.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // diff --git a/ports/stm32/boards/Passport/include/backlight.h b/ports/stm32/boards/Passport/include/backlight.h new file mode 100644 index 0000000..e582e75 --- /dev/null +++ b/ports/stm32/boards/Passport/include/backlight.h @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-License-Identifier: GPL-3.0-or-later +// +// SPDX-FileCopyrightText: 2018 Coinkite, Inc. +// SPDX-License-Identifier: GPL-3.0-only +// +// Backlight driver for LED + +#ifndef STM32_BACKLIGHT_H +#define STM32_BACKLIGHT_H + +#include "stm32h7xx_hal.h" + +#include + +extern void backlight_init(void); +extern void backlight_minimal_init(void); +extern void backlight_intensity(uint16_t intensity); +extern void backlight_adjust(bool turbo); + +#endif //STM32_BACKLIGHT_H diff --git a/ports/stm32/boards/Passport/include/delay.h b/ports/stm32/boards/Passport/include/delay.h index 7126f41..59f8490 100644 --- a/ports/stm32/boards/Passport/include/delay.h +++ b/ports/stm32/boards/Passport/include/delay.h @@ -1,7 +1,7 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // -// SPDX-FileCopyrightText: 2018 Coinkite, Inc. +// SPDX-FileCopyrightText: 2018 Coinkite, Inc. // SPDX-License-Identifier: GPL-3.0-only // /* diff --git a/ports/stm32/boards/Passport/include/display.h b/ports/stm32/boards/Passport/include/display.h new file mode 100644 index 0000000..3e46226 --- /dev/null +++ b/ports/stm32/boards/Passport/include/display.h @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-License-Identifier: GPL-3.0-or-later +// +// display.h - Display rendering functions for the Passport bootloader +#pragma once + +#include "lcd-sharp-ls018B7dh02.h" +#include "passport_fonts.h" + +// Pass this constant to center text horizontally +#define CENTER_X 32767 + +// Bitmap draw mode bitmask +#define DRAW_MODE_NORMAL 0 +#define DRAW_MODE_INVERT 1 +#define DRAW_MODE_WHITE_ONLY 2 +#define DRAW_MODE_BLACK_ONLY 4 + +#define PROGRESS_BAR_HEIGHT 9 +#define PROGRESS_BAR_MARGIN 10 +#define PROGRESS_BAR_Y (SCREEN_HEIGHT - 40) + +extern void display_init(bool clear); +extern uint16_t display_measure_text(char* text, Font* font); +extern uint16_t display_get_char_width(char ch, Font* font); +extern void display_text(char* text, int16_t x, int16_t y, Font* font, bool invert); +extern void display_fill_rect(int16_t x, int16_t y, int16_t w, int16_t h, u_int8_t color); +extern void display_rect(int16_t x, int16_t y, int16_t w, int16_t h, u_int8_t color); +extern void display_image(uint16_t x, uint16_t y, uint16_t image_w, uint16_t image_h, uint8_t* image, uint8_t mode); +extern void display_progress_bar(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint8_t percent); +extern void display_show(void); +extern void display_show_lines(uint16_t y_start, uint16_t y_end); +extern void display_clear(uint8_t color); +extern void display_clean_shutdown(void); diff --git a/ports/stm32/boards/Passport/include/firmware-keys.h b/ports/stm32/boards/Passport/include/firmware-keys.h index fa9044c..ba4a42f 100644 --- a/ports/stm32/boards/Passport/include/firmware-keys.h +++ b/ports/stm32/boards/Passport/include/firmware-keys.h @@ -1,47 +1,74 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // -// SPDX-FileCopyrightText: 2018 Coinkite, Inc. +// SPDX-FileCopyrightText: 2018 Coinkite, Inc. // SPDX-License-Identifier: GPL-3.0-only // #ifndef _FW_KEYS_H_ #define _FW_KEYS_H_ #define FW_MAX_PUB_KEYS 4 +#define FW_PUBLIC_KEY_SIZE 64 +#define FW_USER_KEY 255 -static const uint8_t approved_pubkeys[FW_MAX_PUB_KEYS][64] = { - { /* key 0 */ - 0xef, 0x48, 0x57, 0x7c, 0x45, 0x81, 0xed, 0x46, 0xd4, 0xe9, 0xd5, - 0x2e, 0x66, 0xa1, 0xeb, 0xbb, 0x41, 0x22, 0x57, 0xac, 0xb4, 0x92, - 0xd8, 0xdb, 0x58, 0x2d, 0x08, 0x65, 0xf3, 0xf8, 0x73, 0x9c, 0xd9, - 0x4f, 0x6a, 0xcc, 0x3f, 0x51, 0x7e, 0x72, 0x41, 0xb0, 0x17, 0x90, - 0x97, 0xb5, 0x93, 0x17, 0xed, 0x05, 0xc2, 0x0b, 0x74, 0xf4, 0x8f, - 0x2e, 0x7f, 0x83, 0x74, 0xcb, 0x36, 0xa0, 0xb2, 0xde +static const uint8_t approved_pubkeys[FW_MAX_PUB_KEYS][FW_PUBLIC_KEY_SIZE] = { + { // Key: 00-pub.bin + 0xdd, 0x60, 0x31, 0xc6, 0x40, 0x98, 0x99, 0xcf, 0x7f, 0x7b, 0xc3, 0x47, 0x96, 0xac, 0x92, 0xe4, + 0x44, 0x36, 0x59, 0x53, 0x49, 0x9b, 0x94, 0x36, 0xfc, 0x94, 0x40, 0x59, 0xc4, 0x9b, 0x0e, 0x6a, + 0x45, 0x91, 0x29, 0x8c, 0xa8, 0x36, 0x7e, 0x3a, 0x14, 0xe5, 0x13, 0x72, 0xb2, 0x74, 0xf3, 0xe8, + 0x07, 0x1b, 0x21, 0xfd, 0x3d, 0xed, 0xd7, 0xa2, 0xe2, 0x7b, 0xe8, 0x94, 0x4c, 0x02, 0x7e, 0x01 }, - { /* key 1 */ - 0x02, 0x51, 0x71, 0x27, 0x31, 0x09, 0x15, 0x23, 0x49, 0xd1, 0xb4, - 0xa1, 0xd5, 0xf7, 0x95, 0xfd, 0x76, 0x83, 0x4e, 0xb8, 0x41, 0xf5, - 0x34, 0x99, 0xf8, 0x16, 0xb6, 0x2f, 0xbc, 0xad, 0xa8, 0x5d, 0x7b, - 0xbf, 0x68, 0x91, 0xb9, 0xea, 0x58, 0xa0, 0xe9, 0x02, 0x39, 0x5b, - 0xe1, 0xf4, 0x11, 0xc9, 0x01, 0x60, 0x5f, 0x8a, 0x95, 0x8c, 0x39, - 0x56, 0x23, 0x3f, 0xf4, 0x0e, 0x15, 0x3f, 0x7a, 0xca + { // Key: 01-pub.bin + 0xc6, 0xcd, 0xf9, 0xf6, 0x35, 0x31, 0xe7, 0x67, 0x5b, 0x55, 0x35, 0x9e, 0xb7, 0xe5, 0xca, 0x1f, + 0xb9, 0x84, 0x76, 0x54, 0x02, 0xc4, 0xac, 0xb1, 0x53, 0x5e, 0xcb, 0x5b, 0xd9, 0xd7, 0xb5, 0x8e, + 0x81, 0xe1, 0x51, 0xa6, 0xc5, 0xbe, 0x87, 0x94, 0xa9, 0x9c, 0x6f, 0x82, 0xb0, 0xe3, 0xb4, 0x53, + 0x04, 0xf0, 0xa0, 0x48, 0x7b, 0xb2, 0x2a, 0xe2, 0x1d, 0x26, 0xfa, 0xb7, 0x18, 0xb9, 0x32, 0xf9 }, - { /* key 2 */ - 0x9e, 0x1a, 0x79, 0x6e, 0xa9, 0x94, 0xf5, 0xe7, 0x90, 0x53, 0xc5, - 0x7e, 0x41, 0x6f, 0x62, 0xf9, 0xd7, 0xa2, 0x2c, 0xe3, 0x9b, 0xdf, - 0xef, 0xfd, 0x1c, 0x0a, 0x7e, 0xbb, 0x4d, 0x35, 0x57, 0xda, 0x28, - 0x6c, 0x0b, 0xee, 0x50, 0x1f, 0xa0, 0x4e, 0x15, 0x71, 0xee, 0xec, - 0xad, 0xde, 0x97, 0x2d, 0xc3, 0x90, 0xa0, 0xf1, 0xb9, 0xaf, 0xd8, - 0x6a, 0x8a, 0x63, 0x1c, 0x3d, 0xf6, 0xcb, 0x54, 0x0d + { // Key: 02-pub.bin + 0xea, 0xe2, 0xa4, 0xf7, 0x90, 0x3f, 0xc7, 0xa6, 0x02, 0x58, 0x1f, 0x16, 0x36, 0x49, 0xba, 0xbb, + 0x72, 0xf4, 0xd3, 0x58, 0x8a, 0x2a, 0xd0, 0x34, 0xae, 0x63, 0xbd, 0x18, 0x9e, 0xb0, 0x9c, 0xe9, + 0x19, 0xce, 0x27, 0xc1, 0x40, 0x15, 0x91, 0xbc, 0x56, 0x64, 0xf5, 0x8d, 0x70, 0xb1, 0x38, 0x28, + 0x77, 0x50, 0x80, 0xb1, 0x3d, 0x0f, 0x93, 0xe6, 0xc8, 0xa9, 0x83, 0xe8, 0x70, 0xc2, 0xbe, 0xad }, - { /* key 3 */ - 0x47, 0x96, 0xa4, 0x41, 0x24, 0x1c, 0x6b, 0xc1, 0x15, 0x93, 0xea, - 0x89, 0x38, 0x39, 0xb7, 0x7d, 0xd5, 0xde, 0x56, 0x0d, 0x1a, 0x12, - 0x75, 0xec, 0x3e, 0xeb, 0xb9, 0x72, 0xaf, 0x9b, 0x8f, 0xe6, 0xc0, - 0x28, 0xac, 0x79, 0x4a, 0x4c, 0xde, 0x23, 0x9a, 0xdf, 0x90, 0x2b, - 0x3c, 0x51, 0xca, 0xb8, 0x00, 0xd4, 0x2e, 0x5f, 0xfd, 0x90, 0x4e, - 0xa8, 0x27, 0xc9, 0xbc, 0xaf, 0x97, 0x0d, 0xc6, 0xaa + { // Key: 03-pub.bin + 0xca, 0x32, 0xae, 0xb0, 0xf2, 0x25, 0x7f, 0xa2, 0x0c, 0xac, 0x3a, 0x56, 0xa5, 0x8b, 0x97, 0xde, + 0x99, 0x30, 0xef, 0x14, 0xfd, 0xd6, 0x90, 0x5d, 0x6d, 0x6e, 0x40, 0xb8, 0x30, 0x98, 0xc1, 0x3e, + 0x99, 0x77, 0x25, 0xdb, 0x1c, 0xbe, 0x4d, 0x9b, 0x1b, 0x8a, 0x54, 0x63, 0x0e, 0x89, 0x4b, 0x3e, + 0x23, 0x52, 0x2e, 0x5e, 0x14, 0xf3, 0x7e, 0xbb, 0x3e, 0xd9, 0xae, 0x6e, 0xda, 0xa1, 0xba, 0xcd }, + + // { /* key 0 */ + // 0xef, 0x48, 0x57, 0x7c, 0x45, 0x81, 0xed, 0x46, 0xd4, 0xe9, 0xd5, + // 0x2e, 0x66, 0xa1, 0xeb, 0xbb, 0x41, 0x22, 0x57, 0xac, 0xb4, 0x92, + // 0xd8, 0xdb, 0x58, 0x2d, 0x08, 0x65, 0xf3, 0xf8, 0x73, 0x9c, 0xd9, + // 0x4f, 0x6a, 0xcc, 0x3f, 0x51, 0x7e, 0x72, 0x41, 0xb0, 0x17, 0x90, + // 0x97, 0xb5, 0x93, 0x17, 0xed, 0x05, 0xc2, 0x0b, 0x74, 0xf4, 0x8f, + // 0x2e, 0x7f, 0x83, 0x74, 0xcb, 0x36, 0xa0, 0xb2, 0xde + // }, + // { /* key 1 */ + // 0x02, 0x51, 0x71, 0x27, 0x31, 0x09, 0x15, 0x23, 0x49, 0xd1, 0xb4, + // 0xa1, 0xd5, 0xf7, 0x95, 0xfd, 0x76, 0x83, 0x4e, 0xb8, 0x41, 0xf5, + // 0x34, 0x99, 0xf8, 0x16, 0xb6, 0x2f, 0xbc, 0xad, 0xa8, 0x5d, 0x7b, + // 0xbf, 0x68, 0x91, 0xb9, 0xea, 0x58, 0xa0, 0xe9, 0x02, 0x39, 0x5b, + // 0xe1, 0xf4, 0x11, 0xc9, 0x01, 0x60, 0x5f, 0x8a, 0x95, 0x8c, 0x39, + // 0x56, 0x23, 0x3f, 0xf4, 0x0e, 0x15, 0x3f, 0x7a, 0xca + // }, + // { /* key 2 */ + // 0x9e, 0x1a, 0x79, 0x6e, 0xa9, 0x94, 0xf5, 0xe7, 0x90, 0x53, 0xc5, + // 0x7e, 0x41, 0x6f, 0x62, 0xf9, 0xd7, 0xa2, 0x2c, 0xe3, 0x9b, 0xdf, + // 0xef, 0xfd, 0x1c, 0x0a, 0x7e, 0xbb, 0x4d, 0x35, 0x57, 0xda, 0x28, + // 0x6c, 0x0b, 0xee, 0x50, 0x1f, 0xa0, 0x4e, 0x15, 0x71, 0xee, 0xec, + // 0xad, 0xde, 0x97, 0x2d, 0xc3, 0x90, 0xa0, 0xf1, 0xb9, 0xaf, 0xd8, + // 0x6a, 0x8a, 0x63, 0x1c, 0x3d, 0xf6, 0xcb, 0x54, 0x0d + // }, + // { /* key 3 */ + // 0x47, 0x96, 0xa4, 0x41, 0x24, 0x1c, 0x6b, 0xc1, 0x15, 0x93, 0xea, + // 0x89, 0x38, 0x39, 0xb7, 0x7d, 0xd5, 0xde, 0x56, 0x0d, 0x1a, 0x12, + // 0x75, 0xec, 0x3e, 0xeb, 0xb9, 0x72, 0xaf, 0x9b, 0x8f, 0xe6, 0xc0, + // 0x28, 0xac, 0x79, 0x4a, 0x4c, 0xde, 0x23, 0x9a, 0xdf, 0x90, 0x2b, + // 0x3c, 0x51, 0xca, 0xb8, 0x00, 0xd4, 0x2e, 0x5f, 0xfd, 0x90, 0x4e, + // 0xa8, 0x27, 0xc9, 0xbc, 0xaf, 0x97, 0x0d, 0xc6, 0xaa + // }, }; #endif /* _FW_KEYS_H_ */ diff --git a/ports/stm32/boards/Passport/include/fwheader.h b/ports/stm32/boards/Passport/include/fwheader.h index d752cf0..9d90d28 100644 --- a/ports/stm32/boards/Passport/include/fwheader.h +++ b/ports/stm32/boards/Passport/include/fwheader.h @@ -1,26 +1,30 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // -// SPDX-FileCopyrightText: 2018 Coinkite, Inc. +// SPDX-FileCopyrightText: 2018 Coinkite, Inc. // SPDX-License-Identifier: GPL-3.0-only // #pragma once #include -#define FW_START (BL_FLASH_LAST) +#define FW_START (0x08020000) #define FW_HEADER_SIZE 2048 #define FW_HEADER_MAGIC 0x50415353 #define FW_HDR ((passport_firmware_header_t *)(FW_START)) +#define FW_END (0x081E0000) #define HASH_LEN 32 #define SIGNATURE_LEN 64 +#define DATE_LEN 14 // e.g., "Jan. 01, 2021" + null +#define VERSION_LEN 8 // e.g., 00.00 + null typedef struct __attribute__ ((packed)) { uint32_t magic; uint32_t timestamp; - uint8_t fwversion[8]; + uint8_t fwdate[DATE_LEN]; + uint8_t fwversion[VERSION_LEN]; uint32_t fwlength; } fw_info_t; @@ -37,4 +41,3 @@ typedef struct __attribute__ ((packed)) fw_info_t info; fw_signature_t signature; } passport_firmware_header_t; - diff --git a/ports/stm32/boards/Passport/gpio.h b/ports/stm32/boards/Passport/include/gpio.h similarity index 70% rename from ports/stm32/boards/Passport/gpio.h rename to ports/stm32/boards/Passport/include/gpio.h index 2ceb02e..55e4af7 100644 --- a/ports/stm32/boards/Passport/gpio.h +++ b/ports/stm32/boards/Passport/include/gpio.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // diff --git a/ports/stm32/boards/Passport/include/hash.h b/ports/stm32/boards/Passport/include/hash.h index d98c448..db0e034 100644 --- a/ports/stm32/boards/Passport/include/hash.h +++ b/ports/stm32/boards/Passport/include/hash.h @@ -1,12 +1,18 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // -// SPDX-FileCopyrightText: 2018 Coinkite, Inc. +// SPDX-FileCopyrightText: 2018 Coinkite, Inc. // SPDX-License-Identifier: GPL-3.0-only // #pragma once +#include + #include "fwheader.h" +extern void hash_bl(uint8_t *bl, size_t bllen, uint8_t *sig, uint8_t siglen); extern void hash_fw(fw_info_t *hdr, uint8_t *fw, size_t fwlen, uint8_t *sig, uint8_t siglen); +extern void hash_fw_user(uint8_t *fw, size_t fwlen, uint8_t *hash, uint8_t hashlen, bool exclude_hdr); extern void hash_board(uint8_t *fw_signature, uint8_t fw_signature_len, uint8_t *sig, uint8_t siglen); +extern void get_device_hash(uint8_t *hash); +extern bool get_serial_number(char *serial_buf, uint8_t serial_buf_len); diff --git a/ports/stm32/boards/Passport/keypad-adp-5587.h b/ports/stm32/boards/Passport/include/keypad-adp-5587.h similarity index 96% rename from ports/stm32/boards/Passport/keypad-adp-5587.h rename to ports/stm32/boards/Passport/include/keypad-adp-5587.h index 05980cf..98d8c17 100644 --- a/ports/stm32/boards/Passport/keypad-adp-5587.h +++ b/ports/stm32/boards/Passport/include/keypad-adp-5587.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // @@ -10,9 +10,6 @@ #include "stm32h7xx_hal_i2c.h" #include -extern ring_buffer_t keybuf; -; - #define KBD_ADDR (0x34 << 1) // Use 8-Bit address #define KBD_REG_DEVID 0x00 // Device ID @@ -88,5 +85,6 @@ extern void keypad_init(void); extern int keypad_write(uint8_t address, uint8_t reg, uint8_t data); extern int keypad_read(uint8_t address, uint8_t reg, uint8_t* data, uint8_t len); extern void keypad_test(void); +extern void keypad_ISR(void); #endif diff --git a/ports/stm32/boards/Passport/include/lcd-sharp-ls018B7dh02.h b/ports/stm32/boards/Passport/include/lcd-sharp-ls018B7dh02.h index 56501dd..b64bf06 100644 --- a/ports/stm32/boards/Passport/include/lcd-sharp-ls018B7dh02.h +++ b/ports/stm32/boards/Passport/include/lcd-sharp-ls018B7dh02.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // // LCD driver for Sharp LS018B7DH02 monochrome display @@ -41,10 +41,12 @@ typedef struct _LCDTestScreen { LCDTestLine lines[SCREEN_HEIGHT]; } LCDTestScreen; -void lcd_init(void); +void lcd_init(bool clear); void lcd_deinit(void); void lcd_clear(bool invert); void lcd_update(uint8_t* screen_data, bool invert); -void lcd_show_busy_bar(); void lcd_test(void); +void lcd_prebuffer_line(uint16_t y, uint8_t* line_data, bool invert); +void lcd_update_line_range(uint16_t y_start, uint16_t y_end); + #endif /* __LCD_H__ */ diff --git a/ports/stm32/boards/Passport/include/passport_fonts.h b/ports/stm32/boards/Passport/include/passport_fonts.h new file mode 100644 index 0000000..3aa9f74 --- /dev/null +++ b/ports/stm32/boards/Passport/include/passport_fonts.h @@ -0,0 +1,52 @@ +// Passport wallet font definitions in C +// Autogenerated by bdf-to-passport.py: DO NOT EDIT +#pragma once + +#include +#include +#include + +typedef struct { + int8_t x; + int8_t y; + int8_t w; + int8_t h; + int8_t advance; + uint8_t* bitmap; +} GlyphInfo; + +typedef struct { + int8_t x; + int8_t y; + int8_t w; + int8_t h; + int8_t advance; + uint8_t data_len; +} BBox; + +typedef struct { + uint16_t range_start; + uint16_t range_end; + uint16_t* bitmap_offsets; +} Codepoints; + +typedef struct { + int8_t height; + int8_t advance; + int8_t ascent; + int8_t descent; + int8_t leading; + uint8_t codepoint_start; + uint8_t codepoint_end; + uint8_t num_codepoint_ranges; + BBox* bboxes; + Codepoints* codepoints; + uint8_t* bitmaps; +} Font; + +// Lookup GlyphInfo for a single codepoint or return None +bool glyph_lookup(Font* font, uint8_t cp, GlyphInfo* glyph_info); + +// Font references +extern Font FontTiny; +extern Font FontSmall; diff --git a/ports/stm32/boards/Passport/include/pprng.h b/ports/stm32/boards/Passport/include/pprng.h index 8da33ce..49e8f33 100644 --- a/ports/stm32/boards/Passport/include/pprng.h +++ b/ports/stm32/boards/Passport/include/pprng.h @@ -1,7 +1,7 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // -// SPDX-FileCopyrightText: 2018 Coinkite, Inc. +// SPDX-FileCopyrightText: 2018 Coinkite, Inc. // SPDX-License-Identifier: GPL-3.0-only // /* diff --git a/ports/stm32/boards/Passport/ring_buffer.h b/ports/stm32/boards/Passport/include/ring_buffer.h similarity index 56% rename from ports/stm32/boards/Passport/ring_buffer.h rename to ports/stm32/boards/Passport/include/ring_buffer.h index bfb5c3a..264e8b3 100644 --- a/ports/stm32/boards/Passport/ring_buffer.h +++ b/ports/stm32/boards/Passport/include/ring_buffer.h @@ -1,14 +1,14 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // #ifndef RING_BUFFER_H_ #define RING_BUFFER_H_ - +#if 0 #include #include #include - +#endif #define MAX_RING_BUFFER_SIZE 16 typedef uint8_t ring_buffer_size_t; @@ -24,54 +24,47 @@ typedef struct _ring_buffer_t { /** * Initializes or resets the ring buffer. - * @param buffer The ring buffer to initialize. * @return 0 if successful; -1 otherwise. */ -int ring_buffer_init(ring_buffer_t* buffer); +int ring_buffer_init(void); /** * Adds a byte to a ring buffer. - * @param buffer The buffer in which the data should be placed. * @param data The byte to place. */ -void ring_buffer_enqueue(ring_buffer_t* buffer, uint8_t data); +void ring_buffer_enqueue(uint8_t data); /** * Returns the oldest byte in a ring buffer. - * @param buffer The buffer from which the data should be returned. * @param data A pointer to the location at which the data should be placed. * @return 1 if data was returned; 0 otherwise. */ -uint8_t ring_buffer_dequeue(ring_buffer_t* buffer, uint8_t* data); +uint8_t ring_buffer_dequeue(uint8_t* data); /** * Peeks a ring buffer, i.e. returns an element without removing it. - * @param buffer The buffer from which the data should be returned. * @param data A pointer to the location at which the data should be placed. * @param index The index to peek. * @return 1 if data was returned; 0 otherwise. */ -uint8_t ring_buffer_peek(ring_buffer_t* buffer, uint8_t* data, ring_buffer_size_t index); +uint8_t ring_buffer_peek(uint8_t* data, ring_buffer_size_t index); /** * Returns whether a ring buffer is empty. - * @param buffer The buffer for which it should be returned whether it is empty. * @return 1 if empty; 0 otherwise. */ -uint8_t ring_buffer_is_empty(ring_buffer_t* buffer); +uint8_t ring_buffer_is_empty(void); /** * Returns whether a ring buffer is full. - * @param buffer The buffer for which it should be returned whether it is full. * @return 1 if full; 0 otherwise. */ -uint8_t ring_buffer_is_full(ring_buffer_t* buffer); +uint8_t ring_buffer_is_full(void); /** * Returns the number of items in a ring buffer. - * @param buffer The buffer for which the number of items should be returned. * @return The number of items in the ring buffer. */ -ring_buffer_size_t ring_buffer_num_items(ring_buffer_t* buffer); +ring_buffer_size_t ring_buffer_num_items(void); #endif diff --git a/ports/stm32/boards/Passport/include/se-config.h b/ports/stm32/boards/Passport/include/se-config.h index d4768be..b51e9f3 100644 --- a/ports/stm32/boards/Passport/include/se-config.h +++ b/ports/stm32/boards/Passport/include/se-config.h @@ -1,11 +1,11 @@ -// autogenerated; see bootloader/keylayout.py +// Autogenerated; see tools/se_config_gen // bytes [16..84) of chip config area #define SE_CHIP_CONFIG_1 { \ 0xe1, 0x00, 0x61, 0x00, 0x00, 0x00, 0x8f, 0x80, 0x8f, 0x80, \ - 0x8f, 0x43, 0xaf, 0x80, 0x00, 0x43, 0x00, 0x43, 0x00, 0x00, \ - 0xc3, 0x43, 0xc3, 0x43, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ - 0x00, 0x00, 0x8f, 0x43, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, \ + 0x8f, 0x43, 0xaf, 0x80, 0x00, 0x43, 0x00, 0x43, 0x8f, 0x80, \ + 0x00, 0x00, 0xc3, 0x43, 0x00, 0x43, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x8f, 0x4e, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, \ 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, \ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x00, 0x00, 0x00, \ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 \ @@ -16,21 +16,22 @@ #define SE_CHIP_CONFIG_2 { \ 0x02, 0x15, 0x00, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x5c, 0x00, \ 0xbc, 0x01, 0xfc, 0x01, 0xbc, 0x01, 0x9c, 0x01, 0x9c, 0x01, \ - 0x3c, 0x00, 0xdc, 0x03, 0xdc, 0x03, 0x3c, 0x00, 0x3c, 0x00, \ + 0xbc, 0x01, 0x3c, 0x00, 0xdc, 0x03, 0x9c, 0x01, 0x3c, 0x00, \ 0x3c, 0x00, 0x3c, 0x00, 0xdc, 0x01, 0x3c, 0x00 \ } // key/slot usage and names -#define KEYNUM_pairing 1 +#define KEYNUM_pairing_secret 1 #define KEYNUM_pin_stretch 2 -#define KEYNUM_main_pin 3 +#define KEYNUM_pin_hash 3 #define KEYNUM_pin_attempt 4 #define KEYNUM_lastgood 5 #define KEYNUM_match_count 6 -#define KEYNUM_long_secret 8 -#define KEYNUM_secret 9 -#define KEYNUM_firmware 14 +#define KEYNUM_supply_chain 7 +#define KEYNUM_seed 9 +#define KEYNUM_user_fw_pubkey 10 +#define KEYNUM_firmware_hash 14 /* @@ -68,17 +69,17 @@ KeyConfig[5] = 0x9c01 = KeyConfig(Private=0, PubInfo=0, KeyType=7, Lockable=0, R Slot[6] = 0x0043 = SlotConfig(ReadKey=0, NoMac=0, LimitedUse=0, EncryptRead=0, IsSecret=0, WriteKey=3, WriteConfig=4)=0x4300 KeyConfig[6] = 0x9c01 = KeyConfig(Private=0, PubInfo=0, KeyType=7, Lockable=0, ReqRandom=0, ReqAuth=1, AuthKey=1, PersistentDisable=0, RFU=0, X509id=0)=0x019c - Slot[7] = 0x0000 = SlotConfig(ReadKey=0, NoMac=0, LimitedUse=0, EncryptRead=0, IsSecret=0, WriteKey=0, WriteConfig=0)=0x0000 -KeyConfig[7] = 0x3c00 = KeyConfig(Private=0, PubInfo=0, KeyType=7, Lockable=1, ReqRandom=0, ReqAuth=0, AuthKey=0, PersistentDisable=0, RFU=0, X509id=0)=0x003c + Slot[7] = 0x8f80 = SlotConfig(ReadKey=15, NoMac=0, LimitedUse=0, EncryptRead=0, IsSecret=1, WriteKey=0, WriteConfig=8)=0x808f +KeyConfig[7] = 0xbc01 = KeyConfig(Private=0, PubInfo=0, KeyType=7, Lockable=1, ReqRandom=0, ReqAuth=1, AuthKey=1, PersistentDisable=0, RFU=0, X509id=0)=0x01bc - Slot[8] = 0xc343 = SlotConfig(ReadKey=3, NoMac=0, LimitedUse=0, EncryptRead=1, IsSecret=1, WriteKey=3, WriteConfig=4)=0x43c3 -KeyConfig[8] = 0xdc03 = KeyConfig(Private=0, PubInfo=0, KeyType=7, Lockable=0, ReqRandom=1, ReqAuth=1, AuthKey=3, PersistentDisable=0, RFU=0, X509id=0)=0x03dc + Slot[8] = 0x0000 = SlotConfig(ReadKey=0, NoMac=0, LimitedUse=0, EncryptRead=0, IsSecret=0, WriteKey=0, WriteConfig=0)=0x0000 +KeyConfig[8] = 0x3c00 = KeyConfig(Private=0, PubInfo=0, KeyType=7, Lockable=1, ReqRandom=0, ReqAuth=0, AuthKey=0, PersistentDisable=0, RFU=0, X509id=0)=0x003c Slot[9] = 0xc343 = SlotConfig(ReadKey=3, NoMac=0, LimitedUse=0, EncryptRead=1, IsSecret=1, WriteKey=3, WriteConfig=4)=0x43c3 KeyConfig[9] = 0xdc03 = KeyConfig(Private=0, PubInfo=0, KeyType=7, Lockable=0, ReqRandom=1, ReqAuth=1, AuthKey=3, PersistentDisable=0, RFU=0, X509id=0)=0x03dc - Slot[10] = 0x0000 = SlotConfig(ReadKey=0, NoMac=0, LimitedUse=0, EncryptRead=0, IsSecret=0, WriteKey=0, WriteConfig=0)=0x0000 -KeyConfig[10] = 0x3c00 = KeyConfig(Private=0, PubInfo=0, KeyType=7, Lockable=1, ReqRandom=0, ReqAuth=0, AuthKey=0, PersistentDisable=0, RFU=0, X509id=0)=0x003c + Slot[10] = 0x0043 = SlotConfig(ReadKey=0, NoMac=0, LimitedUse=0, EncryptRead=0, IsSecret=0, WriteKey=3, WriteConfig=4)=0x4300 +KeyConfig[10] = 0x9c01 = KeyConfig(Private=0, PubInfo=0, KeyType=7, Lockable=0, ReqRandom=0, ReqAuth=1, AuthKey=1, PersistentDisable=0, RFU=0, X509id=0)=0x019c Slot[11] = 0x0000 = SlotConfig(ReadKey=0, NoMac=0, LimitedUse=0, EncryptRead=0, IsSecret=0, WriteKey=0, WriteConfig=0)=0x0000 KeyConfig[11] = 0x3c00 = KeyConfig(Private=0, PubInfo=0, KeyType=7, Lockable=1, ReqRandom=0, ReqAuth=0, AuthKey=0, PersistentDisable=0, RFU=0, X509id=0)=0x003c @@ -89,7 +90,7 @@ KeyConfig[12] = 0x3c00 = KeyConfig(Private=0, PubInfo=0, KeyType=7, Lockable=1, Slot[13] = 0x0000 = SlotConfig(ReadKey=0, NoMac=0, LimitedUse=0, EncryptRead=0, IsSecret=0, WriteKey=0, WriteConfig=0)=0x0000 KeyConfig[13] = 0x3c00 = KeyConfig(Private=0, PubInfo=0, KeyType=7, Lockable=1, ReqRandom=0, ReqAuth=0, AuthKey=0, PersistentDisable=0, RFU=0, X509id=0)=0x003c - Slot[14] = 0x8f43 = SlotConfig(ReadKey=15, NoMac=0, LimitedUse=0, EncryptRead=0, IsSecret=1, WriteKey=3, WriteConfig=4)=0x438f + Slot[14] = 0x8f4e = SlotConfig(ReadKey=15, NoMac=0, LimitedUse=0, EncryptRead=0, IsSecret=1, WriteKey=14, WriteConfig=4)=0x4e8f KeyConfig[14] = 0xdc01 = KeyConfig(Private=0, PubInfo=0, KeyType=7, Lockable=0, ReqRandom=1, ReqAuth=1, AuthKey=1, PersistentDisable=0, RFU=0, X509id=0)=0x01dc Slot[15] = 0x0000 = SlotConfig(ReadKey=0, NoMac=0, LimitedUse=0, EncryptRead=0, IsSecret=0, WriteKey=0, WriteConfig=0)=0x0000 diff --git a/ports/stm32/boards/Passport/include/se.h b/ports/stm32/boards/Passport/include/se.h index f571811..7af026b 100644 --- a/ports/stm32/boards/Passport/include/se.h +++ b/ports/stm32/boards/Passport/include/se.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. + * SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. * SPDX-License-Identifier: GPL-3.0-or-later */ #ifndef _SECURE_ELEMENT_H_ @@ -53,6 +53,7 @@ extern void se_crc16_chain(uint8_t length, const uint8_t *data, uint8_t crc[2]); extern void se_write(seopcode_t opcode, uint8_t p1, uint16_t p2, uint8_t *data, uint8_t data_len); extern int se_read(uint8_t *data, uint8_t len); extern int se_read1(void); +extern int se_read_data_slot(int slot_num, uint8_t *data, int len); extern int se_config_read(uint8_t *config); extern int se_pair_unlock(void); extern int se_checkmac(uint8_t keynum, const uint8_t *secret); @@ -60,7 +61,12 @@ extern int se_checkmac_hard(uint8_t keynum, const uint8_t *secret); extern int se_gendig_slot(int slot_num, const uint8_t *slot_contents, uint8_t *digest); extern bool se_is_correct_tempkey(const uint8_t *expected_tempkey); extern int se_pick_nonce(uint8_t *num_in, uint8_t *tempkey); +extern int se_encrypted_read(int data_slot, int read_kn, const uint8_t *read_key, uint8_t *data, int len); extern int se_encrypted_write(int data_slot, int write_kn, const uint8_t *write_key, const uint8_t *data, int len); -extern int se_encrypted_write32(int data_slot, int blk, int write_kn, const uint8_t *write_key, const uint8_t *data); +extern int se_get_counter(uint32_t *result, uint8_t counter_number); +extern int se_add_counter(uint32_t *result, uint8_t counter_number, int incr); +extern int se_gendig_counter(int counter_num, const uint32_t expected_value, uint8_t digest[32]); + +extern uint8_t se_show_error(void); #endif /* _SECURE_ELEMENT_H_ */ diff --git a/ports/stm32/boards/Passport/include/secresult.h b/ports/stm32/boards/Passport/include/secresult.h new file mode 100644 index 0000000..063305b --- /dev/null +++ b/ports/stm32/boards/Passport/include/secresult.h @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-License-Identifier: GPL-3.0-or-later +// +// secresult.h - Secure return values from security-critical functions -- ensures that single bit glitches cannot +// cause the wrong code path to be taken. + +#pragma once + +#include + +// A true/false result with error numbers encoded as alternate values +typedef uint32_t secresult; +#define SEC_TRUE 0xAAAAAAAAU +#define SEC_FALSE 0x00000000U + +// Error values +#define ERR_ROM_SECRETS_TOO_BIG 0x50505050U +#define ERR_INVALID_FIRMWARE_HEADER 0x51515151U +#define ERR_INVALID_FIRMWARE_SIGNATURE 0x52525252U +#define ERR_UNABLE_TO_CONFIGURE_SE 0x53535353U +#define ERR_UNABLE_TO_WRITE_ROM_SECRETS 0x54545454U +#define ERR_UNABLE_TO_UPDATE_FIRMWARE_HASH_IN_SE 0x55555555U +#define ERR_FIRMWARE_HASH_DOES_NOT_MATCH_SE 0x56565656U diff --git a/ports/stm32/boards/Passport/include/secrets.h b/ports/stm32/boards/Passport/include/secrets.h index 8a4ecc0..8f12c22 100644 --- a/ports/stm32/boards/Passport/include/secrets.h +++ b/ports/stm32/boards/Passport/include/secrets.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. + * SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. * SPDX-License-Identifier: GPL-3.0-or-later */ #ifndef _SECRETS_H_ @@ -10,7 +10,6 @@ typedef struct __attribute__ ((packed)) uint8_t pairing_secret[32]; uint8_t se_serial_number[9]; uint8_t otp_key[72]; // key for secret encryption (seed storage) - uint8_t otp_key_long[416]; // same, but for longer secret area uint8_t hash_cache_secret[32]; // encryption for cached pin hash value uint8_t padding[15]; // to align to a flash word (32 bytes) } rom_secrets_t; diff --git a/ports/stm32/boards/Passport/include/spiflash.h b/ports/stm32/boards/Passport/include/spiflash.h index d186365..f55e93a 100644 --- a/ports/stm32/boards/Passport/include/spiflash.h +++ b/ports/stm32/boards/Passport/include/spiflash.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. + * SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. * SPDX-License-Identifier: GPL-3.0-or-later */ #ifndef _SPIFLASH_H_ diff --git a/ports/stm32/boards/Passport/include/utils.h b/ports/stm32/boards/Passport/include/utils.h index f50a93c..b13000e 100644 --- a/ports/stm32/boards/Passport/include/utils.h +++ b/ports/stm32/boards/Passport/include/utils.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. + * SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. * SPDX-License-Identifier: GPL-3.0-or-later */ #ifndef _UTILS_H_ @@ -8,8 +8,10 @@ #include #include -#define MIN(a,b) (((a)<(b))?(a):(b)) -#define MAX(a,b) (((a)>(b))?(a):(b)) +#ifndef MIN + #define MIN(a,b) (((a)<(b))?(a):(b)) + #define MAX(a,b) (((a)>(b))?(a):(b)) +#endif #define CLAMP(x,mn,mx) (((x)>(mx))?(mx):( ((x)<(mn)) ? (mn) : (x))) #define SGN(x) (((x)<0)?-1:(((x)>0)?1:0)) #define ABS(x) (((x)<0)?-(x):(x)) @@ -20,5 +22,25 @@ extern bool check_all_ones(void *ptrV, int len); extern bool check_all_zeros(void *ptrV, int len); extern bool check_equal(void *aV, void *bV, int len); extern void xor_mixin(uint8_t *acc, uint8_t *more, int len); +extern void to_hex(char* buf, uint8_t value); +extern void bytes_to_hex_str(uint8_t* bytes, uint32_t len, char* str, uint32_t split_every, char split_char); + + +#ifndef PASSPORT_BOOTLOADER +extern void print_hex_buf(char* prefix, uint8_t* buf, int len); +#endif + +extern void copy_bytes(uint8_t* src, int src_len, uint8_t* dest, int dest_len); + +#ifndef PASSPORT_BOOTLOADER +#define MIN_SP 0x24074000 +#define EOS_SENTINEL 0xDEADBEEF + +void set_stack_sentinel(); +bool check_stack_sentinel(); +uint32_t getsp(void); +bool check_stack(char* msg, bool print); + +#endif #endif /* _UTILS_H_ */ diff --git a/ports/stm32/boards/Passport/manifest.py b/ports/stm32/boards/Passport/manifest.py index 75d85b6..771fa35 100644 --- a/ports/stm32/boards/Passport/manifest.py +++ b/ports/stm32/boards/Passport/manifest.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: GPL-3.0-or-later # @@ -7,16 +7,30 @@ freeze('$(MPY_DIR)/drivers/display', ('lcd160cr.py', 'lcd160cr_test.py')) freeze('$(MPY_DIR)/drivers/onewire', 'onewire.py') freeze('$(MPY_DIR)/ports/stm32/boards/Passport/modules', ('common.py', 'main.py', 'keypad.py', 'display.py', 'graphics.py', 'passport_fonts.py', 'auth.py', - 'files.py', 'ux.py', 'version.py', 'flow.py', 'actions.py', 'utils.py', 'choosers.py', 'address_explorer.py', + 'files.py', 'ux.py', 'version.py', 'flow.py', 'actions.py', 'utils.py', 'choosers.py', 'menu.py', 'settings.py', 'sram4.py', 'sffile.py', 'collections/deque.py', 'uQR.py', 'constants.py', - 'callgate.py', 'pincodes.py', 'stash.py', 'login_ux.py', 'public_constants.py', 'seed.py', 'chains.py', 'opcodes.py', - 'bip39_utils.py', 'seed_phrase_ux.py', 'sflash.py', 'snake.py', 'stacksats.py', 'se_commands.py', 'serializations.py', - 'backups.py', 'compat7z.py', 'multisig.py', 'psbt.py', 'battery_mon.py', - 'uasyncio/__init__.py', 'uasyncio/core.py', 'uasyncio/queues.py', 'uasyncio/synchro.py')) -freeze('$(MPY_DIR)/ports/stm32/boards/Passport/modules', - ('ur/__init__.py', 'ur/bytewords.py', 'ur/cbor_lite.py', 'ur/constants.py', 'ur/crc32.py', 'ur/fountain_decoder.py', - 'ur/fountain_encoder.py', 'ur/fountain_utils.py', 'ur/random_sampler.py', 'ur/ur_decoder.py', 'ur/ur_encoder.py', - 'ur/ur.py', 'ur/utils.py', 'ur/xoshiro256.py')) + 'callgate.py', 'pincodes.py', 'stash.py', 'login_ux.py', 'public_constants.py', 'seed.py', 'chains.py', + 'opcodes.py', 'bip39_utils.py', 'seed_entry_ux.py', 'sflash.py', 'snake.py', 'stacking_sats.py', + 'se_commands.py', 'serializations.py','seed_check_ux.py', 'export.py', 'compat7z.py', 'multisig.py', 'psbt.py', + 'periodic.py', 'exceptions.py', 'noise_source.py', 'self_test_ux.py', 'flash_cache.py', + 'history.py', 'accounts.py', 'log.py', 'descriptor.py', 'accept_terms_ux.py', 'new_wallet.py', 'stat.py', + 'uasyncio/__init__.py', 'uasyncio/core.py', 'uasyncio/queues.py', 'uasyncio/synchro.py', 'ie.py', + 'schema_evolution.py')) freeze('$(MPY_DIR)/ports/stm32/boards/Passport/modules', ('ur1/__init__.py', 'ur1/bc32.py', 'ur1/bech32.py', 'ur1/bech32_version.py', 'ur1/decode_ur.py', 'ur1/encode_ur.py', 'ur1/mini_cbor.py', 'ur1/utils.py')) +freeze('$(MPY_DIR)/ports/stm32/boards/Passport/modules', + ('ur2/__init__.py', 'ur2/bytewords.py', 'ur2/cbor_lite.py', 'ur2/constants.py', 'ur2/crc32.py', 'ur2/fountain_decoder.py', + 'ur2/fountain_encoder.py', 'ur2/fountain_utils.py', 'ur2/random_sampler.py', 'ur2/ur_decoder.py', 'ur2/ur_encoder.py', + 'ur2/ur.py', 'ur2/utils.py', 'ur2/xoshiro256.py')) +freeze('$(MPY_DIR)/ports/stm32/boards/Passport/modules', + ('data_codecs/__init__.py', 'data_codecs/data_format.py', 'data_codecs/data_decoder.py', 'data_codecs/data_encoder.py', + 'data_codecs/data_sampler.py', 'data_codecs/qr_factory.py', 'data_codecs/qr_codec.py', 'data_codecs/ur1_codec.py', 'data_codecs/ur2_codec.py', + 'data_codecs/multisig_config_sampler.py', 'data_codecs/psbt_txn_sampler.py', 'data_codecs/seed_sampler.py', + 'data_codecs/address_sampler.py', 'data_codecs/http_sampler.py', 'data_codecs/qr_type.py')) +freeze('$(MPY_DIR)/ports/stm32/boards/Passport/modules', + ('wallets/sw_wallets.py', 'wallets/bluewallet.py', 'wallets/electrum.py', 'wallets/constants.py', 'wallets/utils.py', + 'wallets/multisig_json.py', 'wallets/multisig_import.py', 'wallets/generic_json_wallet.py', 'wallets/sparrow.py', + 'wallets/bitcoin_core.py', 'wallets/wasabi.py', 'wallets/btcpay.py', 'wallets/gordian.py', 'wallets/lily.py', + 'wallets/fullynoded.py', 'wallets/dux_reserve.py', 'wallets/specter.py', 'wallets/casa.py', 'wallets/vault.py', + 'wallets/caravan.py')) diff --git a/ports/stm32/boards/Passport/modfoundation.c b/ports/stm32/boards/Passport/modfoundation.c index f270e86..98defb5 100644 --- a/ports/stm32/boards/Passport/modfoundation.c +++ b/ports/stm32/boards/Passport/modfoundation.c @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // // MP C foundation module, supports LCD, backlight, keypad and other devices as they are added @@ -8,6 +8,7 @@ #include "py/runtime.h" #include #include +#include #include "bufhelper.h" @@ -40,16 +41,30 @@ // QRCode includes #include "qrcode.h" -#include "stm32h7xx_hal.h" +#include "adc.h" +#include "busy_bar.h" +#include "dispatch.h" +#include "display.h" #include "flash.h" +#include "frequency.h" +#include "fwheader.h" +#include "firmware-keys.h" #include "gpio.h" -#include "dispatch.h" +#include "pprng.h" +#include "se.h" +#include "stm32h7xx_hal.h" +#include "utils.h" +#include "sha256.h" +#include "se-config.h" +#include "pins.h" +#include "uECC.h" +#include "hash.h" /* lcd class object, expand as needed with instance related details */ typedef struct _mp_obj_lcd_t { mp_obj_base_t base; - const spi_t *spi; + const spi_t* spi; } mp_obj_lcd_t; @@ -59,9 +74,6 @@ typedef struct _mp_obj_backlight_t mp_obj_base_t base; } mp_obj_backlight_t; -/* Backlight class object and globals */ -ring_buffer_t keybuf; - /* keypad class object */ typedef struct _mp_obj_keypad_t { @@ -75,19 +87,22 @@ typedef struct _mp_obj_camera_t } mp_obj_camera_t; /* Board Revision object */ -typedef struct _mp_obj_boardrev_t { +typedef struct _mp_obj_boardrev_t +{ mp_obj_base_t base; } mp_obj_boardrev_t; /* Power Monitor object */ -typedef struct _mp_obj_powermon_t { +typedef struct _mp_obj_powermon_t +{ mp_obj_base_t base; uint16_t current; uint16_t voltage; } mp_obj_powermon_t; /* Noise Output object */ -typedef struct _mp_obj_noise_t { +typedef struct _mp_obj_noise_t +{ mp_obj_base_t base; } mp_obj_noise_t; @@ -130,30 +145,32 @@ typedef struct _mp_obj_QRCode_t #define VIEWFINDER_IMAGE_SIZE ((240 * 240) / 8) #define SETTINGS_FLASH_START 0x81E0000 -#define SETTINGS_FLASH_SIZE 0x20000 -#define SETTINGS_FLASH_END (SETTINGS_FLASH_START + SETTINGS_FLASH_SIZE - 1) +#define SETTINGS_FLASH_SIZE 0x20000 +#define SETTINGS_FLASH_END (SETTINGS_FLASH_START + SETTINGS_FLASH_SIZE - 1) + +// Forward prototypes +void +turbo(bool enable); /*============================================================================= * Start of keypad class *=============================================================================*/ -STATIC mp_obj_t keypad_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) +STATIC mp_obj_t +keypad_make_new(const mp_obj_type_t* type, size_t n_args, size_t n_kw, const mp_obj_t* args) { - mp_obj_keypad_t *keypad = m_new_obj(mp_obj_keypad_t); + mp_obj_keypad_t* keypad = m_new_obj(mp_obj_keypad_t); keypad->base.type = type; - // uint32_t reg = *(uint32_t*)(0x580244d0); - // printf("======================================================\nREG = 0x%08lx\n======================================================\n", reg); - keypad_init(); return MP_OBJ_FROM_PTR(keypad); } -STATIC mp_obj_t keypad_get_keycode(mp_obj_t self) +STATIC mp_obj_t +keypad_get_keycode(mp_obj_t self) { uint8_t buf[1]; - if (ring_buffer_dequeue(&keybuf, &buf[0]) == 0) - { + if (ring_buffer_dequeue(&buf[0]) == 0) { return mp_const_none; } // printf("keypad.get_keycode() 2: %d\n", buf[0]); @@ -161,7 +178,8 @@ STATIC mp_obj_t keypad_get_keycode(mp_obj_t self) } STATIC MP_DEFINE_CONST_FUN_OBJ_1(keypad_get_keycode_obj, keypad_get_keycode); -STATIC mp_obj_t keypad___del__(mp_obj_t self) +STATIC mp_obj_t +keypad___del__(mp_obj_t self) { return mp_const_none; } @@ -169,17 +187,17 @@ STATIC mp_obj_t keypad___del__(mp_obj_t self) STATIC MP_DEFINE_CONST_FUN_OBJ_1(keypad___del___obj, keypad___del__); STATIC const mp_rom_map_elem_t keypad_locals_dict_table[] = { - {MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_foundation)}, - {MP_ROM_QSTR(MP_QSTR_get_keycode), MP_ROM_PTR(&keypad_get_keycode_obj)}, - {MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&keypad___del___obj)}, + { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_foundation) }, + { MP_ROM_QSTR(MP_QSTR_get_keycode), MP_ROM_PTR(&keypad_get_keycode_obj) }, + { MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&keypad___del___obj) }, }; STATIC MP_DEFINE_CONST_DICT(keypad_locals_dict, keypad_locals_dict_table); const mp_obj_type_t keypad_type = { - {&mp_type_type}, + { &mp_type_type }, .name = MP_QSTR_Keypad, .make_new = keypad_make_new, - .locals_dict = (void *)&keypad_locals_dict, + .locals_dict = (void*)&keypad_locals_dict, }; /* End of Keypad class code */ @@ -187,7 +205,8 @@ const mp_obj_type_t keypad_type = { /*============================================================================= * Start of LCD class *=============================================================================*/ -void lcd_obj_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) +void +lcd_obj_print(const mp_print_t* print, mp_obj_t self_in, mp_print_kind_t kind) { mp_printf(print, "foundation obj print"); } @@ -196,28 +215,30 @@ void lcd_obj_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t ki /// def __init__(self, mode: int, key: bytes, iv: bytes = None) -> None: /// ''' -/// Initialize LCD object context. Return a MP LCD object +/// Initialize LCD object context. Return a MP LCD object /// ''' -STATIC mp_obj_t lcd_obj_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) +STATIC mp_obj_t +lcd_obj_make_new(const mp_obj_type_t* type, size_t n_args, size_t n_kw, const mp_obj_t* args) { - mp_obj_lcd_t *lcd = m_new_obj(mp_obj_lcd_t); + mp_obj_lcd_t* lcd = m_new_obj(mp_obj_lcd_t); lcd->base.type = &lcd_type; lcd->spi = &spi_obj[0]; - lcd_init(); + // lcd_init(false); return MP_OBJ_FROM_PTR(lcd); } /* LCD object methods follow */ -STATIC mp_obj_t m_lcd_clear(mp_obj_t self_in, mp_obj_t invert_obj) +STATIC mp_obj_t +m_lcd_clear(mp_obj_t self_in, mp_obj_t invert_obj) { uint8_t invert = mp_obj_get_int(invert_obj); lcd_clear(invert); return mp_const_none; } - STATIC MP_DEFINE_CONST_FUN_OBJ_2(m_lcd_clear_obj, m_lcd_clear); -STATIC mp_obj_t m_lcd_update(mp_obj_t self_in, mp_obj_t lcd_data) +STATIC mp_obj_t +m_lcd_update(mp_obj_t self_in, mp_obj_t lcd_data) { mp_uint_t interrupt_state; mp_buffer_info_t data_info; @@ -232,7 +253,8 @@ STATIC mp_obj_t m_lcd_update(mp_obj_t self_in, mp_obj_t lcd_data) } STATIC MP_DEFINE_CONST_FUN_OBJ_2(m_lcd_update_obj, m_lcd_update); -STATIC mp_obj_t foundation___del__(mp_obj_t self) +STATIC mp_obj_t +foundation___del__(mp_obj_t self) { lcd_deinit(); return mp_const_none; @@ -245,33 +267,35 @@ STATIC MP_DEFINE_CONST_FUN_OBJ_1(foundation___del___obj, foundation___del__); * Class Locals Dictionary table for LCD class */ STATIC const mp_rom_map_elem_t lcd_locals_dict_table[] = { - {MP_ROM_QSTR(MP_QSTR_clear), MP_ROM_PTR(&m_lcd_clear_obj)}, - {MP_ROM_QSTR(MP_QSTR_update), MP_ROM_PTR(&m_lcd_update_obj)}, + { MP_ROM_QSTR(MP_QSTR_clear), MP_ROM_PTR(&m_lcd_clear_obj) }, + { MP_ROM_QSTR(MP_QSTR_update), MP_ROM_PTR(&m_lcd_update_obj) }, }; STATIC MP_DEFINE_CONST_DICT(lcd_locals_dict, lcd_locals_dict_table); const mp_obj_type_t lcd_type = { - {&mp_type_type}, + { &mp_type_type }, .name = MP_QSTR_LCD, .print = lcd_obj_print, .make_new = lcd_obj_make_new, - .locals_dict = (mp_obj_dict_t *)&lcd_locals_dict, + .locals_dict = (mp_obj_dict_t*)&lcd_locals_dict, }; /* End of setup for LCD class */ /*============================================================================= * Start of backlight class *=============================================================================*/ -STATIC mp_obj_t backlight_obj_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) +STATIC mp_obj_t +backlight_obj_make_new(const mp_obj_type_t* type, size_t n_args, size_t n_kw, const mp_obj_t* args) { - mp_obj_backlight_t *backlight = m_new_obj(mp_obj_backlight_t); + mp_obj_backlight_t* backlight = m_new_obj(mp_obj_backlight_t); backlight->base.type = &backlight_type; - backlight_init(); + backlight_minimal_init(); return MP_OBJ_FROM_PTR(backlight); } /* LCD object methods follow */ -STATIC mp_obj_t m_backlight_intensity(mp_obj_t self_in, mp_obj_t intensity_obj) +STATIC mp_obj_t +m_backlight_intensity(mp_obj_t self_in, mp_obj_t intensity_obj) { uint16_t intensity = mp_obj_get_int(intensity_obj); backlight_intensity(intensity); @@ -284,30 +308,28 @@ STATIC MP_DEFINE_CONST_FUN_OBJ_2(m_backlight_intensity_obj, m_backlight_intensit * Class Locals Dictionary table for Backlight class */ STATIC const mp_rom_map_elem_t backlight_locals_dict_table[] = { - {MP_ROM_QSTR(MP_QSTR_intensity), MP_ROM_PTR(&m_backlight_intensity_obj)}, + { MP_ROM_QSTR(MP_QSTR_intensity), MP_ROM_PTR(&m_backlight_intensity_obj) }, }; STATIC MP_DEFINE_CONST_DICT(backlight_locals_dict, backlight_locals_dict_table); const mp_obj_type_t backlight_type = { - {&mp_type_type}, + { &mp_type_type }, .name = MP_QSTR_Backlight, // .print = lcd_obj_print, .make_new = backlight_obj_make_new, - .locals_dict = (mp_obj_dict_t *)&backlight_locals_dict, + .locals_dict = (mp_obj_dict_t*)&backlight_locals_dict, }; /* End of setup for Backlight class */ /*============================================================================= * Start of Camera class *=============================================================================*/ -STATIC mp_obj_t camera_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) +STATIC mp_obj_t +camera_make_new(const mp_obj_type_t* type, size_t n_args, size_t n_kw, const mp_obj_t* args) { - mp_obj_camera_t *o = m_new_obj(mp_obj_camera_t); + mp_obj_camera_t* o = m_new_obj(mp_obj_camera_t); o->base.type = type; - // printf("new Camera()!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n"); - // #camera_init(); - return MP_OBJ_FROM_PTR(o); } @@ -315,7 +337,8 @@ STATIC mp_obj_t camera_make_new(const mp_obj_type_t *type, size_t n_args, size_t /// ''' /// Turn on the camera in preparation for calling snapshot(). /// ''' -STATIC mp_obj_t camera_enable(mp_obj_t self) +STATIC mp_obj_t +camera_enable(mp_obj_t self) { camera_on(); return mp_const_none; @@ -325,7 +348,8 @@ STATIC mp_obj_t camera_enable(mp_obj_t self) /// ''' /// Turn off the camera. /// ''' -STATIC mp_obj_t camera_disable(mp_obj_t self) +STATIC mp_obj_t +camera_disable(mp_obj_t self) { camera_off(); return mp_const_none; @@ -335,19 +359,18 @@ STATIC mp_obj_t camera_disable(mp_obj_t self) /// ''' /// Start a snapshot and wait for it to finish, then convert and copy it into the provided image buffers. /// ''' -STATIC mp_obj_t camera_snapshot_(size_t n_args, const mp_obj_t *args) +STATIC mp_obj_t +camera_snapshot_(size_t n_args, const mp_obj_t* args) { mp_buffer_info_t qr_image_info; mp_get_buffer_raise(args[1], &qr_image_info, MP_BUFFER_WRITE); uint16_t qr_w = mp_obj_get_int(args[2]); uint16_t qr_h = mp_obj_get_int(args[3]); - if (qr_image_info.len != qr_w * qr_h) - { + if (qr_image_info.len != qr_w * qr_h) { printf("ERROR: QR buffer w/h not consistent with buffer size!\n"); return mp_const_false; } - if (qr_image_info.len != QR_IMAGE_SIZE) - { + if (qr_image_info.len != QR_IMAGE_SIZE) { printf("ERROR: QR buffer is the wrong size!\n"); return mp_const_false; } @@ -356,13 +379,11 @@ STATIC mp_obj_t camera_snapshot_(size_t n_args, const mp_obj_t *args) mp_get_buffer_raise(args[4], &viewfinder_image_info, MP_BUFFER_WRITE); uint16_t viewfinder_w = mp_obj_get_int(args[5]); uint16_t viewfinder_h = mp_obj_get_int(args[6]); - if (viewfinder_image_info.len != viewfinder_w * viewfinder_h / 8) - { + if (viewfinder_image_info.len != viewfinder_w * viewfinder_h / 8) { printf("ERROR: Viewfinder buffer w/h not consistent with buffer size!\n"); return mp_const_false; } - if (viewfinder_w > qr_w || viewfinder_h > qr_h) - { + if (viewfinder_w > qr_w || viewfinder_h > qr_h) { // Viewfinder can't be larger than base image printf("ERROR: Viewfinder buffer is larger than QR buffer!\n"); return mp_const_false; @@ -372,20 +393,45 @@ STATIC mp_obj_t camera_snapshot_(size_t n_args, const mp_obj_t *args) return mp_const_false; } - uint16_t *rgb565 = camera_get_frame_buffer(); + uint16_t* rgb565 = camera_get_frame_buffer(); + + //uint32_t start = HAL_GetTick(); + convert_rgb565_to_grayscale_and_mono( + rgb565, qr_image_info.buf, qr_w, qr_h, viewfinder_image_info.buf, viewfinder_w, viewfinder_h); + //uint32_t end = HAL_GetTick(); + //printf("conversion: %lums\n", end - start); + return mp_const_true; +} + +STATIC mp_obj_t +camera_get_line_data(mp_obj_t self_in, mp_obj_t line, mp_obj_t _line_num) +{ + // Get the buffer info from the passed in object + mp_buffer_info_t line_info; + mp_get_buffer_raise(line, &line_info, MP_BUFFER_WRITE); + + int line_num = mp_obj_get_int(_line_num); + if (line_num < 0 || line_num >= CAMERA_HEIGHT || line_info.len < CAMERA_WIDTH * 2) { + printf("line_num = %d line_info.len = %u\n", line_num, line_info.len); + return mp_const_false; + } + + uint16_t* rgb565 = camera_get_frame_buffer(); + + uint32_t pixels_per_line = CAMERA_WIDTH; + + memcpy(line_info.buf, rgb565 + (line_num * pixels_per_line), pixels_per_line * 2); // Two bytes per pixel - uint32_t start = HAL_GetTick(); - convert_rgb565_to_grayscale_and_mono(rgb565, qr_image_info.buf, qr_w, qr_h, viewfinder_image_info.buf, viewfinder_w, viewfinder_h); - uint32_t end = HAL_GetTick(); - printf("conversion: %lums\n", end - start); return mp_const_true; } STATIC MP_DEFINE_CONST_FUN_OBJ_1(camera_enable_obj, camera_enable); STATIC MP_DEFINE_CONST_FUN_OBJ_1(camera_disable_obj, camera_disable); STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(camera_snapshot_obj, 7, 7, camera_snapshot_); +STATIC MP_DEFINE_CONST_FUN_OBJ_3(camera_get_line_data_obj, camera_get_line_data); -STATIC mp_obj_t camera___del__(mp_obj_t self) +STATIC mp_obj_t +camera___del__(mp_obj_t self) { // mp_obj_camera_t *o = MP_OBJ_TO_PTR(self); return mp_const_none; @@ -394,19 +440,21 @@ STATIC mp_obj_t camera___del__(mp_obj_t self) STATIC MP_DEFINE_CONST_FUN_OBJ_1(camera___del___obj, camera___del__); STATIC const mp_rom_map_elem_t camera_locals_dict_table[] = { - {MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_foundation)}, // TODO: Is this right? Should it be "foundation_camera" or "camera"? - {MP_ROM_QSTR(MP_QSTR_enable), MP_ROM_PTR(&camera_enable_obj)}, - {MP_ROM_QSTR(MP_QSTR_disable), MP_ROM_PTR(&camera_disable_obj)}, - {MP_ROM_QSTR(MP_QSTR_snapshot), MP_ROM_PTR(&camera_snapshot_obj)}, - {MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&camera___del___obj)}, + { MP_ROM_QSTR(MP_QSTR___name__), + MP_ROM_QSTR(MP_QSTR_foundation) }, + { MP_ROM_QSTR(MP_QSTR_enable), MP_ROM_PTR(&camera_enable_obj) }, + { MP_ROM_QSTR(MP_QSTR_disable), MP_ROM_PTR(&camera_disable_obj) }, + { MP_ROM_QSTR(MP_QSTR_snapshot), MP_ROM_PTR(&camera_snapshot_obj) }, + { MP_ROM_QSTR(MP_QSTR_get_line_data), MP_ROM_PTR(&camera_get_line_data_obj) }, + { MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&camera___del___obj) }, }; STATIC MP_DEFINE_CONST_DICT(camera_locals_dict, camera_locals_dict_table); STATIC const mp_obj_type_t camera_type = { - {&mp_type_type}, + { &mp_type_type }, .name = MP_QSTR_camera, .make_new = camera_make_new, - .locals_dict = (void *)&camera_locals_dict, + .locals_dict = (void*)&camera_locals_dict, }; /* End of setup for Camera class */ @@ -414,23 +462,27 @@ STATIC const mp_obj_type_t camera_type = { * Start of Power Monitor class *=============================================================================*/ -STATIC mp_obj_t mod_foundation_powermon_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) { - mp_obj_powermon_t *powermon = m_new_obj(mp_obj_powermon_t); +STATIC mp_obj_t +mod_foundation_powermon_make_new(const mp_obj_type_t* type, size_t n_args, size_t n_kw, const mp_obj_t* args) +{ + mp_obj_powermon_t* powermon = m_new_obj(mp_obj_powermon_t); powermon->base.type = type; return MP_OBJ_FROM_PTR(powermon); } -STATIC mp_obj_t mod_foundation_powermon_read(mp_obj_t self) { - HAL_StatusTypeDef ret; +STATIC mp_obj_t +mod_foundation_powermon_read(mp_obj_t self) +{ + int ret; uint16_t current = 0; uint16_t voltage = 0; mp_obj_t tuple[2]; - mp_obj_powermon_t *pPowerMon = (mp_obj_powermon_t *)self; + mp_obj_powermon_t* pPowerMon = (mp_obj_powermon_t*)self; - ret = read_powermon(¤t, &voltage); - if (ret != HAL_OK) { + ret = adc_read_powermon(¤t, &voltage); + if (ret < 0) { tuple[0] = mp_const_none; tuple[1] = mp_const_none; return mp_obj_new_tuple(2, tuple); @@ -444,69 +496,77 @@ STATIC mp_obj_t mod_foundation_powermon_read(mp_obj_t self) { } STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_foundation_powermon_read_obj, mod_foundation_powermon_read); -STATIC mp_obj_t mod_foundation_powermon___del__(mp_obj_t self) { +STATIC mp_obj_t +mod_foundation_powermon___del__(mp_obj_t self) +{ return mp_const_none; } STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_foundation_powermon___del___obj, mod_foundation_powermon___del__); STATIC const mp_rom_map_elem_t mod_foundation_powermon_locals_dict_table[] = { -{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_foundation) }, -{ MP_ROM_QSTR(MP_QSTR_read), MP_ROM_PTR(&mod_foundation_powermon_read_obj) }, -{ MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&mod_foundation_powermon___del___obj) }, + { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_foundation) }, + { MP_ROM_QSTR(MP_QSTR_read), MP_ROM_PTR(&mod_foundation_powermon_read_obj) }, + { MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&mod_foundation_powermon___del___obj) }, }; STATIC MP_DEFINE_CONST_DICT(mod_foundation_powermon_locals_dict, mod_foundation_powermon_locals_dict_table); const mp_obj_type_t powermon_type = { - { &mp_type_type }, - .name = MP_QSTR_PMon, - .make_new = mod_foundation_powermon_make_new, - .locals_dict = (void*)&mod_foundation_powermon_locals_dict, + { &mp_type_type }, + .name = MP_QSTR_PMon, + .make_new = mod_foundation_powermon_make_new, + .locals_dict = (void*)&mod_foundation_powermon_locals_dict, }; /* End of power monitor class */ /*============================================================================= -* Start of Board Revision class -*=============================================================================*/ -STATIC mp_obj_t mod_foundation_boardrev_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) { - mp_obj_boardrev_t *boardrev = m_new_obj(mp_obj_boardrev_t); + * Start of Board Revision class + *=============================================================================*/ +STATIC mp_obj_t +mod_foundation_boardrev_make_new(const mp_obj_type_t* type, size_t n_args, size_t n_kw, const mp_obj_t* args) +{ + mp_obj_boardrev_t* boardrev = m_new_obj(mp_obj_boardrev_t); boardrev->base.type = type; return MP_OBJ_FROM_PTR(boardrev); } -STATIC mp_obj_t mod_foundation_boardrev_read(mp_obj_t self) { +STATIC mp_obj_t +mod_foundation_boardrev_read(mp_obj_t self) +{ HAL_StatusTypeDef ret; uint16_t board_rev = 0; - ret = read_boardrev(&board_rev); - if (ret != HAL_OK) { + ret = adc_read_boardrev(&board_rev); + if (ret < 0) { return mp_const_none; } return mp_obj_new_int_from_uint(board_rev); } STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_foundation_boardrev_read_obj, mod_foundation_boardrev_read); -STATIC mp_obj_t mod_foundation_boardrev___del__(mp_obj_t self) { +STATIC mp_obj_t +mod_foundation_boardrev___del__(mp_obj_t self) +{ return mp_const_none; } STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_foundation_boardrev___del___obj, mod_foundation_boardrev___del__); STATIC const mp_rom_map_elem_t mod_foundation_boardrev_locals_dict_table[] = { -{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_foundation) }, -{ MP_ROM_QSTR(MP_QSTR_read), MP_ROM_PTR(&mod_foundation_boardrev_read_obj) }, -{ MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&mod_foundation_boardrev___del___obj) }, + { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_foundation) }, + { MP_ROM_QSTR(MP_QSTR_read), MP_ROM_PTR(&mod_foundation_boardrev_read_obj) }, + { MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&mod_foundation_boardrev___del___obj) }, }; STATIC MP_DEFINE_CONST_DICT(mod_foundation_boardrev_locals_dict, mod_foundation_boardrev_locals_dict_table); const mp_obj_type_t boardrev_type = { - { &mp_type_type }, - .name = MP_QSTR_Bdrev, - .make_new = mod_foundation_boardrev_make_new, - .locals_dict = (void*)&mod_foundation_boardrev_locals_dict, + { &mp_type_type }, + .name = MP_QSTR_Bdrev, + .make_new = mod_foundation_boardrev_make_new, + .locals_dict = (void*)&mod_foundation_boardrev_locals_dict, }; /* End of board revision class */ @@ -515,25 +575,29 @@ const mp_obj_type_t boardrev_type = { * Start of Noise Output class *=============================================================================*/ -STATIC mp_obj_t mod_foundation_noise_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) { - mp_obj_noise_t *noise = m_new_obj(mp_obj_noise_t); +STATIC mp_obj_t +mod_foundation_noise_make_new(const mp_obj_type_t* type, size_t n_args, size_t n_kw, const mp_obj_t* args) +{ + mp_obj_noise_t* noise = m_new_obj(mp_obj_noise_t); noise->base.type = type; /* * Need to enable the noise amp enables. */ - enable_noise(); + adc_enable_noise(); return MP_OBJ_FROM_PTR(noise); } -STATIC mp_obj_t mod_foundation_noise_read(mp_obj_t self) { +STATIC mp_obj_t +mod_foundation_noise_read(mp_obj_t self) +{ HAL_StatusTypeDef ret; uint32_t noise1 = 0; uint32_t noise2 = 0; mp_obj_t tuple[2]; - ret = read_noise_inputs(&noise1, &noise2); - if (ret != HAL_OK) { + ret = adc_read_noise_inputs(&noise1, &noise2); + if (ret < 0) { tuple[0] = mp_const_none; tuple[1] = mp_const_none; return mp_obj_new_tuple(2, tuple); @@ -545,19 +609,21 @@ STATIC mp_obj_t mod_foundation_noise_read(mp_obj_t self) { } STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_foundation_noise_read_obj, mod_foundation_noise_read); -bool get_random_uint16(uint16_t* result) { +bool +get_random_uint16(uint16_t* result) +{ HAL_StatusTypeDef ret; uint32_t noise1 = 0; uint32_t noise2 = 0; uint16_t r = 0; - for (int i=0; i<4; i++) { + for (int i = 0; i < 4; i++) { r = r << 4; - HAL_Delay(1); // TODO: How long should this be? + HAL_Delay(1); - ret = read_noise_inputs(&noise1, &noise2); - if (ret != HAL_OK) { + ret = adc_read_noise_inputs(&noise1, &noise2); + if (ret < 0) { return false; } @@ -567,51 +633,152 @@ bool get_random_uint16(uint16_t* result) { return true; } -STATIC mp_obj_t mod_foundation_noise_random_bytes(mp_obj_t self, const mp_obj_t buf) { +// Flags to select which entroy sources to combine +#define AVALANCHE_SOURCE 1 +#define MCU_RNG_SOURCE 2 +#define SE_RNG_SOURCE 4 +#define ALS_SOURCE 8 + +// Function to combine multiple sources of randomness together +STATIC mp_obj_t +mod_foundation_noise_random_bytes(mp_obj_t self, const mp_obj_t buf, mp_obj_t _sources) +{ mp_buffer_info_t buf_info; mp_get_buffer_raise(buf, &buf_info, MP_BUFFER_WRITE); - uint8_t* pbuf = (uint8_t*)buf_info.buf; - for (int i=0; ibase.type = type; - if (n_args != 3) - { + if (n_args != 3) { printf("ERROR: QR called with wrong number of arguments!"); return mp_const_none; } @@ -640,14 +807,12 @@ STATIC mp_obj_t QR_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_k mp_get_buffer_raise(args[2], &image_info, MP_BUFFER_READ); unsigned int expected_image_len = o->width * o->height; - if (image_info.len != expected_image_len) - { - printf("ERROR: Invalid buffer size for this decoder. Expected %u\n", expected_image_len); + if (image_info.len != expected_image_len) { + printf("ERROR: Invalid buffer size for this decoder. Expected %u\n", expected_image_len); return mp_const_none; } - if (quirc_init(&o->quirc, o->width, o->height, image_info.buf) < 0) - { + if (quirc_init(&o->quirc, o->width, o->height, image_info.buf) < 0) { printf("ERROR: Unable to initialize quirc!\n"); return mp_const_none; } @@ -658,14 +823,15 @@ STATIC mp_obj_t QR_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_k struct quirc_code code; struct quirc_data data; -#define QR_DEBUG +//#define QR_DEBUG /// def find_qr_codes(self, image: image) -> array of strings: /// ''' /// Find QR codes in image. /// ''' -STATIC mp_obj_t QR_find_qr_codes(mp_obj_t self) +STATIC mp_obj_t +QR_find_qr_codes(mp_obj_t self) { - mp_obj_QR_t *o = MP_OBJ_TO_PTR(self); + mp_obj_QR_t* o = MP_OBJ_TO_PTR(self); #ifdef QR_DEBUG printf("find_qr_codes: %u, %u\n", o->width, o->height); @@ -686,11 +852,9 @@ STATIC mp_obj_t QR_find_qr_codes(mp_obj_t self) printf("num_codes=%d\n", num_codes); #endif - - if (num_codes == 0) - { + if (num_codes == 0) { #ifdef QR_DEBUG - printf("No codes found\n"); + printf("No codes found\n"); #endif return mp_const_none; } @@ -703,13 +867,10 @@ STATIC mp_obj_t QR_find_qr_codes(mp_obj_t self) // Decoding stage quirc_decode_error_t err = quirc_decode(&code, &data); - if (err) - { + if (err) { printf("ERROR: Decode failed: %s\n", quirc_strerror(err)); return mp_const_none; - } - else - { + } else { #ifdef QR_DEBUG printf("Data: %s\n", data.payload); #endif @@ -720,17 +881,19 @@ STATIC mp_obj_t QR_find_qr_codes(mp_obj_t self) // printf("Data: %s\n", payload); vstr_t vstr; - int code_len = strlen((const char *)data.payload); + int code_len = strlen((const char*)data.payload); vstr_init(&vstr, code_len + 1); - vstr_add_strn(&vstr, (const char *)data.payload, code_len); // Can append to vstr if necessary + vstr_add_strn(&vstr, (const char*)data.payload, code_len); // Can append to vstr if necessary + return mp_obj_new_str_from_vstr(&mp_type_str, &vstr); } STATIC MP_DEFINE_CONST_FUN_OBJ_1(QR_find_qr_codes_obj, QR_find_qr_codes); -STATIC mp_obj_t QR___del__(mp_obj_t self) +STATIC mp_obj_t +QR___del__(mp_obj_t self) { - mp_obj_QR_t *o = MP_OBJ_TO_PTR(self); + mp_obj_QR_t* o = MP_OBJ_TO_PTR(self); quirc_destroy(&o->quirc); return mp_const_none; } @@ -738,21 +901,20 @@ STATIC mp_obj_t QR___del__(mp_obj_t self) STATIC MP_DEFINE_CONST_FUN_OBJ_1(QR___del___obj, QR___del__); STATIC const mp_rom_map_elem_t QR_locals_dict_table[] = { - {MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_foundation)}, - {MP_ROM_QSTR(MP_QSTR_find_qr_codes), MP_ROM_PTR(&QR_find_qr_codes_obj)}, - {MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&QR___del___obj)}, + { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_foundation) }, + { MP_ROM_QSTR(MP_QSTR_find_qr_codes), MP_ROM_PTR(&QR_find_qr_codes_obj) }, + { MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&QR___del___obj) }, }; STATIC MP_DEFINE_CONST_DICT(QR_locals_dict, QR_locals_dict_table); STATIC const mp_obj_type_t QR_type = { - {&mp_type_type}, + { &mp_type_type }, .name = MP_QSTR_QR, .make_new = QR_make_new, - .locals_dict = (void *)&QR_locals_dict, + .locals_dict = (void*)&QR_locals_dict, }; /* End of setup for QR decoder class */ - /*============================================================================= * Start of SettingsFlash class *=============================================================================*/ @@ -761,35 +923,40 @@ STATIC const mp_obj_type_t QR_type = { /// ''' /// Initialize SettingsFlash context. /// ''' -STATIC mp_obj_t SettingsFlash_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) +STATIC mp_obj_t +SettingsFlash_make_new(const mp_obj_type_t* type, size_t n_args, size_t n_kw, const mp_obj_t* args) { - mp_obj_SettingsFlash_t *o = m_new_obj(mp_obj_SettingsFlash_t); + mp_obj_SettingsFlash_t* o = m_new_obj(mp_obj_SettingsFlash_t); o->base.type = type; return MP_OBJ_FROM_PTR(o); } -#define FLASH_DEBUG +// #define FLASH_DEBUG /// def write(self, dest_addr, data) -> boolean /// ''' /// Write data to internal flash /// ''' -STATIC mp_obj_t SettingsFlash_write(mp_obj_t self, mp_obj_t dest_addr, mp_obj_t data) +STATIC mp_obj_t +SettingsFlash_write(mp_obj_t self, mp_obj_t dest_addr, mp_obj_t data) { uint32_t flash_addr = mp_obj_get_int(dest_addr); mp_buffer_info_t data_info; mp_get_buffer_raise(data, &data_info, MP_BUFFER_READ); - if (flash_addr < SETTINGS_FLASH_START || - flash_addr + data_info.len > SETTINGS_FLASH_END || + if (flash_addr < SETTINGS_FLASH_START || flash_addr + data_info.len - 1 > SETTINGS_FLASH_END || data_info.len % 4 != 0) { #ifdef FLASH_DEBUG - printf("ERROR: SettingsFlash_write: bad parameters\n"); + printf("ERROR: SettingsFlash_write: bad parameters: flash_addr=0x%08lx\nSETTINGS_FLASH_START=0x%08x\nSETTINGS_FLASH_END=0x%08x\ndata_info.len=0x%04x\n", + flash_addr, + SETTINGS_FLASH_START, + SETTINGS_FLASH_END, + data_info.len); #endif return mp_const_false; } #ifdef FLASH_DEBUG - printf("SettingsFlash_write: %u bytes to 0x%08lx\n",data_info.len, flash_addr); + printf("SettingsFlash_write: %u bytes to 0x%08lx\n", data_info.len, flash_addr); // for (uint32_t i=0; ibase.type = type; return MP_OBJ_FROM_PTR(o); } @@ -872,7 +1042,8 @@ STATIC mp_obj_t System_make_new(const mp_obj_type_t *type, size_t n_args, size_t /// ''' /// Perform a warm reset of the system (should be mostly the same as turning it off and then on) /// ''' -STATIC mp_obj_t System_reset(mp_obj_t self) +STATIC mp_obj_t +System_reset(mp_obj_t self) { passport_reset(); return mp_const_none; @@ -882,60 +1053,535 @@ STATIC mp_obj_t System_reset(mp_obj_t self) /// ''' /// Shutdown power to the Passport /// ''' -STATIC mp_obj_t System_shutdown(mp_obj_t self) +STATIC mp_obj_t +System_shutdown(mp_obj_t self) { - passport_shutdown(); + // We clear the memory display and then shutdown + display_clean_shutdown(); return mp_const_none; } /// def dispatch(self, command: int, buf: bytes, len: int, arg2: int, ) -> array of strings: /// ''' -/// Dispatch system function by command number. This is a carry-over from the old firewall -/// code. We can probably switch this to direct function calls instead. The only benefit is +/// Dispatch system function by command number. This is a carry-over from the old firewall +/// code. We can probably switch this to direct function calls instead. The only benefit is /// that this gives us a nice single point to handle RDP level 2 checks and other security checks. /// ''' -STATIC mp_obj_t System_dispatch(size_t n_args, const mp_obj_t *args) +STATIC mp_obj_t +System_dispatch(size_t n_args, const mp_obj_t* args) { int8_t command = mp_obj_get_int(args[1]); uint16_t arg2 = mp_obj_get_int(args[3]); int result; + turbo(true); + if (args[2] == mp_const_none) { result = se_dispatch(command, NULL, 0, arg2, 0, 0); } else { - mp_buffer_info_t buf_info; // Use MP_BUFFER_WRITE below so any updates are copied back up + mp_buffer_info_t buf_info; // Use MP_BUFFER_WRITE below so any updates are copied back up mp_get_buffer_raise(args[2], &buf_info, MP_BUFFER_WRITE); - // TODO: What are the incoming_sp and incoming_lr for? result = se_dispatch(command, buf_info.buf, buf_info.len, arg2, 0, 0); } + turbo(false); + return mp_obj_new_int(result); } +/// def show_busy_bar(self) -> None +/// ''' +/// Start displaying the busy bar animation for long-running processes +/// Also, enable turbo mode since if we need to wait, speed it almost certainly helpful. +/// ''' +STATIC mp_obj_t +System_show_busy_bar(mp_obj_t self) +{ + turbo(true); + busy_bar_start(); + return mp_const_none; +} + +/// def hide_busy_bar(self) -> None +/// ''' +/// Stop showing the busy bar and disable turbo mode +/// ''' +STATIC mp_obj_t +System_hide_busy_bar(mp_obj_t self) +{ + busy_bar_stop(); + turbo(false); + return mp_const_none; +} #define SECRETS_FLASH_START 0x81C0000 -#define SECRETS_FLASH_SIZE 0x20000 +#define SECRETS_FLASH_SIZE 0x20000 + -/// def erase_rom_secrets(self) -> None + +/// def System_get_software_info(self) -> None /// ''' -/// Erase ROM secrets -/// TODO: This is a temporary function, since ROM secrets will be in the bootloader, -/// in this final bank of flash as they are here. +/// Get version, timestamp & hash of the firmware and bootloader as a tuple /// ''' -STATIC mp_obj_t System_erase_rom_secrets(mp_obj_t self) +STATIC mp_obj_t +System_get_software_info(mp_obj_t self) { - // NOTE: This function doesn't return any error/success info - flash_erase(SECRETS_FLASH_START, SECRETS_FLASH_SIZE / 4); + passport_firmware_header_t* fwhdr = (passport_firmware_header_t*)FW_HDR; + + mp_obj_t tuple[4]; + + // Firmware version + tuple[0] = mp_obj_new_str_copy( + &mp_type_str, (const uint8_t*)fwhdr->info.fwversion, strlen((const char*)fwhdr->info.fwversion)); + + // Firmware date + tuple[1] = mp_obj_new_int_from_uint(fwhdr->info.timestamp); + + uint32_t boot_counter = 0; + se_get_counter(&boot_counter, 1); + tuple[2] = mp_obj_new_int_from_uint(boot_counter); + + // User-signed firmware? + tuple[3] = (fwhdr->signature.pubkey1 == FW_USER_KEY) ? mp_const_true : mp_const_false; + + return mp_obj_new_tuple(4, tuple); +} + +/// def System_progress_bar(self, progress) -> None +/// ''' +/// Draw a progress bar to the specified amount (0-1.0) +/// ''' +STATIC mp_obj_t +System_progress_bar(mp_obj_t self, mp_obj_t _progress) +{ + int8_t progress = mp_obj_get_int(_progress); + display_progress_bar( + PROGRESS_BAR_MARGIN, PROGRESS_BAR_Y, SCREEN_WIDTH - (PROGRESS_BAR_MARGIN * 2), PROGRESS_BAR_HEIGHT, progress); + + // Showing just the lines that changed is much faster and avoids full-screen flicker + display_show_lines(PROGRESS_BAR_Y, PROGRESS_BAR_Y + PROGRESS_BAR_HEIGHT); + return mp_const_none; } +/// def System_read_ambient(self) -> None +/// ''' +/// Read the ambient light sensor and bucket it to a level from 0-100 +/// ''' +STATIC mp_obj_t +System_read_ambient(mp_obj_t self) +{ + uint16_t millivolts; + adc_read_als(&millivolts); + millivolts = MIN(millivolts, 3200); + // printf("millivolts = %u\n", millivolts); + int level = millivolts / 32; + + return mp_obj_new_int(level); +} + +uint8_t turbo_count = 0; +void +turbo(bool enable) +{ + if (enable) { + if (turbo_count == 0) { + frequency_turbo(true); + } + turbo_count++; + } else { + if (turbo_count == 0) { + // printf("ERROR: Tried to disable turbo mode when it was not already enabled!\n"); + return; + } + if (turbo_count == 1) { + frequency_turbo(false); + } + turbo_count--; + } +} + +/// def System_turbo(self, progress) -> None +/// ''' +/// Enable or disable turbo mode (fastest MCU frequency) +/// ''' +STATIC mp_obj_t +System_turbo(mp_obj_t self, mp_obj_t _enable) +{ + bool enable = mp_obj_is_true(_enable); + + turbo(enable); + // printf("%s: %lu, %lu, %lu, %lu, %lu\n", enable ? "enable" : "disabled", HAL_RCC_GetSysClockFreq(), SystemCoreClock, HAL_RCC_GetHCLKFreq(), + // HAL_RCC_GetPCLK1Freq(), HAL_RCC_GetPCLK2Freq()); + + return mp_const_none; +} + + +/// def System_sha256(self, buffer, digest) -> None +/// ''' +/// Perform a sha256 hash on the given data (bytearray) +/// ''' +STATIC mp_obj_t +System_sha256(mp_obj_t self, mp_obj_t data, mp_obj_t digest) +{ + mp_buffer_info_t data_info; + mp_get_buffer_raise(data, &data_info, MP_BUFFER_READ); + + mp_buffer_info_t digest_info; + mp_get_buffer_raise(digest, &digest_info, MP_BUFFER_WRITE); + + SHA256_CTX ctx; + sha256_init(&ctx); + sha256_update(&ctx, (void *)data_info.buf, data_info.len); + sha256_final(&ctx, digest_info.buf); + + return mp_const_none; +} + +// Simple header verification +bool verify_header(passport_firmware_header_t *hdr) +{ + if (hdr->info.magic != FW_HEADER_MAGIC) goto fail; + if (hdr->info.timestamp == 0) goto fail; + if (hdr->info.fwversion[0] == 0x0) goto fail; + if (hdr->info.fwlength < FW_HEADER_SIZE) goto fail; + + if ((hdr->signature.pubkey1 != FW_USER_KEY) && (hdr->signature.pubkey1 > FW_MAX_PUB_KEYS)) goto fail; + if (hdr->signature.pubkey1 != FW_USER_KEY) + { + if (hdr->signature.pubkey2 > FW_MAX_PUB_KEYS) goto fail; + } + + return true; + +fail: + return false; +} + +/// def System_validate_firmware_header(self, header) -> None +/// ''' +/// Validate the given firmware header bytes as a potential candidate to be installed. +/// ''' +STATIC mp_obj_t +System_validate_firmware_header(mp_obj_t self, mp_obj_t header) +{ + mp_buffer_info_t header_info; + mp_get_buffer_raise(header, &header_info, MP_BUFFER_READ); + + // Existing header + passport_firmware_header_t* fwhdr = (passport_firmware_header_t*)FW_HDR; + + // New header + passport_firmware_header_t* new_fwhdr = (passport_firmware_header_t*)header_info.buf; + + mp_obj_t tuple[3]; + + bool is_valid = verify_header(header_info.buf); + + if (is_valid) { + // Ensure they are not trying to install an older version of firmware, but allow + // a reinstall of the same version. Also allow installation of user firmware regardless of + // timestamp and then allow installing a Foundation-signed build. + if ((new_fwhdr->signature.pubkey1 != FW_USER_KEY && fwhdr->signature.pubkey1 != FW_USER_KEY ) && + (new_fwhdr->info.timestamp < fwhdr->info.timestamp)) { + tuple[0] = mp_const_false; + tuple[1] = mp_obj_new_str_copy(&mp_type_str, (const uint8_t*)new_fwhdr->info.fwversion, strlen((const char*)new_fwhdr->info.fwversion)); + + // Include an error string + vstr_t vstr; + vstr_init(&vstr, 80); + char* msg = "The selected firmware is older than the currently installed firmware and cannot be installed.\n\nCurrent Version:\n "; + vstr_add_strn(&vstr, (const char*)msg, strlen(msg)); + + vstr_add_strn(&vstr, (const char*)fwhdr->info.fwdate, strlen((const char*)new_fwhdr->info.fwdate)); + + msg = "\n\nSelected Version:\n "; + vstr_add_strn(&vstr, (const char*)msg, strlen(msg)); + + vstr_add_strn(&vstr, (const char*)new_fwhdr->info.fwdate, strlen((const char*)new_fwhdr->info.fwdate)); + tuple[2] = mp_obj_new_str_from_vstr(&mp_type_str, &vstr); + + return mp_obj_new_tuple(3, tuple); + } + } else { + // Invalid header + tuple[0] = mp_const_false; + tuple[1] = mp_obj_new_str_copy(&mp_type_str, (const uint8_t*)new_fwhdr->info.fwversion, strlen((const char*)new_fwhdr->info.fwversion)); + + // Include an error string + vstr_t vstr; + vstr_init(&vstr, 80); + char* msg = "The selected firmware header is invalid and cannot be installed."; + vstr_add_strn(&vstr, (const char*)msg, strlen(msg)); + tuple[2] = mp_obj_new_str_from_vstr(&mp_type_str, &vstr); + + return mp_obj_new_tuple(3, tuple); + } + + // is_valid + tuple[0] = mp_const_true; + + // Firmware version + tuple[1] = mp_obj_new_str_copy(&mp_type_str, (const uint8_t*)new_fwhdr->info.fwversion, strlen((const char*)new_fwhdr->info.fwversion)); + + // No error message + tuple[2] = mp_const_none; + + return mp_obj_new_tuple(3, tuple); +} + +/// def System_set_user_firmware_pubkey(self, pubkey) -> None +/// ''' +/// Set the user firmware public key so the user can install custom firmware +/// ''' +STATIC mp_obj_t +System_set_user_firmware_pubkey(mp_obj_t self, mp_obj_t pubkey) +{ + uint8_t pin_hash[32]; + + mp_buffer_info_t pubkey_info; + mp_get_buffer_raise(pubkey, &pubkey_info, MP_BUFFER_READ); + // uint8_t* p = (uint8_t*)pubkey_info.buf; + // printf("WRITE: len=%d pubkey=%02x%02x%02x%02x...\n",pubkey_info.len, p[0], p[1], p[2], p[3]); + + pinAttempt_t pa_args; + pa_args.magic_value = PA_MAGIC_V1; + memcpy(&pa_args.cached_main_pin, g_cached_main_pin, sizeof(g_cached_main_pin)); + + // Get the hash that proves user knows the PIN + int rv = pin_cache_restore(&pa_args, pin_hash); + if (rv) { + return mp_const_false; + } + + // printf("pin hash=%02x%02x%02x%02x...", pin_hash[0], pin_hash[1], pin_hash[2],pin_hash[3]); + + rv = se_encrypted_write(KEYNUM_user_fw_pubkey, KEYNUM_pin_hash, pin_hash, pubkey_info.buf, pubkey_info.len); + // printf("rv=%d\n", rv); + return rv == 0 ? mp_const_true : mp_const_false; +} + +/// def System_get_user_firmware_pubkey(self, pubkey) -> None +/// ''' +/// Get the user firmware public key +/// ''' +STATIC mp_obj_t +System_get_user_firmware_pubkey(mp_obj_t self, mp_obj_t pubkey) +{ + uint8_t buf[72]; + + mp_buffer_info_t pubkey_info; + mp_get_buffer_raise(pubkey, &pubkey_info, MP_BUFFER_READ); + + if (pubkey_info.len < 64) { + return mp_const_false; + } + + se_pair_unlock(); + int rv = se_read_data_slot(KEYNUM_user_fw_pubkey, buf, sizeof(buf)); + if (rv == 0) { + memcpy(pubkey_info.buf, buf, 64); + return mp_const_true; + } + return mp_const_false; +} + +/// def System_supply_chain_challenge(self, challenge, response) -> None +/// ''' +/// Perform the supply chain challenge (HMAC) +/// ''' +STATIC mp_obj_t +System_supply_chain_challenge(mp_obj_t self, mp_obj_t challenge, mp_obj_t response) +{ + mp_buffer_info_t challenge_info; + mp_get_buffer_raise(challenge, &challenge_info, MP_BUFFER_READ); + + mp_buffer_info_t response_info; + mp_get_buffer_raise(response, &response_info, MP_BUFFER_WRITE); + + se_pair_unlock(); + int rc = se_hmac32(KEYNUM_supply_chain, challenge_info.buf, response_info.buf); + if (rc == 0) { + return mp_const_true; + } + return mp_const_false; +} + + +uint8_t supply_chain_validation_server_pubkey[64] = { + 0x75, 0xF6, 0xCD, 0xDB, 0x93, 0x49, 0x59, 0x9D, 0x4B, 0xB2, 0xDF, 0x82, 0xBC, 0xF9, 0x8E, 0x85, + 0x45, 0x6C, 0xFB, 0xE2, 0x87, 0x57, 0xFF, 0x77, 0x5D, 0xB0, 0x4C, 0xAE, 0x70, 0x1B, 0xDC, 0x00, + 0x53, 0x4E, 0x0C, 0x70, 0x01, 0x90, 0x6C, 0x6F, 0xFB, 0xA6, 0x15, 0xAF, 0xDB, 0x67, 0xDE, 0xF9, + 0x46, 0x96, 0x4B, 0xB4, 0x39, 0xD0, 0x02, 0x3E, 0xF6, 0x59, 0xF5, 0x80, 0xBB, 0x31, 0x11, 0x3E +}; + +/// def System_verify_supply_chain_server_signature(self, hash, signature) -> None +/// ''' +/// Verify server signature +/// ''' +STATIC mp_obj_t +System_verify_supply_chain_server_signature(mp_obj_t self, mp_obj_t hash, mp_obj_t signature) +{ + mp_buffer_info_t hash_info; + mp_get_buffer_raise(hash, &hash_info, MP_BUFFER_READ); + + mp_buffer_info_t signature_info; + mp_get_buffer_raise(signature, &signature_info, MP_BUFFER_READ); + + int rc = uECC_verify(supply_chain_validation_server_pubkey, + hash_info.buf, hash_info.len, + signature_info.buf, uECC_secp256k1()); + + return rc == 0 ? mp_const_false : mp_const_true; +} + +#define SHA256_BLOCK_LENGTH 64 +#define SHA256_DIGEST_LENGTH 32 + +void _hmac_sha256(uint8_t* key, uint32_t key_len, uint8_t* msg, uint32_t msg_len, uint8_t* hmac) { + uint8_t i_key_pad[SHA256_BLOCK_LENGTH]; + memset(i_key_pad, 0, SHA256_BLOCK_LENGTH); + memcpy(i_key_pad, key, key_len); + + uint8_t o_key_pad[SHA256_BLOCK_LENGTH]; + for (int i = 0; i < SHA256_BLOCK_LENGTH; i++) { + o_key_pad[i] = i_key_pad[i] ^ 0x5c; + i_key_pad[i] ^= 0x36; + } + + // First hash + SHA256_CTX ctx; + sha256_init(&ctx); + sha256_update(&ctx, i_key_pad, SHA256_BLOCK_LENGTH); + memset(i_key_pad, 0, SHA256_BLOCK_LENGTH); + + // Add the data + sha256_update(&ctx, msg, msg_len); + + // Hash + sha256_final(&ctx, hmac); + + // Second hash + sha256_init(&ctx); + sha256_update(&ctx, o_key_pad, SHA256_BLOCK_LENGTH); + sha256_update(&ctx, hmac, SHA256_DIGEST_LENGTH); + sha256_final(&ctx, hmac); +} + +/// def System_hmac_sha256(self, key, msg, hmac) -> None +/// ''' +/// Calculate an hmac using the given key and data +/// ''' +STATIC mp_obj_t +System_hmac_sha256(size_t n_args, const mp_obj_t* args) +{ + mp_buffer_info_t key_info; + mp_get_buffer_raise(args[1], &key_info, MP_BUFFER_READ); + // uint8_t* pkey = (uint8_t*)key_info.buf; + // printf("key: 0x%02x 0x%02x 0x%02x 0x%02x (len=%d)\n", pkey[0], pkey[1], pkey[2], pkey[3], key_info.len); + + mp_buffer_info_t msg_info; + mp_get_buffer_raise(args[2], &msg_info, MP_BUFFER_READ); + // uint8_t* pmsg = (uint8_t*)msg_info.buf; + // printf("msg: 0x%02x 0x%02x 0x%02x 0x%02x (len=%d)\n", pmsg[0], pmsg[1], pmsg[2], pmsg[3], msg_info.len); + + mp_buffer_info_t hmac_info; + mp_get_buffer_raise(args[3], &hmac_info, MP_BUFFER_WRITE); + // printf("hmac:(len=%d)\n", hmac_info.len); + + _hmac_sha256(key_info.buf, key_info.len, msg_info.buf, msg_info.len, hmac_info.buf); + + return mp_const_none; +} + +#define MAX_SERIAL_NUMBER_LEN 20 +/// def System_get_serial_number(self) -> None +/// ''' +/// Get the serial number +/// ''' +STATIC mp_obj_t +System_get_serial_number(mp_obj_t self) +{ + char serial[MAX_SERIAL_NUMBER_LEN]; + + get_serial_number(serial, MAX_SERIAL_NUMBER_LEN); + + return mp_obj_new_str_copy(&mp_type_str, (const uint8_t*)serial, strlen(serial)); +} + +/// def System_get_device_hash(self, hash) -> None +/// ''' +/// Get the device hash +/// ''' +STATIC mp_obj_t +System_get_device_hash(mp_obj_t self, mp_obj_t hash) +{ + mp_buffer_info_t hash_info; + mp_get_buffer_raise(hash, &hash_info, MP_BUFFER_WRITE); + + get_device_hash(hash_info.buf); + + return mp_const_none; +} + +/// def System_get_backup_pw_hash(self, hash) -> None +/// ''' +/// Get the hash to use as the "entropy" for the backup password. +/// It's based on the device hash plus the seed. +/// ''' +STATIC mp_obj_t +System_get_backup_pw_hash(mp_obj_t self, mp_obj_t hash) +{ + uint8_t device_hash[32]; + + mp_buffer_info_t hash_info; + mp_get_buffer_raise(hash, &hash_info, MP_BUFFER_WRITE); + + get_device_hash(device_hash); + pinAttempt_t pin_attempt; + memset(&pin_attempt, 0, sizeof(pinAttempt_t)); + pin_fetch_secret(&pin_attempt); + + SHA256_CTX ctx; + sha256_init(&ctx); + sha256_update(&ctx, (void *)device_hash, 32); + sha256_update(&ctx, (void *)pin_attempt.secret, SE_SECRET_LEN); + sha256_final(&ctx, hash_info.buf); + + // Double SHA + sha256_init(&ctx); + sha256_update(&ctx, (void *)hash_info.buf, 32); + sha256_final(&ctx, hash_info.buf); + + return mp_const_none; +} + + STATIC MP_DEFINE_CONST_FUN_OBJ_1(System_reset_obj, System_reset); STATIC MP_DEFINE_CONST_FUN_OBJ_1(System_shutdown_obj, System_shutdown); STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(System_dispatch_obj, 4, 4, System_dispatch); -STATIC MP_DEFINE_CONST_FUN_OBJ_1(System_erase_rom_secrets_obj, System_erase_rom_secrets); - -STATIC mp_obj_t System___del__(mp_obj_t self) +STATIC MP_DEFINE_CONST_FUN_OBJ_1(System_show_busy_bar_obj, System_show_busy_bar); +STATIC MP_DEFINE_CONST_FUN_OBJ_1(System_hide_busy_bar_obj, System_hide_busy_bar); +STATIC MP_DEFINE_CONST_FUN_OBJ_1(System_get_software_info_obj, System_get_software_info); +STATIC MP_DEFINE_CONST_FUN_OBJ_2(System_progress_bar_obj, System_progress_bar); +STATIC MP_DEFINE_CONST_FUN_OBJ_1(System_read_ambient_obj, System_read_ambient); +STATIC MP_DEFINE_CONST_FUN_OBJ_2(System_turbo_obj, System_turbo); +STATIC MP_DEFINE_CONST_FUN_OBJ_3(System_sha256_obj, System_sha256); +STATIC MP_DEFINE_CONST_FUN_OBJ_2(System_validate_firmware_header_obj, System_validate_firmware_header); +STATIC MP_DEFINE_CONST_FUN_OBJ_2(System_set_user_firmware_pubkey_obj, System_set_user_firmware_pubkey); +STATIC MP_DEFINE_CONST_FUN_OBJ_2(System_get_user_firmware_pubkey_obj, System_get_user_firmware_pubkey); +STATIC MP_DEFINE_CONST_FUN_OBJ_3(System_supply_chain_challenge_obj, System_supply_chain_challenge); +STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(System_hmac_sha256_obj, 4, 4, System_hmac_sha256); +STATIC MP_DEFINE_CONST_FUN_OBJ_3(System_verify_supply_chain_server_signature_obj, System_verify_supply_chain_server_signature); +STATIC MP_DEFINE_CONST_FUN_OBJ_1(System_get_serial_number_obj, System_get_serial_number); +STATIC MP_DEFINE_CONST_FUN_OBJ_2(System_get_device_hash_obj, System_get_device_hash); +STATIC MP_DEFINE_CONST_FUN_OBJ_2(System_get_backup_pw_hash_obj, System_get_backup_pw_hash); + + +STATIC mp_obj_t +System___del__(mp_obj_t self) { return mp_const_none; } @@ -943,20 +1589,35 @@ STATIC mp_obj_t System___del__(mp_obj_t self) STATIC MP_DEFINE_CONST_FUN_OBJ_1(System___del___obj, System___del__); STATIC const mp_rom_map_elem_t System_locals_dict_table[] = { - {MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_foundation)}, - {MP_ROM_QSTR(MP_QSTR_reset), MP_ROM_PTR(&System_reset_obj)}, - {MP_ROM_QSTR(MP_QSTR_shutdown), MP_ROM_PTR(&System_shutdown_obj)}, - {MP_ROM_QSTR(MP_QSTR_dispatch), MP_ROM_PTR(&System_dispatch_obj)}, - {MP_ROM_QSTR(MP_QSTR_erase_rom_secrets), MP_ROM_PTR(&System_erase_rom_secrets_obj)}, - {MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&System___del___obj)}, + { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_foundation) }, + { MP_ROM_QSTR(MP_QSTR_reset), MP_ROM_PTR(&System_reset_obj) }, + { MP_ROM_QSTR(MP_QSTR_shutdown), MP_ROM_PTR(&System_shutdown_obj) }, + { MP_ROM_QSTR(MP_QSTR_dispatch), MP_ROM_PTR(&System_dispatch_obj) }, + { MP_ROM_QSTR(MP_QSTR_show_busy_bar), MP_ROM_PTR(&System_show_busy_bar_obj) }, + { MP_ROM_QSTR(MP_QSTR_hide_busy_bar), MP_ROM_PTR(&System_hide_busy_bar_obj) }, + { MP_ROM_QSTR(MP_QSTR_get_software_info), MP_ROM_PTR(&System_get_software_info_obj) }, + { MP_ROM_QSTR(MP_QSTR_progress_bar), MP_ROM_PTR(&System_progress_bar_obj) }, + { MP_ROM_QSTR(MP_QSTR_read_ambient), MP_ROM_PTR(&System_read_ambient_obj) }, + { MP_ROM_QSTR(MP_QSTR_turbo), MP_ROM_PTR(&System_turbo_obj) }, + { MP_ROM_QSTR(MP_QSTR_sha256), MP_ROM_PTR(&System_sha256_obj) }, + { MP_ROM_QSTR(MP_QSTR_validate_firmware_header), MP_ROM_PTR(&System_validate_firmware_header_obj) }, + { MP_ROM_QSTR(MP_QSTR_set_user_firmware_pubkey), MP_ROM_PTR(&System_set_user_firmware_pubkey_obj) }, + { MP_ROM_QSTR(MP_QSTR_get_user_firmware_pubkey), MP_ROM_PTR(&System_get_user_firmware_pubkey_obj) }, + { MP_ROM_QSTR(MP_QSTR_supply_chain_challenge), MP_ROM_PTR(&System_supply_chain_challenge_obj) }, + { MP_ROM_QSTR(MP_QSTR_verify_supply_chain_server_signature), MP_ROM_PTR(&System_verify_supply_chain_server_signature_obj) }, + { MP_ROM_QSTR(MP_QSTR_hmac_sha256), MP_ROM_PTR(&System_hmac_sha256_obj) }, + { MP_ROM_QSTR(MP_QSTR_get_serial_number), MP_ROM_PTR(&System_get_serial_number_obj) }, + { MP_ROM_QSTR(MP_QSTR_get_device_hash), MP_ROM_PTR(&System_get_device_hash_obj) }, + { MP_ROM_QSTR(MP_QSTR_get_backup_pw_hash), MP_ROM_PTR(&System_get_backup_pw_hash_obj) }, + { MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&System___del___obj) }, }; STATIC MP_DEFINE_CONST_DICT(System_locals_dict, System_locals_dict_table); STATIC const mp_obj_type_t System_type = { - {&mp_type_type}, + { &mp_type_type }, .name = MP_QSTR_System, .make_new = System_make_new, - .locals_dict = (void *)&System_locals_dict, + .locals_dict = (void*)&System_locals_dict, }; /* End of setup for System class */ @@ -964,51 +1625,66 @@ STATIC const mp_obj_type_t System_type = { * Start of bip39 class *=============================================================================*/ +extern word_info_t bip39_word_info[]; +extern word_info_t bytewords_word_info[]; // TODO: Restructure this so bip39 and bytewords are separate + /// def __init__(self, mode: int, key: bytes, iv: bytes = None) -> boolean: /// ''' /// Initialize System context. /// ''' -STATIC mp_obj_t bip39_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) +STATIC mp_obj_t +bip39_make_new(const mp_obj_type_t* type, size_t n_args, size_t n_kw, const mp_obj_t* args) { - mp_obj_bip39_t *o = m_new_obj(mp_obj_bip39_t); + mp_obj_bip39_t* o = m_new_obj(mp_obj_bip39_t); o->base.type = type; return MP_OBJ_FROM_PTR(o); } -/// def get_words_matching_prefix(self) -> None +#define MATCHES_LEN 80 + +/// def get_words_matching_prefix(self, prefix, max_matches, word_list) -> None /// ''' /// Return a comma-separated list of BIP39 seed words that match the given keypad /// digits prefix (e.g., '222'). /// ''' -STATIC mp_obj_t bip39_get_words_matching_prefix(mp_obj_t self, mp_obj_t prefix, mp_obj_t _max_matches) +STATIC mp_obj_t +bip39_get_words_matching_prefix(size_t n_args, const mp_obj_t* args) { - uint32_t start = HAL_GetTick(); - - mp_check_self(mp_obj_is_str_or_bytes(prefix)); - GET_STR_DATA_LEN(prefix, prefix_str, prefix_len); - - printf("bip39_get_words_matching_prefix: prefix_str=%s len=%d\n", prefix_str, prefix_len); + mp_check_self(mp_obj_is_str_or_bytes(args[1])); + GET_STR_DATA_LEN(args[1], prefix_str, prefix_len); + + int max_matches = mp_obj_get_int(args[2]); + + // Must be "bip39" or "bytewords" + mp_check_self(mp_obj_is_str_or_bytes(args[3])); + GET_STR_DATA_LEN(args[3], word_list_str, word_list_len); + + const word_info_t* word_info = NULL; + uint32_t num_words = 0; + if (strcmp("bip39", (char*)word_list_str) == 0) { + word_info = bip39_word_info; + num_words = 2048; + } else if (strcmp("bytewords", (char*)word_list_str) == 0) { + word_info = bytewords_word_info; + num_words = 256; + } else { + return mp_const_none; + } - int max_matches = mp_obj_get_int(_max_matches); - // TODO: change this to calculate dynamically based on max_matches and max seed word length,including comma separators - #define MATCHES_LEN 80 char matches[MATCHES_LEN]; - get_words_matching_prefix((char*)prefix_str, matches, MATCHES_LEN, max_matches); + get_words_matching_prefix((char*)prefix_str, matches, MATCHES_LEN, max_matches, word_info, num_words); // Return the string vstr_t vstr; - int matches_len = strlen((const char *)matches); + int matches_len = strlen((const char*)matches); vstr_init(&vstr, matches_len + 1); - vstr_add_strn(&vstr, (const char *)matches, matches_len); - - uint32_t end = HAL_GetTick(); - printf("bip39_get_words_matching_prefix: %lums\n", end - start); + vstr_add_strn(&vstr, (const char*)matches, matches_len); return mp_obj_new_str_from_vstr(&mp_type_str, &vstr); } -STATIC MP_DEFINE_CONST_FUN_OBJ_3(bip39_get_words_matching_prefix_obj, bip39_get_words_matching_prefix); +STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(bip39_get_words_matching_prefix_obj, 4, 4, bip39_get_words_matching_prefix); #include "bip39.h" @@ -1017,20 +1693,21 @@ STATIC MP_DEFINE_CONST_FUN_OBJ_3(bip39_get_words_matching_prefix_obj, bip39_get_ /// Call trezorcrypto's mnemonic_to_entropy() C function since it's not exposed through their /// Python interface. /// ''' -STATIC mp_obj_t bip39_mnemonic_to_entropy(mp_obj_t self, mp_obj_t mnemonic, mp_obj_t entropy) +STATIC mp_obj_t +bip39_mnemonic_to_entropy(mp_obj_t self, mp_obj_t mnemonic, mp_obj_t entropy) { mp_check_self(mp_obj_is_str_or_bytes(mnemonic)); GET_STR_DATA_LEN(mnemonic, mnemonic_str, mnemonic_len); mp_buffer_info_t entropy_info; mp_get_buffer_raise(entropy, &entropy_info, MP_BUFFER_WRITE); - + int len = mnemonic_to_entropy((const char*)mnemonic_str, entropy_info.buf); return mp_obj_new_int(len); } STATIC MP_DEFINE_CONST_FUN_OBJ_3(bip39_mnemonic_to_entropy_obj, bip39_mnemonic_to_entropy); - -STATIC mp_obj_t bip39___del__(mp_obj_t self) +STATIC mp_obj_t +bip39___del__(mp_obj_t self) { return mp_const_none; } @@ -1038,18 +1715,18 @@ STATIC mp_obj_t bip39___del__(mp_obj_t self) STATIC MP_DEFINE_CONST_FUN_OBJ_1(bip39___del___obj, bip39___del__); STATIC const mp_rom_map_elem_t bip39_locals_dict_table[] = { - {MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_foundation)}, - {MP_ROM_QSTR(MP_QSTR_get_words_matching_prefix), MP_ROM_PTR(&bip39_get_words_matching_prefix_obj)}, - {MP_ROM_QSTR(MP_QSTR_mnemonic_to_entropy), MP_ROM_PTR(&bip39_mnemonic_to_entropy_obj)}, - {MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&bip39___del___obj)}, + { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_foundation) }, + { MP_ROM_QSTR(MP_QSTR_get_words_matching_prefix), MP_ROM_PTR(&bip39_get_words_matching_prefix_obj) }, + { MP_ROM_QSTR(MP_QSTR_mnemonic_to_entropy), MP_ROM_PTR(&bip39_mnemonic_to_entropy_obj) }, + { MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&bip39___del___obj) }, }; STATIC MP_DEFINE_CONST_DICT(bip39_locals_dict, bip39_locals_dict_table); STATIC const mp_obj_type_t bip39_type = { - {&mp_type_type}, + { &mp_type_type }, .name = MP_QSTR_bip39, .make_new = bip39_make_new, - .locals_dict = (void *)&bip39_locals_dict, + .locals_dict = (void*)&bip39_locals_dict, }; /* End of setup for bip39 class */ @@ -1057,9 +1734,8 @@ STATIC const mp_obj_type_t bip39_type = { * Start of QRCode class - renders QR codes to a buffer passed down from MP *=============================================================================*/ - -// We only have versions here that can be rendered on a -uint16_t version_capacity[] = { +// We only have versions here that can be rendered on Pasport's display +uint16_t version_capacity_alphanumeric[] = { 25, // 1 47, // 2 77, // 3 @@ -1086,13 +1762,41 @@ uint16_t version_capacity[] = { 1704 // 24 }; +uint16_t version_capacity_binary[] = { + 17, // 1 + 32, // 2 + 53, // 3 + 78, // 4 + 106, // 5 + 134, // 6 + 154, // 7 + 192, // 8 + 230, // 9 + 271, // 10 + 321, // 11 + 367, // 12 + 425, // 13 + 458, // 14 + 520, // 15 + 586, // 16 + 644, // 17 + 718, // 18 + 792, // 19 + 858, // 20 + 929, // 21 + 1003, // 22 + 1091, // 23 + 1171 // 24 +}; + /// def __init__(self, mode: int, key: bytes, iv: bytes = None) -> boolean: /// ''' /// Initialize QRCode context. /// ''' -STATIC mp_obj_t QRCode_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) +STATIC mp_obj_t +QRCode_make_new(const mp_obj_type_t* type, size_t n_args, size_t n_kw, const mp_obj_t* args) { - mp_obj_QRCode_t *o = m_new_obj(mp_obj_QRCode_t); + mp_obj_QRCode_t* o = m_new_obj(mp_obj_QRCode_t); o->base.type = type; return MP_OBJ_FROM_PTR(o); } @@ -1104,10 +1808,12 @@ QRCode qrcode; /// ''' /// Render a QR code with the given data, version and ecc level /// ''' -STATIC mp_obj_t QRCode_render(size_t n_args, const mp_obj_t *args) +STATIC mp_obj_t +QRCode_render(size_t n_args, const mp_obj_t* args) { mp_check_self(mp_obj_is_str_or_bytes(args[1])); GET_STR_DATA_LEN(args[1], text_str, text_len); + // printf("text_str=%s text_len=%d\n", text_str, text_len); uint8_t version = mp_obj_get_int(args[2]); uint8_t ecc = mp_obj_get_int(args[3]); @@ -1115,7 +1821,7 @@ STATIC mp_obj_t QRCode_render(size_t n_args, const mp_obj_t *args) mp_buffer_info_t output_info; mp_get_buffer_raise(args[4], &output_info, MP_BUFFER_WRITE); - uint8_t result = qrcode_initBytes(&qrcode, (uint8_t *)output_info.buf, version, ecc, (uint8_t *)text_str, text_len); + uint8_t result = qrcode_initBytes(&qrcode, (uint8_t*)output_info.buf, version, ecc, (uint8_t*)text_str, text_len); return result == 0 ? mp_const_false : mp_const_true; } @@ -1124,15 +1830,17 @@ STATIC mp_obj_t QRCode_render(size_t n_args, const mp_obj_t *args) /// ''' /// Return the QR code version that best fits this data (assumes ECC level 0 for now) /// ''' -STATIC mp_obj_t QRCode_fit_to_version(mp_obj_t self, mp_obj_t data_size) +STATIC mp_obj_t +QRCode_fit_to_version(mp_obj_t self, mp_obj_t data_size, mp_obj_t is_alphanumeric) { - int num_entries = sizeof(version_capacity)/sizeof(uint16_t); - uint16_t size = mp_obj_get_int(data_size); - // printf("QRCode_fit_to_version: size=%u\n", size); + uint16_t is_alpha = mp_obj_get_int(is_alphanumeric); + uint16_t *lookup_table = is_alpha ? version_capacity_alphanumeric : version_capacity_binary; - for (int i=0; i= size) { + int num_entries = sizeof(version_capacity_alphanumeric) / sizeof(uint16_t); + + for (int i = 0; i < num_entries; i++) { + if (lookup_table[i] >= size) { return mp_obj_new_int(i + 1); } } @@ -1142,9 +1850,10 @@ STATIC mp_obj_t QRCode_fit_to_version(mp_obj_t self, mp_obj_t data_size) } STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(QRCode_render_obj, 5, 5, QRCode_render); -STATIC MP_DEFINE_CONST_FUN_OBJ_2(QRCode_fit_to_version_obj, QRCode_fit_to_version); +STATIC MP_DEFINE_CONST_FUN_OBJ_3(QRCode_fit_to_version_obj, QRCode_fit_to_version); -STATIC mp_obj_t QRCode___del__(mp_obj_t self) +STATIC mp_obj_t +QRCode___del__(mp_obj_t self) { return mp_const_none; } @@ -1152,24 +1861,21 @@ STATIC mp_obj_t QRCode___del__(mp_obj_t self) STATIC MP_DEFINE_CONST_FUN_OBJ_1(QRCode___del___obj, QRCode___del__); STATIC const mp_rom_map_elem_t QRCode_locals_dict_table[] = { - {MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_foundation)}, - {MP_ROM_QSTR(MP_QSTR_render), MP_ROM_PTR(&QRCode_render_obj)}, - {MP_ROM_QSTR(MP_QSTR_fit_to_version), MP_ROM_PTR(&QRCode_fit_to_version_obj)}, - {MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&QRCode___del___obj)}, + { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_foundation) }, + { MP_ROM_QSTR(MP_QSTR_render), MP_ROM_PTR(&QRCode_render_obj) }, + { MP_ROM_QSTR(MP_QSTR_fit_to_version), MP_ROM_PTR(&QRCode_fit_to_version_obj) }, + { MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&QRCode___del___obj) }, }; STATIC MP_DEFINE_CONST_DICT(QRCode_locals_dict, QRCode_locals_dict_table); STATIC const mp_obj_type_t QRCode_type = { - {&mp_type_type}, + { &mp_type_type }, .name = MP_QSTR_QRCode, .make_new = QRCode_make_new, - .locals_dict = (void *)&QRCode_locals_dict, + .locals_dict = (void*)&QRCode_locals_dict, }; /* End of setup for QRCode class */ - - - /* * Add additional class local dictionary table and data structure here * And add the Class name and MP_ROM_PTR() to the globals table @@ -1184,27 +1890,27 @@ STATIC const mp_obj_type_t QRCode_type = { * optimized to word-sized integers by the build system (interned strings). */ STATIC const mp_rom_map_elem_t foundation_module_globals_table[] = { - {MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_foundation)}, - {MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&foundation___del___obj)}, - {MP_ROM_QSTR(MP_QSTR_Backlight), MP_ROM_PTR(&backlight_type)}, - {MP_ROM_QSTR(MP_QSTR_Keypad), MP_ROM_PTR(&keypad_type)}, - {MP_ROM_QSTR(MP_QSTR_LCD), MP_ROM_PTR(&lcd_type)}, - {MP_ROM_QSTR(MP_QSTR_Camera), MP_ROM_PTR(&camera_type)}, - {MP_ROM_QSTR(MP_QSTR_Boardrev), MP_ROM_PTR(&boardrev_type)}, - {MP_ROM_QSTR(MP_QSTR_Powermon), MP_ROM_PTR(&powermon_type)}, - {MP_ROM_QSTR(MP_QSTR_Noise), MP_ROM_PTR(&noise_type)}, - {MP_ROM_QSTR(MP_QSTR_QR), MP_ROM_PTR(&QR_type)}, - {MP_ROM_QSTR(MP_QSTR_SettingsFlash), MP_ROM_PTR(&SettingsFlash_type)}, - {MP_ROM_QSTR(MP_QSTR_System), MP_ROM_PTR(&System_type)}, - {MP_ROM_QSTR(MP_QSTR_bip39), MP_ROM_PTR(&bip39_type)}, - {MP_ROM_QSTR(MP_QSTR_QRCode), MP_ROM_PTR(&QRCode_type)}, + { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_foundation) }, + { MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&foundation___del___obj) }, + { MP_ROM_QSTR(MP_QSTR_Backlight), MP_ROM_PTR(&backlight_type) }, + { MP_ROM_QSTR(MP_QSTR_Keypad), MP_ROM_PTR(&keypad_type) }, + { MP_ROM_QSTR(MP_QSTR_LCD), MP_ROM_PTR(&lcd_type) }, + { MP_ROM_QSTR(MP_QSTR_Camera), MP_ROM_PTR(&camera_type) }, + { MP_ROM_QSTR(MP_QSTR_Boardrev), MP_ROM_PTR(&boardrev_type) }, + { MP_ROM_QSTR(MP_QSTR_Powermon), MP_ROM_PTR(&powermon_type) }, + { MP_ROM_QSTR(MP_QSTR_Noise), MP_ROM_PTR(&noise_type) }, + { MP_ROM_QSTR(MP_QSTR_QR), MP_ROM_PTR(&QR_type) }, + { MP_ROM_QSTR(MP_QSTR_SettingsFlash), MP_ROM_PTR(&SettingsFlash_type) }, + { MP_ROM_QSTR(MP_QSTR_System), MP_ROM_PTR(&System_type) }, + { MP_ROM_QSTR(MP_QSTR_bip39), MP_ROM_PTR(&bip39_type) }, + { MP_ROM_QSTR(MP_QSTR_QRCode), MP_ROM_PTR(&QRCode_type) }, }; STATIC MP_DEFINE_CONST_DICT(foundation_module_globals, foundation_module_globals_table); /* Define module object. */ const mp_obj_module_t foundation_user_cmodule = { - .base = {&mp_type_module}, - .globals = (mp_obj_dict_t *)&foundation_module_globals, + .base = { &mp_type_module }, + .globals = (mp_obj_dict_t*)&foundation_module_globals, }; MP_REGISTER_MODULE(MP_QSTR_foundation, foundation_user_cmodule, PASSPORT_FOUNDATION_ENABLED); diff --git a/ports/stm32/boards/Passport/modfoundation.h b/ports/stm32/boards/Passport/modfoundation.h index b94583c..287d65f 100644 --- a/ports/stm32/boards/Passport/modfoundation.h +++ b/ports/stm32/boards/Passport/modfoundation.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // diff --git a/ports/stm32/boards/Passport/modtcc-codecs.c b/ports/stm32/boards/Passport/modtcc-codecs.c index 0694bf0..e87876b 100644 --- a/ports/stm32/boards/Passport/modtcc-codecs.c +++ b/ports/stm32/boards/Passport/modtcc-codecs.c @@ -1,7 +1,7 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // -// SPDX-FileCopyrightText: 2018 Coinkite, Inc. +// SPDX-FileCopyrightText: 2018 Coinkite, Inc. // SPDX-License-Identifier: GPL-3.0-only // /* @@ -9,8 +9,8 @@ * * Licensed under GNU License * see LICENSE file for details - * - * + * + * * Various encodes/decoders/serializers: base58, base32, bech32, etc. * */ @@ -62,7 +62,7 @@ STATIC mp_obj_t modtcc_b58_encode(mp_obj_t data) } vstr.len = rl-1; // strip NUL - + return mp_obj_new_str_from_vstr(&mp_type_str, &vstr); } STATIC MP_DEFINE_CONST_FUN_OBJ_1(modtcc_b58_encode_obj, modtcc_b58_encode); @@ -73,13 +73,13 @@ STATIC mp_obj_t modtcc_b58_decode(mp_obj_t enc) uint8_t tmp[128]; - int rl = base58_decode_check(s, HASHER_SHA2, tmp, sizeof(tmp)); + int rl = base58_decode_check(s, HASHER_SHA2D, tmp, sizeof(tmp)); if(rl <= 0) { // transcription error from user is very likely mp_raise_ValueError("corrupt base58"); } - + return mp_obj_new_bytes(tmp, rl); } STATIC MP_DEFINE_CONST_FUN_OBJ_1(modtcc_b58_decode_obj, modtcc_b58_decode); @@ -107,7 +107,7 @@ STATIC mp_obj_t modtcc_b32_encode(mp_obj_t data) } vstr.len = last - vstr.buf; // strips NUL - + return mp_obj_new_str_from_vstr(&mp_type_str, &vstr); } STATIC MP_DEFINE_CONST_FUN_OBJ_1(modtcc_b32_encode_obj, modtcc_b32_encode); @@ -124,13 +124,13 @@ STATIC mp_obj_t modtcc_b32_decode(mp_obj_t enc) // transcription error from user is very likely mp_raise_ValueError("corrupt base32"); } - + return mp_obj_new_bytes(tmp, last-tmp); } STATIC MP_DEFINE_CONST_FUN_OBJ_1(modtcc_b32_decode_obj, modtcc_b32_decode); // -// +// // Bech32 aka. Segwit addresses, but hopefylly not specific to segwit addresses only. // // @@ -210,6 +210,7 @@ STATIC mp_obj_t modtcc_bech32_encode(mp_obj_t hrp_obj, mp_obj_t segwit_version_o const uint8_t *data, size_t data_len); */ + // printf("hrp=%s, data_len=%d\n", hrp, data_len); int rv = bech32_encode(vstr.buf, hrp, data, data_len); if(rv != 1) { @@ -217,7 +218,7 @@ STATIC mp_obj_t modtcc_bech32_encode(mp_obj_t hrp_obj, mp_obj_t segwit_version_o } vstr.len = strlen(vstr.buf); - + return mp_obj_new_str_from_vstr(&mp_type_str, &vstr); } STATIC MP_DEFINE_CONST_FUN_OBJ_3(modtcc_bech32_encode_obj, modtcc_bech32_encode); @@ -262,14 +263,7 @@ int bech32_decode( } // re-pack 5-bit data into 8-bit bytes (after version) -#ifndef __APPLE__ -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wvla-larger-than=" -#endif uint8_t packed[tmp_len]; -#ifndef __APPLE__ -#pragma GCC diagnostic pop -#endif size_t packed_len = 0; int cv_ok = sw_convert_bits(packed, &packed_len, 8, tmp + 1, tmp_len - 1, 5, false); @@ -317,7 +311,7 @@ const mp_obj_module_t mp_module_tcc = { .base = { &mp_type_module }, .globals = (mp_obj_dict_t*)&mp_module_tcc_globals, }; - + MP_REGISTER_MODULE(MP_QSTR_tcc, mp_module_tcc, 1); // EOF diff --git a/ports/stm32/boards/Passport/modules/accept_terms_ux.py b/ports/stm32/boards/Passport/modules/accept_terms_ux.py new file mode 100644 index 0000000..c475815 --- /dev/null +++ b/ports/stm32/boards/Passport/modules/accept_terms_ux.py @@ -0,0 +1,76 @@ +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# login_ux.py - UX related to PIN code entry/login. +# +# NOTE: Mark3 hardware does not support secondary wallet concept. +# + +from common import settings +from utils import UXStateMachine +from ux import ux_show_story, ux_shutdown, ux_confirm, ux_show_text_as_ur +from data_codecs.qr_type import QRType + +# Separate PIN state machines to keep the logic cleaner in each and make it easier to change messaging in each + +class AcceptTermsUX(UXStateMachine): + + def __init__(self): + # States + self.INTRO = 1 + self.SHOW_URL_QR = 2 + self.TERMS_INFO = 3 + self.CONFIRM_TERMS = 4 + + initial_state = self.INTRO + + # print('AcceptTermsUX init') + super().__init__(initial_state) + + + async def show(self): + while True: + # print('show: state={}'.format(self.state)) + if self.state == self.INTRO: + # Already accepted + if settings.get('terms_ok'): + return + + ch = await ux_show_story("""\ +Congratulations on taking the first step towards sovereignty and ownership over your Bitcoin! + +Open the setup guide by scanning the QR code on the following screen.""", + left_btn='SHUTDOWN', right_btn='CONTINUE', scroll_label='MORE') + + if ch == 'x': + # We only return from here if the user chose to not shutdown + await ux_shutdown() + elif ch == 'y': + self.goto(self.SHOW_URL_QR) + + elif self.state == self.SHOW_URL_QR: + # Show QR code + url = 'https://foundationdevices.com/setup' + result = await ux_show_text_as_ur(title='Setup Guide', qr_text=url, qr_type=QRType.QR, left_btn='BACK', right_btn='NEXT') + if result == 'x': + self.goto_prev() + else: + self.goto(self.TERMS_INFO) + + elif self.state == self.TERMS_INFO: + ch = await ux_show_story("Please accept our Terms of Use. You can read the full terms in the Passport setup guide.", + left_btn='BACK', right_btn='CONTINUE', scroll_label='MORE', center=True, center_vertically=True) + if ch == 'x': + self.goto_prev() + elif ch == 'y': + self.goto(self.CONFIRM_TERMS) + + elif self.state == self.CONFIRM_TERMS: + accepted_terms = await ux_confirm('I confirm that I have read and accept the Terms of Use.', + negative_btn='BACK', positive_btn='I CONFIRM') + if accepted_terms: + # Note fact they accepted the terms. Annoying to ask user more than once. + settings.set('terms_ok', 1) + return + else: + self.goto_prev() diff --git a/ports/stm32/boards/Passport/modules/accounts.py b/ports/stm32/boards/Passport/modules/accounts.py new file mode 100644 index 0000000..ace71dd --- /dev/null +++ b/ports/stm32/boards/Passport/modules/accounts.py @@ -0,0 +1,141 @@ +# SPDX-FileCopyrightText: 2021 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# accounts.py - Single sig and multisig accounts feature for better organization and privacy/isolation +# + +from menu import MenuSystem, MenuItem +from flow import SettingsMenu, AccountMenu +import stash +import common +from common import settings, system +from utils import UXStateMachine, to_json, to_str, save_new_account, make_account_name_num, account_exists, get_accounts +from wallets.utils import get_next_account_num +from ux import ux_enter_text, ux_show_story +from constants import DEFAULT_ACCOUNT_ENTRY, MAX_ACCOUNT_NAME_LEN + +# from wallets.constants import * + +# Set the account reference in common and update xfp/xpub with the current account +def set_active_account(label=None, arg=None, menu_title=None, index=None): + account = arg + # print('Setting active_account={}'.format(account)) + common.active_account = account + +def clear_active_account(label=None, arg=None, menu_title=None, index=None): + # print('Clearing active_account') + common.active_account = None + +async def new_account(*a): + new_account_ux = NewAccountUX() + await new_account_ux.show() + + +class NewAccountUX(UXStateMachine): + def __init__(self): + # States + self.SELECT_ACCOUNT_NUM = 1 + self.ENTER_ACCOUNT_NAME = 2 + + self.account_num = 0 + self.account_name = '' + super().__init__(self.SELECT_ACCOUNT_NUM) + + async def show(self): + while True: + # print('show: state={}'.format(self.state)) + if self.state == self.SELECT_ACCOUNT_NUM: + # Pick the next expected acct_num as the default value here + next_acct_num = get_next_account_num() + + acct_num = await ux_enter_text( + title="Account", + label="Account Number", + initial_text='{}'.format(next_acct_num), + left_btn='BACK', + right_btn='ENTER', + num_only=True, + max_length=9) + + if acct_num == None: + return + + # Use the entered account number + self.account_num = acct_num + + self.goto(self.ENTER_ACCOUNT_NAME) + + elif self.state == self.ENTER_ACCOUNT_NAME: + # Default the name to the label as a starting point for the user + self.account_name = '' + + name = await ux_enter_text( + 'Account Name', + label='New Account Name', + initial_text=self.account_name, + right_btn='SAVE', + max_length=MAX_ACCOUNT_NAME_LEN) + if name == None: + self.goto_prev() + continue + + # See if an account with this name already exists + if account_exists(name): + result = await ux_show_story('An account with the name "{}" already exists. Please choose a different name.'.format(name), + title='Duplicate', center=True, center_vertically=True, right_btn='RENAME') + if result == 'x': + self.goto_prev() + else: + self.account_name = name # Start off with the name the user entered + continue + + await save_new_account(name, self.account_num) + return + +MAX_ACCOUNTS = 20 +def max_accounts_reached(): + accounts = get_accounts() + return len(accounts) >= MAX_ACCOUNTS + +class AllAccountsMenu(MenuSystem): + + @classmethod + def construct(cls): + # Dynamic menu with user-defined names of accounts + # from actions import import_multisig_from_sd, import_multisig_from_qr + + rv = [] + try: + accounts = get_accounts() + + # print('accounts={}'.format(to_str(accounts))) + + for acct in accounts: + acct_num = acct.get('acct_num') + name_num = make_account_name_num(acct.get('name'), acct_num) + rv.append( + MenuItem( + name_num, + menu=AccountMenu, + menu_title=name_num, + action=set_active_account, + arg=acct)) + + # Show Resume or new account menu depending on status + rv.append(MenuItem('New Account', f=new_account, predicate=lambda: not max_accounts_reached())) + except Exception as e: + # print('accounts={}'.format(accounts)) + print('e={}'.format(e)) + rv.append(MenuItem('', f=lambda: None)) + + # print('rv={}'.format(rv)) + return rv + + def update_contents(self): + # Reconstruct the list of wallets on this dynamic menu, because + # we added or changed them and are showing that same menu again. + tmp = self.construct() + self.replace_items(tmp, True) + # Clear active account, if any, usually when menu is activated after returning from an account submenu + if common.active_account != None: + clear_active_account() diff --git a/ports/stm32/boards/Passport/modules/actions.py b/ports/stm32/boards/Passport/modules/actions.py index 02d0e5d..f6a43de 100644 --- a/ports/stm32/boards/Passport/modules/actions.py +++ b/ports/stm32/boards/Passport/modules/actions.py @@ -1,7 +1,7 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: GPL-3.0-or-later # -# SPDX-FileCopyrightText: 2018 Coinkite, Inc. +# SPDX-FileCopyrightText: 2018 Coinkite, Inc. # SPDX-License-Identifier: GPL-3.0-only # # (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard @@ -17,159 +17,416 @@ import version from files import CardMissingError, CardSlot # import main from uasyncio import sleep_ms -from common import settings, system, noise -from utils import imported, pretty_short_delay, xfp2str,swab32 -from ux import (the_ux, ux_confirm, ux_dramatic_pause, ux_enter_pin, - ux_enter_number, ux_enter_text, ux_scan_qr_code, ux_shutdown, +import common +from common import settings, system, noise, dis +from utils import (UXStateMachine, imported, pretty_short_delay, xfp2str, to_str, + truncate_string_to_width, set_next_addr, scan_for_address, get_accounts, run_chooser, + make_account_name_num, is_valid_address, save_next_addr) +from wallets.utils import get_export_mode, get_addr_type_from_address, get_deriv_path_from_addr_type_and_acct +from ux import (the_ux, ux_confirm, ux_enter_pin, + ux_enter_text, ux_scan_qr_code, ux_shutdown, ux_show_story, ux_show_story_sequence, ux_show_text_as_ur, ux_show_word_list) from se_commands import * +from data_codecs.qr_type import QRType import trezorcrypto +from seed_check_ux import SeedCheckUX -async def test_normal_menu(): - goto_top_menu() +async def needs_microsd(): + # Standard msg shown if no SD card detected when we need one. + return await ux_show_story("Please insert a microSD card.", title='MicroSD', center=True, center_vertically=True) + +async def about_info(*a): + from common import system + from display import FontTiny + from utils import swab32 + + while True: + serial = system.get_serial_number() + my_xfp = settings.get('xfp', 0) + xpub = settings.get('xpub', None) + + msg = '''Serial Number: +{serial} + +Master Fingerprint: +{xfp} +Reversed Fingerprint: +{rev_xfp} -async def start_selftest(*args): +Master XPUB: +{xpub}'''.format(serial=serial, + xfp=xfp2str(my_xfp) if my_xfp else '', + rev_xfp=xfp2str(swab32(my_xfp)) if my_xfp else '', + xpub=xpub if xpub != None else '') - if len(args) and not version.is_factory_mode(): - # called from inside menu, not directly - if not await ux_confirm('''Selftest destroys settings on other profiles (not seeds). Requires microSD card and might have other consequences. Recommended only for factory.'''): + result = await ux_show_story(msg, center=True, center_vertically=True, font=FontTiny, right_btn='REGULATORY') + if result == 'y': + await regulatory_info() + else: return - with imported('selftest') as st: - await st.start_selftest() +async def regulatory_info(): + from display import FontTiny - settings.save() + msg = """\ +Passport -async def needs_microsd(): - # Standard msg shown if no SD card detected when we need one. - await ux_show_story("Please insert a microSD card before attempting this operation.") +Foundation Devices +6 Liberty Square #6018 +Boston, MA 02109 USA""" + await ux_show_story(msg, title='Regulatory', center=True, font=FontTiny, overlay=(None, 303 - 34 - 72, 'fcc_ce_logos')) -async def needs_primary(): - # Standard msg shown if action can't be done w/o main PIN - await ux_show_story("Only the holder of the main PIN (not the secondary) can perform this function. Please start over with the main PIN.") +# async def account_info(*a): +# # show the XPUB, and other useful information +# import common +# import stash +# from display import FontTiny +# +# xfp = settings.get('xfp', 0) +# if xfp == None: +# xfp = '' +# else: +# xfp = xfp2str(xfp) +# +# # Can only get these values if the derivation path is known +# xpub = '' +# path = '' +# if common.active_account.deriv_path: +# with stash.SensitiveValues() as sv: +# path = common.active_account.deriv_path +# node = sv.derive_path(path) +# xpub = sv.chain.serialize_public(node, common.active_account.addr_type) +# print('account_info(): xpub={}'.format(xpub)) +# +# msg = ''' +# Account Number: +# {acct_num} +# +# Derivation Path: +# {path} +# +# Account Fingerprint: +# {xfp} +# +# Account XPUB: +# {xpub}'''.format(acct_num=common.active_account.acct_num, +# path=path, +# xfp=xfp, +# xpub=xpub) +# +# await ux_show_story( +# msg, +# title=common.active_account.name, +# center=True, +# font=FontTiny) +async def rename_account(menu, label, item): + from export import auto_backup + from utils import account_exists, do_rename_account + from constants import MAX_ACCOUNT_NAME_LEN + + account = common.active_account + + while True: + new_name = await ux_enter_text('Rename', label="Enter account name", initial_text=account.get('name'), + right_btn='RENAME', max_length=MAX_ACCOUNT_NAME_LEN) -async def accept_terms(*a): - # do nothing if they have accepted the terms once (ever), otherwise - # force them to read message... + if new_name == None: + # User selected BACK + return - if settings.get('terms_ok'): + # See if an account with this name already exists + if account_exists(new_name): + result = await ux_show_story('An account with the name "{}" already exists. Please choose a different name.'.format(new_name), + title='Duplicate', center=True, center_vertically=True, right_btn='RENAME') + if result == 'x': + self.goto_prev() + else: + continue + + # Get the accounts and replace the name and save it + await do_rename_account(account.get('acct_num'), new_name) + # Pop so we skip over the sub-menu for the account + the_ux.pop() return - while 1: - ch = await ux_show_story("""\ -Welcome to Passport! Congratulations for taking the first step towards sovereignty and ownership of your Bitcoin. +async def delete_account(menu, label, item): + from utils import do_delete_account, make_account_name_num + from ux import the_ux -Please accept our Terms of Use. You can read the full terms at: + account = common.active_account -foundationdevices.com/passport-terms""", left_btn='SHUTDOWN', right_btn='CONTINUE', scroll_label='MORE') + # Confirm the deletion + name_num = make_account_name_num(account.get('name'), account.get('acct_num')) - print('accept_terms() ch={}'.format(ch)) - if ch == 'y': - accepted_terms = await ux_confirm('I confirm that I have read and accept the Terms of Use.', negative_btn='BACK', positive_btn='I ACCEPT') - if accepted_terms: - # Note fact they accepted the terms. Annoying to ask user more than once. - settings.set('terms_ok', 1) - settings.save() - break + if not await ux_confirm('Are you sure you want to delete this account?\n\n{}'.format(name_num)): + return - elif ch == 'x': - # We only return from here if the user chose to not shutdown - await ux_shutdown() + await do_delete_account(account.get('acct_num')) + # Pop so we skip over the sub-menu for the account we just deleted + the_ux.pop() -async def view_ident(*a): - # show the XPUB, and other ident on screen - from common import settings - import stash +class VerifyAddressUX(UXStateMachine): - tpl = '''\ -Master Key Fingerprint: + def __init__(self): + # States + self.SELECT_ACCOUNT = 1 + self.SELECT_SIG_TYPE = 2 + self.VERIFY_ADDRESS = 3 -{xfp} + # print('VerifyAddressUX init') + super().__init__(self.SELECT_ACCOUNT) -Fingerprint as LE32: + self.acct_num = None + self.sig_type = None + self.multisig_wallet = None -{xfp_le} + # Account chooser + def account_chooser(self): + choices = [] + values = [] + + accounts = get_accounts() + accounts.sort(key=lambda a: a.get('acct_num', 0)) + + for acct in accounts: + acct_num = acct.get('acct_num') + account_name_num = make_account_name_num(acct.get('name'), acct_num) + choices.append(account_name_num) + values.append(acct_num) + + def select_account(index, text): + self.acct_num = values[index] + + return 0, choices, select_account + + # Select the sig type and if multisig, the specific multisig wallet + def sig_type_chooser(self): + from multisig import MultisigWallet + choices = ['Single-sig'] + values = ['single-sig'] + + num_multisigs = MultisigWallet.get_count() + for ms_idx in range(num_multisigs): + ms = MultisigWallet.get_by_idx(ms_idx) + choices.append('%d/%d: %s' % (ms.M ,ms.N, ms.name)) + values.append(ms) + + def select_sig_type(index, text): + if index == 0: + self.sig_type = 'single-sig' + self.multisig_wallet = None + else: + self.sig_type = 'multisig' + self.multisig_wallet = values[index] + + return 0, choices, select_sig_type + + async def show(self): + while True: + # print('show: state={}'.format(self.state)) + if self.state == self.SELECT_ACCOUNT: + self.acct_num = None + accounts = get_accounts() + if len(accounts) == 1: + self.acct_num = 0 + self.goto(self.SELECT_SIG_TYPE, save_curr=False) # Don't save this since we're skipping this state + continue -Extended Master Key: + await run_chooser(self.account_chooser, 'Account', show_checks=False) + if self.acct_num == None: + return -{xpub} -''' - my_xfp = settings.get('xfp', 0) - my_xfp_le = swab32(my_xfp) - msg = tpl.format(xpub=settings.get('xpub', '(none yet)'), - xfp=xfp2str(my_xfp), xfp_le=xfp2str(my_xfp_le), - serial=version.serial_number()) + self.goto(self.SELECT_SIG_TYPE) - if stash.bip39_passphrase: - msg += '\nBIP39 passphrase is in effect.\n' + elif self.state == self.SELECT_SIG_TYPE: + # Multisig only possible for account 0, so skip this if not account 0 + if self.acct_num > 0: + self.sig_type = 'single-sig' + self.goto(self.VERIFY_ADDRESS, save_curr=False) # Don't save this since we're skipping this state + continue - await ux_show_story(msg, center=True) + # Choose a wallet from the available list + multisigs = settings.get('multisig', []) + if len(multisigs) == 0: + self.sig_type = 'single-sig' + else: + await run_chooser(self.sig_type_chooser, 'Type', show_checks=False) + if self.sig_type == None: + if not self.goto_prev(): + # Nothing to return back to, so we must have skipped one or more steps...were' done + return + continue + + # print('self.sig_type={}'.format(self.sig_type)) + + self.goto(self.VERIFY_ADDRESS) + + elif self.state == self.VERIFY_ADDRESS: + # Scan the address to be verified - should be a normal QR code + system.turbo(True); + address = await ux_scan_qr_code('Verify Address') + if address == None: + return + # Ensure lowercase + address = address.lower() -async def maybe_dev_menu(*a): - from common import is_devmode + # Strip prefix if present + if address.startswith('bitcoin:'): + address = address[8:] - if not is_devmode: - ok = await ux_confirm('Developer features could be used to weaken security or release key material.\n\nDo not proceed unless you know what you are doing and why.') + if not is_valid_address(address): + result = await ux_show_story('That is not a valid Bitcoin address.', title='Error', left_btn='BACK', + right_btn='SCAN', center=True, center_vertically=True) + if result == 'x': + if not self.goto_prev(): + # Nothing to return back to, so we must have skipped one or more steps...were' done + return + continue - if not ok: - return None + # Get the address type from the address + is_multisig = self.sig_type == 'multisig' + # print('address={} acct_num={} is_multisig={}'.format(address, self.acct_num, is_multisig)) + addr_type = get_addr_type_from_address(address, is_multisig) + deriv_path = get_deriv_path_from_addr_type_and_acct(addr_type, self.acct_num, is_multisig) - from flow import DevelopersMenu - return DevelopersMenu + # Scan addresses to see if it's valid + addr_idx = await scan_for_address(self.acct_num, address, addr_type, deriv_path, self.multisig_wallet) + if addr_idx >= 0: + # Remember where to start from next time + save_next_addr(self.acct_num, addr_type, addr_idx) + + dis.fullscreen('Address Verified') + await sleep_ms(1000) + return + else: + # User asked to stop searching + return + + +async def verify_address(*a): + verify_address_ux = VerifyAddressUX() + await verify_address_ux.show() -async def microsd_upgrade(*a): - # Upgrade vis microSD card +async def update_firmware(*a): + # Upgrade via microSD card # - search for a particular file # - verify it lightly # - erase serial flash # - copy it over (slow) # - reboot into bootloader, which finishes install + from common import sf, dis + from constants import FW_HEADER_SIZE, FW_ACTUAL_HEADER_SIZE, FW_MAX_SIZE + import trezorcrypto - fn = await file_picker('Pick firmware image to use (.BIN)') + # Don't show any files that are pubkeys + def no_pubkeys(filename): + return not filename.endswith('-pub.bin') + fn = await file_picker('On the next screen, select the firmware file you want to install.', suffix='.bin', title='Select File', taster=no_pubkeys) + # print('\nselected fn = {}\n'.format(fn)) if not fn: return failed = None + system.turbo(True) + with CardSlot() as card: with open(fn, 'rb') as fp: - from common import sf, dis import os offset = 0 s = os.stat(fn) size = s[6] - # we also put a copy of special signed header at the end of the flash - # from sigheader import FW_HEADER_OFFSET, FW_HEADER_SIZE + if size < FW_HEADER_SIZE: + await ux_show_story('Firmware file is too small.', title='Error', left_btn='BACK', right_btn='OK', center=True, center_vertically=True) + return + + if size > FW_MAX_SIZE: + await ux_show_story('Firmware file is too large.', title='Error', left_btn='BACK', right_btn='OK', center=True, center_vertically=True) + return + + # Read the header + header = fp.read(FW_HEADER_SIZE) + if len(header) != FW_HEADER_SIZE: + system.turbo(False) + await ux_show_story('Firmware file is too small, and the system misreported its size.', title='Error', left_btn='BACK', right_btn='OK', center=True, center_vertically=True) + return - # # read just the signature header - # hdr = bytearray(FW_HEADER_SIZE) - # fp.seek(offset + FW_HEADER_OFFSET) - # rv = fp.readinto(hdr) - # assert rv == FW_HEADER_SIZE + # Validate the header + is_valid, version, error_msg = system.validate_firmware_header(header) + if not is_valid: + system.turbo(False) + await ux_show_story('Firmware header is invalid.\n\n{}'.format(error_msg), title='Error', left_btn='BACK', right_btn='OK', center=True, center_vertically=True) + return - # check header values + system.turbo(False) + + # Give the user a chance to confirm/back out + if not await ux_confirm('Please make sure your Passport is backed up before proceeding.\n\n' + + 'Are you sure you want to update the firmware?\n\nNew Version:\n{}'.format(version), + title='Update', scroll_label='MORE'): + return + + if not await ux_confirm('Do not remove the batteries or shutdown Passport during the firmware update.\n\nWe recommend using fresh batteries.', + title='Reminder', negative_btn='CANCEL', positive_btn='OK'): + return + + # Start the update + system.turbo(True) # copy binary into serial flash fp.seek(offset) + # Calculate the update request hash so that the booloader knows this was requested by the user, not + # injected into SPI flash by some external attacker. + # Hash the firmware header + header_hash = bytearray(32) + + # Only hash the bytes that contain the passport_firmware_header_t to match what's hashed in the bootloader + firmware_header = header[0:FW_ACTUAL_HEADER_SIZE] + system.sha256(firmware_header, header_hash) + system.sha256(header_hash, header_hash) # Double sha + + # Get the device hash + device_hash = bytearray(32) + system.get_device_hash(device_hash) + + # Combine them + s = trezorcrypto.sha256() + s.update(header_hash) + s.update(device_hash) + + # Result + update_hash = s.digest() + + # Erase first page + sf.sector_erase(0) + while sf.is_busy(): + await sleep_ms(10) + buf = bytearray(256) # must be flash page size - pos = 0 + buf[0:32] = update_hash # Copy into the buf we'll use to write to SPI flash + + sf.write(0, buf) # Need to write the entire page of 256 bytes + + # Start one page in so that we can use the first page for storing a hash. + # The hash combines the firmware hash with the device hash. + pos = 256 update_display = 0 - while pos <= size: + while pos <= size + 256: # print('pos = {}'.format(pos)) # Update progress bar every 50 flash pages if update_display % 50 == 0: - dis.fullscreen("Preparing Update...", percent=pos/size) + dis.splash(message='Preparing Update...', progress=(pos-256)/size) update_display += 1 here = fp.readinto(buf) @@ -190,16 +447,27 @@ async def microsd_upgrade(*a): pos += here - if failed: + system.turbo(False) await ux_show_story(failed, title='Sorry!') return + # Save an entry to the settings indicating that we are doing an update + (curr_version, _, _, _) = system.get_software_info() + settings.set('update', '{}->{}'.format(curr_version, version)) # old_version->new_version + await settings.save() + + # NOTE: We intentionally stay in turbo mode here as we reboot to keep the final splash display fast. + # Bootloader will go back to top speed anyway. + # continue process... - print("RESTARTING!") + # print("RESTARTING!") # Show final progress bar at 100% and change message - dis.fullscreen("Restarting...", percent=1) + dis.splash(message='Restarting...', progress=1) # TODO: Make 0-100 to be consistent with progress bar + + system.turbo(False) + await sleep_ms(1000) import machine @@ -210,45 +478,55 @@ async def reset_self(*a): machine.soft_reset() # NOT REACHED - -# TODO: Convert this to a state machine async def initial_pin_setup(*a): - from common import pa, dis, settings, loop + from common import pa, dis, loop - # First time they select a PIN of any type. - title = 'Choose PIN' + # TODO: Move the messaging into EnterInitialPinUX state machine + # First time they select a PIN while 1: - ch = await ux_show_story('''\ -Passport uses two PINs. Each PIN must be 2 to 6 digits long. - -The first is your Security Code. This PIN is used to verify your Passport has not been swapped or tampered with. - -The second is your Login PIN. This PIN allows you to unlock and use Passport. -''', title=title, scroll_label='MORE') - if ch == 'y': - + # ch = await ux_show_story('''\ + # Passport uses a PIN from 6 to 12 digits long. + # + # There is an additional security feature that you can use. After entering 2 or more digits of your PIN, press and hold the VALIDATE \ + # button and Passport will show you two Security Words unique to your device and PIN prefix. + # + # Remember these two words, and remember how many digits you entered before checking them. When logging in, you can \ + # repeat this process and you should see the same words. + # + # If you see different words, then either: + # + # 1. You entered a different number of digits + # + # 2. You entered the wrong first digits of your PIN + # + # 3. Your Passport has been tampered with''', title='PIN Info', scroll_label='MORE') + # if ch == 'y': while 1: ch = await ux_show_story('''\ -There is no way to recover a lost PIN or factory reset your Passport. +Now it's time to set your 6-12 digit PIN. + +There is no way to recover a lost PIN or reset Passport. -Please write down your PINs somewhere safe or store them in a password manager.''', title='WARNING', scroll_label='MORE') +Please record your PIN somewhere safe.''', title='Set PIN', scroll_label='MORE') if ch != 'y': break - # do the actual picking - from login_ux import EnterNewPinUX - new_pin_ux = EnterNewPinUX() + # Enter the PIN + from login_ux import EnterInitialPinUX + new_pin_ux = EnterInitialPinUX() await new_pin_ux.show() pin = new_pin_ux.pin - print('pin = {}'.format(pin)) + # print('pin = {}'.format(pin)) if pin is None: - return + continue # New pin is being saved - dis.fullscreen("Saving...") + dis.fullscreen("Saving PIN...") + + system.show_busy_bar() try: assert pa.is_blank() @@ -260,23 +538,19 @@ Please write down your PINs somewhere safe or store them in a password manager.' ok = pa.login() assert ok - # must re-read settings after login, because they are encrypted - # with a key derived from the main secret. - settings.set_key() - settings.load() except Exception as e: print("Exception: {}".format(e)) + finally: + system.hide_busy_bar() - from menu import MenuSystem - from flow import NoWalletMenu - return MenuSystem(NoWalletMenu) + return async def login_countdown(minutes): # show a countdown, which may need to # run for multiple **days** from common import dis - from display import FontSmall, FontLarge + from display import FontSmall sec = minutes * 60 while sec: @@ -288,7 +562,7 @@ async def login_countdown(minutes): y += 14 y += 5 - dis.text(None, y, pretty_short_delay(sec), font=FontLarge) + dis.text(None, y, pretty_short_delay(sec), font=FontSmall) dis.show() dis.busy_bar(1) @@ -304,122 +578,79 @@ async def block_until_login(*a): # Force user to enter a valid PIN. # from login_ux import LoginUX - from common import pa, loop, settings, dis + from common import pa, loop, dis - print('pa.is_successful() = {}'.format(pa.is_successful())) + # print('pa.is_successful() = {}'.format(pa.is_successful())) while not pa.is_successful(): login_ux = LoginUX() - try: - await login_ux.show() - except Exception as e: - print('ERROR when logging in: {}'.format(e)) - # not allowed! - pass - - settings.set_key() - settings.load() - - # Apply screen brightness - dis.set_brightness(settings.get('screen_brightness', 100)) - - print('!!!!LOGGED IN!!!!') - - -async def logout_now(*a): - # wipe memory and lock up - from utils import clean_shutdown - clean_shutdown() - - -async def login_now(*a): - # wipe memory and reboot - from utils import clean_shutdown - clean_shutdown(2) - -async def start_seed_import(menu, label, item): - import seed - return seed.WordNestMenu(item.arg) - + # try: + await login_ux.show() + # except Exception as e: + # print('ERROR when logging in: {}'.format(e)) + # # not allowed! + # pass -async def start_b39_pw(menu, label, item): - if not settings.get('b39skip', False): - ch = await ux_show_story('''\ -You may add a passphrase to your BIP39 seed words. \ -This creates an entirely new wallet, for every possible passphrase. - -By default, Passport uses an empty string as the passphrase. - -On the next menu, you can enter a passphrase by selecting \ -individual letters, choosing from the word list (recommended), \ -or by typing numbers. - -Please write down the fingerprint of all your wallets, so you can \ -confirm when you've got the right passphrase. (If you are writing down \ -the passphrase as well, it's okay to put them together.) There is no way for \ -Passport to know if your password is correct, and if you have it wrong, \ -you will be looking at an empty wallet. - -Limitations: 100 characters max length, ASCII \ -characters 32-126 (0x20-0x7e) only. - -OK to start. -X to go back. Or press 2 to hide this message forever. -''') - if ch == '2': - settings.set('b39skip', True) - if ch == 'x': - return + # print('!!!!LOGGED IN!!!!') + system.turbo(False) +async def create_new_seed(*a): + from ubinascii import hexlify as b2a_hex import seed - return seed.PassphraseMenu() + system.show_busy_bar() -async def create_new_wallet(*a): - from ubinascii import hexlify as b2a_hex - import seed - wallet_seed_bytes = seed.create_new_wallet_seed() - print('wallet_seed_bytes = {}'.format(b2a_hex(wallet_seed_bytes))) + wallet_seed_bytes = await seed.create_new_wallet_seed() + # print('wallet_seed_bytes = {}'.format(b2a_hex(wallet_seed_bytes))) mnemonic_str = trezorcrypto.bip39.from_data(wallet_seed_bytes) - print('mnemonic = {}'.format(mnemonic_str)) + # print('mnemonic = {}'.format(mnemonic_str)) mnemonic_words = mnemonic_str.split(' ') - # Show new wallet seed words to user - msg = 'Seed words (%d):\n' % len(mnemonic_words) - msg += '\n'.join('%2d: %s' % (i+1, w) for i, w in enumerate(mnemonic_words)) + # Save the wallet so we can work with it (needs to be saved for backup to work) + await seed.save_wallet_seed(wallet_seed_bytes) - trezor_seed = trezorcrypto.bip39.seed(mnemonic_str, '') - print('trezor_seed = {}'.format(b2a_hex(trezor_seed))) + # Update xpub/xfp in settings after creating new wallet + import stash + with stash.SensitiveValues() as sv: + sv.capture_xpub() - result = await ux_show_story(msg, sensitive=True, title="New Seed", right_btn='DONE') - if result == 'y': - # TODO: Quiz user on all words in random order to ensure they remember them all + system.hide_busy_bar() - # Set the seed into the SE - seed.save_wallet_seed(wallet_seed_bytes) + while True: + ch = await ux_show_story('''Now let's create a backup of your seed. We recommend backing up Passport to the two included microSD cards. - goto_top_menu() +Experienced users can always view and record the 24-word seed in the Advanced settings menu.''', title='Backup') + if ch == 'x': + if await ux_confirm("Are you sure you want to cancel the backup?\n\nWithout a microSD backup or the seed phrase, you won't be able to recover your funds"): + # Go back to the outer loop and show the selection again + break + # Ensure microSD card is inserted before continuing + try: + with CardSlot() as card: + # TODO: Call the export.make_complete_backup() directly and have it return True/False to indicate if the backup completed + await make_microsd_backup() + break + except CardMissingError: + ch = await needs_microsd() + if ch == 'x': + continue -async def import_wallet(menu, label, item): - from foundation import bip39 - from ubinascii import hexlify as b2a_hex - import seed + await goto_top_menu() - entropy = bytearray(33) # Includes and extra byte for the checksum bits +async def restore_wallet_from_seed(menu, label, item): + result = await ux_show_story('''On the next screen you'll be able to restore your seed using predictive text input. - result = ux_show_story('''On the next screen you'll be able to restore your seed using predictive text input. If you'd like to enter "car" for example, please type 2-2-7 and select "car" from the dropdown.''') - if result == 'x': +If you'd like to enter "car" for example, type 2-2-7 and select "car" from the dropdown.''', title='Restore Seed') + if result == 'x': return fake_it = False if fake_it: - # mnemonic = 'circle ecology lazy world fuel plate column priority crouch midnight scorpion cute defense enforce mention display dove review churn term canvas donate square broken' - mnemonic = 'park minute parrot ketchup river vital gravity wagon peanut inform craft amount erosion regular rent attack rubber then auto visa upon either fresh other' - # mnemonic = 'fabric humor guess asset day palace wealth spare trend seek focus empower hair advance myself defy grain inhale market noodle right need joke scatter' + pass else: - from seed_phrase_ux import SeedEntryUX + from seed_entry_ux import SeedEntryUX seed_phrase_entry = SeedEntryUX(seed_len=item.arg) await seed_phrase_entry.show() if not seed_phrase_entry.is_seed_valid: @@ -428,88 +659,83 @@ async def import_wallet(menu, label, item): # Seed is valid, so go ahead and convert the mnemonic to seed bits and save it mnemonic = ' '.join(seed_phrase_entry.words) - print('mnemonic = {}'.format(mnemonic)) - bip = bip39() # TODO: Can't we have static methods? - bip.mnemonic_to_entropy(mnemonic, entropy) - entropy = entropy[:32] # Trim off the checksum byte - print('entropy = {}'.format(b2a_hex(entropy))) - - seed.save_wallet_seed(entropy) - print('Wallet was imported successfully!') - - # TODO: Show post-creation story + # print('mnemonic = {}'.format(mnemonic)) + await handle_seed_data_format(mnemonic) - goto_top_menu() - - -async def convert_bip39_to_bip32(*a): +async def handle_seed_data_format(mnemonic): import seed - import stash - - if not await ux_confirm('''This operation computes the extended master private key using your BIP39 seed words and passphrase, and then saves the resulting value (xprv) as the wallet secret. - -The seed words themselves are erased forever, but effectively there is no other change. If a BIP39 passphrase is currently in effect, its value is captured during this process and will be 'in effect' going forward, but the passphrase itself is erased and unrecoverable. The resulting wallet cannot be used with any other passphrase. + from foundation import bip39 + from common import dis, pa + from ubinascii import hexlify as b2a_hex -A reboot is part of this process. PIN code, and funds are not affected. -''', negative_btn='BACK', positive_btn='LOCK DOWN'): - return + entropy = bytearray(33) # Includes and extra byte for the checksum bits - print('bip={}'.format(stash.bip39_passphrase)) - if not stash.bip39_passphrase: - if not await ux_confirm('''You do not have a BIP39 passphrase set right now, so this command does little except forget the seed words. It does not enhance security.'''): - return + # Don't let them import seed if there is already a wallet. + if not pa.is_secret_blank(): + await ux_show_story('''Unable to import seed phrase because this Passport is alread configured with a seed. - await seed.remember_bip39_passphrase() +First use Advanced > Erase Passport to remove the current seed.''', right_btn='OK') + return False - settings.save() + bip = bip39() + len = bip.mnemonic_to_entropy(mnemonic, entropy) - await login_now() + if len == 264: # 24 words x 11 bits each + trim_pos = 32 + elif len == 198: # 18 words x 11 bits each + trim_pos = 24 + elif len == 132: # 12 words x 11 bits each + trim_pos = 16 + entropy = entropy[:trim_pos] # Trim off the excess (including checksum bits) + # print('entropy = {}'.format(b2a_hex(entropy))) + # Entropy is now the right length - SecretStash.encode() adds a marker byte to indicate length of the secret + # so we can decode it correctly. + await seed.save_wallet_seed(entropy) + # print('Seed was imported successfully!') -async def clear_seed(*a): - # Erase the seed words, and private key from this wallet! - # This is super dangerous for the customer's money. - import seed - from common import pa - - if not await ux_confirm('''Are you sure you want to erase the current wallet? All funds will be lost if not backed up.'''): - return + # Update xpub/xfp in settings after creating new wallet + import stash + with stash.SensitiveValues() as sv: + sv.capture_xpub() - confirmed = await ux_confirm('''Without a proper backup, this action will cause you to lose all funds associated with this wallet.\n -Are you sure you read this message and understand the risks?''') - if not confirmed: - return + # Show post-creation message + dis.fullscreen('Successfully Imported!') + await sleep_ms(1000) - seed.clear_seed() - # NOT REACHED -- reset happens + await goto_top_menu() + return True -async def clear_seed_no_reset(*a): +async def erase_wallet(menu, label, item): # Erase the seed words, and private key from this wallet! # This is super dangerous for the customer's money. import seed from common import pa - if not await ux_confirm('''Are you sure you want to erase the current wallet? All funds will be lost if not backed up.'''): + if not await ux_confirm('Are you sure you want to erase this Passport? All funds will be lost if not backed up.'): return - confirmed = await ux_confirm('''Without a proper backup, this action will cause you to lose all funds associated with this wallet.\n -Are you sure you read this message and understand the risks?''') - if not confirmed: + if not await ux_confirm('Without a proper backup, this action will cause you to lose all funds associated with this device.\n\n' + + 'Please confirm that you understand these risks.', scroll_label='MORE', negative_btn='BACK', positive_btn='CONFIRM'): return - seed.clear_seed(False) + await seed.erase_wallet(item.arg) # NOT REACHED -- reset happens async def view_seed_words(*a): import stash + from common import dis if not await ux_confirm( - 'The next screen will show the seed words (and if defined, your BIP39 passphrase).\n\n' + - 'Anyone who knows these words can control all funds in this wallet.\n\n' + - 'Do you want to display this sensitive information?'): + 'The next screen will show your seed words and, if defined, your passphrase.\n\n' + + 'Anyone who knows these words can control your funds.\n\n' + + 'Do you want to display this sensitive information?', scroll_label='MORE', center=False): return + dis.fullscreen('Retrieving Seed...') + system.show_busy_bar() + try: with stash.SensitiveValues() as sv: assert sv.mode == 'words' # protected by menu item predicate @@ -521,110 +747,46 @@ async def view_seed_words(*a): pw = stash.bip39_passphrase if pw: - msg += '\n\nBIP39 Passphrase:\n%s' % stash.bip39_passphrase + msg += '\n\nPassphrase:\n%s' % stash.bip39_passphrase + + system.hide_busy_bar() - await ux_show_story(msg, sensitive=True, right_btn='DONE') + ch = await ux_show_story(msg, sensitive=True, right_btn='VERIFY') + if ch == 'y': + seed_check = SeedCheckUX(seed_words=words, title='Verify Seed') + await seed_check.show() + return stash.blank_object(msg) - except: + + except Exception as e: + print('Exception: {}'.format(e)) + system.hide_busy_bar() # Unable to read seed! await ux_show_story('Unable to retrieve seed.') + async def start_login_sequence(): # Boot up login sequence here. # - from common import pa, settings, dis, loop - - - # if pa.is_blank(): - # # Blank devices, with no PIN set all, can continue w/o login - - # # Do green-light set immediately after firmware upgrade - # if version.is_fresh_version(): - # pa.greenlight_firmware() - # dis.show() - - # goto_top_menu() - # return - - # # Allow impatient devs and crazy people to skip the PIN - # guess = settings.get('_skip_pin', None) - # if guess is not None: - # try: - # dis.fullscreen("(Skip PIN)") - # pa.setup(guess) - # pa.login() - # except: - # pass + from common import pa - # if that didn't work, or no skip defined, force - # them to login successfully. - print('start_login_sequence 1') while not pa.is_successful(): - print('start_login_sequence 2') # always get a PIN and login first await block_until_login() - # print('start_login_sequence 3') - - # # Must re-read settings after login - # settings.set_key() - # print('start_login_sequence 4') - # settings.load() - # print('start_login_sequence 5') - - # # implement "login countdown" feature - # delay = settings.get('lgto', 0) - # if delay: - # pa.reset() - # await login_countdown(delay) - # await block_until_login() - - # # Do green-light set immediately after firmware upgrade - # if version.is_fresh_version(): - # pa.greenlight_firmware() - # dis.show() - - # # Populate xfp/xpub values, if missing. - # # - can happen for first-time login of d-u-r-e-s-s wallet - # # - may indicate lost settings, which we can easily recover from - # # - these values are important to USB protocol - # if not (settings.get('xfp', 0) and settings.get('xpub', 0)) and not pa.is_secret_blank(): - # try: - # import stash - - # # Recalculate xfp/xpub values (depends both on secret and chain) - # with stash.SensitiveValues() as sv: - # sv.capture_xpub() - # except Exception as exc: - # # just in case, keep going; we're not useless and this - # # is early in boot process - # print("XFP save failed: %s" % exc) - - # # Allow USB protocol, now that we are auth'ed - # # from usb import enable_usb - # # enable_usb(loop, False) - - - -def goto_top_menu(): + +async def goto_top_menu(*a): # Start/restart menu system from menu import MenuSystem - from flow import NoPINMenu, MainMenu, NoWalletMenu + from flow import MainMenu, NoSeedMenu from common import pa - - # if version.is_factory_mode(): - # m = MenuSystem(???, title='Factory') - # elif pa.is_blank(): - # # let them play a little before picking a PIN first time - # m = MenuSystem( - # NoPINMenu, should_cont=lambda: pa.is_blank(), title='Setup') - # else: - # assert pa.is_successful(), "nonblank but wrong pin" - - - m = MenuSystem(NoWalletMenu if pa.is_secret_blank() else MainMenu) + # print('pa.is_secret_blank()={}'.format(pa.is_secret_blank())) + if pa.is_secret_blank(): + m = MenuSystem(NoSeedMenu) + else: + m = MenuSystem(MainMenu) the_ux.reset(m) @@ -633,23 +795,24 @@ def goto_top_menu(): SENSITIVE_NOT_SECRET = ''' -The file created is sensitive--in terms of privacy--but should not \ +The file created is sensitive in terms of privacy, but should not \ compromise your funds directly.''' PICK_ACCOUNT = '''\n\nPress 1 to enter a non-zero account number.''' -async def dump_summary(*A): +async def export_summary(*A): # save addresses, and some other public details into a file if not await ux_confirm('''\ Saves a text file to microSD with a summary of the *public* details \ of your wallet. For example, this gives the XPUB (extended public key) \ -that you will need to import other wallet software to track balance.''' + SENSITIVE_NOT_SECRET): +that you will need to import other wallet software to track balance.''' + SENSITIVE_NOT_SECRET, + title='Export', negative_btn='BACK', positive_btn='CONTINUE'): return # pick a semi-random file name, save it. - with imported('backups') as bk: - await bk.make_summary_file() + with imported('export') as exp: + await exp.make_summary_file() def electrum_export_story(background=False): @@ -662,295 +825,193 @@ You can then open that file in Electrum without ever connecting this Passport to + SENSITIVE_NOT_SECRET) -async def electrum_skeleton(*a): - # save xpub, and some other public details into a file: NOT MULTISIG - - ch = await ux_show_story(electrum_export_story()) - - account_num = 0 - if ch == '1': - account_num = await ux_enter_number('Account Number:', 9999) - elif ch != 'y': - return - - # pick segwit or classic derivation+such - from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH - from menu import MenuSystem, MenuItem - - # Ordering and terminology from similar screen in Electrum. I prefer - # 'classic' instead of 'legacy' personally. - rv = [] - - rv.append(MenuItem("Legacy (P2PKH)", f=electrum_skeleton_step2, - arg=(AF_CLASSIC, account_num))) - rv.append(MenuItem("P2SH-Segwit", f=electrum_skeleton_step2, - arg=(AF_P2WPKH_P2SH, account_num))) - rv.append(MenuItem("Native Segwit", f=electrum_skeleton_step2, - arg=(AF_P2WPKH, account_num))) - - return MenuSystem(rv, title="Electrum") - - -async def xpub_qr(*a): - # Create and show a QR code that BlueWallet can import - - # pick segwit or classic derivation+such - from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH - from menu import MenuSystem, MenuItem - - # TODO: Insert a step to choose a different account_num - account_num = 0 - - # Ordering and terminology from similar screen in Electrum. I prefer - # 'classic' instead of 'legacy' personally. - rv = [] - - rv.append(MenuItem("Native Segwit (zpub)", f=xpub_qr_step2, - arg=(AF_P2WPKH, account_num))) - - return MenuSystem(rv, title="BlueWallet") - - -async def xpub_qr_step2(_1, _2, item): - from ubinascii import hexlify - import ujson - - addr_fmt, account_num = item.arg - - with imported('backups') as bk: - wallet = bk.generate_electrum_wallet(addr_fmt, account_num) - print('wallet={}'.format(wallet)) - # xpub = wallet['keystore']['xpub'] - - # msg = '''{"keystore": {"ckcc_xpub": "xpub661MyMwAqRbcEd36dwxWycMGRYR9kioqmtd5XScTXxXWcDBNWf9svbcTSJw1nFLQRUFnbvFuEiB4QqygXakhZ3Jx3hh1pnV5uWCCwAk3kAK", "xpub": "zpub6qUao2NtyxySY7tDdSS13Chc3TqMrTF7jxCGExBUD9daszd5ibBME2t359in7m8TiToTSeHGTgCaVMNwKqrRdydyp68jyQ2owZy2UvVCh76", "label": "Coldcard Import 6FCC570C", "ckcc_xfp": 207080559, "type": "hardware", "hw_type": "coldcard", "derivation": "m/84'/0'/0'"}, "wallet_type": "standard", "use_encryption": false, "seed_version": 17}''' - - encoded_msg = ujson.dumps(wallet).encode('ascii') - print('encoded_msg={}'.format(encoded_msg)) - hex_msg = hexlify(encoded_msg) - str_msg = hex_msg.decode('ascii') - - await ux_show_text_as_ur(title='BlueWallet', qr_text=str_msg) - - -async def bitcoin_core_skeleton(*A): - # save output descriptors into a file - # - user has no choice, it's going to be bech32 with m/84'/{coin_type}'/0' path - - ch = await ux_show_story('''\ -This saves a command onto the microSD card that includes the public keys. \ -You can then run that command in Bitcoin Core without ever connecting this Passport to a computer.\ -''' + PICK_ACCOUNT + SENSITIVE_NOT_SECRET) - - account_num = 0 - if ch == '1': - account_num = await ux_enter_number('Account Number:', 9999) - elif ch != 'y': - return - - # no choices to be made, just do it. - with imported('backups') as bk: - await bk.make_bitcoin_core_wallet(account_num) - - -async def electrum_skeleton_step2(_1, _2, item): - # pick a semi-random file name, render and save it. - with imported('backups') as bk: - addr_fmt, account_num = item.arg - await bk.make_json_wallet('Electrum wallet', lambda: bk.generate_electrum_wallet(addr_fmt, account_num)) - - -async def wasabi_skeleton(*A): - # save xpub, and some other public details into a file - # - user has no choice, it's going to be bech32 with m/84'/0'/0' path +# async def electrum_skeleton(*a): +# # save xpub, and some other public details into a file: NOT MULTISIG +# +# ch = await ux_show_story(electrum_export_story()) +# +# account_num = 0 +# if ch == '1': +# account_num = await ux_enter_number('Account Number:', 9999) +# elif ch != 'y': +# return +# +# # pick segwit or classic derivation+such +# from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH +# from menu import MenuSystem, MenuItem +# +# # Ordering and terminology from similar screen in Electrum. I prefer +# # 'classic' instead of 'legacy' personally. +# rv = [] +# +# rv.append(MenuItem("Legacy (P2PKH)", f=electrum_skeleton_step2, +# arg=(AF_CLASSIC, account_num))) +# rv.append(MenuItem("P2SH-Segwit", f=electrum_skeleton_step2, +# arg=(AF_P2WPKH_P2SH, account_num))) +# rv.append(MenuItem("Native Segwit", f=electrum_skeleton_step2, +# arg=(AF_P2WPKH, account_num))) +# +# return MenuSystem(rv, title="Electrum") - if await ux_show_story('''\ -This saves a skeleton Wasabi wallet file onto the microSD card. \ -You can then open that file in Wasabi without ever connecting this Passport to a computer.\ -''' + SENSITIVE_NOT_SECRET) != 'y': - return - # no choices to be made, just do it. - with imported('backups') as bk: - await bk.make_json_wallet('Wasabi wallet', lambda: bk.generate_wasabi_wallet(), 'new-wasabi.json') +# async def bitcoin_core_skeleton(*A): +# # save output descriptors into a file +# # - user has no choice, it's going to be bech32 with m/84'/{coin_type}'/0' path +# +# ch = await ux_show_story('''\ +# This saves a command onto the microSD card that includes the public keys. \ +# You can then run that command in Bitcoin Core without ever connecting this Passport to a computer.\ +# ''' + PICK_ACCOUNT + SENSITIVE_NOT_SECRET) +# +# account_num = 0 +# if ch == '1': +# account_num = await ux_enter_number('Account Number:', 9999) +# elif ch != 'y': +# return +# +# # no choices to be made, just do it. +# with imported('export') as exp: +# dis.fullscreen('Generating...') +# body = exp.make_bitcoin_core_wallet(account_num) +# await write_text_file('bitcoin-core.txt', body, 'Bitcoin Core') + +# async def electrum_skeleton_step2(_1, _2, item): +# # pick a semi-random file name, render and save it. +# with imported('export') as exp: +# addr_fmt, account_num = item.arg +# await exp.make_json_wallet('Electrum wallet', lambda: exp.generate_electrum_wallet(addr_fmt, account_num)) + +# async def generic_skeleton(*a): +# # like the Multisig export, make a single JSON file with +# # basically all useful XPUB's in it. +# +# if await ux_show_story('''\ +# Saves JSON file onto MicroSD card, with XPUB values that are needed to watch typical \ +# single-signer UTXO associated with this Coldcard.''' + SENSITIVE_NOT_SECRET) != 'y': +# return +# +# account_num = await ux_enter_number('Account Number:', 9999) +# +# # no choices to be made, just do it. +# import export +# await export.make_json_wallet('Generic Export', +# lambda: export.generate_generic_export(account_num), +# 'coldcard-export.json') + +# async def wasabi_skeleton(*A): +# # save xpub, and some other public details into a file +# # - user has no choice, it's going to be bech32 with m/84'/0'/0' path +# +# if await ux_show_story('''\ +# This saves a skeleton Wasabi wallet file onto the microSD card. \ +# You can then open that file in Wasabi without ever connecting this Passport to a computer.\ +# ''' + SENSITIVE_NOT_SECRET) != 'y': +# return +# +# # no choices to be made, just do it. +# with imported('export') as exp: +# await exp.make_json_wallet('Wasabi wallet', lambda: exp.generate_wasabi_wallet(), 'new-wasabi.json') -async def backup_everything(*A): +async def make_microsd_backup(*A): # save everything, using a password, into single encrypted file, typically on SD - with imported('backups') as bk: - await bk.make_complete_backup() + with imported('export') as exp: + await exp.make_complete_backup() -async def verify_backup(*A): +async def verify_microsd_backup(*A): # check most recent backup is "good" # read 7z header, and measure checksums - with imported('backups') as bk: - - fn = await file_picker('Select file containing the backup to be verified. No password will be required.', suffix='.7z', max_size=bk.MAX_BACKUP_FILE_SIZE) + with imported('export') as exp: + fn = await file_picker('Select the backup to verify.', + suffix='.7z', max_size=exp.MAX_BACKUP_FILE_SIZE, folder_path='/sd/backups') if fn: # do a limited CRC-check over encrypted file - await bk.verify_backup_file(fn) - - -def import_from_dice(*a): - import seed - return seed.import_from_dice() - - -async def import_xprv(*A): - # read an XPRV from a text file and use it. - import chains - import ure - from common import pa - from stash import SecretStash - from ubinascii import hexlify as b2a_hex - from backups import restore_from_dict - - assert pa.is_secret_blank() # "must not have secret" - - def contains_xprv(fname): - # just check if likely to be valid; not full check - try: - with open(fname, 'rt') as fd: - for ln in fd: - # match tprv and xprv, plus y/zprv etc - if 'prv' in ln: - return True - return False - except OSError: - # directories? - return False - - # pick a likely-looking file. - fn = await file_picker('Select file containing the XPRV to be imported.', - min_size=50, max_size=2000, taster=contains_xprv) - - if not fn: - return - - node, chain, addr_fmt = None, None, None - - # open file and do it - pat = ure.compile(r'.prv[A-Za-z0-9]+') - with CardSlot() as card: - with open(fn, 'rt') as fd: - for ln in fd.readlines(): - if 'prv' not in ln: - continue + await exp.verify_backup_file(fn) - found = pat.search(ln) - if not found: - continue - - found = found.group(0) - - for ch in chains.AllChains: - for kk in ch.slip132: - if found[0] == ch.slip132[kk].hint: - try: - node = trezorcrypto.bip32.deserialize(found, - ch.slip132[kk].pub, ch.slip132[kk].priv) - chain = ch - addr_fmt = kk - break - except ValueError: - pass - if node: - break - - if not node: - # unable - await ux_show_story('''\ -Sorry, wasn't able to find an extended private key to import. It should be at \ -the start of a line, and probably starts with "xprv".''', title="FAILED") - return - # encode it in our style - d = dict(chain=chain.ctype, raw_secret=b2a_hex( - SecretStash.encode(xprv=node))) - # This function was added by coinkite - # TODO: Important enough to add blank() back into trezor? - # node.blank() - - # TODO: capture the address format implied by SLIP32 version bytes - # addr_fmt = - - # restore as if it was a backup (code reuse) - await restore_from_dict(d) +EMPTY_RESTORE_MSG = '''\ +Before restoring from a backup, you must erase this Passport. Make sure your device is backed up. - # not reached; will do reset. +Navigate to Advanced > Erase Passport.''' -EMPTY_RESTORE_MSG = '''\ -Before restoring from a backup, you must erase the current wallet. \ -Please make sure your current wallet is backed up.\n\n\ -Visit the advanced settings and choose 'Erase Wallet'.''' +FULL_PARTIAL_MSG = '''A wallet seed already exists. +Do you want to perform a FULL restore or a PARTIAL restore of accounts only?''' -async def restore_everything(*A): +async def restore_microsd_backup(*A): from common import pa + partial_restore = False + if not pa.is_secret_blank(): await ux_show_story(EMPTY_RESTORE_MSG) return - # restore everything, using a password, from single encrypted 7z file - fn = await file_picker('Select file containing the backup to be restored, and ' - 'then enter the password.', suffix='.7z', max_size=10000) + # if not pa.is_secret_blank(): + # result = await ux_show_story(FULL_PARTIAL_MSG, left_btn='FULL', right_btn='PARTIAL') + # if result == 'x': + # await ux_show_story(EMPTY_RESTORE_MSG) + # return + # else: + # partial_restore = True - if fn: - with imported('backups') as bk: - await bk.restore_complete(fn) + # TODO: Insert step here to pick a backups-* folder when we add the XFP to the folder name + # Choose a backup file -- must be in 7z format + fn = await file_picker('Select the backup to restore and then enter the six-word password.', + suffix='.7z', max_size=10000, folder_path='/sd/backups') -async def restore_everything_cleartext(*A): - # Asssume no password on backup file; devs and crazy people only - from common import pa - - if not pa.is_secret_blank(): - await ux_show_story(EMPTY_RESTORE_MSG) - return + if fn: + with imported('export') as exp: + await exp.restore_complete(fn, partial_restore) - # restore everything, using NO password, from single text file, like would be wrapped in 7z - fn = await file_picker('Select the cleartext file containing the backup to be restored.', - suffix='.txt', max_size=10000) - if fn: - with imported('backups') as bk: - prob = await bk.restore_complete_doit(fn, []) - if prob: - await ux_show_story(prob, title='FAILED') +async def format_sd_card(*A): + if not await ux_confirm('Erase and reformat the microSD card.', negative_btn='BACK', positive_btn='FORMAT'): + return + from files import format_microsd_card -# async def wipe_filesystem(*A): -# if not await ux_confirm('''\ -# Erase internal filesystem and rebuild it. Resets contents of internal flash area \ -# used for code patches. Does not affect funds, settings or seed words. \ -# Does not affect SD card, if any.'''): -# return + system.turbo(True) + format_microsd_card() + system.turbo(False) -# from files import wipe_flash_filesystem -# wipe_flash_filesystem() +async def list_files(*a): + # list files, don't do anything with them? + fn = await file_picker('List all files on the microSD card. Select a file to show the SHA256 hash.', min_size=0) + if not fn: + return + from utils import B2A + chk = trezorcrypto.sha256() -async def wipe_sd_card(*A): - if not await ux_confirm('''\ -Erases and reformats microSD card. This is not a secure erase but more of a quick format.'''): + system.show_busy_bar() + try: + with CardSlot() as card: + with open(fn, 'rb') as fp: + while 1: + data = fp.read(1024) + if not data: break + chk.update(data) + except CardMissingError: + system.hide_busy_bar() + await needs_microsd() return - from files import wipe_microsd_card - wipe_microsd_card() + basename = fn.rsplit('/', 1)[-1] + digest = B2A(chk.digest()) + system.hide_busy_bar() -async def list_files(*A): - # list files, don't do anything with them? - fn = await file_picker('List files on microSD') - return + await ux_show_story('File:\n %s\n\n%s' % (basename, digest), title='SHA256') -async def file_picker(msg, suffix=None, min_size=None, max_size=None, taster=None, choices=None, none_msg=None): +async def file_picker(msg, suffix=None, min_size=None, max_size=None, taster=None, choices=None, none_msg=None, title='Select', folder_path=None): # present a menu w/ a list of files... to be read # - optionally, enforce a max size, and provide a "tasting" function # - if msg==None, don't prompt, just do the search and return list @@ -960,16 +1021,23 @@ async def file_picker(msg, suffix=None, min_size=None, max_size=None, taster=Non import uos from utils import get_filesize + system.turbo(True) + if choices is None: choices = [] try: with CardSlot() as card: sofar = set() - for path in card.get_paths(): + if folder_path == None: + folder_path = card.get_paths() + else: + folder_path = [folder_path] + + for path in folder_path: files = uos.ilistdir(path) for fn, ftype, *var in files: - print("fn={} ftype={} var={}".format(fn, ftype, var)) + # print("fn={} ftype={} var={} suffix={}".format(fn, ftype, var, suffix)) if ftype == 0x4000: # ignore subdirs continue @@ -983,7 +1051,7 @@ async def file_picker(msg, suffix=None, min_size=None, max_size=None, taster=Non full_fname = path + '/' + fn - # Conside file size + # Consider file size # sigh, OS/filesystem variations file_size = var[1] if len( var) == 2 else get_filesize(full_fname) @@ -1015,33 +1083,28 @@ async def file_picker(msg, suffix=None, min_size=None, max_size=None, taster=Non choices.append((label, path, fn)) except CardMissingError: + system.turbo(False) # don't show anything if we're just gathering data if msg is not None: await needs_microsd() return None + system.turbo(False) + if msg is None: return choices if not choices: - msg = none_msg or 'Passport is unable to find the correct file on your microSD card. ' + msg = none_msg or 'Unable to find an applicable file on the microSD card.' if not none_msg: if suffix: - msg += 'The filename must end in "%s". ' % suffix - - msg += '\n\nPlease check the files on your microSD card and try again. ' + msg += '\n\nThe filename must end in "%s".' % suffix - await ux_show_story(msg) + await ux_show_story(msg, center=True, center_vertically=True, title=title) return - # tell them they need to pick; can quit here too, but that's obvious. - if len(choices) != 1: - msg += '\n\nThere are %d files to pick from.' % len(choices) - else: - msg += '\n\nThere is only one file to pick from.' - - ch = await ux_show_story(msg) + ch = await ux_show_story(msg, center=True, center_vertically=True, title=title) if ch == 'x': return @@ -1051,18 +1114,12 @@ async def file_picker(msg, suffix=None, min_size=None, max_size=None, taster=Non picked.append('/'.join(item.arg)) the_ux.pop() + choices.sort() + items = [MenuItem(label, f=clicked, arg=(path, fn)) for label, path, fn in choices] - if 0: - # don't like; and now showing count on previous page - if len(choices) == 1: - # if only one choice, we could make the choice for them ... except very confusing - items.append(MenuItem(' (one file)', f=None)) - else: - items.append(MenuItem(' (%d files)' % len(choices), f=None)) - - menu = MenuSystem(items, title='Select a File') + menu = MenuSystem(items, title='Select File') the_ux.push(menu) await menu.interact() @@ -1071,13 +1128,17 @@ async def file_picker(msg, suffix=None, min_size=None, max_size=None, taster=Non async def sign_tx_from_sd(*a): - # Top menu choice of top menu! Signing! - # - check if any signable in SD card, if so do it - # - if nothing, then talk about USB connection + # Check if any signable in SD card, if so do it from public_constants import MAX_TXN_LEN + import stash + + if stash.bip39_passphrase: + title = '[%s]' % xfp2str(settings.get('xfp')) + else: + title = 'Select PSBT' + def is_psbt(filename): - print("filename=" + filename) if '-signed' in filename.lower(): return False @@ -1096,8 +1157,8 @@ async def sign_tx_from_sd(*a): if not choices: await ux_show_story("""\ -Please copy an unsigned PSBT transaction onto your microSD card and insert into Passport. -""") +Copy an unsigned PSBT transaction onto the microSD card and insert it into Passport. +""", title=title) return if len(choices) == 1: @@ -1105,7 +1166,7 @@ Please copy an unsigned PSBT transaction onto your microSD card and insert into label, path, fn = choices[0] input_psbt = path + '/' + fn else: - input_psbt = await file_picker('Choose PSBT file to be signed.', choices=choices) + input_psbt = await file_picker('Choose a PSBT to sign.', choices=choices, title=title) if not input_psbt: return @@ -1125,7 +1186,7 @@ async def sign_message_on_sd(*a): lines = fd.readlines() return (1 <= len(lines) <= 5) - fn = await file_picker('Choose text file to be signed.', + fn = await file_picker('Choose text file to sign.', suffix='txt', min_size=2, max_size=500, taster=is_signable, none_msg='No suitable files found. Must be one line of text, in a .TXT file, optionally followed by a subkey derivation path on a second line.') @@ -1140,88 +1201,56 @@ async def sign_message_on_sd(*a): async def change_pin(*a): # Help user change pins with appropriate warnings. - from login_ux import ChangePINUX + from login_ux import ChangePinUX - change_pin_ux = ChangePINUX() + change_pin_ux = ChangePinUX() await change_pin_ux.show() - # Reset pin to blank (all zeroes) -async def set_blank_pin(*a): - from common import pa - - args = {} - old_pin_1 = await ux_enter_pin(title='Enter Old PIN', heading='Security Code') - old_pin_2 = await ux_enter_pin(title='Enter Old PIN', heading='Login PIN') - old_pin = old_pin_1 + old_pin_2 - args['old_pin'] = old_pin.encode() - blank_pin = [32] * 0 - args['new_pin'] = bytearray(blank_pin) - try: - pa.change(**args) - except Exception as e: - print('Exception: {}'.format(e)) - +# async def set_blank_pin(*a): +# from common import pa +# +# args = {} +# old_pin = await ux_enter_pin(title='Enter Old PIN', heading='Old PIN') +# args['old_pin'] = old_pin.encode() +# blank_pin = [32] * 0 +# args['new_pin'] = bytearray(blank_pin) +# try: +# pa.change(**args) +# except Exception as e: +# print('Exception: {}'.format(e)) async def show_version(*a): - # show firmware, bootload versions. - from common import settings - import callgate - import version - from ubinascii import hexlify as b2a_hex + # show firmware, bootloader versions + from utils import get_month_str + from utime import localtime - built, rel, *_ = version.get_mpy_version() - bl = callgate.get_bootloader_version()[0] - chk = str(b2a_hex(callgate.get_firmware_hash(0))[-8:], 'ascii') + system.turbo(True) + (fw_version, fw_timestamp, boot_counter, user_signed) = system.get_software_info() - msg = '''\ -Passport Firmware - {rel} - {built} + time = localtime(fw_timestamp) + fw_date = '{} {}, {}'.format(get_month_str(time[1]), time[2], time[0]-30) -Bootloader: - {bl} - {chk} - -Serial: - {ser} - -Hardware: - {hw} -''' - - await ux_show_story(msg.format(rel=rel, built=built, bl=bl, chk=chk, - ser=version.serial_number(), hw=version.hw_label)) - - -async def set_firmware_highwater(*a): - # rarely? used command - import callgate - - have = version.get_mpy_version()[0] - ts = version.get_header_value('timestamp') - - hw = callgate.get_firmware_highwater() - - if hw == ts: - await ux_show_story('''Current version (%s) already marked as high-water mark.''' % have) - return - - ok = await ux_confirm('''Mark current version (%s) as the minimum, and prevent any downgrades below this version. - -Rarely needed as critical security updates will set this automatically.''' % have) + msg = '''\ +Current Firmware: +v{fw_version} +{fw_date}'''.format(fw_version=fw_version, + fw_date=fw_date) - if not ok: - return + if user_signed: + msg += '\nSigned by User' - rv = callgate.set_firmware_highwater(ts) + msg += ''' - # add error display here? meh. +Boot Counter: +{boot_counter}\ +'''.format(boot_counter=boot_counter) + system.turbo(False) - assert rv == 0, "Failed: %r" % rv + await ux_show_story(msg, center=True, center_vertically=True) -async def import_multisig(*a): +async def import_multisig_from_sd(*a): # pick text file from SD card, import as multisig setup file def possible(filename): @@ -1230,52 +1259,206 @@ async def import_multisig(*a): if 'pub' in ln: return True - fn = await file_picker('Pick multisig wallet file to import (.txt)', suffix='.txt', + fn = await file_picker('Select multisig wallet file to import (.txt)', suffix='.txt', min_size=100, max_size=20*200, taster=possible) if not fn: return + system.turbo(True); try: with CardSlot() as card: with open(fn, 'rt') as fp: data = fp.read() except CardMissingError: + system.turbo(False); await needs_microsd() return + # print('data={}'.format(data)) + from auth import maybe_enroll_xpub + from utils import problem_file_line, show_top_menu + from export import offer_backup try: possible_name = (fn.split('/')[-1].split('.'))[0] maybe_enroll_xpub(config=data, name=possible_name) + await show_top_menu() # wait for interaction with the enroll + + system.turbo(False); + await offer_backup() + except Exception as e: + system.turbo(False); + await ux_show_story('Unable to import multisig configuration.\n\n{}\n{}'.format(e, problem_file_line(e)), title='Error') + + +async def import_multisig_from_qr(*a): + system.turbo(True); + data = await ux_scan_qr_code('Import Multisig') + system.turbo(False); + + if data != None: + # TOnly need to decode this for QR codes...from SD card it's already in bytes + data = data.decode('utf-8') + await handle_import_multisig_config(data) + +async def handle_import_multisig_config(data): + from auth import maybe_enroll_xpub + from export import offer_backup + from utils import show_top_menu + + try: + possible_name = "ms" + maybe_enroll_xpub(config=data, name=possible_name) + await show_top_menu() # wait for interaction with the enroll + + await offer_backup() except Exception as e: - await ux_show_story('Failed to import.\n\n\n'+str(e)) + await ux_show_story('Unable to import multisig configuration.\n\n'+str(e), title='Error') + + +# Scan QR code and magically determine what to do +async def magic_scan(menu, label, item): + from common import dis + from data_codecs.data_format import get_flow_for_data -async def sign_tx_from_qr(menu, label, item): title = item.arg - data = await ux_scan_qr_code(title) - # data = '70736274ff010071020000000131d2b534ed8dccf2546323b58a9fc2b0a28475f522fab0b9eb820b3c7bfe43f20000000000ffffffff021027000000000000160014fb141c2c8020a9242da603b6b1a88b981c9bec33c421010000000000160014395b1557687176af8d96fdf26bd4ecf1b44fb406000000000001011fa086010000000000160014f338b1a82c03f057b6b5200d6642492c3c8742d4220602fce63395dc5ff807b501c16fc6d719277c96936b3727667b0f3d19dd23e3905418fa96b9d0540000800000008000000080000000000000000000002202034bf621fed4dfff08f7b8d8e9b5de5bc556ad3885a30807855617d5bcb3e22c2518fa96b9d0540000800000008000000080010000000000000000' + + while True: + system.turbo(True) + data = await ux_scan_qr_code(title) + system.turbo(False) + + # Run the samplers to figure out what type of data was scanned and run the corresponding flow, if any + if data == None: + return + + flow = get_flow_for_data(data) + if flow == None: + # Show error to user + result = await ux_show_story('Unrecognized data format.', title='Error', right_btn='RETRY', center=True, center_vertically=True) + if result == 'y': + continue + return + else: + # Run the flow + retry = await flow(data) + if retry: + continue + return + +# Handler for psbt +async def handle_psbt_data_format(data): + from common import dis if data != None: - from auth import sign_psbt_buf + try: + from auth import sign_psbt_buf + + # The data can be a string or may already be a bytes object + if isinstance(data, bytes): + data_buf = data + else: + data_buf = bytes(data, 'utf-8') + # print("data_buf={}".format(data_buf)) + system.show_busy_bar() + dis.fullscreen('Analyzing...') + await sign_psbt_buf(data_buf) + except Exception as e: + # print('Signing exception:{}'.format(e)) + result = await ux_show_story('Error signing transaction:\n\n{}'.format(e), title='Error', right_btn='RETRY') + return result == 'y' + finally: + system.hide_busy_bar() + + return False + +async def import_user_firmware_pubkey(*a): + from common import system, dis + from ubinascii import hexlify + + result = await ux_show_story('''Passport allows you to compile your own firmware version and sign it \ +with your private key. + +To enable this, you must first import your corresponding public key. + +On the next screen, you can select your public key and import it into Passport.''', title='Import PubKey') + if result == 'x': + return + + fn = await file_picker('Select public key file (*-pub.bin)', suffix='-pub.bin') + if fn == None: + return + + system.turbo(True) + with CardSlot() as card: + with open(fn, 'rb') as fd: + fd.seek(24) # Skip the header + pubkey = fd.read(64) # Read the pubkey + + # print('pubkey = {}'.format(hexlify(pubkey))) + + result = system.set_user_firmware_pubkey(pubkey) + if result: + dis.fullscreen('Successfully Imported!') + else: + dis.fullscreen('Unable to Import') + await sleep_ms(1000) + # print('system.set_user_firmware_pubkey() = {}'.format(result)) + system.turbo(False) + + +async def read_user_firmware_pubkey(*a): + from common import system + from ubinascii import hexlify + + pubkey = bytearray(64) + + system.turbo(True) + result = system.get_user_firmware_pubkey(pubkey) + # print('system.get_user_firmware_pubkey() = {}'.format(result)) + # print(' len={} pubkey = {}'.format(len(pubkey), hexlify(pubkey))) + system.turbo(False) - # print("data=", data) - # The data can be a string or may already be a bytes object - data_buf = data if isinstance(data, bytes) else bytes(data, 'utf8') - # print("data_buf={}".format(data_buf)) - await sign_psbt_buf(data_buf) async def enter_passphrase(menu, label, item): + import sys + from seed import set_bip39_passphrase + from constants import MAX_PASSPHRASE_LENGTH + title = item.arg - passphrase = await ux_enter_text(title, label="Enter a Passphrase") + passphrase = await ux_enter_text(title, label="Enter a Passphrase", max_length=MAX_PASSPHRASE_LENGTH) + + # print("Chosen passphrase = {}".format(passphrase)) + + if not await ux_confirm('Are you sure you want to apply the passphrase:\n\n{}'.format(passphrase)): + return + + # Applying the passphrase takes a bit of time so show message + from common import dis + dis.fullscreen("Applying Passphrase...") + + system.show_busy_bar() + + result = None + try: + err = set_bip39_passphrase(passphrase) + + if err: + await ux_show_story('Unable to apply passphrase: {}'.format(err)) + else: + result = settings.get('xpub') + + except BaseException as exc: + sys.print_exception(exc) - print("Chosen passphrase = {}".format(passphrase)) + system.hide_busy_bar() async def enter_seed_phrase(menu, label, item): - from seed_phrase_ux import SeedEntryUX - seed_pharase_entry = SeedEntryUX(seed_len=item.arg) - await seed_pharase_entry.show() - print('seed words = {}'.format(seed_pharase_entry.words)) + from seed_entry_ux import SeedEntryUX + seed_phrase_entry = SeedEntryUX(seed_len=item.arg) + await seed_phrase_entry.show() + # print('seed words = {}'.format(seed_phrase_entry.words)) async def sample_stories(menu, label, item): result = await ux_show_story_sequence( @@ -1292,53 +1475,103 @@ async def sample_stories(menu, label, item): # TODO: Go back and reimplement this as a state machine like LoginUX async def validate_passport_hw(*a): from pincodes import PinAttempt + from ubinascii import unhexlify as a2b_hex + from common import system if settings.get('validated_ok'): return while True: - # Show a story with explanatory text regarding validation - result = await ux_show_story('''\ -First, let's make sure your Passport has not been tampered with during shipping. - -Navigate to: -foundationdevices.com/passport-validation + # Explain what validation is + result = await ux_show_story('''\ +Next, let's make sure your Passport has not been tampered with during shipping. -You will be presented with a validation page containing a QR code. +The setup guide will direct you to a page containing a QR code. On the next screen, scan that QR code. Your Passport will show you 4 Security Words in response. -Enter those 4 words in the same order into the web page and click the Validate button.''', title='Validation', left_btn='SHUTDOWN', scroll_label='MORE') - +Enter those 4 words in the same order into the validation page.''', + title='Validation', left_btn='SHUTDOWN', scroll_label='MORE') if result == 'y': while True: # Scan a QR code + system.turbo(True) qr_data = await ux_scan_qr_code('Validation') + # qr_data = 'af7e2098fd626650b342398905b82a73c835213dc1b4b1ca10f783d4e4593c95' + system.turbo(False) # Generate the 4 validation words - # 4 words gives 2048^4 possible combinations => 17,592,186,044,416 (~17.5 trillion) + # 4 words gives 2048 choose 4 possibilties = 730,862,190,080 if qr_data == None: while True: result = await ux_show_story('''No QR code was scanned. Do you want to try again, or skip validation?\n\nIMPORTANT: If you skip validation now, there will be no way to do so in the future.''', left_btn='SKIP', right_btn='RETRY') if result == 'x': # Confirm? - confirmed = await ux_confirm('Are you sure you want to permanently skip suoply chain validation?') + confirmed = await ux_confirm('Are you sure you want to permanently skip supply chain validation?') if confirmed: # 2 means validation was skipped, but we won't show it again settings.set('validated_ok', 2) - settings.save() return else: # Break out of the inner loop and back out the the QR validation scan loop break else: - words = PinAttempt.supply_chain_validation_words( - qr_data.encode()) - print(words) + + # Split the data up into the challenge and the signature + parts = qr_data.split(' ') + # print('parts={}'.format(parts)) + + if len(parts) != 2: + # print('ERROR: len={}'.format(len(parts))) + result = await ux_show_story('The QR code is not formatted correctly. Are you sure you are ' + + 'on the correct website?', + left_btn='SHUTDOWN', + right_btn='RETRY') + if result == 'x': + await ux_shutdown() + else: + # Retry in the outer loop - scan the QR code again + break + + system.turbo(True) + challenge = parts[0] + challenge_hash = bytearray(32) + system.sha256(challenge, challenge_hash) + # print('challenge: {}'.format(challenge)) + # print('challenge_hash: {}'.format(challenge_hash)) + + signature_str = parts[1] + signature = a2b_hex(signature_str) + # print('signature_str: {}'.format(signature_str)) + # print('signature: {}'.format(signature)) + system.turbo(False) + + # Let's make sure that this challenge was actually signed by the Foundation server + if system.verify_supply_chain_server_signature(challenge_hash, signature) == False: + result = await ux_show_story('The QR code you scanned does not appear to be from the ' + + 'Foundation Devices validation server.\n\nPlease ensure that you navigated to the correct website.', + left_btn='SHUTDOWN', + right_btn='RETRY') + if result == 'x': + await ux_shutdown() + else: + # Retry in the outer loop - scan the QR code again + break + + await ux_show_story('The QR code you scanned does not appear to be from the ' + + 'Foundation Devices validation server!\n\nPlease ensure ' + + 'that you navigated to the correct website.', 'Error', right_btn='OK') + return + + # print('CHALLENGE SIGNATURE IS VALID!!!!!') + + # The challenge was properly signed by the Foundation server, so now generate the response + words = PinAttempt.supply_chain_validation_words(a2b_hex(challenge)) + # print('Validation words={}'.format(words)) numbered_words = [] for i in range(len(words)): numbered_words.append('{}. {}'.format(i+1, words[i])) @@ -1364,7 +1597,6 @@ Enter those 4 words in the same order into the web page and click the Validate b elif result == 'y': # Note that they confirmed the validation words were correct settings.set('validated_ok', 1) - settings.save() return elif result == 'x': await ux_shutdown() @@ -1378,49 +1610,40 @@ async def test_ur(*a): async def test_ur_encoder(_1, _2, item): - await ux_show_text_as_ur(title='Test UR Encoding', msg='Animated UR Code', qr_text=b'Y\x01\x00\x91n\xc6\\\xf7|\xad\xf5\\\xd7\xf9\xcd\xa1\xa1\x03\x00&\xdd\xd4.\x90[w\xad\xc3nO-<\xcb\xa4O\x7f\x04\xf2\xdeD\xf4-\x84\xc3t\xa0\xe1I\x13o%\xb0\x18RTYa\xd5_\x7fz\x8c\xdem\x0e.\xc4?;-\xcbdJ"\t\xe8\xc9\xe3J\xf5\xc4ty\x84\xa5\xe8s\xc9\xcf_\x96^%\xee)\x03\x9f\xdf\x8c\xa7O\x1cv\x9f\xc0~\xb7\xeb\xae\xc4n\x06\x95\xae\xa6\xcb\xd6\x0b>\xc4\xbb\xff\x1b\x9f\xfe\x8a\x9er@\x12\x93w\xb9\xd3q\x1e\xd3\x8dA/\xbbDB%o\x1eoY^\x0f\xc5\x7f\xedE\x1f\xb0\xa0\x10\x1f\xb7k\x1f\xb1\xe1\xb8\x8c\xfd\xfd\xaa\x94b\x94\xa4}\xe8\xff\xf1s\xf0!\xc0\xe6\xf6[\x05\xc0\xa4\x94\xe5\x07\x91\'\n\x00P\xa7:\xe6\x9bg%PZ.\xc8\xa5y\x14W\xc9\x87m\xd3J\xad\xd1\x92\xa5:\xa0\xdcf\xb5V\xc0\xc2\x15\xc7\xce\xb8$\x8bq|"\x95\x1ee0[V\xa3pn>\x86\xeb\x01\xc8\x03\xbb\xf9\x15\xd8\x0e\xdc\xd6MM') + await ux_show_text_as_ur(title='Test UR Encoding', msg='Animated UR Code', qr_text=b'Y\x01\x00\x91n\xc6\\\xf7|\xad\xf5\\\xd7\xf9\xcd\xa1\xa1\x03\x00&\xdd\xd4.\x90[w\xad\xc3nO-<\xcb\xa4O\x7f\x04\xf2\xdeD\xf4-\x84\xc3t\xa0\xe1I\x13o%\xb0\x18RTYa\xd5_\x7fz\x8c\xdem\x0e.\xc4?;-\xcbdJ"\t\xe8\xc9\xe3J\xf5\xc4ty\x84\xa5\xe8s\xc9\xcf_\x96^%\xee)\x03\x9f\xdf\x8c\xa7O\x1cv\x9f\xc0~\xb7\xeb\xae\xc4n\x06\x95\xae\xa6\xcb\xd6\x0b>\xc4\xbb\xff\x1b\x9f\xfe\x8a\x9er@\x12\x93w\xb9\xd3q\x1e\xd3\x8dA/\xbbDB%o\x1eoY^\x0f\xc5\x7f\xedE\x1f\xb0\xa0\x10\x1f\xb7k\x1f\xb1\xe1\xb8\x8c\xfd\xfd\xaa\x94b\x94\xa4}\xe8\xff\xf1s\xf0!\xc0\xe6\xf6[\x05\xc0\xa4\x94\xe5\x07\x91\'\n\x00P\xa7:\xe6\x9bg%PZ.\xc8\xa5y\x14W\xc9\x87m\xd3J\xad\xd1\x92\xa5:\xa0\xdcf\xb5V\xc0\xc2\x15\xc7\xce\xb8$\x8bq|"\x95\x1ee0[V\xa3pn>\x86\xeb\x01\xc8\x03\xbb\xf9\x15\xd8\x0e\xdc\xd6MM', + qr_type=QRType.UR1, qr_args=None) +async def test_num_entry(*a): + num = await ux_enter_text('Enter Number', label='Enter an integer', num_only=True) + dis.fullscreen('Number = {}'.format(num)) + await sleep_ms(2000) async def play_snake(*a): from snake import snake_game await snake_game() -async def play_stacksats(*a): - from stacksats import stacksats_game - await stacksats_game() +async def play_stacking_sats(*a): + from stacking_sats import stacking_sats_game + await stacking_sats_game() # Secure Element Test Actions -async def se_get_version(*a): - version = bytearray(64) - system.dispatch(CMD_GET_BOOTLOADER_VERSION, version, 0) - print('version={}'.format(version)) - ver_str = version[:16].decode('utf8') - print('ver_str = {}'.format(ver_str)) - - # Hex format for the rest - import binascii - s = binascii.hexlify(version[16:]).decode('utf8') - lines = [s[i:i+16] for i in range(0, len(s), 16)] - data = '\n'.join(lines) - await ux_show_story("SE Version\n\n{}\n\n{}".format(ver_str, data)) - async def se_get_config(*a): config = bytearray(128) system.dispatch(CMD_GET_SE_CONFIG, config, 0) - import binascii - s = binascii.hexlify(config).decode('utf8') + import ubinascii + s = ubinascii.hexlify(config).decode('utf8') lines = [s[i:i+16] for i in range(0, len(s), 16)] data = '\n'.join(lines) await ux_show_story("SE Config\n\n" + data) -async def gen_random(*a): - from binascii import hexlify +async def gen_random(_1, _2, item): + from ubinascii import hexlify seed = bytearray(32) - valid = noise.random_bytes(seed) - print('Seed = {}'.format(hexlify(seed))) + valid = noise.random_bytes(seed, item.arg) + # print('Random bytes = {}'.format(hexlify(seed))) async def show_power_monitor(*a): from foundation import Powermon @@ -1428,24 +1651,15 @@ async def show_power_monitor(*a): for i in range(10): (current, voltage) = powermon.read() - print('current={} voltage={}'.format(current, voltage)) + # print('current={} voltage={}'.format(current, voltage)) await sleep_ms(1000) - async def show_board_rev(*a): from foundation import Boardrev boardrev = Boardrev() rev = boardrev.read() - print('Board rev={}'.format(rev)) - -async def factory_setup(*a): - system.dispatch(CMD_FACTORY_SETUP, None, 0); - -async def erase_rom_secrets(*a): - confirm = await ux_confirm('Are you sure you want to erase the ROM secrets?\n\nThis will UNPAIR your device from the current Secure Element chip!\n\nYou will need to insert a new chip to recover.') - if confirm: - system.erase_rom_secrets() + # print('Board rev={}'.format(rev)) async def erase_user_settings(*a): confirm = await ux_confirm('Are you sure you want to erase the User Settings?\n\nWhen restarting, you will be prompted to accept terms again and go through Supply Chain Authentication again.') @@ -1461,9 +1675,13 @@ async def coming_soon(*a): await ux_show_story('This feature is under development. Stay tuned!', title='Coming Soon') async def dump_settings(*a): - print('Current Settings:\n{}'.format(settings.curr_dict)) + print('Current Settings:\n{}'.format(to_str(settings.curr_dict))) -async def test_ur1(*a): +async def dump_flash_cache(*a): + from common import flash_cache + print('Current Flash Cache:\n{}'.format(to_str(flash_cache.current))) + +async def test_ur1_old(*a): from ur1.decode_ur import decode_ur from ur1.encode_ur import encode_ur @@ -1501,6 +1719,291 @@ async def test_ur1(*a): else: print('decode_ur() failed!') + +async def test_ur1(*a): + from ur1.decode_ur import decode_ur + from ur1.encode_ur import encode_ur + from ubinascii import unhexlify, hexlify + + # Encoding + data = '70736274ff01005e0200000001ec609ee4ba6cd94f9fc5aac5ec6a07cf15fa57079501ec866c77885ab8b1d44c0100000000ffffffff015802000000000000220020e62c3bf1cf1e7a78133c4ef8fb21a68b2f0a5519da30d742c4ae321180f5970200000000000100fd88010200000000010153a921bb59af10165684d1dbbb42bff6fedf1867517605319cd46d0a8e1490030000000000ffffffff029c0b000000000000220020e5f40bdb0b4938b97a940912062537bc9b717491c96a5fe949323328e8ae2cf3f1430000000000002200202e691b3a2c57d0b77d4979101dfd3bbd7737dfdf7702f3d4ce9e9d547a04be550400483045022100eacf00d9c626257a3267d4ef69fad45e4f4cefdafeee7825fa09833f39acc8ac02203628f2659b68fd4570b8c835c30225a9ef16d4f65ab7a81e26e61af50ed61e5a01473044022071ce9d40247ce983771e4db332795d92607c34a75e86427ecd936495ff5f32dd022033b162bb1204c72dfe700b965ab56ca1a5137907e2bddf06f7fa8149991849b801695221025c5d8a75673f9810802a54d387f73bcecab2ce327d2c7cb3d01e9423b81f7144210309296fc7ca56609d0bab5623bff5d315a9aaa0646a900598cc384b8f8b3f09ca210328adad6ad3627bdf5fa7562754c3ca8bf01ed742e086654848e595f43b2f14e053ae0000000001012bf1430000000000002200202e691b3a2c57d0b77d4979101dfd3bbd7737dfdf7702f3d4ce9e9d547a04be552202020696b21057f70b9476a75229c93428d0cddceaaac9488e4b2a1f37d24918fe384830450221009fb917bf041cb7e00ace7b57e40d0265ed32d09862b90a13be20db0bb6f65d22022023707aa2f23bde856b1954e7189d9695b6f983e11b0b18fedfe893eaaf76a1f901220203d9277a7c106434329ba12cb7e6a7e3059cd1d3ce675e49a6b998d8497940fc424730440220076e268c09acadf5b22872450463d40c41cb4a231b86c8375aca591a8b2eac23022050e497440a3fd3f6dded6c4b72d54dde1bd62bc3c456111c0696e6458c5236620101030401000000220603d9277a7c106434329ba12cb7e6a7e3059cd1d3ce675e49a6b998d8497940fc421c317184b630000080000000800000008002000080010000000000000022060234745d1d85a741aa0921cfd89fea4a3540c818d7599af30afb637c1960a4e9031c83bd41063000008000000080000000800200008001000000000000002206020696b21057f70b9476a75229c93428d0cddceaaac9488e4b2a1f37d24918fe381c1799c1ce3000008000000080000000800200008001000000000000000105695221020696b21057f70b9476a75229c93428d0cddceaaac9488e4b2a1f37d24918fe38210234745d1d85a741aa0921cfd89fea4a3540c818d7599af30afb637c1960a4e9032103d9277a7c106434329ba12cb7e6a7e3059cd1d3ce675e49a6b998d8497940fc4253ae0000' + data = hexlify(data) + + # self.qr_sizes = [500, 200, 60] + result = encode_ur(data, fragment_capacity=200) + if result == [ + 'ur:bytes/1of18/0p90t76gsdfeachgyjsh8plf6g34lepg2r6mk2ehc0ekvwtrtphq3w3c32/tyy9cdesxuenvv3hx3nxvvp3xqcr2efsxgcrqvpsxqcrqvt9vvmrqwt9v56xycfkvdjrjdrx89nxxdtpv93n2etrxesnqdmrvccn2enpx5mnqdeex5crzetr8qmrvcehxuursdtpvguxyvtyxs6xxvp3xqcrqvpsxqcrqenxvenxvenxvccrzdfcxqerqvpsxqcrqvps', + 'ur:bytes/2of18/0p90t76gsdfeachgyjsh8plf6g34lepg2r6mk2ehc0ekvwtrtphq3w3c32/xqcrqvpjxgcrqv3sv5mrycenvfnrzcmxx9jnwcfh8qcnxvmrx3jkvwrxvgerzcfk8p3rye3svy6n2vfev3snxvryxu6ryce5v9jnxv33xyurqe348ymnqv3sxqcrqvpsxqcrqvpsxycrqeny8qurqvfsxgcrqvpsxqcrqvpsxycrzdfnvyunyvtzvg6njctxxycrzd34', + 'ur:bytes/3of18/0p90t76gsdfeachgyjsh8plf6g34lepg2r6mk2ehc0ekvwtrtphq3w3c32/xcurgep3v33xyc35xf3xve3kvejkge338qmrwdf3xumrqdfnxyukxep5xejrqcfcv5cngwfsxqenqvpsxqcrqvpsxqcxvenxvenxvenxxqerjcesvgcrqvpsxqcrqvpsxqcrqv3jxqcryvr9x4nrgvrzv33rqc358yensc3exasnjdps8ycnyvpkxg6nxdmzvvukyde3', + 'ur:bytes/4of18/0p90t76gsdfeachgyjsh8plf6g34lepg2r6mk2ehc0ekvwtrtphq3w3c32/xu6rjvtr8ymxzdtxv5ungwfnxgenxv3cv5uxzefjvdnrxe33xsenqvpsxqcrqvpsxqcrqvpjxgcrqv3sxfjnvwf3vgekzvnrx5mkgvrzxumkgdpexuunzvp3v3nxgvmzvfjrwdenxajxverxxumnqvnxxdjrgcm989jnjep4xsmkzvp5vfjn2dfsxscrqdpcxvcrgdfs', + 'ur:bytes/5of18/0p90t76gsdfeachgyjsh8plf6g34lepg2r6mk2ehc0ekvwtrtphq3w3c32/xgerzvpsv4skxe3sxpjrjcekxgmrydfhvyenyd3hvs6x2e3k89nxzep5x4jnge35vdjkverpvejk2efh8qer2enpxqunsvenvcenjctrvvuxzcesxgerqvekxguxvv3kx5ukyd3cvejrgdfhxp3rscecxv6kxvesxger2cfev4nrzdnyx3nrvdtpvgmkzwp3v5ervefk', + 'ur:bytes/6of18/0p90t76gsdfeachgyjsh8plf6g34lepg2r6mk2ehc0ekvwtrtphq3w3c32/x9skvdfsv4jrvvt9x4snqvf5xuenqdp5xqeryvphx93k2wtyxscrydphvdjnjwpnxumnzef5v33rxvejxuun2epexgmrqdmrxv6xzde4v5urvdpjxajkxepexvmrgwf4venr2e3nxfjxgvpjxgcrxvmzxymrycnzxyerqdrrxuexgen9xucrqc3exc6kzc34xe3kzvtp', + 'ur:bytes/7of18/0p90t76gsdfeachgyjsh8plf6g34lepg2r6mk2ehc0ekvwtrtphq3w3c32/x5cnxdeexqmk2vnzv3jxvvpkvcmkvcfcxy6rjwfexyurgwtz8qcrzd3ex5eryvfsxg6kxdty8psnwdfkxuekvwfcxycrsvpjvy6ngepn8qmkvdenvf3k2cmpvgexxefnxgmkgvnrxa3kyvmyxqck2wf5xgekywp3vcmnzdp5xgcnqves8yerjdnxvvmkxcf4xcmrqwty', + 'ur:bytes/8of18/0p90t76gsdfeachgyjsh8plf6g34lepg2r6mk2ehc0ekvwtrtphq3w3c32/xp3xzc34xcerxcnxvc6kgve3x4snjctpvycrvdpkvyunqvp48yuxxcen8q6xywrx8p3rxe3s893kzv33xqenywrpv3skgdnpvsenvv3hvfjxvdtxvymn2d3jxu6ngcenvdsnscnxxqck2ephxsex2vpcxcmr2dpcxsux2dfex4nrgvmzxfnrzdr9xq6nxct9xqcrqvps', + 'ur:bytes/9of18/0p90t76gsdfeachgyjsh8plf6g34lepg2r6mk2ehc0ekvwtrtphq3w3c32/xqcrqvp3xqcnycnxxy6rxvpsxqcrqvpsxqcrqvpsxgerqvpjxqex2d3ex93rxcfjvv6nwepsvgmnwep58ymnjvfsx9jxvepnvf3xgdehxvmkgenyvcmnwvpjvcekgdrrv5uk2wtyx56rwcfsx33x2df4xgerqv3sxgcrvwfkvgerzvp4xanrwvrz8y6rwdnpxu6nyv3e', + 'ur:bytes/10of18/0p90t76gsdfeachgyjsh8plf6g34lepg2r6mk2ehc0ekvwtrtphq3w3c32/vvunxdpj8pjrqcmyv33k2ctpv93njdpc8pjngc3jvyckvvehvsergwf38pnx2vecxsurxvp5x5cryv33xqcrjenz8ycnwcnxxq6rzcmzxajnqvrpvdjnwc34xajngvryxqervdt9vsenyeps8yurvvnz8ycxzvfnvfjnyvryvgcxyc3kvcmr2epjxgcryv3sxgenwvph', + 'ur:bytes/11of18/0p90t76gsdfeachgyjsh8plf6g34lepg2r6mk2ehc0ekvwtrtphq3w3c32/v9snye3jxd3xgefcx5mxyvfex56x2de38qukgwfk8y6kydnx8yurxef3x93rqc338pnx2erxv5urjvm9v9skvdekvyckvwfsxyeryvpjxqekgwfjxumkzdmrxycrvdpnxsenywtzvycnycmzxajnvcfhv5enqdfevdjrzepnvdjnvde4v56rjcfkvgunjwry8q6rjdee', + 'ur:bytes/12of18/0p90t76gsdfeachgyjsh8plf6g34lepg2r6mk2ehc0ekvwtrtphq3w3c32/xscxvce5xg6rwvesxs6rqv3jxqcrwdn9xgmrsces89skxctyvc6kyv3j8qmnydp4xq6rvvmyxscxxdp3vd3rgcfjxvckywpkvvurxde4v93kzdfex9snsc3jv4skxv3nxqeryvp4xpjngwfhxs6rqcfnvejrxe3kv3jx2epkvv6xydejvs6ngeryv5ckyepkxf3xxvmr', + 'ur:bytes/13of18/0p90t76gsdfeachgyjsh8plf6g34lepg2r6mk2ehc0ekvwtrtphq3w3c32/xs6nvvf3x93nqd3exejnvdp48p3n2v3nxcmryvp3xqcnqvesxscrzvpsxqcrqvpjxgcrvvpnvsunydehvymkxvfsxc6rxdpnxgukycf3xf3kydm9xesnwefnxq6njcmyx9jrxcm9xcmn2ef589snvc3e8yuxgwp58ymnjdpsve3ngv33vvenzde38q6xyd3nxqcrqvps', + 'ur:bytes/14of18/0p90t76gsdfeachgyjsh8plf6g34lepg2r6mk2ehc0ekvwtrtphq3w3c32/8qcrqvpsxqcrqwpsxqcrqvpsxqurqvpjxqcrqvpcxqcrzvpsxqcrqvpsxqcrqvpsxqcryv3sxccryve5xu6r2ep3vsur2cfhxsckzcfs8yerzcmxvsurjen9vy6xzve4xscxxwp38pjrwdfe89skvvesv9nxyd3nxa3nzwfkxpsngefexqenzcecxd3xgdp3xqmrxvps', + 'ur:bytes/15of18/0p90t76gsdfeachgyjsh8plf6g34lepg2r6mk2ehc0ekvwtrtphq3w3c32/xqcrqwpsxqcrqvpsxqurqvpsxqcrqvpcxqcryvpsxqcrsvpsxycrqvpsxqcrqvpsxqcrqvpsxgerqd3sxgcrvwfkvgerzvp4xanrwvrz8y6rwdnpxu6nyv3evvunxdpj8pjrqcmyv33k2ctpv93njdpc8pjngc3jvyckvvehvsergwf38pnx2vecx93nzdee893nzcm9', + 'ur:bytes/16of18/0p90t76gsdfeachgyjsh8plf6g34lepg2r6mk2ehc0ekvwtrtphq3w3c32/xvcrqvpsxqurqvpsxqcrqvpcxqcrqvpsxqcrsvpsxgcrqvps8qcrqvfsxqcrqvpsxqcrqvpsxqcrqvp3xq6nvwf4xgerzvpjxqmrjdnzxgcnqdfhvcmnqc3exsmnvcfhx5erywtr8yengv3cvscxxeryvdjkzctpvvungwpcv56xyvnpx9nrxdmyxg6rjvfcvejnxwpj', + 'ur:bytes/17of18/0p90t76gsdfeachgyjsh8plf6g34lepg2r6mk2ehc0ekvwtrtphq3w3c32/xycryve5xu6r2ep3vsur2cfhxsckzcfs8yerzcmxvsurjen9vy6xzve4xscxxwp38pjrwdfe89skvvesv9nxyd3nxa3nzwfkxpsngefexqenyvfsxdjrjv3hxasnwce3xqmrgve5xverjcnpxyexxc3hv5mxzdm9xvcr2wtrvsckgvmrv5mrwdt9xsukzdnz8yunsepc', + 'ur:bytes/18of18/0p90t76gsdfeachgyjsh8plf6g34lepg2r6mk2ehc0ekvwtrtphq3w3c32/xsunwwf5xpnxxdpjx5ekzefsxqcrqyyqyag' + ]: + print('encode_ur() worked!') + else: + print('encode_ur() failed!') + print('result={}'.format(result)) + + + # Decoding + workloads = [ + 'ur:bytes/1of18/0p90t76gsdfeachgyjsh8plf6g34lepg2r6mk2ehc0ekvwtrtphq3w3c32/tyy9cdesxuenvv3hx3nxvvp3xqcr2efsxgcrqvpsxqcrqvt9vvmrqwt9v56xycfkvdjrjdrx89nxxdtpv93n2etrxesnqdmrvccn2enpx5mnqdeex5crzetr8qmrvcehxuursdtpvguxyvtyxs6xxvp3xqcrqvpsxqcrqenxvenxvenxvccrzdfcxqerqvpsxqcrqvps', + 'ur:bytes/2of18/0p90t76gsdfeachgyjsh8plf6g34lepg2r6mk2ehc0ekvwtrtphq3w3c32/xqcrqvpjxgcrqv3sv5mrycenvfnrzcmxx9jnwcfh8qcnxvmrx3jkvwrxvgerzcfk8p3rye3svy6n2vfev3snxvryxu6ryce5v9jnxv33xyurqe348ymnqv3sxqcrqvpsxqcrqvpsxycrqeny8qurqvfsxgcrqvpsxqcrqvpsxycrzdfnvyunyvtzvg6njctxxycrzd34', + 'ur:bytes/3of18/0p90t76gsdfeachgyjsh8plf6g34lepg2r6mk2ehc0ekvwtrtphq3w3c32/xcurgep3v33xyc35xf3xve3kvejkge338qmrwdf3xumrqdfnxyukxep5xejrqcfcv5cngwfsxqenqvpsxqcrqvpsxqcxvenxvenxvenxxqerjcesvgcrqvpsxqcrqvpsxqcrqv3jxqcryvr9x4nrgvrzv33rqc358yensc3exasnjdps8ycnyvpkxg6nxdmzvvukyde3', + 'ur:bytes/4of18/0p90t76gsdfeachgyjsh8plf6g34lepg2r6mk2ehc0ekvwtrtphq3w3c32/xu6rjvtr8ymxzdtxv5ungwfnxgenxv3cv5uxzefjvdnrxe33xsenqvpsxqcrqvpsxqcrqvpjxgcrqv3sxfjnvwf3vgekzvnrx5mkgvrzxumkgdpexuunzvp3v3nxgvmzvfjrwdenxajxverxxumnqvnxxdjrgcm989jnjep4xsmkzvp5vfjn2dfsxscrqdpcxvcrgdfs', + 'ur:bytes/5of18/0p90t76gsdfeachgyjsh8plf6g34lepg2r6mk2ehc0ekvwtrtphq3w3c32/xgerzvpsv4skxe3sxpjrjcekxgmrydfhvyenyd3hvs6x2e3k89nxzep5x4jnge35vdjkverpvejk2efh8qer2enpxqunsvenvcenjctrvvuxzcesxgerqvekxguxvv3kx5ukyd3cvejrgdfhxp3rscecxv6kxvesxger2cfev4nrzdnyx3nrvdtpvgmkzwp3v5ervefk', + 'ur:bytes/6of18/0p90t76gsdfeachgyjsh8plf6g34lepg2r6mk2ehc0ekvwtrtphq3w3c32/x9skvdfsv4jrvvt9x4snqvf5xuenqdp5xqeryvphx93k2wtyxscrydphvdjnjwpnxumnzef5v33rxvejxuun2epexgmrqdmrxv6xzde4v5urvdpjxajkxepexvmrgwf4venr2e3nxfjxgvpjxgcrxvmzxymrycnzxyerqdrrxuexgen9xucrqc3exc6kzc34xe3kzvtp', + 'ur:bytes/7of18/0p90t76gsdfeachgyjsh8plf6g34lepg2r6mk2ehc0ekvwtrtphq3w3c32/x5cnxdeexqmk2vnzv3jxvvpkvcmkvcfcxy6rjwfexyurgwtz8qcrzd3ex5eryvfsxg6kxdty8psnwdfkxuekvwfcxycrsvpjvy6ngepn8qmkvdenvf3k2cmpvgexxefnxgmkgvnrxa3kyvmyxqck2wf5xgekywp3vcmnzdp5xgcnqves8yerjdnxvvmkxcf4xcmrqwty', + 'ur:bytes/8of18/0p90t76gsdfeachgyjsh8plf6g34lepg2r6mk2ehc0ekvwtrtphq3w3c32/xp3xzc34xcerxcnxvc6kgve3x4snjctpvycrvdpkvyunqvp48yuxxcen8q6xywrx8p3rxe3s893kzv33xqenywrpv3skgdnpvsenvv3hvfjxvdtxvymn2d3jxu6ngcenvdsnscnxxqck2ephxsex2vpcxcmr2dpcxsux2dfex4nrgvmzxfnrzdr9xq6nxct9xqcrqvps', + 'ur:bytes/9of18/0p90t76gsdfeachgyjsh8plf6g34lepg2r6mk2ehc0ekvwtrtphq3w3c32/xqcrqvp3xqcnycnxxy6rxvpsxqcrqvpsxqcrqvpsxgerqvpjxqex2d3ex93rxcfjvv6nwepsvgmnwep58ymnjvfsx9jxvepnvf3xgdehxvmkgenyvcmnwvpjvcekgdrrv5uk2wtyx56rwcfsx33x2df4xgerqv3sxgcrvwfkvgerzvp4xanrwvrz8y6rwdnpxu6nyv3e', + 'ur:bytes/10of18/0p90t76gsdfeachgyjsh8plf6g34lepg2r6mk2ehc0ekvwtrtphq3w3c32/vvunxdpj8pjrqcmyv33k2ctpv93njdpc8pjngc3jvyckvvehvsergwf38pnx2vecxsurxvp5x5cryv33xqcrjenz8ycnwcnxxq6rzcmzxajnqvrpvdjnwc34xajngvryxqervdt9vsenyeps8yurvvnz8ycxzvfnvfjnyvryvgcxyc3kvcmr2epjxgcryv3sxgenwvph', + 'ur:bytes/11of18/0p90t76gsdfeachgyjsh8plf6g34lepg2r6mk2ehc0ekvwtrtphq3w3c32/v9snye3jxd3xgefcx5mxyvfex56x2de38qukgwfk8y6kydnx8yurxef3x93rqc338pnx2erxv5urjvm9v9skvdekvyckvwfsxyeryvpjxqekgwfjxumkzdmrxycrvdpnxsenywtzvycnycmzxajnvcfhv5enqdfevdjrzepnvdjnvde4v56rjcfkvgunjwry8q6rjdee', + 'ur:bytes/12of18/0p90t76gsdfeachgyjsh8plf6g34lepg2r6mk2ehc0ekvwtrtphq3w3c32/xscxvce5xg6rwvesxs6rqv3jxqcrwdn9xgmrsces89skxctyvc6kyv3j8qmnydp4xq6rvvmyxscxxdp3vd3rgcfjxvckywpkvvurxde4v93kzdfex9snsc3jv4skxv3nxqeryvp4xpjngwfhxs6rqcfnvejrxe3kv3jx2epkvv6xydejvs6ngeryv5ckyepkxf3xxvmr', + 'ur:bytes/13of18/0p90t76gsdfeachgyjsh8plf6g34lepg2r6mk2ehc0ekvwtrtphq3w3c32/xs6nvvf3x93nqd3exejnvdp48p3n2v3nxcmryvp3xqcnqvesxscrzvpsxqcrqvpjxgcrvvpnvsunydehvymkxvfsxc6rxdpnxgukycf3xf3kydm9xesnwefnxq6njcmyx9jrxcm9xcmn2ef589snvc3e8yuxgwp58ymnjdpsve3ngv33vvenzde38q6xyd3nxqcrqvps', + 'ur:bytes/14of18/0p90t76gsdfeachgyjsh8plf6g34lepg2r6mk2ehc0ekvwtrtphq3w3c32/8qcrqvpsxqcrqwpsxqcrqvpsxqurqvpjxqcrqvpcxqcrzvpsxqcrqvpsxqcrqvpsxqcryv3sxccryve5xu6r2ep3vsur2cfhxsckzcfs8yerzcmxvsurjen9vy6xzve4xscxxwp38pjrwdfe89skvvesv9nxyd3nxa3nzwfkxpsngefexqenzcecxd3xgdp3xqmrxvps', + 'ur:bytes/15of18/0p90t76gsdfeachgyjsh8plf6g34lepg2r6mk2ehc0ekvwtrtphq3w3c32/xqcrqwpsxqcrqvpsxqurqvpsxqcrqvpcxqcryvpsxqcrsvpsxycrqvpsxqcrqvpsxqcrqvpsxgerqd3sxgcrvwfkvgerzvp4xanrwvrz8y6rwdnpxu6nyv3evvunxdpj8pjrqcmyv33k2ctpv93njdpc8pjngc3jvyckvvehvsergwf38pnx2vecx93nzdee893nzcm9', + 'ur:bytes/16of18/0p90t76gsdfeachgyjsh8plf6g34lepg2r6mk2ehc0ekvwtrtphq3w3c32/xvcrqvpsxqurqvpsxqcrqvpcxqcrqvpsxqcrsvpsxgcrqvps8qcrqvfsxqcrqvpsxqcrqvpsxqcrqvp3xq6nvwf4xgerzvpjxqmrjdnzxgcnqdfhvcmnqc3exsmnvcfhx5erywtr8yengv3cvscxxeryvdjkzctpvvungwpcv56xyvnpx9nrxdmyxg6rjvfcvejnxwpj', + 'ur:bytes/17of18/0p90t76gsdfeachgyjsh8plf6g34lepg2r6mk2ehc0ekvwtrtphq3w3c32/xycryve5xu6r2ep3vsur2cfhxsckzcfs8yerzcmxvsurjen9vy6xzve4xscxxwp38pjrwdfe89skvvesv9nxyd3nxa3nzwfkxpsngefexqenyvfsxdjrjv3hxasnwce3xqmrgve5xverjcnpxyexxc3hv5mxzdm9xvcr2wtrvsckgvmrv5mrwdt9xsukzdnz8yunsepc', + 'ur:bytes/18of18/0p90t76gsdfeachgyjsh8plf6g34lepg2r6mk2ehc0ekvwtrtphq3w3c32/xsunwwf5xpnxxdpjx5ekzefsxqcrqyyqyag' + ] + encoded_data = decode_ur(workloads) + result = unhexlify(encoded_data).decode('utf-8') + + # print('test_random_part_order: result = '.format(result)) + if result == '70736274ff01005e0200000001ec609ee4ba6cd94f9fc5aac5ec6a07cf15fa57079501ec866c77885ab8b1d44c0100000000ffffffff015802000000000000220020e62c3bf1cf1e7a78133c4ef8fb21a68b2f0a5519da30d742c4ae321180f5970200000000000100fd88010200000000010153a921bb59af10165684d1dbbb42bff6fedf1867517605319cd46d0a8e1490030000000000ffffffff029c0b000000000000220020e5f40bdb0b4938b97a940912062537bc9b717491c96a5fe949323328e8ae2cf3f1430000000000002200202e691b3a2c57d0b77d4979101dfd3bbd7737dfdf7702f3d4ce9e9d547a04be550400483045022100eacf00d9c626257a3267d4ef69fad45e4f4cefdafeee7825fa09833f39acc8ac02203628f2659b68fd4570b8c835c30225a9ef16d4f65ab7a81e26e61af50ed61e5a01473044022071ce9d40247ce983771e4db332795d92607c34a75e86427ecd936495ff5f32dd022033b162bb1204c72dfe700b965ab56ca1a5137907e2bddf06f7fa8149991849b801695221025c5d8a75673f9810802a54d387f73bcecab2ce327d2c7cb3d01e9423b81f7144210309296fc7ca56609d0bab5623bff5d315a9aaa0646a900598cc384b8f8b3f09ca210328adad6ad3627bdf5fa7562754c3ca8bf01ed742e086654848e595f43b2f14e053ae0000000001012bf1430000000000002200202e691b3a2c57d0b77d4979101dfd3bbd7737dfdf7702f3d4ce9e9d547a04be552202020696b21057f70b9476a75229c93428d0cddceaaac9488e4b2a1f37d24918fe384830450221009fb917bf041cb7e00ace7b57e40d0265ed32d09862b90a13be20db0bb6f65d22022023707aa2f23bde856b1954e7189d9695b6f983e11b0b18fedfe893eaaf76a1f901220203d9277a7c106434329ba12cb7e6a7e3059cd1d3ce675e49a6b998d8497940fc424730440220076e268c09acadf5b22872450463d40c41cb4a231b86c8375aca591a8b2eac23022050e497440a3fd3f6dded6c4b72d54dde1bd62bc3c456111c0696e6458c5236620101030401000000220603d9277a7c106434329ba12cb7e6a7e3059cd1d3ce675e49a6b998d8497940fc421c317184b630000080000000800000008002000080010000000000000022060234745d1d85a741aa0921cfd89fea4a3540c818d7599af30afb637c1960a4e9031c83bd41063000008000000080000000800200008001000000000000002206020696b21057f70b9476a75229c93428d0cddceaaac9488e4b2a1f37d24918fe381c1799c1ce3000008000000080000000800200008001000000000000000105695221020696b21057f70b9476a75229c93428d0cddceaaac9488e4b2a1f37d24918fe38210234745d1d85a741aa0921cfd89fea4a3540c818d7599af30afb637c1960a4e9032103d9277a7c106434329ba12cb7e6a7e3059cd1d3ce675e49a6b998d8497940fc4253ae0000': + print('decode_ur() worked!') + else: + print('decode_ur() failed!') + print('result={}'.format(result)) + async def battery_mon(*a): from battery_mon import battery_mon await battery_mon() + +async def generate_settings_error(*a): + from settings import Settings + s = Settings() + s.load() + s.set('sats_highscore', 1234) + s.save() + +async def generate_settings_error2(*a): + from common import settings + + for i in range(100): + settings.set('test', i) + await settings.save() + await sleep_ms(100) + +async def toggle_demo(*a): + import stash + + import common + common.demo_active = not common.demo_active + +# Repeatedly fetch seed values to try to make it fail (sometimes it does) +async def test_fetch_seeds(*a): + good = 0 + bad = 0 + + for i in range(100): + try: + with stash.SensitiveValues() as sv: + assert sv.mode == 'words' # protected by menu item predicate + + words = trezorcrypto.bip39.from_data(sv.raw).split(' ') + + msg = 'Seed words (%d):\n' % len(words) + msg += '\n'.join('%2d: %s' % (i+1, w) for i, w in enumerate(words)) + + pw = stash.bip39_passphrase + if pw: + msg += '\n\nBIP39 Passphrase:\n%s' % stash.bip39_passphrase + + # print('msg={}'.format(msg)) + stash.blank_object(msg) + good += 1 + + except Exception as e: + bad +=1 + # print('Exception: {}'.format(e)) + # print('ERROR fetching words!') + + + # print('good={} bad={}'.format(good, bad)) + +async def read_ambient(*a): + for i in range(10): + level = system.read_ambient() + # print('Ambient level = {}'.format(level)) + + +async def test_seed_check(*a): + seed_words = ['oxygen', 'weapon', 'flee', 'kite', 'bid', 'video', 'coach', 'wish', + 'invest', 'river', 'vocal', 'sugar', 'help', 'delay', 'outer', 'cruise', + 'pupil', 'friend', 'disease', 'afraid', 'century', 'actor', 'another', 'impact'] + seed_check = SeedCheckUX(seed_words=seed_words) + result = await seed_check.show() + + # print('seed_check.is_check_valid = {}'.format(seed_check.is_check_valid)) + + +async def test_derive_addresses(*a): + import utime + import stash + import chains + from public_constants import AF_P2WPKH + from common import system + + n = 100 + chain = chains.current_chain() + + addrs = [] + path = "m/84'/0'/{account}'/{change}/{idx}" + + system.turbo(True) + start_time = utime.ticks_ms() + with stash.SensitiveValues() as sv: + for idx in range(n): + subpath = path.format(account=0, change=0, idx=idx) + node = sv.derive_path(subpath, register=False) + addr = chain.address(node, AF_P2WPKH) + addrs.append(addr) + # print("{} => {}".format(subpath, addr)) + + stash.blank_object(node) + end_time = utime.ticks_ms() + system.turbo(False) + + print('Elapsed = {} secs'.format( round(float(end_time - start_time) / 1000, 2) )) + + idx = 0 + for addr in addrs: + subpath = path.format(account=0, change=0, idx=idx) + print("{} => {}".format(subpath, addr)) + idx += 1 + + +# Test function only +async def supply_chain_challenge(*a): + from trezorcrypto import sha256 + from common import noise, system + from noise_source import NoiseSource + from ubinascii import hexlify + + # Make a challenge + challenge = bytearray(32) + noise.random_bytes(challenge, NoiseSource.ALL) + # print('challenge: {}'.format(hexlify(challenge).decode('utf-8'))) + + # Hash the challenge with slot 7 + response = bytearray(32) + if system.supply_chain_challenge(challenge, response) == False: + pass + # print('ERROR: Unable to complete supply chain challenge!') + else: + # This is the secret in the SE now + # NOTE: Padded at the end with zeros? + slot7 = b'\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01' + # slot7 = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + expected_response = bytearray(32) + system.hmac_sha256(slot7, challenge, expected_response) + + # print('expected_response: {}'.format(hexlify(expected_response).decode('utf-8'))) + # print('response: {}\n'.format(hexlify(response).decode('utf-8'))) + +async def test_write_flash_cache(*a): + from common import flash_cache, system + + system.turbo(True) + flash_cache.set('utxos', {'foo': 1234, 'bar': [1,2,3,4]}) + system.turbo(False) + +async def test_read_flash_cache(*a): + from common import flash_cache, system + system.turbo(True) + flash_cache.load() + + utxos = flash_cache.get('utxos', None) + # print('utxos={}'.format(utxos)) + system.turbo(False) + +async def toggle_screenshot_mode(*a): + import common + common.screenshot_mode_enabled = not common.screenshot_mode_enabled + # print('common.screenshot_mode_enabled={}'.format(common.screenshot_mode_enabled)) + +async def toggle_snapshot_mode(*a): + import common + common.snapshot_mode_enabled = not common.snapshot_mode_enabled + # print('common.snapshot_mode_enabled={}'.format(common.snapshot_mode_enabled)) + +async def toggle_battery_mon(*a): + import common + common.enable_battery_mon = not common.enable_battery_mon + # print('common.enable_battery_mon={}'.format(common.enable_battery_mon)) + +# Remove all account info and multisig info - TESTING ONLY +async def clear_accts(*a): + from common import settings + settings.remove('multisig') + settings.remove('accounts') + settings.remove('wallet_prog') + +async def test_folders(*a): + import uos + import os + from files import CardSlot, CardMissingError + from utils import get_backups_folder_path + + try: + with CardSlot() as card: + path = get_backups_folder_path() + try: + print('Creating backups') + uos.mkdir(path) + except Exception as e: + print('Backups folder already exists!') + pass + + fname = '{}/passport-backup-1.bin'.format(path) + with open(fname, 'wb') as fd: + fd.write('{ "acb": 123, "def": 456 }') + + except Exception as e: + print('Exception: {}'.format(e)) + +async def make_accounts_menu(menu, label, item): + from accounts import AllAccountsMenu + # List of all created accounts and ability to create a new account + rv = AllAccountsMenu.construct() + return AllAccountsMenu(rv, title=item.arg) + +async def reset_device(*a): + from common import system + system.reset() + +async def test_battery_calcs(*a): + from periodic import calc_battery_percent + + for voltage in range(2400,3101,10): + p = calc_battery_percent(0, voltage); # current is ignored for now + # print('voltage={} => {}%\n'.format(voltage, p)) + print('{},{}'.format(voltage, p)) + +async def clear_ovc(*a): + from history import OutptValueCache + from common import flash_cache + OutptValueCache.clear() diff --git a/ports/stm32/boards/Passport/modules/address_explorer.py b/ports/stm32/boards/Passport/modules/address_explorer.py deleted file mode 100644 index 2d6c2fb..0000000 --- a/ports/stm32/boards/Passport/modules/address_explorer.py +++ /dev/null @@ -1,231 +0,0 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. -# SPDX-License-Identifier: GPL-3.0-or-later -# -# SPDX-FileCopyrightText: 2018 Coinkite, Inc. -# SPDX-License-Identifier: GPL-3.0-only -# -# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard -# and is covered by GPLv3 license found in COPYING. -# -# address_explorer.py -# -# Address Explorer menu functionality -# -import chains -import stash -from actions import goto_top_menu -from display import FontTiny -from menu import MenuItem, MenuSystem, start_chooser -from public_constants import AFC_BECH32 -from ux import the_ux, ux_show_story - -SCREEN_CHAR_WIDTH = const(16) - - -async def choose_first_address(*a): - # Choose from a truncated list of index 0 common addresses, remember - # the last address the user selected and use it as the default - from common import settings, dis - chain = chains.current_chain() - - dis.fullscreen('Loading...') - - with stash.SensitiveValues() as sv: - - def truncate_address(addr): - # Truncates address to width of screen, replacing middle chars - middle = "-" - leftover = SCREEN_CHAR_WIDTH - len(middle) - start = addr[0:(leftover+1) // 2] - end = addr[len(addr) - (leftover // 2):] - return start + middle + end - - # Create list of choices (address_index_0, path, addr_fmt) - choices = [] - for name, path, addr_fmt in chains.CommonDerivations: - if '{coin_type}' in path: - path = path.replace('{coin_type}', str(chain.b44_cointype)) - subpath = path.format(account=0, change=0, idx=0) - node = sv.derive_path(subpath, register=False) - address = chain.address(node, addr_fmt) - choices.append((truncate_address(address), path, addr_fmt)) - - dis.progress_bar_show(len(choices) / len(chains.CommonDerivations)) - - stash.blank_object(node) - - picked = None - - async def clicked(_1, _2, item): - if picked is None: - picked = item.arg - the_ux.pop() - - items = [MenuItem(address, f=clicked, arg=i) for i, (address, path, addr_fmt) - in enumerate(choices)] - menu = MenuSystem(items, title='Address List') - menu.goto_idx(settings.get('axi', 0)) - the_ux.push(menu) - - await menu.interact() - - if picked is None: - return None - - # update last clicked address - settings.set('axi', picked) - address, path, addr_fmt = choices[picked] - - return (path, addr_fmt) - - -async def show_n_addresses(path, addr_fmt, start, n): - # Displays n addresses from start - from common import dis - import version - - def make_msg(start): - msg = '' - if start == 0: - msg = "Press 1 to save to MicroSD." - msg += '\n\n' - msg += "Addresses %d..%d:\n\n" % (start, start + n - 1) - - addrs = [] - chain = chains.current_chain() - - dis.fullscreen('Loading...') - - with stash.SensitiveValues() as sv: - - for idx in range(start, start + n): - subpath = path.format(account=0, change=0, idx=idx) - node = sv.derive_path(subpath, register=False) - addr = chain.address(node, addr_fmt) - addr1 = addr[:16] - addr2 = addr[16:] - addrs.append(addr) - - msg += "%s =>\n %s\n %s\n\n" % (subpath, addr1, addr2) - - dis.progress_bar_show(idx/n) - - stash.blank_object(node) - - msg += "Press 9 to see next group.\nPress 7 to see prev. group." - - return msg, addrs - - msg, addrs = make_msg(start) - - while 1: - ch = await ux_show_story(msg, right_btn='VIEW QR', font=FontTiny) - - if ch == '1': - # save addresses to microSD signal - await make_address_summary_file(path, addr_fmt) - # .. continue on same screen in case they want to write to multiple cards - - if ch == 'x': - return - - if ch == 'y': - from ux import show_qr_codes - await show_qr_codes(addrs, bool(addr_fmt & AFC_BECH32), start) - continue - - if ch == '7' and start > 0: - # go backwards in explorer - start -= n - elif ch == '9': - # go forwards - start += n - - msg, addrs = make_msg(start) - - -def generate_address_csv(path, addr_fmt, n): - # Produce CSV file contents as a generator - - yield '"Index","Payment Address","Derivation"\n' - - ch = chains.current_chain() - - with stash.SensitiveValues() as sv: - for idx in range(n): - subpath = path.format(account=0, change=0, idx=idx) - node = sv.derive_path(subpath, register=False) - - yield '%d,"%s","%s"\n' % (idx, ch.address(node, addr_fmt), subpath) - - stash.blank_object(node) - - -async def make_address_summary_file(path, addr_fmt, fname_pattern='addresses.txt'): - # write addresses into a text file on the microSD - from common import dis - from files import CardSlot, CardMissingError - from actions import needs_microsd - - # simple: always set number of addresses. - # - takes 60 seconds, to write 250 addresses on actual hardware - count = 250 - - dis.fullscreen('Saving 0-%d' % count) - - # generator function - body = generate_address_csv(path, addr_fmt, count) - - # pick filename and write - try: - with CardSlot() as card: - fname, nice = card.pick_filename(fname_pattern) - - # do actual write - with open(fname, 'wb') as fd: - for idx, part in enumerate(body): - fd.write(part.encode()) - - if idx % 5 == 0: - dis.progress_bar_show(idx / count) - - except CardMissingError: - await needs_microsd() - return - except Exception as e: - await ux_show_story('Failed to write!\n\n\n'+str(e)) - return - - msg = '''Address summary file written:\n\n%s''' % nice - await ux_show_story(msg) - - -async def address_explore(*a): - # explore addresses based on derivation path chosen - # by proxy external index=0 address - while 1: - ch = await ux_show_story('''\ -The following menu lists the first payment address \ -produced by various common wallet systems. - -Choose the address that your desktop or mobile wallet \ -has shown you as the first receive address. - -WARNING: Please understand that exceeding the gap limit \ -of your wallet, or choosing the wrong address on the next screen \ -may make it very difficult to recover your funds.''') - - if ch == 'y': - break - if ch == 'x': - return - - picked = await choose_first_address() - if picked is None: - return - - path, addr_fmt = picked - - await show_n_addresses(path, addr_fmt, 0, 10) - -# EOF diff --git a/ports/stm32/boards/Passport/modules/auth.py b/ports/stm32/boards/Passport/modules/auth.py index 6853876..0c34486 100644 --- a/ports/stm32/boards/Passport/modules/auth.py +++ b/ports/stm32/boards/Passport/modules/auth.py @@ -1,7 +1,7 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: GPL-3.0-or-later # -# SPDX-FileCopyrightText: 2018 Coinkite, Inc. +# SPDX-FileCopyrightText: 2018 Coinkite, Inc. # SPDX-License-Identifier: GPL-3.0-only # # (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard @@ -27,7 +27,7 @@ from public_constants import (AF_CLASSIC, AFC_BECH32, AFC_SCRIPT, MAX_TXN_LEN, SUPPORTED_ADDR_FORMATS) from sffile import SFFile from utils import HexWriter, cleanup_deriv_path, problem_file_line, xfp2str -from ux import (abort_and_goto, ux_dramatic_pause, ux_show_story, ux_show_story_sequence) +from ux import (abort_and_goto, ux_show_story, ux_show_story_sequence) # Where in SPI flash the two transactions are (in and out) TXN_INPUT_OFFSET = 0 @@ -43,14 +43,14 @@ class UserAuthorizedAction: self.result = None self.ux_done = False - def done(self): + async def done(self): # drop them back into menu system, but at top. self.ux_done = True from actions import goto_top_menu - m = goto_top_menu() + m = await goto_top_menu() m.show() - def pop_menu(self): + async def pop_menu(self): # drop them back into menu system, but try not to affect # menu position. self.ux_done = True @@ -60,7 +60,7 @@ class UserAuthorizedAction: if the_ux.top_of_stack() == self: empty = the_ux.pop() if empty: - goto_top_menu() + await goto_top_menu() restore_menu() @@ -72,15 +72,19 @@ class UserAuthorizedAction: async def failure(self, msg, exc=None, title='Failure'): self.failed = msg - self.done() + await self.done() if exc: - print("%s:" % msg) + # print("%s:" % msg) sys.print_exception(exc) - msg += "\n\n(%s)" % problem_file_line(exc) - from common import dis + exception_str = str(exc) + if exception_str: + msg += '\n\n' + msg += exception_str + msg += '\n\n' + msg += problem_file_line(exc) # may be a user-abort waiting, but we want to see error msg; so clear it # ux_clear_keys(True) @@ -90,18 +94,14 @@ class UserAuthorizedAction: # Confirmation text for user when signing text messages. # -MSG_SIG_TEMPLATE = '''\ -Ok to sign this? - --=-- +MSG_SIG_TEMPLATE = ''' {msg} --=-- Using the key associated with address: {subpath} => -{addr} - -Press Y if OK, otherwise X to cancel.''' +{addr}''' # RFC2440 style signatures, popular # since the genesis block, but not really part of any BIP as far as I know. @@ -118,22 +118,22 @@ RFC_SIGNATURE_TEMPLATE = '''\ def sign_message_digest(digest, subpath, prompt): # do the signature itself! - from common import dis + from common import dis, system if prompt: - dis.fullscreen(prompt, percent=.25) + dis.fullscreen(prompt, percent=25) with stash.SensitiveValues() as sv: - dis.progress_bar_show(.50) + system.progress_bar(50) node = sv.derive_path(subpath) pk = node.private_key() sv.register(pk) - dis.progress_bar_show(.75) + system.progress_bar(75) rv = trezorcrypto.secp256k1.sign(pk, digest) - dis.progress_bar_show(1) + system.progress_bar(100) return rv @@ -145,14 +145,14 @@ class ApproveMessageSign(UserAuthorizedAction): self.subpath = subpath self.approved_cb = approved_cb - from common import dis + from common import dis, system dis.fullscreen('Wait...') with stash.SensitiveValues() as sv: node = sv.derive_path(subpath) self.address = sv.chain.address(node, addr_fmt) - dis.progress_bar_show(1) + system.progress_bar(100) async def interact(self): # Prompt user w/ details and get approval @@ -160,7 +160,7 @@ class ApproveMessageSign(UserAuthorizedAction): story = MSG_SIG_TEMPLATE.format( msg=self.text, addr=self.address, subpath=self.subpath) - ch = await ux_show_story(story) + ch = await ux_show_story(story, title='Sign File', right_btn='SIGN') if ch != 'y': # they don't want to! @@ -179,9 +179,9 @@ class ApproveMessageSign(UserAuthorizedAction): if self.approved_cb: # don't kill menu depth for file case UserAuthorizedAction.cleanup() - self.pop_menu() + await self.pop_menu() else: - self.done() + await self.done() @staticmethod def validate(text): @@ -317,19 +317,18 @@ def sign_txt_file(filename): break except OSError as exc: - prob = 'Failed to write!\n\n%s\n\n' % exc + prob = 'Unable to write!\n\n%s\n\n' % exc sys.print_exception(exc) # fall thru to try again # prompt them to input another card? - ch = await ux_show_story(prob+"Please insert an SDCard to receive signed message, " - "and press OK.", title="Need Card") + ch = await ux_show_story(prob+"Please insert a microSD card to save signed message, ", title="Need Card") if ch == 'x': return - # done. - msg = "Created new file:\n\n%s" % out_fn - await ux_show_story(msg, title='File Signed') + # Done + msg = "Signed filename:\n\n%s" % out_fn + await ux_show_story(msg, title='File Signed', right_btn='DONE') # UserAuthorizedAction.check_busy() UserAuthorizedAction.active_request = ApproveMessageSign( @@ -345,7 +344,6 @@ class ApproveTransaction(UserAuthorizedAction): super().__init__() self.psbt_len = psbt_len self.do_finalize = bool(flags & STXN_FINALIZE) - self.do_visualize = bool(flags & STXN_VISUALIZE) self.stxn_flags = flags self.psbt = None self.psbt_sha = psbt_sha @@ -387,7 +385,7 @@ class ApproveTransaction(UserAuthorizedAction): msg.write('\n\nWarnings:') for label, m in self.psbt.warnings: msg.write('\n%s: %s\n' % (label, m)) - + return msg.getvalue() # ADDRESSES # 05 1f d9 76 c3 a1 e5 70 a4 a1 fe 8d b7 b0 c7 d5 02 70 9d 80 69 8f 5a 99 da @@ -397,188 +395,173 @@ class ApproveTransaction(UserAuthorizedAction): async def interact(self): # Prompt user w/ details and get approval - from common import dis + from common import dis, system # step 1: parse PSBT from sflash into in-memory objects. dis.fullscreen("Validating...") - if self.psbt == None: + try: + system.show_busy_bar() + + if self.psbt == None: + try: + # Read TXN from SPI Flash (we put it there whether it came from a QR code or an SD card) + with SFFile(TXN_INPUT_OFFSET, length=self.psbt_len) as fd: + self.psbt = psbtObject.read_psbt(fd) + except BaseException as exc: + system.hide_busy_bar() + if isinstance(exc, MemoryError): + msg = "Transaction is too complex" + exc = None + else: + msg = "PSBT parse failed" + + return await self.failure(msg, exc) + + # Do some analysis/validation try: - # Read TXN from SPI Flash (we put it there whether it came from a QR code or an SD card) - with SFFile(TXN_INPUT_OFFSET, length=self.psbt_len) as fd: - self.psbt = psbtObject.read_psbt(fd) + await self.psbt.validate() # might do UX: accept multisig import + self.psbt.consider_inputs() + self.psbt.consider_keys() + self.psbt.consider_outputs() + except FraudulentChangeOutput as exc: + system.hide_busy_bar() + print('FraudulentChangeOutput: ' + exc.args[0]) + return await self.failure(exc.args[0], title='Change Fraud') + except FatalPSBTIssue as exc: + system.hide_busy_bar() + print('FatalPSBTIssue: ' + exc.args[0]) + return await self.failure(exc.args[0]) except BaseException as exc: + system.hide_busy_bar() + del self.psbt + self.psbt = None + gc.collect() + if isinstance(exc, MemoryError): msg = "Transaction is too complex" exc = None else: - msg = "PSBT parse failed" + msg = "Invalid PSBT" return await self.failure(msg, exc) - # Do some analysis/validation - try: - await self.psbt.validate() # might do UX: accept multisig import - self.psbt.consider_inputs() - self.psbt.consider_keys() - self.psbt.consider_outputs() - except FraudulentChangeOutput as exc: - print('FraudulentChangeOutput: ' + exc.args[0]) - return await self.failure(exc.args[0], title='Change Fraud') - except FatalPSBTIssue as exc: - print('FatalPSBTIssue: ' + exc.args[0]) - return await self.failure(exc.args[0]) - except BaseException as exc: - del self.psbt - gc.collect() - - if isinstance(exc, MemoryError): - msg = "Transaction is too complex" - exc = None - else: - msg = "Invalid PSBT" + # step 2: figure out what we are approving, so we can get sign-off + # - outputs, amounts + # - fee + # + # notes: + # - try to handle lots of outputs + # - cannot calc fee as sat/byte, only as percent + # - somethings are 'warnings': + # - fee too big + # - inputs we can't sign (no key) + # + try: + outputs = uio.StringIO() + outputs.write('Amount:') - return await self.failure(msg, exc) + first = True + for idx, tx_out in self.psbt.output_iter(): + outp = self.psbt.outputs[idx] + if outp.is_change: + continue - # step 2: figure out what we are approving, so we can get sign-off - # - outputs, amounts - # - fee - # - # notes: - # - try to handle lots of outputs - # - cannot calc fee as sat/byte, only as percent - # - somethings are 'warnings': - # - fee too big - # - inputs we can't sign (no key) - # - try: - outputs = uio.StringIO() - outputs.write('Amount:') + if first: + first = False + else: + outputs.write('\n') - first = True - for idx, tx_out in self.psbt.output_iter(): - outp = self.psbt.outputs[idx] - if outp.is_change: - continue + outputs.write(self.render_output(tx_out)) if first: - first = False - else: - outputs.write('\n') - - outputs.write(self.render_output(tx_out)) - - # print('total_out={} total_in={} change={}'.format=(self.psbt.total_value_out, self.psbt.total_value_in, self.psbt.total_value_in - self.psbt.total_value_out)) - pages = [ - {'title': 'Sign Txn', 'msg': outputs.getvalue(), 'center': True, 'center_vertically': True}, - {'title': 'Sign Txn', 'msg': self.render_change_text(), 'center': True, 'center_vertically': True}, - ] - - warnings = self.render_warnings() - print('warnings = "{}"'.format(warnings)) - if warnings != None: - pages.append( - {'title': 'Sign Txn', 'msg': warnings, 'center': True, 'center_vertically': True, 'right_btn': 'SIGN!'} - ) - - if self.do_visualize: - # stop here and just return the text of approval message itself - self.result = await self.save_visualization(msg, (self.stxn_flags & STXN_SIGNED)) - del self.psbt - self.done() - - return - - result = await ux_show_story_sequence(pages) + # All outputs are change, so no amount is being "sent" to another wallet + outputs.write('\nNone') - except MemoryError: - # recovery? maybe. - try: - del self.psbt - del msg - except: - pass # might be NameError since we don't know how far we got - gc.collect() + # print('total_out={} total_in={} change={}'.format=(self.psbt.total_value_out, self.psbt.total_value_in, self.psbt.total_value_in - self.psbt.total_value_out)) + pages = [ + {'title': 'Sign Txn', 'msg': outputs.getvalue(), 'center': True, 'center_vertically': True}, + {'title': 'Sign Txn', 'msg': self.render_change_text(), 'center': True, 'center_vertically': True}, + ] - msg = "Transaction is too complex" - return await self.failure(msg) + warnings = self.render_warnings() + # print('warnings = "{}"'.format(warnings)) + if warnings != None: + pages.append( + {'title': 'Sign Txn', 'msg': warnings, 'center': True, 'center_vertically': True, 'right_btn': 'SIGN', 'clear_keys': True} + ) - if result != 'y': - # User chose not to sign the transaction - self.refused = True + # Stop busy bar to show info to user + system.hide_busy_bar() - # TODO: ux_confirm() instead? - # await ux_dramatic_pause("Refused.", 1) + result = await ux_show_story_sequence(pages) - del self.psbt + except MemoryError: + system.hide_busy_bar() - self.done() - return - - # do the actual signing. - try: - gc.collect() - self.psbt.sign_it() - except FraudulentChangeOutput as exc: - return await self.failure(exc.args[0], title='Change Fraud') - except MemoryError: - msg = "Transaction is too complex" - return await self.failure(msg) - except BaseException as exc: - return await self.failure("Signing failed late", exc) + # recovery? maybe. + try: + del self.psbt + self.psbt = None + del msg + except: + pass # might be NameError since we don't know how far we got + gc.collect() - if self.approved_cb: - # for micro sd case - await self.approved_cb(self.psbt) - self.done() - return + msg = "Transaction is too complex" + return await self.failure(msg) - try: - # re-serialize the PSBT back out - with SFFile(TXN_OUTPUT_OFFSET, max_size=MAX_TXN_LEN, message="Saving...") as fd: - await fd.erase() + if result != 'y': + # User chose not to sign the transaction + self.refused = True - if self.do_finalize: - self.psbt.finalize(fd) - else: - self.psbt.serialize(fd) + del self.psbt + self.psbt = None - self.result = (fd.tell(), fd.checksum.digest()) + await self.done() + return - self.done() + # do the actual signing. + try: + gc.collect() + self.psbt.sign_it() + except FraudulentChangeOutput as exc: + return await self.failure(exc.args[0], title='Change Fraud') + except MemoryError: + msg = "Transaction is too complex" + return await self.failure(msg) + except BaseException as exc: + return await self.failure("Signing failed late", exc) - except BaseException as exc: - return await self.failure("PSBT output failed", exc) + # print('---------------- SIGNING COMPLETE ----------------------') - # TODO: I don't think we're planning to support this, so consider removing it - def save_visualization(self, msg, sign_text=False): - # write text into spi flash, maybe signing it as we go - # - return length and checksum - txt_len = msg.seek(0, 2) - msg.seek(0) + if self.approved_cb: + # for micro sd case + await self.approved_cb(self.psbt) + await self.done() + return - chk = self.chain.hash_message(msg_len=txt_len) if sign_text else None + try: + system.show_busy_bar() + # re-serialize the PSBT back out + with SFFile(TXN_OUTPUT_OFFSET, max_size=MAX_TXN_LEN, message="Saving...") as fd: + await fd.erase() - with SFFile(TXN_OUTPUT_OFFSET, max_size=txt_len+300, message="Visualizing...") as fd: - await fd.erase() + if self.do_finalize: + self.psbt.finalize(fd) + else: + self.psbt.serialize(fd) - while 1: - blk = msg.read(256).encode('ascii') - if not blk: - break - if chk: - chk.update(blk) - fd.write(blk) + self.result = (fd.tell(), fd.checksum.digest()) - if chk: - from ubinascii import b2a_base64 - # append the signature - digest = trezorcrypto.sha256(chk.digest()).digest() - sig = sign_message_digest(digest, 'm', None) - fd.write(b2a_base64(sig).decode('ascii').strip()) - fd.write('\n') + await self.done() - return (fd.tell(), fd.checksum.digest()) + except BaseException as exc: + return await self.failure("PSBT output failed", exc) + finally: + system.hide_busy_bar() + finally: + system.hide_busy_bar() def render_change_text(self): # Produce text report of what the "change" outputs are (based on our opinion). @@ -588,14 +571,16 @@ class ApproveTransaction(UserAuthorizedAction): msg.write('Change Amount:') total = 0 addrs = [] + # print('len(outputs)={}'.format(len(self.psbt.outputs))) for idx, tx_out in self.psbt.output_iter(): outp = self.psbt.outputs[idx] if not outp.is_change: continue + # print('idx: {} output:{}'.format(idx, self.chain.render_address(tx_out.scriptPubKey))) total += tx_out.nValue addrs.append(self.chain.render_address(tx_out.scriptPubKey)) - if not addrs: + if len(addrs) == 0: msg.write('\nNo change') return msg.getvalue() @@ -702,19 +687,20 @@ def sign_transaction(psbt_len, flags=0x0, psbt_sha=None): def sign_psbt_file(filename): # sign a PSBT file found on a microSD card from files import CardSlot, CardMissingError - from common import dis - from sram4 import tmp_buf + from common import dis, system + # from sram4 import tmp_buf -- the fd.readinto() below doesn't work for some odd reason, even though the fd.readinto() for firmware updates + tmp_buf = bytearray(1024) from utils import HexStreamer, Base64Streamer, HexWriter, Base64Writer - UserAuthorizedAction.cleanup() - #print("sign: %s" % filename) + # print("sign: %s" % filename) # copy file into our spiflash # - can't work in-place on the card because we want to support writing out to different card # - accepts hex or base64 encoding, but binary prefered with CardSlot() as card: with open(filename, 'rb') as fd: + dis.fullscreen('Reading...') # see how long it is @@ -760,7 +746,7 @@ def sign_psbt_file(filename): out.write(here) total += len(here) - dis.progress_bar_show(total / psbt_len) + system.progress_bar((total * 100) // psbt_len) # might have been whitespace inflating initial estimate of PSBT size assert total <= psbt_len @@ -808,24 +794,23 @@ def sign_psbt_file(filename): if is_comp: # write out as hex too, if it's final - out2_full, out2_fn = card.pick_filename( - base+'-final.txn', out_path) + out2_full, out2_fn = card.pick_filename(base+'-final.txn', out_path) if out2_full: - with HexWriter(open(out2_full, 'wt')) as fd: + with HexWriter(open(out2_full, 'w+t')) as fd: # save transaction, in hex - psbt.finalize(fd) + txid = psbt.finalize(fd) # success and done! break except OSError as exc: - prob = 'Failed to write!\n\n%s\n\n' % exc + prob = 'Unable to write!\n\n%s\n\n' % exc sys.print_exception(exc) # fall thru to try again # prompt them to input another card? - ch = await ux_show_story(prob+"Please insert an SDCard to receive signed transaction, " - "and press OK.", title="Need Card") + ch = await ux_show_story(prob+"Please insert an microSD card to receive the signed transaction.", + title="Need Card") if ch == 'x': return @@ -834,6 +819,9 @@ def sign_psbt_file(filename): if out2_fn: msg += '\n\nFinalized transaction (ready for broadcast):\n\n%s' % out2_fn + if txid: + msg += '\n\nFinal TXID:\n'+txid + await ux_show_story(msg, title='PSBT Signed') UserAuthorizedAction.cleanup() @@ -844,10 +832,10 @@ def sign_psbt_file(filename): # kill any menu stack, and put our thing at the top abort_and_goto(UserAuthorizedAction.active_request) -def sign_psbt_buf(psbt_buf): +async def sign_psbt_buf(psbt_buf): # sign a PSBT file found on a microSD card from uio import BytesIO - from common import dis + from common import dis, system from sram4 import tmp_buf from utils import HexStreamer, Base64Streamer, HexWriter, Base64Writer @@ -867,32 +855,31 @@ def sign_psbt_buf(psbt_buf): fd.seek(0) if taste[0:5] == b'psbt\xff': - print('tastes like text PSBT') + # print('tastes like text PSBT') decoder = None def output_encoder(x): return x elif taste[0:10] == b'70736274ff': - print('tastes like binary PSBT') + # print('tastes like binary PSBT') decoder = HexStreamer() output_encoder = HexWriter psbt_len //= 2 elif taste[0:6] == b'cHNidP': - print('tastes like Base64 PSBT') + # print('tastes like Base64 PSBT') decoder = Base64Streamer() output_encoder = Base64Writer psbt_len = (psbt_len * 3 // 4) + 10 else: + print('Not a PSBT Transaction!') return total = 0 with SFFile(TXN_INPUT_OFFSET, max_size=psbt_len) as out: - print('sign 1') # blank flash await out.erase() - print('sign 2') while 1: n = fd.readinto(tmp_buf) - print('sign copy to SPI flash 1: n={}'.format(n)) + # print('sign copy to SPI flash 1: n={}'.format(n)) if not n: break @@ -909,348 +896,51 @@ def sign_psbt_buf(psbt_buf): out.write(here) total += len(here) - print('sign copy to SPI flash 2: {}/{} = {}'.format(total, psbt_len, total/psbt_len)) - dis.progress_bar_show(total / psbt_len) - - print('sign 3') + # print('sign copy to SPI flash 2: {}/{} = {}'.format(total, psbt_len, total/psbt_len)) + system.progress_bar((total * 100) // psbt_len) # might have been whitespace inflating initial estimate of PSBT size assert total <= psbt_len psbt_len = total - print('sign 4') # Create a new BytesIO() to hold the result async def done(psbt): - print('sign 5: done') + from common import system, last_scanned_qr_type + system.hide_busy_bar() signed_bytes = None with BytesIO() as bfd: with output_encoder(bfd) as fd: - print('sign 6: done') - if psbt.is_complete(): - print('sign 7: done') - psbt.finalize(fd) - print('sign 8: done') - else: - print('sign 9: done') - psbt.serialize(fd) - print('sign 10: done') - + # Always serialize back to PSBT for QR codes + psbt.serialize(fd) bfd.seek(0) signed_bytes = bfd.read() - print('signed_bytes={}'.format(signed_bytes)) - - print('sign 11: done') + # print('len(signed_bytes)={}'.format(len(signed_bytes))) + # print('signed_bytes={}'.format(signed_bytes)) gc.collect() from ur1.encode_ur import encode_ur - from ubinascii import hexlify - signed_str = hexlify(signed_bytes) - print('signed_str={}'.format(signed_str)) - from ux import DisplayURCode - o = DisplayURCode('Signed Txn', 'Scan to Wallet', signed_str) - await o.interact_bare() - + from data_codecs.qr_type import QRType + from ubinascii import hexlify as b2a_hex + + signed_hex = b2a_hex(signed_bytes) + # lines = [signed_hex[i:i+100] for i in range(0, len(signed_hex), 100)] + # print('signed_bytes hex') + # for line in lines: + # print(line) + + # print('last_scanned_qr_type={}'.format(last_scanned_qr_type)) + # print('len(signed_hex)={}'.format(len(signed_hex))) + o = DisplayURCode('Signed Txn', signed_hex, last_scanned_qr_type or QRType.UR2, None, is_binary=True) + result = await o.interact_bare() UserAuthorizedAction.cleanup() - print('sign 12: done') UserAuthorizedAction.active_request = ApproveTransaction(psbt_len, approved_cb=done) - print('sign 13: done') - - # kill any menu stack, and put our thing at the top - abort_and_goto(UserAuthorizedAction.active_request) - print('sign 14: done') - -async def sign_psbt_buf_OLD(psbt_buf): - # sign a PSBT string - from common import dis - from sram4 import tmp_buf - from utils import HexStreamer, Base64Streamer, HexWriter, Base64Writer - - UserAuthorizedAction.cleanup() - - # Determine encoding used - psbt_len = len(psbt_buf) - taste = psbt_buf[0:10] - print('sign_psbt_buf: 1') - if taste[0:5] == b'psbt\xff': - print('sign_psbt_buf: 2') - print("sign 1") - decoder = None - def output_encoder(x): return x - elif taste[0:10] == b'70736274ff': - print("sign 2") - decoder = HexStreamer() - output_encoder = HexWriter - psbt_len //= 2 - elif taste[0:6] == b'cHNidP': - print("sign 3") - decoder = Base64Streamer() - output_encoder = Base64Writer - psbt_len = (psbt_len * 3 // 4) + 10 - - print('sign_psbt_buf: 3') - - async def done(psbt): - if psbt.is_complete(): - psbt.finalize(fd) - else: - psbt.serialize(fd) - - ch = await ux_show_signed_transaction() - - await ux_show_story(msg, title='PSBT Signed') - - UserAuthorizedAction.cleanup() - - print('sign_psbt_buf: 4') - UserAuthorizedAction.active_request = ApproveTransaction( - psbt_len, approved_cb=done, psbt_buf=psbt_buf) - print('sign_psbt_buf: 5') - - # Kill any menu stack, and put our thing at the top - abort_and_goto(UserAuthorizedAction.active_request) - print('sign_psbt_buf: 6') - -class RemoteBackup(UserAuthorizedAction): - def __init__(self): - super().__init__() - # self.result ... will be (len, sha256) of the resulting file at zero - - async def interact(self): - try: - # Lead the user thru a complex UX. - from backups import make_complete_backup - - r = await make_complete_backup(write_sflash=True) - - if r: - # expect (length, sha) - self.result = r - else: - self.refused = True - - except BaseException as exc: - self.failed = "Error during backup process." - print("Backup failure: ") - sys.print_exception(exc) - finally: - self.done() - - -def start_remote_backup(): - # tell the local user the secret words, and then save to SPI flash - # USB caller has to come back and download encrypted contents. - - UserAuthorizedAction.cleanup() - UserAuthorizedAction.active_request = RemoteBackup() - - # kill any menu stack, and put our thing at the top - abort_and_goto(UserAuthorizedAction.active_request) - - -class NewPassphrase(UserAuthorizedAction): - def __init__(self, pw): - super().__init__() - self._pw = pw - # self.result ... will be (len, sha256) of the resulting file at zero - - async def interact(self): - # prompt them - from common import settings - - showit = False - while 1: - if showit: - ch = await ux_show_story('''Given:\n\n%s\n\nShould we switch to that wallet now? - -OK to continue, X to cancel.''' % self._pw, title="Passphrase") - else: - ch = await ux_show_story('''BIP39 passphrase (%d chars long) has been provided over USB connection. Should we switch to that wallet now? - -Press 2 to view the provided passphrase.\n\nOK to continue, X to cancel.''' % len(self._pw), title="Passphrase") - - if ch == '2': - showit = True - continue - break - - try: - if ch != 'y': - # User chose not to sign the transaction - self.refused = True - - # TODO: ux_confirm() instead? - # await ux_dramatic_pause("Refused.", 1) - else: - from seed import set_bip39_passphrase - - # full screen message shown: "Working..." - err = set_bip39_passphrase(self._pw) - - if err: - await self.failure(err) - else: - self.result = settings.get('xpub') - - except BaseException as exc: - self.failed = "Exception" - sys.print_exception(exc) - finally: - self.done() - - if self.result: - new_xfp = settings.get('xfp') - await ux_show_story('''Above is the master key fingerprint of the current wallet.''', - title="[%s]" % xfp2str(new_xfp)) - - -def start_bip39_passphrase(pw): - # tell the local user the secret words, and then save to SPI flash - # USB caller has to come back and download encrypted contents. - - UserAuthorizedAction.cleanup() - UserAuthorizedAction.active_request = NewPassphrase(pw) - - # kill any menu stack, and put our thing at the top - abort_and_goto(UserAuthorizedAction.active_request) - - -class ShowAddressBase(UserAuthorizedAction): - title = 'Address:' - - def __init__(self, *args): - super().__init__() - - from common import dis - dis.fullscreen('Wait...') - - # this must set self.address and do other slow setup - self.setup(*args) - - async def interact(self): - # Just show the address... no real confirmation needed. - from common import dis - - msg = self.get_msg() - msg += '\n\nCompare this payment address to the one shown on your other, less-trusted, software.' - msg += ' Press 4 to view QR Code.' - - # TODO: Add a menu button to view QR code? - while 1: - ch = await ux_show_story(msg, title=self.title) - - if ch == '4': - q = ux.QRDisplay( - [self.address], (self.addr_fmt & AFC_BECH32)) - await q.interact_bare() - continue - - break - - self.done() - UserAuthorizedAction.cleanup() # because no results to store - - -class ShowPKHAddress(ShowAddressBase): - - def setup(self, addr_fmt, subpath): - self.subpath = subpath - self.addr_fmt = addr_fmt - - with stash.SensitiveValues() as sv: - node = sv.derive_path(subpath) - self.address = sv.chain.address(node, addr_fmt) - - def get_msg(self): - return '''{addr}\n\n= {sp}''' .format(addr=self.address, sp=self.subpath) - - -class ShowP2SHAddress(ShowAddressBase): - - def setup(self, ms, addr_fmt, xfp_paths, witdeem_script): - - self.witdeem_script = witdeem_script - self.addr_fmt = addr_fmt - self.ms = ms - - # calculate all the pubkeys involved. - self.subpath_help = ms.validate_script( - witdeem_script, xfp_paths=xfp_paths) - - self.address = ms.chain.p2sh_address(addr_fmt, witdeem_script) - - def get_msg(self): - return '''\ -{addr} - -Wallet: - - {name} - {M} of {N} - -Paths: - -{sp}'''.format(addr=self.address, name=self.ms.name, - M=self.ms.M, N=self.ms.N, sp='\n\n'.join(self.subpath_help)) - - -def start_show_p2sh_address(M, N, addr_format, xfp_paths, witdeem_script): - # Show P2SH address to user, also returns it. - # - first need to find appropriate multisig wallet associated - # - they must provide full redeem script, and we will re-verify it and check pubkeys inside it - - import ustruct - from multisig import MultisigWallet, MultisigOutOfSpace - - try: - assert addr_format in SUPPORTED_ADDR_FORMATS - assert addr_format & AFC_SCRIPT - except: - raise AssertionError('Unknown/unsupported addr format') - - # Search for matching multisig wallet that we must already know about - xfps = [i[0] for i in xfp_paths] - - idx = MultisigWallet.find_match(M, N, xfps) - assert idx >= 0, 'Multisig wallet with those fingerprints not found' - - ms = MultisigWallet.get_by_idx(idx) - assert ms - assert ms.M == M - assert ms.N == N - - # UserAuthorizedAction.check_busy(ShowAddressBase) - UserAuthorizedAction.active_request = ShowP2SHAddress( - ms, addr_format, xfp_paths, witdeem_script) - - # kill any menu stack, and put our thing at the top - abort_and_goto(UserAuthorizedAction.active_request) - - # provide the value back to attached desktop - return UserAuthorizedAction.active_request.address - - -def start_show_address(addr_format, subpath): - try: - assert addr_format in SUPPORTED_ADDR_FORMATS - assert not (addr_format & AFC_SCRIPT) - except: - raise AssertionError('Unknown/unsupported addr format') - - # require a path to a key - subpath = cleanup_deriv_path(subpath) - - # serAuthorizedAction.check_busy(ShowAddressBase) - UserAuthorizedAction.active_request = ShowPKHAddress(addr_format, subpath) # kill any menu stack, and put our thing at the top abort_and_goto(UserAuthorizedAction.active_request) - # provide the value back to attached desktop - return UserAuthorizedAction.active_request.address - class NewEnrollRequest(UserAuthorizedAction): def __init__(self, ms, auto_export=False): @@ -1278,9 +968,6 @@ class NewEnrollRequest(UserAuthorizedAction): # User chose not to sign the transaction self.refused = True - # TODO: ux_confirm() instead? - # await ux_dramatic_pause("Refused.", 1) - except MultisigOutOfSpace: return await self.failure('No space left') except BaseException as exc: @@ -1288,13 +975,14 @@ class NewEnrollRequest(UserAuthorizedAction): sys.print_exception(exc) finally: UserAuthorizedAction.cleanup() # because no results to store - self.pop_menu() - + await self.pop_menu() -def maybe_enroll_xpub(sf_len=None, config=None, name=None, ux_reset=False): +def maybe_enroll_xpub(sf_len=None, config=None, name=None): # Offer to import (enroll) a new multisig wallet. Allow reject by user. from multisig import MultisigWallet + from common import system + system.turbo(True) UserAuthorizedAction.cleanup() if sf_len: @@ -1306,14 +994,10 @@ def maybe_enroll_xpub(sf_len=None, config=None, name=None, ux_reset=False): ms = MultisigWallet.from_file(config, name=name) UserAuthorizedAction.active_request = NewEnrollRequest(ms) + system.turbo(False) - if ux_reset: - # for USB case, and import from PSBT - # kill any menu stack, and put our thing at the top - abort_and_goto(UserAuthorizedAction.active_request) - else: - # menu item case: add to stack - from ux import the_ux - the_ux.push(UserAuthorizedAction.active_request) + # Add to stack + from ux import the_ux + the_ux.push(UserAuthorizedAction.active_request) # EOF diff --git a/ports/stm32/boards/Passport/modules/backups.py b/ports/stm32/boards/Passport/modules/backups.py deleted file mode 100644 index d63e010..0000000 --- a/ports/stm32/boards/Passport/modules/backups.py +++ /dev/null @@ -1,802 +0,0 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. -# SPDX-License-Identifier: GPL-3.0-or-later -# -# SPDX-FileCopyrightText: 2018 Coinkite, Inc. -# SPDX-License-Identifier: GPL-3.0-only -# -# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard -# and is covered by GPLv3 license found in COPYING. -# -# backups.py - Save and restore backup data. -# -import gc -import sys - -import chains -import compat7z -import seed -import stash -import trezorcrypto -import ujson -import version -from ubinascii import hexlify as b2a_hex -from ubinascii import unhexlify as a2b_hex -from uio import StringIO -from utils import imported, xfp2str -from ux import ux_confirm, ux_show_story -from common import noise - -# we make passwords with this number of words -num_pw_words = const(12) - -# max size we expect for a backup data file (encrypted or cleartext) -MAX_BACKUP_FILE_SIZE = const(10000) # bytes - - -def render_backup_contents(): - # simple text format: - # key = value - # or #comments - # but value is JSON - from common import settings, pa - - rv = StringIO() - - def COMMENT(val=None): - if val: - rv.write('\n# %s\n' % val) - else: - rv.write('\n') - - def ADD(key, val): - rv.write('%s = %s\n' % (key, ujson.dumps(val))) - - rv.write('# Passport backup file! DO NOT CHANGE.\n') - - chain = chains.current_chain() - - COMMENT('Private key details: ' + chain.name) - - with stash.SensitiveValues(for_backup=True) as sv: - - if sv.mode == 'words': - ADD('mnemonic', trezorcrypto.bip39.from_data(sv.raw)) - - if sv.mode == 'master': - ADD('bip32_master_key', b2a_hex(sv.raw)) - - ADD('chain', chain.ctype) - ADD('xprv', chain.serialize_private(sv.node)) - ADD('xpub', chain.serialize_public(sv.node)) - - # BTW: everything is really a duplicate of this value - ADD('raw_secret', b2a_hex(sv.secret).rstrip(b'0')) - - if pa.has_duress_pin(): - COMMENT('Duress Wallet (informational)') - dpk = sv.duress_root() - ADD('duress_xprv', chain.serialize_private(dpk)) - ADD('duress_xpub', chain.serialize_public(dpk)) - - # save the so-called long-secret - ADD('long_secret', b2a_hex(pa.ls_fetch())) - - COMMENT('Firmware version (informational)') - date, vers, timestamp = version.get_mpy_version()[0:3] - ADD('fw_date', date) - ADD('fw_version', vers) - ADD('fw_timestamp', timestamp) - ADD('serial', version.serial_number()) - - COMMENT('User preferences') - - # user preferences - for k, v in settings.current.items(): - if k[0] == '_': - continue # debug stuff in simulator - if k == 'xpub': - continue # redundant, and wrong if bip39pw - if k == 'xfp': - continue # redundant, and wrong if bip39pw - ADD('setting.' + k, v) - - import hsm - if hsm.hsm_policy_available(): - ADD('hsm_policy', hsm.capture_backup()) - - rv.write('\n# EOF\n') - - return rv.getvalue() - - -async def restore_from_dict(vals): - # Restore from a dict of values. Already JSON decoded. - # Reboot on success, return string on failure - from common import pa, dis, settings - from pincodes import SE_SECRET_LEN - - #print("Restoring from: %r" % vals) - - # step1: the private key - # - prefer raw_secret over other values - # - TODO: fail back to other values - try: - chain = chains.get_chain(vals.get('chain', 'BTC')) - - assert 'raw_secret' in vals - raw = bytearray(SE_SECRET_LEN) - rs = vals.pop('raw_secret') - if len(rs) % 2: - rs += '0' - x = a2b_hex(rs) - raw[0:len(x)] = x - - # check we can decode this right (might be different firmare) - opmode, bits, node = stash.SecretStash.decode(raw) - assert node - - # verify against xprv value (if we have it) - if 'xprv' in vals: - check_xprv = chain.serialize_private(node) - assert check_xprv == vals['xprv'], 'xprv mismatch' - - except Exception as e: - return ('Unable to decode raw_secret and ' - 'restore the seed value!\n\n\n'+str(e)) - - ls = None - if 'long_secret' in vals: - try: - ls = a2b_hex(vals.pop('long_secret')) - except Exception as exc: - sys.print_exception(exc) - # but keep going. - - dis.fullscreen("Saving...") - dis.progress_bar_show(.25) - - # clear (in-memory) settings and change also nvram key - # - also captures xfp, xpub at this point - pa.change(new_secret=raw) - - # force the right chain - pa.new_main_secret(raw, chain) # updates xfp/xpub - - # NOTE: don't fail after this point... they can muddle thru w/ just right seed - - if ls is not None: - try: - pa.ls_change(ls) - except Exception as exc: - sys.print_exception(exc) - # but keep going - - # restore settings from backup file - - for idx, k in enumerate(vals): - dis.progress_bar_show(idx / len(vals)) - if not k.startswith('setting.'): - continue - - if k == 'xfp' or k == 'xpub': - continue - - settings.set(k[8:], vals[k]) - - # write out - settings.save() - - if ('hsm_policy' in vals): - import hsm - hsm.restore_backup(vals['hsm_policy']) - - await ux_show_story('Everything has been successfully restored. ' - 'We must now reboot to install the ' - 'updated settings and/or seed.', title='Success!') - - from machine import reset - reset() - - -async def make_complete_backup(fname_pattern='backup.7z', write_sflash=False): - - # pick a password: like bip39 but no checksum word - # - b = bytearray(32) - while 1: - noise.random_bytes(b) - words = trezorcrypto.bip39.from_data(b).split(' ')[0:num_pw_words] - - ch = await seed.show_words(words, - prompt="Record this (%d word) backup file password:\n", escape='6') - - if ch == '6' and not write_sflash: - # Secret feature: plaintext mode - # - only safe for people living in faraday cages inside locked vaults. - if await ux_confirm("The file will **NOT** be encrypted and " - "anyone who finds the file will get all of your money for free!"): - words = [] - fname_pattern = 'backup.txt' - break - continue - - if ch == 'x': - return - - break - - if words: - # quiz them, but be nice and do a shorter test. - ch = await seed.word_quiz(words, limited=(num_pw_words//3)) - if ch == 'x': - return - - return await write_complete_backup(words, fname_pattern, write_sflash) - - -async def write_complete_backup(words, fname_pattern, write_sflash): - # Just do the writing - from common import dis, pa, settings - from files import CardSlot, CardMissingError - - # Show progress: - dis.fullscreen('Encrypting...' if words else 'Generating...') - body = render_backup_contents().encode() - - gc.collect() - - if words: - # NOTE: Takes a few seconds to do the key-streching, but little actual - # time to do the encryption. - - pw = ' '.join(words) - zz = compat7z.Builder(password=pw, progress_fcn=dis.progress_bar_show) - zz.add_data(body) - - hdr, footer = zz.save('passport-backup.txt') - - filesize = len(body) + MAX_BACKUP_FILE_SIZE - - del body - - gc.collect() - else: - # cleartext dump - zz = None - filesize = len(body)+10 - - if write_sflash: - # for use over USB and unit testing: commit file into SPI flash - from sffile import SFFile - - with SFFile(0, max_size=filesize, message='Saving...') as fd: - await fd.erase() - - if zz: - fd.write(hdr) - fd.write(zz.body) - fd.write(footer) - else: - fd.write(body) - - return fd.tell(), fd.checksum.digest() - - for copy in range(25): - # choose a filename - - try: - with CardSlot() as card: - fname, nice = card.pick_filename(fname_pattern) - - # do actual write - with open(fname, 'wb') as fd: - if zz: - fd.write(hdr) - fd.write(zz.body) - fd.write(footer) - else: - fd.write(body) - - except Exception as e: - # includes CardMissingError - import sys - sys.print_exception(e) - # catch any error - ch = await ux_show_story('Failed to write! Please insert formated microSD card, ' - 'and press OK to try again.\n\nX to cancel.\n\n\n'+str(e)) - if ch == 'x': - break - continue - - if copy == 0: - while 1: - msg = '''Backup file written:\n\n%s\n\n\ -To view or restore the file, you must have the full password.\n\n\ -Insert another SD card and press 2 to make another copy.''' % (nice) - - ch = await ux_show_story(msg, escape='2') - - if ch == 'y': - return - if ch == '2': - break - - else: - ch = await ux_show_story('''File (#%d) written:\n\n%s\n\n\ -Press OK for another copy, or press X to stop.''' % (copy+1, nice), escape='2') - if ch == 'x': - break - - -async def verify_backup_file(fname_or_fd): - # read 7z header, and measure checksums - # - no password is wanted/required - # - really just checking CRC32, but that's enough against truncated files - from files import CardSlot, CardMissingError - from actions import needs_microsd - prob = None - fd = None - - # filename already picked, open it. - try: - with CardSlot() as card: - prob = 'Unable to open backup file.' - fd = open(fname_or_fd, 'rb') if isinstance( - fname_or_fd, str) else fname_or_fd - - prob = 'Unable to read backup file headers. Might be truncated.' - compat7z.check_file_headers(fd) - - prob = 'Unable to verify backup file contents.' - zz = compat7z.Builder() - files = zz.verify_file_crc(fd, MAX_BACKUP_FILE_SIZE) - - assert len(files) == 1 - fname, fsize = files[0] - assert fname == 'passport-backup.txt' - assert 400 < fsize < MAX_BACKUP_FILE_SIZE, 'size' - - except CardMissingError: - await needs_microsd() - return - except Exception as e: - await ux_show_story(prob + '\n\nError: ' + str(e)) - return - finally: - if fd: - fd.close() - - await ux_show_story("Backup file CRC checks out okay.\n\nPlease note this is only a check against accidental truncation and similar. Targeted modifications can still pass this test.") - - -async def restore_complete(fname_or_fd): - from ux import the_ux - - async def done(words): - # remove all pw-picking from menu stack - seed.WordNestMenu.pop_all() - - prob = await restore_complete_doit(fname_or_fd, words) - - if prob: - await ux_show_story(prob, title='FAILED') - - # give them a menu to pick from, and start picking - m = seed.WordNestMenu(num_words=num_pw_words, - has_checksum=False, done_cb=done) - - the_ux.push(m) - - -async def restore_complete_doit(fname_or_fd, words): - # Open file, read it, maybe decrypt it; return string if any error - # - some errors will be shown, None return in that case - # - no return if successful (due to reboot) - from common import dis - from files import CardSlot, CardMissingError - from actions import needs_microsd - - # build password - password = ' '.join(words) - - prob = None - - try: - with CardSlot() as card: - # filename already picked, taste it and maybe consider using its data. - try: - fd = open(fname_or_fd, 'rb') if isinstance( - fname_or_fd, str) else fname_or_fd - except: - return 'Unable to open backup file.\n\n' + str(fname_or_fd) - - try: - if not words: - contents = fd.read() - else: - try: - compat7z.check_file_headers(fd) - except Exception as e: - return 'Unable to read backup file. Has it been touched?\n\nError: ' \ - + str(e) - - dis.fullscreen("Decrypting...") - try: - zz = compat7z.Builder() - fname, contents = zz.read_file(fd, password, MAX_BACKUP_FILE_SIZE, - progress_fcn=dis.progress_bar_show) - - # simple quick sanity checks - assert fname == 'passport-backup.txt' - assert contents[0:1] == b'#' and contents[-1:] == b'\n' - - except Exception as e: - # assume everything here is "password wrong" errors - #print("pw wrong? %s" % e) - - return ('Unable to decrypt backup file. Incorrect password?' - '\n\nTried:\n\n' + password) - finally: - fd.close() - except CardMissingError: - await needs_microsd() - return - - vals = {} - for line in contents.decode().split('\n'): - if not line: - continue - if line[0] == '#': - continue - - try: - k, v = line.split(' = ', 1) - #print("%s = %s" % (k, v)) - - vals[k] = ujson.loads(v) - except: - print("unable to decode line: %r" % line) - # but keep going! - - # this leads to reboot if it works, else errors shown, etc. - return await restore_from_dict(vals) - - -def generate_public_contents(): - # Generate public details about wallet. - # - # simple text format: - # key = value - # or #comments - # but value is JSON - from common import settings - from public_constants import AF_CLASSIC - - num_rx = 5 - - chain = chains.current_chain() - - with stash.SensitiveValues() as sv: - - yield ('''\ -# Coldcard Wallet Summary File -## For wallet with master key fingerprint: {xfp} - -Wallet operates on blockchain: {nb} - -For BIP44, this is coin_type '{ct}', and internally we use -symbol {sym} for this blockchain. - -## IMPORTANT WARNING - -Do **not** deposit to any address in this file unless you have a working -wallet system that is ready to handle the funds at that address! - -## Top-level, 'master' extended public key ('m/'): - -{xpub} - -What follows are derived public keys and payment addresses, as may -be needed for different systems. - - -'''.format(nb=chain.name, xpub=chain.serialize_public(sv.node), - sym=chain.ctype, ct=chain.b44_cointype, xfp=xfp2str(sv.node.my_fingerprint()))) - - for name, path, addr_fmt in chains.CommonDerivations: - - if '{coin_type}' in path: - path = path.replace('{coin_type}', str(chain.b44_cointype)) - - if '{' in name: - name = name.format(core_name=chain.core_name) - - show_slip132 = ('Core' not in name) - - yield ('''## For {name}: {path}\n\n'''.format(name=name, path=path)) - yield ('''First %d receive addresses (account=0, change=0):\n\n''' % num_rx) - - submaster = None - for i in range(num_rx): - subpath = path.format(account=0, change=0, idx=i) - - # find the prefix of the path that is hardneded - if "'" in subpath: - hard_sub = subpath.rsplit("'", 1)[0] + "'" - else: - hard_sub = 'm' - - if hard_sub != submaster: - # dump the xpub needed - - if submaster: - yield "\n" - - node = sv.derive_path(hard_sub, register=False) - yield ("%s => %s\n" % (hard_sub, chain.serialize_public(node))) - if show_slip132 and addr_fmt != AF_CLASSIC and (addr_fmt in chain.slip132): - yield ("%s => %s ##SLIP-132##\n" % ( - hard_sub, chain.serialize_public(node, addr_fmt))) - - submaster = hard_sub - node.blank() - del node - - # show the payment address - node = sv.derive_path(subpath, register=False) - yield ('%s => %s\n' % (subpath, chain.address(node, addr_fmt))) - - node.blank() - del node - - yield ('\n\n') - - from multisig import MultisigWallet - if MultisigWallet.exists(): - yield '\n# Your Multisig Wallets\n\n' - from uio import StringIO - - for ms in MultisigWallet.get_all(): - fp = StringIO() - - ms.render_export(fp) - print("\n---\n", file=fp) - - yield fp.getvalue() - del fp - - -async def write_text_file(fname_pattern, body, title, total_parts=72): - # - total_parts does need not be precise - from common import dis, pa, settings - from files import CardSlot, CardMissingError - from actions import needs_microsd - - # choose a filename - try: - with CardSlot() as card: - fname, nice = card.pick_filename(fname_pattern) - - # do actual write - with open(fname, 'wb') as fd: - for idx, part in enumerate(body): - dis.progress_bar_show(idx / total_parts) - fd.write(part.encode()) - - except CardMissingError: - await needs_microsd() - return - except Exception as e: - await ux_show_story('Failed to write!\n\n\n'+str(e)) - return - - msg = '''%s file written:\n\n%s''' % (title, nice) - await ux_show_story(msg) - - -async def make_summary_file(fname_pattern='public.txt'): - from common import dis - - # record **public** values and helpful data into a text file - dis.fullscreen('Generating...') - - # generator function: - body = generate_public_contents() - - await write_text_file(fname_pattern, body, 'Summary') - - -async def make_bitcoin_core_wallet(account_num=0, fname_pattern='bitcoin-core.txt'): - from common import dis, settings - import ustruct - xfp = xfp2str(settings.get('xfp')) - - dis.fullscreen('Generating...') - - # make the data - examples = [] - payload = ujson.dumps( - list(generate_bitcoin_core_wallet(examples, account_num))) - - body = '''\ -# Bitcoin Core Wallet Import File - -https://github.com/Coldcard/firmware/blob/master/docs/bitcoin-core-usage.md - -## For wallet with master key fingerprint: {xfp} - -Wallet operates on blockchain: {nb} - -## Bitcoin Core RPC - -The following command can be entered after opening Window -> Console -in Bitcoin Core, or using bitcoin-cli: - -importmulti '{payload}' - -## Resulting Addresses (first 3) - -'''.format(payload=payload, xfp=xfp, nb=chains.current_chain().name) - - body += '\n'.join('%s => %s' % t for t in examples) - - body += '\n' - - await write_text_file(fname_pattern, body, 'Bitcoin Core') - - -def generate_bitcoin_core_wallet(example_addrs, account_num): - # Generate the data for an RPC command to import keys into Bitcoin Core - # - yields dicts for json purposes - from descriptor import append_checksum - from common import settings - import ustruct - - from public_constants import AF_P2WPKH - - chain = chains.current_chain() - - derive = "84'/{coin_type}'/{account}'".format( - account=account_num, coin_type=chain.b44_cointype) - - with stash.SensitiveValues() as sv: - prefix = sv.derive_path(derive) - xpub = chain.serialize_public(prefix) - - for i in range(3): - sp = '0/%d' % i - node = sv.derive_path(sp, master=prefix) - a = chain.address(node, AF_P2WPKH) - example_addrs.append(('m/%s/%s' % (derive, sp), a)) - - xfp = settings.get('xfp') - txt_xfp = xfp2str(xfp).lower() - - chain = chains.current_chain() - - _, vers, _ = version.get_mpy_version() - - for internal in [False, True]: - desc = "wpkh([{fingerprint}/{derive}]{xpub}/{change}/*)".format( - derive=derive.replace("'", "h"), - fingerprint=txt_xfp, - coin_type=chain.b44_cointype, - account=0, - xpub=xpub, - change=(1 if internal else 0)) - - yield { - 'desc': append_checksum(desc), - 'range': [0, 1000], - 'timestamp': 'now', - 'internal': internal, - 'keypool': True, - 'watchonly': True - } - - -def generate_wasabi_wallet(): - # Generate the data for a JSON file which Wasabi can open directly as a new wallet. - from common import settings - import ustruct - import version - - # bitcoin (xpub) is used, even for testnet case (ie. no tpub) - # - altho, doesn't matter; the wallet operates based on it's own settings for test/mainnet - # regardless of the contents of the wallet file - btc = chains.BitcoinMain - - with stash.SensitiveValues() as sv: - xpub = btc.serialize_public(sv.derive_path("84'/0'/0'")) - - xfp = settings.get('xfp') - txt_xfp = xfp2str(xfp) - - chain = chains.current_chain() - assert chain.ctype in {'BTC', 'TBTC'}, "Only Bitcoin supported" - - _, vers, _ = version.get_mpy_version() - - return dict(MasterFingerprint=txt_xfp, - ColdCardFirmwareVersion=vers, - ExtPubKey=xpub) - - -def generate_electrum_wallet(addr_type, account_num=0): - # Generate line-by-line JSON details about wallet. - # - # Much reverse enginerring of Electrum here. It's a complex - # legacy file format. - from common import settings - from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH - - chain = chains.current_chain() - - xfp = settings.get('xfp') - - # Must get the derivation path, and the SLIP32 version bytes right! - if addr_type == AF_CLASSIC: - mode = 44 - elif addr_type == AF_P2WPKH: - mode = 84 - elif addr_type == AF_P2WPKH_P2SH: - mode = 49 - else: - raise ValueError(addr_type) - - derive = "m/{mode}'/{coin_type}'/{account}'".format(mode=mode, - account=account_num, coin_type=chain.b44_cointype) - - with stash.SensitiveValues() as sv: - top = chain.serialize_public(sv.derive_path(derive), addr_type) - - # most values are nicely defaulted, and for max forward compat, don't want to set - # anything more than I need to - - rv = dict(seed_version=17, use_encryption=False, wallet_type='standard') - - lab = 'Passport Import %s' % xfp2str(xfp) - if account_num: - lab += ' Acct#%d' % account_num - - # the important stuff. - rv['keystore'] = dict(ckcc_xfp=xfp, - ckcc_xpub=settings.get('xpub'), - hw_type='passport', type='hardware', - label=lab, derivation=derive, xpub=top) - - return rv - - -async def make_json_wallet(label, generator, fname_pattern='new-wallet.json'): - # Record **public** values and helpful data into a JSON file - - from common import dis, pa, settings - from files import CardSlot, CardMissingError - from actions import needs_microsd - - dis.fullscreen('Generating...') - - body = generator() - - # choose a filename - - try: - with CardSlot() as card: - fname, nice = card.pick_filename(fname_pattern) - - # do actual write - with open(fname, 'wt') as fd: - ujson.dump(body, fd) - - except CardMissingError: - await needs_microsd() - return - except Exception as e: - await ux_show_story('Failed to write!\n\n\n'+str(e)) - return - - msg = '''%s file written:\n\n%s''' % (label, nice) - await ux_show_story(msg) - -# EOF diff --git a/ports/stm32/boards/Passport/graphics/battery.pxd/QuickLook/Icon.tiff b/ports/stm32/boards/Passport/modules/battery.pxd/QuickLook/Icon.tiff similarity index 100% rename from ports/stm32/boards/Passport/graphics/battery.pxd/QuickLook/Icon.tiff rename to ports/stm32/boards/Passport/modules/battery.pxd/QuickLook/Icon.tiff diff --git a/ports/stm32/boards/Passport/graphics/battery.pxd/QuickLook/Thumbnail.tiff b/ports/stm32/boards/Passport/modules/battery.pxd/QuickLook/Thumbnail.tiff similarity index 100% rename from ports/stm32/boards/Passport/graphics/battery.pxd/QuickLook/Thumbnail.tiff rename to ports/stm32/boards/Passport/modules/battery.pxd/QuickLook/Thumbnail.tiff diff --git a/ports/stm32/boards/Passport/graphics/battery.pxd/data/086DBC0B-DCEF-4AA4-BBA9-464BB18375A5 b/ports/stm32/boards/Passport/modules/battery.pxd/data/086DBC0B-DCEF-4AA4-BBA9-464BB18375A5 similarity index 100% rename from ports/stm32/boards/Passport/graphics/battery.pxd/data/086DBC0B-DCEF-4AA4-BBA9-464BB18375A5 rename to ports/stm32/boards/Passport/modules/battery.pxd/data/086DBC0B-DCEF-4AA4-BBA9-464BB18375A5 diff --git a/ports/stm32/boards/Passport/graphics/battery.pxd/data/18FA2DFE-89BE-4617-A245-FB33EB5955E9 b/ports/stm32/boards/Passport/modules/battery.pxd/data/18FA2DFE-89BE-4617-A245-FB33EB5955E9 similarity index 100% rename from ports/stm32/boards/Passport/graphics/battery.pxd/data/18FA2DFE-89BE-4617-A245-FB33EB5955E9 rename to ports/stm32/boards/Passport/modules/battery.pxd/data/18FA2DFE-89BE-4617-A245-FB33EB5955E9 diff --git a/ports/stm32/boards/Passport/graphics/battery.pxd/data/382CF34A-D9D4-4DD0-A7D4-8982624BB2D4 b/ports/stm32/boards/Passport/modules/battery.pxd/data/382CF34A-D9D4-4DD0-A7D4-8982624BB2D4 similarity index 100% rename from ports/stm32/boards/Passport/graphics/battery.pxd/data/382CF34A-D9D4-4DD0-A7D4-8982624BB2D4 rename to ports/stm32/boards/Passport/modules/battery.pxd/data/382CF34A-D9D4-4DD0-A7D4-8982624BB2D4 diff --git a/ports/stm32/boards/Passport/graphics/battery.pxd/data/3A1928B0-6F6D-4934-ACCB-865C1942A171 b/ports/stm32/boards/Passport/modules/battery.pxd/data/3A1928B0-6F6D-4934-ACCB-865C1942A171 similarity index 100% rename from ports/stm32/boards/Passport/graphics/battery.pxd/data/3A1928B0-6F6D-4934-ACCB-865C1942A171 rename to ports/stm32/boards/Passport/modules/battery.pxd/data/3A1928B0-6F6D-4934-ACCB-865C1942A171 diff --git a/ports/stm32/boards/Passport/graphics/battery.pxd/data/41C0228F-C5B5-410D-A7D6-F3BF45F86D6B b/ports/stm32/boards/Passport/modules/battery.pxd/data/41C0228F-C5B5-410D-A7D6-F3BF45F86D6B similarity index 100% rename from ports/stm32/boards/Passport/graphics/battery.pxd/data/41C0228F-C5B5-410D-A7D6-F3BF45F86D6B rename to ports/stm32/boards/Passport/modules/battery.pxd/data/41C0228F-C5B5-410D-A7D6-F3BF45F86D6B diff --git a/ports/stm32/boards/Passport/graphics/battery.pxd/data/5407B7C2-FB5C-4F3B-8E83-20C02E4178D5 b/ports/stm32/boards/Passport/modules/battery.pxd/data/5407B7C2-FB5C-4F3B-8E83-20C02E4178D5 similarity index 100% rename from ports/stm32/boards/Passport/graphics/battery.pxd/data/5407B7C2-FB5C-4F3B-8E83-20C02E4178D5 rename to ports/stm32/boards/Passport/modules/battery.pxd/data/5407B7C2-FB5C-4F3B-8E83-20C02E4178D5 diff --git a/ports/stm32/boards/Passport/graphics/battery.pxd/data/6B9FA88C-D70C-48BD-B9FE-540F000D44FA b/ports/stm32/boards/Passport/modules/battery.pxd/data/6B9FA88C-D70C-48BD-B9FE-540F000D44FA similarity index 100% rename from ports/stm32/boards/Passport/graphics/battery.pxd/data/6B9FA88C-D70C-48BD-B9FE-540F000D44FA rename to ports/stm32/boards/Passport/modules/battery.pxd/data/6B9FA88C-D70C-48BD-B9FE-540F000D44FA diff --git a/ports/stm32/boards/Passport/graphics/battery.pxd/metadata.info b/ports/stm32/boards/Passport/modules/battery.pxd/metadata.info similarity index 100% rename from ports/stm32/boards/Passport/graphics/battery.pxd/metadata.info rename to ports/stm32/boards/Passport/modules/battery.pxd/metadata.info diff --git a/ports/stm32/boards/Passport/modules/battery_mon.py b/ports/stm32/boards/Passport/modules/battery_mon.py deleted file mode 100644 index 1fb13de..0000000 --- a/ports/stm32/boards/Passport/modules/battery_mon.py +++ /dev/null @@ -1,94 +0,0 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. -# SPDX-License-Identifier: GPL-3.0-or-later -# -from uasyncio import sleep_ms -from common import dis, noise -from display import Display, FontSmall -from settings import Settings -from ux import KeyInputHandler, ux_show_story -from files import CardSlot, CardMissingError -from foundation import Powermon -import utime - -UPDATE_RATE = 1000 # in ms - -def update(input, now, powermon, fd, prev_time, isRunning): - font = FontSmall - if (now - prev_time > UPDATE_RATE): - prev_time = now - - # Grab PWRMON_V, PWRMON_I - (current, voltage) = powermon.read() - voltage = round(voltage * (44.7 + 22.1) / 44.7) - print('time = {}, current={}, voltage={}'.format(now, current, voltage)) - if voltage < 2550: - isRunning = False - return None # make sure to exit before battery dies - - # Write to SD card - fd.write('{}, {}, {}\n'.format(now, current, voltage)) - # fd.write('{"now":{}, "current":{}, "voltage":{}},'.format(now, current, voltage)) - - dis.clear() - dis.draw_header() - dis.text(None, Display.HALF_HEIGHT - 3 * font.leading // 4 - 9, 'Time: {}'.format(now)) - dis.text(None, Display.HALF_HEIGHT - 9, 'Current: {}'.format(current)) - dis.text(None, Display.HALF_HEIGHT + 3 * font.leading // 4 - 9, 'Voltage: {}'.format(voltage)) - dis.draw_footer('BACK', '', input.is_pressed('x'), input.is_pressed('y')) - dis.show() - - return None - - - - -async def battery_mon(): - isRunning = True - input = KeyInputHandler(down='udplrxy', up='xy') - powermon = Powermon() - prev_time = 0 - (n1, _) = noise.read() - FILENAME = 'battery_mon_test_' + str(n1) + '.txt' - - while(True): - try: - with CardSlot() as card: - # fname, nice = card.pick_filename(fname_pattern) - fname = FILENAME - - # do actual write - with open(fname, 'wb') as fd: - print("writing to SD card...") - fd.write('Time, Current, Voltage\n') - while isRunning: - event = await input.get_event() - if event != None: - key, event_type = event - if event_type == 'up': - if key == 'x': - isRunning = False - - update(input, utime.ticks_ms(), powermon, fd, prev_time, isRunning) - - await sleep_ms(1) - - fd.close() - - break - - except Exception as e: - # includes CardMissingError - import sys - sys.print_exception(e) - # catch any error - ch = await ux_show_story('Failed to write! Please insert formated microSD card, ' - 'and press OK to try again.\n\nX to cancel.\n\n\n'+str(e)) - if ch == 'x': - break - continue - - return None - - - - \ No newline at end of file diff --git a/ports/stm32/boards/Passport/modules/bip39_utils.py b/ports/stm32/boards/Passport/modules/bip39_utils.py index 923065e..7d6e688 100644 --- a/ports/stm32/boards/Passport/modules/bip39_utils.py +++ b/ports/stm32/boards/Passport/modules/bip39_utils.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: GPL-3.0-or-later # # bip39_utils.py - Utility functions for working with BIP39 seed phrases @@ -43,12 +43,10 @@ def word_to_keypad_numbers(word): return result -# TODO: Simple linear search -- could replace with a binary search -def get_words_matching_prefix(prefix, max=5): +def get_words_matching_prefix(prefix, max=5, word_list='bip39'): + from foundation import bip39 - from foundation import bip39 - - bip = bip39() - matches = bip.get_words_matching_prefix(prefix, max) - - return matches.split(',') + # This actually handles bytewords too, depsite the name :( + bip = bip39() + matches = bip.get_words_matching_prefix(prefix, max, word_list) + return matches.split(',') diff --git a/ports/stm32/boards/Passport/modules/callgate.py b/ports/stm32/boards/Passport/modules/callgate.py index 0bc5436..ab6c5af 100644 --- a/ports/stm32/boards/Passport/modules/callgate.py +++ b/ports/stm32/boards/Passport/modules/callgate.py @@ -1,7 +1,7 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: GPL-3.0-or-later # -# SPDX-FileCopyrightText: 2018 Coinkite, Inc. +# SPDX-FileCopyrightText: 2018 Coinkite, Inc. # SPDX-License-Identifier: GPL-3.0-only # # (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard @@ -12,52 +12,6 @@ from se_commands import * from common import system -def get_bootloader_version(): - # version string and related details - # something like: ('1.0.0', [('time', '20180220.092345'), ('git', 'master@f8d1758')]) - rv = bytearray(64) - ln = system.dispatch(0, rv, 0) - ver, *args = str(rv[0:ln], 'utf8').split(' ') - return ver, [tuple(i.split('=', 1)) for i in args] - - -def get_firmware_hash(salt=0): - # salted hash over code - rv = bytearray(32) - system.dispatch(CMD_GET_FIRMWARE_HASH, rv, salt) - return rv - - -def enter_dfu(msg=0): - # enter DFU while showing a message - # 0 = normal DFU - # 1 = downgrade attack detected - # 2 = blankish - # 3 = i am bricked - # - system.dispatch(CMD_UPGRADE_FIRMWARE, msg) - - -def show_logout(dont_clear=0): - # wipe memory and die, shows standard message - # dont_clear=1 => don't clear OLED - # 2=> restart system after wipe - system.dispatch(CMD_RESET, dont_clear) - - -def get_genuine(): - system.dispatch(CMD_LED_CONTROL, None, LED_READ) - - -def clear_genuine(): - system.dispatch(CMD_LED_CONTROL, None, LED_RED) - - -def set_genuine(): - # does checksum over firmware, and might set green - return system.dispatch(CMD_LED_CONTROL, None, LED_ATTEMPT_TO_SET_GREEN) - - # Fill buf with random bytes def fill_random(buf): system.dispatch(CMD_GET_RANDOM_BYTES, buf, 0) @@ -68,17 +22,6 @@ def get_is_bricked(): return system.dispatch(CMD_IS_BRICKED, None, 0) != 0 -def get_firmware_highwater(): - arg = bytearray(8) - system.dispatch(CMD_FIRMWARE_CONTROL, arg, GET_MIN_FIRMWARE_VERSION) - return arg - - -def set_firmware_highwater(ts): - arg = bytearray(ts) - return system.dispatch(CMD_FIRMWARE_CONTROL, arg, UPDATE_HIGH_WATERMARK) - - def get_anti_phishing_words(pin_buf): return system.dispatch(CMD_GET_ANTI_PHISHING_WORDS, pin_buf, len(pin_buf)) diff --git a/ports/stm32/boards/Passport/modules/chains.py b/ports/stm32/boards/Passport/modules/chains.py index e348f5d..57bc125 100644 --- a/ports/stm32/boards/Passport/modules/chains.py +++ b/ports/stm32/boards/Passport/modules/chains.py @@ -1,7 +1,7 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: GPL-3.0-or-later # -# SPDX-FileCopyrightText: 2018 Coinkite, Inc. +# SPDX-FileCopyrightText: 2018 Coinkite, Inc. # SPDX-License-Identifier: GPL-3.0-only # # (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard @@ -41,10 +41,6 @@ class ChainsBase: # # but without high bit set - @classmethod - def msg_signing_prefix(cls): - return cls.name.encode() + b' Signed Message:\n' - @classmethod def msg_signing_prefix(cls): # see strMessageMagic ... but usually just the coin's name @@ -66,7 +62,7 @@ class ChainsBase: def deserialize_node(cls, text, addr_fmt): # xpub/xprv to object addr_fmt = AF_CLASSIC if addr_fmt == AF_P2SH else addr_fmt - return trezorcrypto.bip32.deserialize(text, cls.slip132[addr_fmt].pub, cls.slip132[addr_fmt].priv) + return trezorcrypto.bip32.deserialize(text, cls.slip132[addr_fmt].pub, True) @classmethod def p2sh_address(cls, addr_fmt, witdeem_script): diff --git a/ports/stm32/boards/Passport/modules/choosers.py b/ports/stm32/boards/Passport/modules/choosers.py index 907def1..c6fd8dc 100644 --- a/ports/stm32/boards/Passport/modules/choosers.py +++ b/ports/stm32/boards/Passport/modules/choosers.py @@ -1,7 +1,7 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: GPL-3.0-or-later # -# SPDX-FileCopyrightText: 2018 Coinkite, Inc. +# SPDX-FileCopyrightText: 2018 Coinkite, Inc. # SPDX-License-Identifier: GPL-3.0-only # # (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard @@ -12,126 +12,64 @@ from common import settings -def max_fee_chooser(): - from psbt import DEFAULT_MAX_FEE_PERCENTAGE - limit = settings.get('fee_limit', DEFAULT_MAX_FEE_PERCENTAGE) +def shutdown_timeout_chooser(): + DEFAULT_SHUTDOWN_TIMEOUT = (2*60) # 2 minutes - ch = ['No Limit', '10%', '25%', '50%'] - va = [-1, 10, 25, 50] + timeout = settings.get('shutdown_timeout', DEFAULT_SHUTDOWN_TIMEOUT) # in seconds - try: - which = va.index(limit) - except ValueError: - which = 0 - - def set(idx, text): - settings.set('fee_limit', va[idx]) - - return which, ch, set - - -def idle_timeout_chooser(): - from ux import DEFAULT_IDLE_TIMEOUT - - timeout = settings.get('idle_to', DEFAULT_IDLE_TIMEOUT) # in seconds - - ch = [' 5 minutes', - ' 15 minutes', - ' 30 minutes', - ' 60 minutes'] - va = [5*60, 15*60, 30*60, 60*60] - - try: - which = va.index(timeout) - except ValueError: - which = 0 - - def set_idle_timeout(idx, text): - settings.set('idle_to', va[idx]) - - return which, ch, set_idle_timeout - - -def countdown_chooser(): - # Login countdown length, stored in minutes - # - ch = ['Disabled', + ch = [' 1 minute', + ' 2 minutes', ' 5 minutes', '15 minutes', '30 minutes', - ' 1 hour', - ' 2 hours', - ' 4 hours', - ' 8 hours', - '12 hours', - '24 hours', - '48 hours', - ' 3 days', - ' 1 week', - '28 days later', - ] - va = [0, 5, 15, 30, 60, 2*60, 4*60, 8*60, 12 * - 60, 24*60, 48*60, 72*60, 7*24*60, 28*24*60] - assert len(ch) == len(va) + '60 minutes', + 'Never'] + va = [1*60, 2*60, 5*60, 15*60, 30*60, 60*60, 0] - timeout = settings.get('lgto', 0) # in minutes try: which = va.index(timeout) except ValueError: - which = 0 - - def set_login_countdown(idx, text): - settings.set('lgto', va[idx]) - - return which, ch, set_login_countdown - - -def chain_chooser(): - from chains import AllChains - - chain = settings.get('chain', 'BTC') - - ch = [(i.ctype, i.menu_name or i.name) for i in AllChains] - - # find index of current choice - try: - which = [n for n, (k, v) in enumerate(ch) if k == chain][0] - except IndexError: - which = 0 - - def set_chain(idx, text): - val = ch[idx][0] - assert ch[idx][1] == text - settings.set('chain', val) + which = 1 - try: - # update xpub stored in settings - import stash - with stash.SensitiveValues() as sv: - sv.capture_xpub() - except ValueError: - # no secrets yet, not an error - pass + def set_shutdown_timeout(idx, text): + settings.set('shutdown_timeout', va[idx]) - return which, [t for _, t in ch], set_chain + return which, ch, set_shutdown_timeout def brightness_chooser(): screen_brightness = settings.get('screen_brightness', 100) - ch = ['Off', '25%', '50%', '75%', '100%'] - va = [0, 25, 50, 75, 100] + ch = ['Off', '25%', '50%', '75%', '100%', 'Automatic'] + va = [0, 25, 50, 75, 100, 999] try: which = va.index(screen_brightness) except ValueError: - which = 100 + which = 4 def set(idx, text): from common import dis - settings.set('screen_brightness', va[idx]) dis.set_brightness(va[idx]) + settings.set('screen_brightness', va[idx]) return which, ch, set + +def enable_passphrase_chooser(): + # Should the Passphrase menu be enabled in the main menu? + ch = ['Disabled', 'Enabled'] + va = [False, True] + assert len(ch) == len(va) + + enable_passphrase = settings.get('enable_passphrase', False) + try: + which = va.index(enable_passphrase) + except ValueError: + which = 0 + + def set_enable_passphrase(idx, text): + settings.set('enable_passphrase', va[idx]) + + return which, ch, set_enable_passphrase # EOF diff --git a/ports/stm32/boards/Passport/modules/collections/deque.py b/ports/stm32/boards/Passport/modules/collections/deque.py index be868e1..45761d0 100644 --- a/ports/stm32/boards/Passport/modules/collections/deque.py +++ b/ports/stm32/boards/Passport/modules/collections/deque.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2018 Coinkite, Inc. +# SPDX-FileCopyrightText: 2018 Coinkite, Inc. # SPDX-License-Identifier: GPL-3.0-only # # (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard diff --git a/ports/stm32/boards/Passport/modules/common.py b/ports/stm32/boards/Passport/modules/common.py index 570fddf..a5ef831 100644 --- a/ports/stm32/boards/Passport/modules/common.py +++ b/ports/stm32/boards/Passport/modules/common.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: GPL-3.0-or-later # @@ -11,9 +11,12 @@ system = None # Keypad keypad = None -# Flash-based settings +# Internal flash-based settings settings = None +# External SPI flash cache +flash_cache = None + # Display dis = None @@ -28,4 +31,38 @@ pa = None sf = None # Avalanche noise source -noise = None \ No newline at end of file +noise = None + +# Battery level +battery_voltage = 0 +battery_level = 100 + +# Demo +demo_active = False +demo_count = 0 + +# Last time the user interacted (i.e., pressed/released any key) +import utime +last_activity_time = utime.ticks_ms() + +# Screenshot mode +screenshot_mode_enabled = False + +# Snapshot mode +snapshot_mode_enabled = False + +# Power monitor +powermon = None + +# Battery Monitor +enable_battery_mon = False + +# Active account +active_account = None + +# Multisig wallet to associate with New Account flow +new_multisig_wallet = None +is_new_wallet_a_duplicate = False + +# The QRTYpe of the last QR code that was scanned +last_scanned_qr_type = None diff --git a/ports/stm32/boards/Passport/modules/compat7z.py b/ports/stm32/boards/Passport/modules/compat7z.py index 9590540..673cedf 100644 --- a/ports/stm32/boards/Passport/modules/compat7z.py +++ b/ports/stm32/boards/Passport/modules/compat7z.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2018 Coinkite, Inc. +# SPDX-FileCopyrightText: 2018 Coinkite, Inc. # SPDX-License-Identifier: GPL-3.0-only # # (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard @@ -18,6 +18,7 @@ from ubinascii import crc32 from ustruct import unpack, pack, calcsize from ucollections import namedtuple from trezorcrypto import sha256 # uhashlib also works +import trezorcrypto from uio import BytesIO from common import noise @@ -25,8 +26,9 @@ def masked_crc(bits): return crc32(bits) & 0xffffffff def urandom(l): + from noise_source import NoiseSource rv = bytearray(l) - noise.random_bytes(rv) + noise.random_bytes(rv, NoiseSource.ALL) return rv def encode_utf_16_le(s): @@ -46,7 +48,7 @@ def decode_utf_16_le(s): ''' Size of encoding sequence depends from first byte: First_Byte Extra_Bytes Value - (binary) + (binary) 0xxxxxxx : ( xxxxxxx ) 10xxxxxx BYTE y[1] : ( xxxxxx << (8 * 1)) + y 110xxxxx BYTE y[2] : ( xxxxx << (8 * 2)) + y @@ -74,7 +76,7 @@ def read_var64(f): y = unpack("> pos) return (x << pos) + y - + def write_var64(n): # write their funky 64-bit variable-width unsigned number. # up to 64 bits of uint, but typically just single bytes @@ -100,7 +102,7 @@ def test_var64(): f = StringIO(write_var64(i)) assert read_var64(f) == i, '%d != %s' % (i, b2a_hex(f.getvalue())) ''' - + def check_file_headers(f): # read the file-header and the "first" other header # assume f is seekable @@ -113,7 +115,7 @@ def check_file_headers(f): sh = SectionHeader.read(f) if sh.actual_crc() != fh.crc: - print('act=%r expect=%r bits=%r' % (sh.actual_crc(), fh.crc, fh.bits)) + # print('act=%r expect=%r bits=%r' % (sh.actual_crc(), fh.crc, fh.bits)) raise ValueError("Second header has wrong CRC") if sh.size > 10000: @@ -128,7 +130,7 @@ def check_file_headers(f): if len(th) != sh.size: raise IndexError("Truncated file? %s" % e.message) - # Look for properties about compression. this could be + # Look for properties about compression. this could be # faked-out but good enough for now if b'\x24\x06\xf1\x07\x01' not in th: raise RuntimeError("Not marked as AES+SHA encrypted?") @@ -144,7 +146,7 @@ def check_file_headers(f): f.seek(0) return - + class FileHeader(object): def __init__(self): @@ -175,12 +177,12 @@ class FileHeader(object): def write(self): self.bits = self.magic + pack(' %r" % (fname, unpacked_size, shdr)) + # print("Section ok: '%s' of %d bytes => %r" % (fname, unpacked_size, shdr)) files.append((fname, unpacked_size)) @@ -312,7 +315,8 @@ class Builder(object): if not self.aes: # do this late, so easier to test w/ known values. #self.aes = AES.AESCipher(self.key, mode=AES.MODE_CBC, IV=self.iv) - self.aes = tcc.AES(tcc.AES.CBC | tcc.AES.Encrypt, self.key, self.iv) + # self.aes = tcc.AES(tcc.AES.CBC | tcc.AES.Encrypt, self.key, self.iv) + self.aes = trezorcrypto.aes(trezorcrypto.aes.CBC, self.key, self.iv) here = len(raw) self.pt_crc = crc32(raw, self.pt_crc) @@ -326,7 +330,7 @@ class Builder(object): self.unpacked_size += here assert len(raw) % 16 == 0, b2a_hex(raw) - self.body += self.aes.update(raw) + self.body += self.aes.encrypt(raw) def calculate_key(self, password, progress_fcn=None): @@ -344,8 +348,9 @@ class Builder(object): temp = pack(' +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: GPL-3.0-or-later # @@ -7,3 +7,35 @@ CAMERA_HEIGHT = 396 VIEWFINDER_WIDTH = 240 VIEWFINDER_HEIGHT = 240 + +# External SPI Flash constants + +# Must write with a multiple of this size +SPI_FLASH_PAGE_SIZE = 256 + +# Must erase with a multiple of these sizes +SPI_FLASH_SECTOR_SIZE = 4096 +SPI_FLASH_BLOCK_SIZE = 65536 +SPI_FLASH_TOTAL_SIZE = 2048 * 1024 + +# Flash cache +FLASH_CACHE_TOTAL_SIZE = 256 * 1024 +FLASH_CACHE_START = SPI_FLASH_TOTAL_SIZE - FLASH_CACHE_TOTAL_SIZE +FLASH_CACHE_END = SPI_FLASH_TOTAL_SIZE +FLASH_CACHE_BLOCK_SIZE = 16 * 1024 +FLASH_CACHE_CHECKSUM_SIZE = 32 +FLASH_CACHE_MAX_JSON_LEN = FLASH_CACHE_BLOCK_SIZE - FLASH_CACHE_CHECKSUM_SIZE + +# Flash usage for PSBT signing +PSBT_MAX_SIZE = (SPI_FLASH_TOTAL_SIZE - FLASH_CACHE_TOTAL_SIZE) // 2 + +# Flash firmware constants +FW_MAX_SIZE = SPI_FLASH_TOTAL_SIZE - FLASH_CACHE_TOTAL_SIZE +FW_HEADER_SIZE = 2048 +FW_ACTUAL_HEADER_SIZE = 170 # passport_firmware_header_t uses this many bytes + +MAX_PASSPHRASE_LENGTH = 64 +MAX_ACCOUNT_NAME_LEN = 20 +MAX_MULTISIG_NAME_LEN = 20 + +DEFAULT_ACCOUNT_ENTRY = {'name': 'Primary', 'acct_num': 0} diff --git a/ports/stm32/boards/Passport/modules/data_codecs/__init__.py b/ports/stm32/boards/Passport/modules/data_codecs/__init__.py new file mode 100644 index 0000000..3574f74 --- /dev/null +++ b/ports/stm32/boards/Passport/modules/data_codecs/__init__.py @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-License-Identifier: BSD-2-Clause-Patent +# diff --git a/ports/stm32/boards/Passport/modules/data_codecs/address_sampler.py b/ports/stm32/boards/Passport/modules/data_codecs/address_sampler.py new file mode 100644 index 0000000..e7055f7 --- /dev/null +++ b/ports/stm32/boards/Passport/modules/data_codecs/address_sampler.py @@ -0,0 +1,26 @@ +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# saddress_sampler.py +# +# Indicate if the given data is a Bitcoin address +# + +from .data_sampler import DataSampler + +class AddressSampler(DataSampler): + # Check if the given data looks like a Bitcoin address + @classmethod + def sample(cls, data): + try: + # TODO: Implement address sampler (not used yet though) + return False + except Exception as e: + # Lots of files could contain non-UTF-8 data, so we just ignore these errors as expected + return False + + # Number of bytes required to successfully recognize this format + @classmethod + def min_sample_size(cls): + # https://blog.hubspot.com/marketing/bitcoin-address#:~:text=Bitcoin%20Address%20Example,%E2%80%9D%2C%20or%20%E2%80%9Cbc1%E2%80%9D. + return 26 diff --git a/ports/stm32/boards/Passport/modules/data_codecs/data_decoder.py b/ports/stm32/boards/Passport/modules/data_codecs/data_decoder.py new file mode 100644 index 0000000..f9c0e14 --- /dev/null +++ b/ports/stm32/boards/Passport/modules/data_codecs/data_decoder.py @@ -0,0 +1,44 @@ +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# data_decoder.py +# +# Base class for all data decoders +# + +# Collects data segments, indicates when the data is complete, and decodes it to a common +# format for the specified data category +class DataDecoder: + def __init__(self): + pass + + # Decode the given data into the expected format + def add_data(self, data): + pass + + def received_parts(self): + return 0 + + def total_parts(self): + return 1 + + def is_complete(self): + return False + + # Return any error message if decoding or adding data failed for some reason + def get_error(self): + return None + + def get_type(self): + return None + + def decode(self): + pass + + # Return what type of data this is: + # - Multisig Quorum info + # - Spending transaction + # - Wallet seed + # - etc. + def get_data_format(self): + pass diff --git a/ports/stm32/boards/Passport/modules/data_codecs/data_encoder.py b/ports/stm32/boards/Passport/modules/data_codecs/data_encoder.py new file mode 100644 index 0000000..335ad46 --- /dev/null +++ b/ports/stm32/boards/Passport/modules/data_codecs/data_encoder.py @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# data_decoder.py +# +# Base class for all data decoders +# + +# Collects data segments, indicates when the data is complete, and decodes it to a common +# format for the specified data category +class DataEncoder: + def __init__(self): + pass + + def get_num_supported_sizes(self): + return 1 + + def get_max_len(self, index): + return 0 + + def encode(self, data, is_binary=False, max_fragment_len=None): + pass + + def next_part(self): + return None + + # Return any error message if decoding or adding data failed for some reason + def get_error(self): + return None diff --git a/ports/stm32/boards/Passport/modules/data_codecs/data_format.py b/ports/stm32/boards/Passport/modules/data_codecs/data_format.py new file mode 100644 index 0000000..31fbfe1 --- /dev/null +++ b/ports/stm32/boards/Passport/modules/data_codecs/data_format.py @@ -0,0 +1,36 @@ +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# data_format.py +# +# Simple types to act as an enums for all data formats that we read from file or QR code +# + +from .multisig_config_sampler import MultisigConfigSampler +from .psbt_txn_sampler import PsbtTxnSampler +from .seed_sampler import SeedSampler +from .address_sampler import AddressSampler +from .http_sampler import HttpSampler + +from actions import handle_psbt_data_format, handle_import_multisig_config, handle_seed_data_format #, handle_validate_address +from ie import handle_http + +class QRType: + QR = 0 # Standard QR code with no additional encoding + UR1 = 1 # UR 1.0 pre-standard from Blockchain Commons + UR2 = 2 # UR 2.0 standard from Blockchain Commons + + +samplers = [ + { 'sampler': PsbtTxnSampler, 'flow': handle_psbt_data_format }, + { 'sampler': MultisigConfigSampler, 'flow': handle_import_multisig_config }, + { 'sampler': SeedSampler, 'flow': handle_seed_data_format }, + { 'sampler': HttpSampler, 'flow': handle_http }, + # { 'sampler': AddressSampler, 'flow': handle_validate_address }, +] + +def get_flow_for_data(data, expected=None): + for entry in samplers: + if entry['sampler'].sample(data) == True: + return entry['flow'] + return None diff --git a/ports/stm32/boards/Passport/modules/data_codecs/data_sampler.py b/ports/stm32/boards/Passport/modules/data_codecs/data_sampler.py new file mode 100644 index 0000000..947bdb8 --- /dev/null +++ b/ports/stm32/boards/Passport/modules/data_codecs/data_sampler.py @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# data_sampler.py +# +# Base class for all data samplers +# + +# Determine if the provided data matches the format of the sampler. +class DataSampler: + # Check if the given bytes look like UR1 data + # Return True if it matches or False if not + @classmethod + def sample(cls, data): + pass + + # Number of bytes required to successfully recognize this format + @classmethod + def min_sample_size(cls): + return 1 diff --git a/ports/stm32/boards/Passport/modules/data_codecs/http_sampler.py b/ports/stm32/boards/Passport/modules/data_codecs/http_sampler.py new file mode 100644 index 0000000..fbf364c --- /dev/null +++ b/ports/stm32/boards/Passport/modules/data_codecs/http_sampler.py @@ -0,0 +1,28 @@ +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# http_sampler.py +# +# Indicate if the given data is an http URL (very basic!) +# + +from .data_sampler import DataSampler + +class HttpSampler(DataSampler): + # Check if the given data looks like a URL + @classmethod + def sample(cls, data): + try: + result = False + if isinstance(data, str): + data = data.lower() + result = data.startswith('http://') or data.startswith('https://') + return result + except Exception as e: + # Lots of files could contain non-UTF-8 data, so we just ignore these errors as expected + return False + + # Number of bytes required to successfully recognize this format + @classmethod + def min_sample_size(cls): + return 4 diff --git a/ports/stm32/boards/Passport/modules/data_codecs/multisig_config_sampler.py b/ports/stm32/boards/Passport/modules/data_codecs/multisig_config_sampler.py new file mode 100644 index 0000000..198a2a7 --- /dev/null +++ b/ports/stm32/boards/Passport/modules/data_codecs/multisig_config_sampler.py @@ -0,0 +1,28 @@ +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# multisig_config_sampler.py +# +# Sampler for Multisig configuration files +# + +import ure +from .data_sampler import DataSampler + +class MultisigConfigSampler(DataSampler): + # Check if the given bytes look like a multisig configuration file. + # Return True if it matches or False if not. + @classmethod + def sample(cls, data): + try: + return data.find(b'Name:') >= 0 and data.find(b'Policy:') >= 0 + except Exception as e: + # Lots of files could contain non-UTF-8 data, so we just ignore these errors as expected + return False + + # Number of bytes required to successfully recognize this format + # Zero means it potentially needs the entire file, but you can call + # sample() at any time to test the data. + @classmethod + def min_sample_size(cls): + return 0 diff --git a/ports/stm32/boards/Passport/modules/data_codecs/psbt_txn_sampler.py b/ports/stm32/boards/Passport/modules/data_codecs/psbt_txn_sampler.py new file mode 100644 index 0000000..01fd213 --- /dev/null +++ b/ports/stm32/boards/Passport/modules/data_codecs/psbt_txn_sampler.py @@ -0,0 +1,31 @@ +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# psbt_txn_sampler.py +# +# Sampler for PSBT formats +# + +from .data_sampler import DataSampler +from ubinascii import hexlify as b2a_hex + +class PsbtTxnSampler(DataSampler): + # Check if the given bytes look like a PSBT. + # We check binary, hex-encoded and base64-encoded since the PSBT code can handle all those. + # Return True if it matches or False if not. + @classmethod + def sample(cls, data): + print('psbt sampler: data={}'.format(b2a_hex(data))) + if data[0:5] == b'psbt\xff': + return True + if data[0:10] == b'70736274ff': # hex-encoded + return True + if data[0:6] == b'cHNidP': # base64-encoded + return True + + return False + + # Number of bytes required to successfully recognize this format + @classmethod + def min_sample_size(cls): + return 10 diff --git a/ports/stm32/boards/Passport/modules/data_codecs/qr_codec.py b/ports/stm32/boards/Passport/modules/data_codecs/qr_codec.py new file mode 100644 index 0000000..8b8444c --- /dev/null +++ b/ports/stm32/boards/Passport/modules/data_codecs/qr_codec.py @@ -0,0 +1,69 @@ +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# qr_decoder.py +# +# Basic QR codec +# + +from .data_encoder import DataEncoder +from .data_decoder import DataDecoder +from .data_sampler import DataSampler +from .qr_type import QRType + +class QRDecoder(DataDecoder): + def __init__(self): + self.data = None + + def add_data(self, data): + self.data = data + + def received_parts(self): + return 0 if self.data == None else 1 + + def total_parts(self): + return 1 + + def is_complete(self): + return self.data != None + + def get_error(self): + return None + + def decode(self): + return self.data + + def get_data_format(self): + return QRType.QR + +class QREncoder(DataEncoder): + def __init__(self, _args): + self.data = None + + def get_num_supported_sizes(self): + return 1 + + def get_max_len(self, index): + return 300 + + def encode(self, data, is_binary=False, max_fragment_len=None): + self.data = data + + def next_part(self): + return self.data + + def get_error(self): + return None + + + +class QRSampler(DataSampler): + # Any data can be accepted + @classmethod + def sample(cls, data): + return True + + # Number of bytes required to successfully recognize this format + @classmethod + def min_sample_size(cls): + return 1 diff --git a/ports/stm32/boards/Passport/modules/data_codecs/qr_factory.py b/ports/stm32/boards/Passport/modules/data_codecs/qr_factory.py new file mode 100644 index 0000000..f74f738 --- /dev/null +++ b/ports/stm32/boards/Passport/modules/data_codecs/qr_factory.py @@ -0,0 +1,41 @@ +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# qr_factory.py +# +# QR decoders +# + +from .qr_codec import QREncoder, QRDecoder, QRSampler +from .ur1_codec import UR1Encoder, UR1Decoder, UR1Sampler +from .ur2_codec import UR2Encoder, UR2Decoder, UR2Sampler +from .qr_type import QRType + +qrs = [ + { 'type': QRType.UR2, 'encoder': UR2Encoder, 'decoder': UR2Decoder, 'sampler': UR2Sampler}, + { 'type': QRType.UR1, 'encoder': UR1Encoder, 'decoder': UR1Decoder, 'sampler': UR1Sampler}, + { 'type': QRType.QR, 'encoder': QREncoder, 'decoder': QRDecoder, 'sampler': QRSampler}, +] + +def make_qr_encoder(qr_type, args): + for entry in qrs: + if entry['type'] == qr_type: + return entry['encoder'](args) + return None + +def make_qr_decoder(qr_type): + for entry in qrs: + if entry['type'] == qr_type: + return entry['decoder']() + return None + +# Given a data sample, return the QRType of it +def get_qr_type_for_data(data): + for entry in qrs: + if entry['sampler'].sample(data) == True: + return entry['type'] + return None + +def get_qr_decoder_for_data(data): + qr_type = get_qr_type_for_data(data) + return make_qr_decoder(qr_type) diff --git a/ports/stm32/boards/Passport/modules/data_codecs/qr_type.py b/ports/stm32/boards/Passport/modules/data_codecs/qr_type.py new file mode 100644 index 0000000..10294d3 --- /dev/null +++ b/ports/stm32/boards/Passport/modules/data_codecs/qr_type.py @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# qr_type.py +# +# QR types +# + +class QRType: + QR = 0 # Standard QR code with no additional encoding + UR1 = 1 # UR 1.0 pre-standard from Blockchain Commons + UR2 = 2 # UR 2.0 standard from Blockchain Commons diff --git a/ports/stm32/boards/Passport/modules/data_codecs/seed_sampler.py b/ports/stm32/boards/Passport/modules/data_codecs/seed_sampler.py new file mode 100644 index 0000000..622ca5d --- /dev/null +++ b/ports/stm32/boards/Passport/modules/data_codecs/seed_sampler.py @@ -0,0 +1,28 @@ +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# seed_sampler.py +# +# Sampler for PSBT formats +# + +from .data_sampler import DataSampler + +class SeedSampler(DataSampler): + # Check if the given bytes look like a seed. + # Return True if it matches or False if not. + @classmethod + def sample(cls, data): + try: + s = data.decode('utf-8') + words = s.split(' ') + num_words = len(words) + return num_words == 12 or num_words == 24 + except Exception as e: + # Lots of files could contain non-UTF-8 data, so we just ignore these errors as expected + return False + + # Number of bytes required to successfully recognize this format + @classmethod + def min_sample_size(cls): + return 0 diff --git a/ports/stm32/boards/Passport/modules/data_codecs/ur1_codec.py b/ports/stm32/boards/Passport/modules/data_codecs/ur1_codec.py new file mode 100644 index 0000000..f5b3aaf --- /dev/null +++ b/ports/stm32/boards/Passport/modules/data_codecs/ur1_codec.py @@ -0,0 +1,122 @@ +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# ur1_codec.py +# +# UR 1.0 codec +# +import re +from ubinascii import unhexlify + +from .data_encoder import DataEncoder +from .data_decoder import DataDecoder +from .data_sampler import DataSampler +from .qr_type import QRType +from ur1.decode_ur import decode_ur, extract_single_workload, Workloads +from ur1.encode_ur import encode_ur + +class UR1Decoder(DataDecoder): + def __init__(self): + self.workloads = Workloads() + self._received_parts = 0 + self._total_parts = 0; + self.error = None + + # Decode the given data into the expected format + def add_data(self, data): + try: + self.workloads.add(data) + self._received_parts, self._total_parts = self.workloads.get_progress() + return True + except Exception as e: + self.error = '{}'.format(e) + return False + + def received_parts(self): + return self._received_parts + + def total_parts(self): + return self._total_parts + + def is_complete(self): + return self.workloads.is_complete() + + def get_error(self): + return self.error + + def get_type(self): + return self.decoder.expected_type() + + def decode(self): + from common import system + try: + system.show_busy_bar() + encoded_data = decode_ur(self.workloads.workloads) + system.hide_busy_bar() + # print('UR1: encoded_data={}'.format(encoded_data)) + data = unhexlify(encoded_data) # TODO: Should this be optional (e.g., PSBT in binary)? + # print('UR1: data={}'.format(data)) + return data + except Exception as e: + self.error = '{}'.format(e) + print('UR1Decoder.decode() ERROR: {}'.format(e)) + return None + + def get_data_format(self): + return QRType.UR1 + +class UR1Encoder(DataEncoder): + def __init__(self, _args): + self.parts = [] + self.next_index = 0 + self.qr_sizes = [500, 200, 60] + + def get_num_supported_sizes(self): + return len(self.qr_sizes) + + def get_max_len(self, index): + if index < 0 or index >= len(self.qr_sizes): + return 0 + return self.qr_sizes[index] + + # Encode the given data + def encode(self, data, is_binary=False, max_fragment_len=500): + from ubinascii import hexlify + + # Convert from + if isinstance(data, str): + data = data.encode('utf8') + + if not is_binary: + data = hexlify(data) + # print('UR1: hex data={}'.format(data)) + data = data.decode('utf8') + + # print('UR1: data={}'.format(data)) + + self.parts = encode_ur(data, fragment_capacity=max_fragment_len) + + def next_part(self): + from utils import to_str + part = self.parts[self.next_index] + self.next_index = (self.next_index + 1) % len(self.parts) + # print('UR1: part={}'.format(to_str(part))) + return part.upper() + + # Return any error info + def get_error(self): + return None + +class UR1Sampler(DataSampler): + # Check if the given bytes look like UR1 data + # Return True if it matches or False if not + @classmethod + def sample(cls, data): + r = re.compile('^ur:bytes/(\d)+of(\d)+/') + m = r.match(data.lower()) + return m != None + + # Number of bytes required to successfully recognize this format + @classmethod + def min_sample_size(cls): + return 20 diff --git a/ports/stm32/boards/Passport/modules/data_codecs/ur2_codec.py b/ports/stm32/boards/Passport/modules/data_codecs/ur2_codec.py new file mode 100644 index 0000000..febbb80 --- /dev/null +++ b/ports/stm32/boards/Passport/modules/data_codecs/ur2_codec.py @@ -0,0 +1,114 @@ +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# ur2_codec.py +# +# UR 2.0 Codec +# +import math +import re + +from .data_encoder import DataEncoder +from .data_decoder import DataDecoder +from .data_sampler import DataSampler +from .qr_type import QRType + +from ur2.ur_decoder import URDecoder +from ur2.ur_encoder import UREncoder + +from ur2.cbor_lite import CBORDecoder +from ur2.cbor_lite import CBOREncoder + +from ur2.ur import UR +from utils import to_str + +class UR2Decoder(DataDecoder): + def __init__(self): + self.decoder = URDecoder() + + # Decode the given data into the expected format + def add_data(self, data): + try: + return self.decoder.receive_part(data) + except Exception as e: + print('EXCEPTION: {}'.format(e)) + return False + + def received_parts(self): + return len(self.decoder.received_part_indexes()) + + def total_parts(self): + return self.decoder.expected_part_count() + + def is_complete(self): + return self.decoder.is_complete() + + def get_error(self): + if self.decoder.is_failure(): + return self.decoder.result_error() + else: + return None + + def get_type(self): + return self.decoder.expected_type() + + def decode(self): + try: + message = self.decoder.result_message() + # print('UR2: message={}'.format(message.cbor)) + cbor_decoder = CBORDecoder(message.cbor) + (data, length) = cbor_decoder.decodeBytes() + # print('UR2: data={}'.format(data)) + return data + except Exception as e: + self.error = '{}'.format(e) + # print('CBOR decode error: {}'.format(e)) + return None + + def get_data_format(self): + return QRType.UR2 + +class UR2Encoder(DataEncoder): + def __init__(self, _args): + self.qr_sizes = [280, 100, 70] + self.type = None + + def get_num_supported_sizes(self): + return len(self.qr_sizes) + + def get_max_len(self, index): + if index < 0 or index >= len(self.qr_sizes): + return 0 + return self.qr_sizes[index] + + # Encode the given data + def encode(self, data, is_binary=False, max_fragment_len=500): + encoder = CBOREncoder() + # print('UR2: data={}'.format(to_str(data))) + encoder.encodeBytes(data) + # TODO: Need to change this interface most likely to allow for different types like crypto-psbt + ur_obj = UR("bytes", encoder.get_bytes()) + self.ur_encoder = UREncoder(ur_obj, max_fragment_len) + + # UR2.0's next_part() returns the initial pieces split into max_fragment_len bytes, but then switches over to + # an infinite series of encodings that combine various pieces of the data in an attempt to fill in any holes missed. + def next_part(self): + return self.ur_encoder.next_part() + + # Return any error message if decoding or adding data failed for some reason + def get_error(self): + return None + +class UR2Sampler(DataSampler): + # Check if the given bytes look like UR1 data + # Return True if it matches or False if not + @classmethod + def sample(cls, data): + r = re.compile('^ur:[a-z\d-]+\/(\d)+-(\d)+\/') + m = r.match(data.lower()) + return m != None + + # Number of bytes required to successfully recognize this format + @classmethod + def min_sample_size(cls): + return 20 diff --git a/ports/stm32/boards/Passport/modules/descriptor.py b/ports/stm32/boards/Passport/modules/descriptor.py new file mode 100644 index 0000000..b15023d --- /dev/null +++ b/ports/stm32/boards/Passport/modules/descriptor.py @@ -0,0 +1,60 @@ +# (c) Copyright 2019 by Coinkite Inc. This file is part of Coldcard +# and is covered by GPLv3 license found in COPYING. +# +# descriptor.py - Bitcoin Core's descriptors and their specialized checksums. +# +# Based on: https://github.com/bitcoin/bitcoin/blob/master/src/script/descriptor.cpp +# + +def polymod(c, val): + c0 = c >> 35 + c = ((c & 0x7ffffffff) << 5) ^ val + if (c0 & 1): + c ^= 0xf5dee51989 + if (c0 & 2): + c ^= 0xa9fdca3312 + if (c0 & 4): + c ^= 0x1bab10e32d + if (c0 & 8): + c ^= 0x3706b1677a + if (c0 & 16): + c ^= 0x644d626ffd + + return c + +def descriptor_checksum(desc): + INPUT_CHARSET = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ " + CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" + + c = 1 + cls = 0 + clscount = 0 + for ch in desc: + pos = INPUT_CHARSET.find(ch) + if pos == -1: + raise ValueError(ch) + + c = polymod(c, pos & 31) + cls = cls * 3 + (pos >> 5) + clscount += 1 + if clscount == 3: + c = polymod(c, cls) + cls = 0 + clscount = 0 + + if clscount > 0: + c = polymod(c, cls) + for j in range(0, 8): + c = polymod(c, 0) + c ^= 1 + + rv = '' + for j in range(0, 8): + rv += CHECKSUM_CHARSET[(c >> (5 * (7 - j))) & 31] + + return rv + +def append_checksum(desc): + return desc + "#" + descriptor_checksum(desc) + +# EOF diff --git a/ports/stm32/boards/Passport/modules/display.py b/ports/stm32/boards/Passport/modules/display.py index 9bbefd7..3b49a24 100644 --- a/ports/stm32/boards/Passport/modules/display.py +++ b/ports/stm32/boards/Passport/modules/display.py @@ -1,7 +1,7 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: GPL-3.0-or-later # -# SPDX-FileCopyrightText: 2018 Coinkite, Inc. +# SPDX-FileCopyrightText: 2018 Coinkite, Inc. # SPDX-License-Identifier: GPL-3.0-only # # (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard @@ -17,9 +17,9 @@ from foundation import Powermon import framebuf import uzlib from graphics import Graphics -from passport_fonts import FontLarge, FontSmall, FontTiny, lookup +from passport_fonts import FontSmall, FontTiny, lookup from uasyncio import sleep_ms - +from common import system class Display: @@ -30,9 +30,9 @@ class Display: HALF_WIDTH = WIDTH // 2 HEIGHT = 303 HALF_HEIGHT = HEIGHT // 2 - HEADER_HEIGHT = 38 - FOOTER_HEIGHT = 34 - SCROLLBAR_WIDTH = 6 + HEADER_HEIGHT = 40 + FOOTER_HEIGHT = 32 + SCROLLBAR_WIDTH = 8 BATTERY_MAX = 3000 BATTERY_MIN = 2500 @@ -49,9 +49,6 @@ class Display: self.backlight = Backlight() - self.powermon = Powermon() - self.v_avg = [3000 for i in range(10)] - self.clear() self.show() @@ -106,15 +103,14 @@ class Display: self.dis.fill_rect(side_space + bw, self.HEIGHT - bottom_space + bw, width - bw2, bar_height - bw2, 0) self.dis.fill_rect(side_space + bw + 1, self.HEIGHT - bottom_space + bw + 1, int((width - bw2 - 2) * percent), bar_height - bw2 - 2, 1) - def progress_bar_show(self, percent): - self.progress_bar(percent) - def set_brightness(self, val): - # normal = 128, max brightness=254, off <= 10 - # This is be done with the backlight object (10 to 254 for val) - self.backlight.intensity(val) + # 0-100 are valid + if val >= 0 and val <= 100: + self.backlight.intensity(val) def fullscreen(self, msg, percent=None, line2=None): + system.turbo(True) + # show a simple message "fullscreen". headingFont = FontSmall subheadingFont = FontTiny @@ -127,26 +123,28 @@ class Display: else: y = self.HALF_HEIGHT - (headingFont.height // 2) self.text(None, y, msg, font=headingFont) + if percent is not None: self.progress_bar(percent) + self.show() + system.turbo(False) + + def splash(self, message=None, progress=None): + system.turbo(True) - def splash(self): # Display a splash screen with some version numbers self.clear() - self.icon(None, self.HALF_HEIGHT - 80, 'splash') - - # from version import get_mpy_version - # timestamp, label, *_ = get_mpy_version() - - timestamp, label, *_ = ('11/21/2020', '0.1.0', None) - - # Show version and timestamp info - y = self.HEIGHT - FontTiny.leading - 4 - self.text(8, y, 'Version ' + label, font=FontTiny) - self.text(-8, y, timestamp, font=FontTiny) - + logo_w, logo_h = self.icon_size('splash') + self.icon(None, self.HALF_HEIGHT - logo_h//2, 'splash') + if message != None: + y = self.HEIGHT - 68 # Same position as in the bootloader splash + self.text(None, y, message, font=FontSmall) + + if progress != None: + self.progress_bar(progress) self.show() + system.turbo(False) def width(self, msg, font): return sum(lookup(font, ord(ch)).advance for ch in msg) @@ -196,21 +194,27 @@ class Display: def text_input(self, x, y, msg, font=FontSmall, invert=0, cursor_pos=None, visible_spaces=False, fixed_spacing=None, cursor_shape='line', max_chars_per_line=0): if max_chars_per_line > 0: + # TODO: Improve this by splitting lines based on actual pixel widths instead of max_chars_per_line # Split text into multiple lines and draw them separately lines = [msg[i:i+max_chars_per_line] for i in range(0, len(msg), max_chars_per_line)] - for line in lines: - self.text(x, y, line, font, invert, cursor_pos, + # Special case to draw cursor by itself when no text is entered yet + if len(lines) == 0: + self.text(x, y, '', font, invert, cursor_pos, visible_spaces, fixed_spacing, cursor_shape) - y += font.leading - cursor_pos -= max_chars_per_line + else: + for line in lines: + self.text(x, y, line, font, invert, cursor_pos, + visible_spaces, fixed_spacing, cursor_shape) + y += font.leading + cursor_pos -= max_chars_per_line else: self.text(x, y, msg, font, invert, cursor_pos, visible_spaces, fixed_spacing, cursor_shape) - def text(self, x, y, msg, font=FontSmall, invert=0, cursor_pos=None, visible_spaces=False, fixed_spacing=None, cursor_shape='line'): + def text(self, x, y, msg, font=FontSmall, invert=0, cursor_pos=None, visible_spaces=False, fixed_spacing=None, cursor_shape='line', scrollbar_visible=False): # Draw at x,y (top left corner of first letter) # using font. Use invert=1 to get reverse video @@ -219,6 +223,8 @@ class Display: w = self.width(msg, font) if x == None: x = max(0, self.HALF_WIDTH - (w // 2)) + if scrollbar_visible: + x = x - self.SCROLLBAR_WIDTH // 2 else: # measure from right edge (right justify) x = max(0, self.WIDTH - w + 1 + x) @@ -232,13 +238,11 @@ class Display: curr_pos = 0 for ch in msg: if visible_spaces and ch == ' ': - ch = '_' # TODO: Replace this with a difference character code that is not an ASCII symbol + ch = '_' fn = lookup(font, ord(ch)) if fn is None: - # use last char in font as error char for junk we don't - # know how to render + # Use last char in font as error char for junk we don't know how to render fn = font.lookup(font.code_range.stop) - # TODO: This is always the same per font - can reuse this buffer if there are performance issues bits = bytearray(fn.w * fn.h) bits[0:len(fn.bits)] = fn.bits if invert: @@ -292,32 +296,34 @@ class Display: def scrollbar(self, scroll_percent, content_to_height_ratio): # Draw scrollbar only if the content doesn't fit on screen if content_to_height_ratio < 1: - sb_width = 7 - sb_left = self.WIDTH - sb_width + # We add one for the left border, but everything else is based on the constant + sb_left = self.WIDTH - (self.SCROLLBAR_WIDTH + 1) # Draw a rectangle background for the entire thing # NOTE: We go up one pixel to cover the header divider (looks better) self.dis.fill_rect(sb_left, self.HEADER_HEIGHT - 2, - sb_width, self.HEIGHT - self.HEADER_HEIGHT + 2, 1) + self.SCROLLBAR_WIDTH + 1, self.HEIGHT - self.HEADER_HEIGHT + 2, 1) self.dis.fill_rect(sb_left+1, self.HEADER_HEIGHT - 2, - sb_width - 2, self.HEIGHT - self.HEADER_HEIGHT + 2, 0) + self.SCROLLBAR_WIDTH - 1, self.HEIGHT - self.HEADER_HEIGHT + 2, 0) # Draw the scrollbar track - self.icon(sb_left + 1, self.HEADER_HEIGHT - 3, 'scrollbar') + bg_w, bg_h = self.icon_size('scrollbar') + for i in range((self.SCROLLBAR_WIDTH + bg_w//2) // bg_w): + self.icon(sb_left + (bg_w * i) + 1, self.HEADER_HEIGHT - 3, 'scrollbar') # Draw the thumb in the right position mm = self.HEIGHT - self.HEADER_HEIGHT - self.FOOTER_HEIGHT + 4 pos = min(int(mm * scroll_percent), mm) + self.HEADER_HEIGHT - 2 thumb_height = min(int(mm * content_to_height_ratio), mm) - thumb_width = sb_width - 2 + thumb_width = self.SCROLLBAR_WIDTH - 1 thumb_left = sb_left + 1 self.dis.fill_rect(thumb_left, pos, thumb_width, thumb_height, 0) # Round the thumb corners self.set_pixel(thumb_left, pos, 1) - self.set_pixel(thumb_left + 4, pos, 1) + self.set_pixel(thumb_left + self.SCROLLBAR_WIDTH - 2, pos, 1) self.set_pixel(thumb_left, pos + thumb_height - 1, 1) - self.set_pixel(thumb_left + 4, pos + thumb_height - 1, 1) + self.set_pixel(thumb_left + self.SCROLLBAR_WIDTH - 2, pos + thumb_height - 1, 1) # Draw separator lines above and below the thumb if scroll_percent > 0: @@ -329,7 +335,7 @@ class Display: # Draw a thumb pattern in the middle notch_height = 3 - notch_width = 3 + notch_width = self.SCROLLBAR_WIDTH - 3 # Reserve 3 pixels at the top and bottom (the 6 below) num_notches = min((thumb_height - 6) // notch_height, 9) @@ -342,8 +348,12 @@ class Display: notch_y += notch_height def draw_header(self, title='Passport', wordmark=False, left_text=None): + import stash + import common + from utils import truncate_string_to_width + from common import battery_level, battery_voltage, demo_active, demo_count LEFT_MARGIN = 11 - title_y = 8 + title_y = 10 # Fill background self.dis.fill_rect(0, 0, self.WIDTH, self.HEADER_HEIGHT, 0) @@ -352,38 +362,38 @@ class Display: self.hline(self.HEADER_HEIGHT - 2, 0) self.hline(self.HEADER_HEIGHT - 1, 0) - # Title + # Title - restrict length so it doesn't overwrite battery or left text + MAX_HEADER_TITLE_WIDTH = self.WIDTH - 68 + title = truncate_string_to_width(title, FontSmall, MAX_HEADER_TITLE_WIDTH ) self.text(None, title_y, title, font=FontSmall, invert=0) # Left text - if left_text != None: - self.text(LEFT_MARGIN, title_y, left_text, - font=FontSmall, invert=0) - - # Get battery level and shift into array - # for i in range(10): - # self.v_avg[i] = self.v_avg[i+1] - for i in range(9,0,-1): - self.v_avg[i] = self.v_avg[i-1] - (current, voltage) = self.powermon.read() - self.v_avg[0] = round(voltage * (44.7 + 22.1) / 44.7) # Voltage divider on PCB - - # Calculate average of array - voltage_average = 0 - for i in range(10): - voltage_average += self.v_avg[i] - # print('v_avg[{}] = {}'.format(i, self.v_avg[i])) - voltage_average = voltage_average / 10 - # print('voltage_average = {}'.format(voltage_average)) - - # Normalize to battery operating range - batteryLife = 100 # round(100 * (voltage_average - self.BATTERY_MIN) / (self.BATTERY_MAX - self.BATTERY_MIN)) - # print('batteryLife = {}'.format(batteryLife)) - - battery_icon = self.get_battery_icon(batteryLife) + left_text_y = title_y + 5 + if demo_active: + left_text = '{}'.format(demo_count) + + if common.snapshot_mode_enabled: + self.text(6, left_text_y, 'Cam', font=FontTiny, invert=0) + elif common.enable_battery_mon: + # Draw some stats rather than other left_text + v = str(int(battery_voltage)) + p ='{}%'.format(int(battery_level)) + self.text(6, title_y - 5, v, font=FontTiny, invert=0) + self.text(6, title_y + 9, p, font=FontTiny, invert=0) + + elif left_text != None: + self.text(LEFT_MARGIN, left_text_y, left_text, font=FontTiny, invert=0) + else: + left_x = 2 + if stash.bip39_passphrase: + pass_w, pass_h = self.icon_size('passphrase_icon') + self.icon(4, ((self.HEADER_HEIGHT - 4) // 2 - pass_h // 2) + 2, 'passphrase_icon', invert=0) + left_x += pass_w + 2 + + battery_icon = self.get_battery_icon(battery_level) batt_w, batt_h = self.icon_size(battery_icon) - self.icon(self.WIDTH - batt_w - 11, ((self.HEADER_HEIGHT - 4) // - 2 - batt_h // 2) + 2, battery_icon, invert=0) + self.icon(self.WIDTH - batt_w - 6, ((self.HEADER_HEIGHT - 4) // + 2 - batt_h // 2) + 3, battery_icon, invert=0) def draw_button(self, x, y, w, h, label, font=FontTiny, invert=0): self.draw_rect(x, y, w, h, border_w=1, @@ -392,7 +402,7 @@ class Display: label_w = self.width(label, font) x = x + (w // 2 - label_w // 2) y = y + (h // 2 - font.ascent // 2) - self.text(x, y, label, font, invert) + self.text(x, y - 1, label, font, invert) def draw_footer(self, left_btn='', right_btn='', left_down=False, right_down=False): btn_w = self.WIDTH // 2 @@ -421,7 +431,113 @@ class Display: return 'battery_50' elif level >= 30: return 'battery_25' - elif level >= 10: + else: return 'battery_low' - + # Save a screenshot in PPM (Portable Pixel Map) -- a very simple format + # that doesn't need a big library to be included. + def screenshot(self): + from files import CardSlot + from noise_source import NoiseSource + from utils import bytes_to_hex_str + import common + + common.system.turbo(True) + white = b'\xEE' + black = b'\x00' + + fname_rnd = bytearray(4) + + # Just use MCU nois source as it's faster and this is not a security-related use + common.noise.random_bytes(fname_rnd, NoiseSource.MCU) + + try: + with CardSlot() as card: + # Need to use get_sd_root() here to prefix the /sd/ or we get EPERM errors + fname = '{}/screenshot-{}.pgm'.format(card.get_sd_root(), bytes_to_hex_str(fname_rnd)) + print('Saving screenshot to: {}'.format(fname)) + + with open(fname, 'wb') as fd: + hdr = '''P5 +# Created by Passport +{} {} +255\n'''.format(self.WIDTH, self.HEIGHT) + + # Write the header + fd.write(bytes(hdr, 'utf-8')) + + # Write the pixels + for y in range(self.HEIGHT): + for x in range(self.WIDTH): + p = self.dis.pixel(x, y) + fd.write(black if p else white) + + except Exception as e: + print('EXCEPTION: {}'.format(e)) + # This method is not async, so no error or warning if you don't have an SD card inserted + + print('Screenshot saved.') + common.system.turbo(False) + + # Save a camera snapshot in PPM (Portable Pixel Map) -- a very simple format + # that doesn't need a big library to be included. + def snapshot(self): + from files import CardSlot + from utils import random_hex + import common + from common import qr_buf, viewfinder_buf + from constants import VIEWFINDER_WIDTH, VIEWFINDER_HEIGHT, CAMERA_WIDTH, CAMERA_HEIGHT + from foundation import Camera + + common.system.turbo(True) + + # Create the Camera connection + cam = Camera() + cam.enable() + + # Take the picture - no viewfinder for now + result = cam.snapshot(qr_buf, CAMERA_WIDTH, CAMERA_HEIGHT, + viewfinder_buf, VIEWFINDER_WIDTH, VIEWFINDER_HEIGHT) + + try: + with CardSlot() as card: + # Need to use get_sd_root() here to prefix the /sd/ or we get EPERM errors + fname = '{}/snapshot-{}.ppm'.format(card.get_sd_root(), random_hex(4)) + # print('Saving camera snapshot to: {}'.format(fname)) + + # PPM file format + # http://paulbourke.net/dataformats/ppm/ + with open(fname, 'wb') as fd: + hdr = '''P6 +# Created by Passport +{} {} +255\n'''.format(396, 330) + + # Write the header + fd.write(bytes(hdr, 'utf-8')) + + line = bytearray(396 * 2) # Two bytes per pixel + pixel = bytearray(3) + + # Write the pixels + for y in range(330): + # print('Line {}'.format(y)) + result = cam.get_line_data(line, y) + if not result: + print('ERROR: Unable to get line data for line {}!'.format(y)) + common.system.turbo(False) + return + + for x in range(396): + rgb565 = (line[x*2 + 1] << 8) | line[x*2] + pixel[0] = (rgb565 & 0xF800) >> 8 + pixel[1] = (rgb565 & 0x07E0) >> 3 + pixel[2] = (rgb565 & 0x001F) << 3 + fd.write(pixel) + + except Exception as e: + print('EXCEPTION: {}'.format(e)) + # This method is not async, so no error or warning if you don't have an SD card inserted + + # print('Camera snapshot saved.') + common.system.turbo(False) diff --git a/ports/stm32/boards/Passport/modules/exceptions.py b/ports/stm32/boards/Passport/modules/exceptions.py new file mode 100644 index 0000000..430ac4b --- /dev/null +++ b/ports/stm32/boards/Passport/modules/exceptions.py @@ -0,0 +1,22 @@ +# (c) Copyright 2020 by Coinkite Inc. This file is part of Coldcard +# and is covered by GPLv3 license found in COPYING. +# +# exceptions.py - Exceptions defined by us. +# + +# Caution: limited ability in Micropython to override system exceptions. + +# PSBT / transaction related +class FatalPSBTIssue(RuntimeError): + pass + +class FraudulentChangeOutput(FatalPSBTIssue): + def __init__(self, out_idx, msg): + super().__init__('Output #%d: %s' % (out_idx, msg)) + +class IncorrectUTXOAmount(FatalPSBTIssue): + def __init__(self, in_idx, msg): + super().__init__('Input #%d: %s' % (in_idx, msg)) + + +# EOF diff --git a/ports/stm32/boards/Passport/modules/export.py b/ports/stm32/boards/Passport/modules/export.py new file mode 100644 index 0000000..a865793 --- /dev/null +++ b/ports/stm32/boards/Passport/modules/export.py @@ -0,0 +1,1103 @@ +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# SPDX-FileCopyrightText: 2018 Coinkite, Inc. +# SPDX-License-Identifier: GPL-3.0-only +# +# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard +# and is covered by GPLv3 license found in COPYING. +# +# export.py - Save and restore backup data. +# +import gc +import sys +import os + +import chains +import compat7z +import seed +import stash +import trezorcrypto +import ujson +import version +from ubinascii import hexlify as b2a_hex +from ubinascii import unhexlify as a2b_hex +from uio import StringIO +from utils import imported, xfp2str, get_bytewords_for_buf, to_str, run_chooser, ensure_folder_exists, file_exists, is_dir, get_accounts +from ux import ux_confirm, ux_show_story +from common import noise + +# we make passwords with this number of words +NUM_PW_WORDS = const(6) + +# max size we expect for a backup data file (encrypted or cleartext) +MAX_BACKUP_FILE_SIZE = const(10000) # bytes + + +def ms_has_master_xfp(xpubs): + from common import settings + master_xfp = settings.get('xfp', None) + + for xpub in xpubs: + (xfp, _) = xpub + # print('ms_has_master_xfp: xfp={} master_xfp={}'.format(xfp, master_xfp)) + if xfp == master_xfp: + # print('Including this one') + return True + + # print('EXCLUDING this one') + return False + +def render_backup_contents(): + # simple text format: + # key = value + # or #comments + # but value is JSON + from common import settings, pa, system + from utils import get_month_str + from utime import localtime + + rv = StringIO() + + def COMMENT(val=None): + if val: + rv.write('\n# %s\n' % val) + else: + rv.write('\n') + + def ADD(key, val): + rv.write('%s = %s\n' % (key, ujson.dumps(val))) + + rv.write('# Passport backup file! DO NOT CHANGE.\n') + + chain = chains.current_chain() + + COMMENT('Private Key Details: ' + chain.name) + + with stash.SensitiveValues(for_backup=True) as sv: + + if sv.mode == 'words': + ADD('mnemonic', trezorcrypto.bip39.from_data(sv.raw)) + + if sv.mode == 'master': + ADD('bip32_master_key', b2a_hex(sv.raw)) + + ADD('chain', chain.ctype) + ADD('xfp', xfp2str(sv.get_xfp())) + ADD('xprv', chain.serialize_private(sv.node)) + ADD('xpub', chain.serialize_public(sv.node)) + + # BTW: everything is really a duplicate of this value + ADD('raw_secret', b2a_hex(sv.secret).rstrip(b'0')) + + COMMENT('Firmware Version (informational):') + (fw_version, fw_timestamp, _, _) = system.get_software_info() + time = localtime(fw_timestamp) + fw_date = '{} {}, {}'.format(get_month_str(time[1]), time[2], time[0]-30) + + ADD('fw_version', fw_version) + ADD('fw_date', fw_date) + + COMMENT('User Preferences:') + + # user preferences - sort so that accounts is processed before multisig + multisig_ids = [] + for k, v in sorted(settings.curr_dict.items()): + # print('render handling key "{}"'.format(k)) + if k[0] == '_': + continue # debug stuff in simulator + if k == 'xpub': + continue # redundant, and wrong if bip39pw + if k == 'xfp': + continue # redundant, and wrong if bip39pw + + # if k == 'accounts': + # # Filter out accounts that have a passphrase + # print('Filtering out accounts that have a bip39_hash') + # v = list(filter(lambda acct: acct.get('bip39_hash', '') == '', v)) + # multisig_ids = [acct.get('multisig_id', None) for acct in v] + # multisig_ids = list(filter(lambda ms: ms != None, multisig_ids)) # Don't include None entries + # print('multisig_ids={}'.format(multisig_ids)) + # + if k == 'multisig': + # Only backup multisig entries that have the master XFP - plausible deniability in your backups + # "Passphrase wallets? I don't have any passphrase wallets!" + # print('ms={}'.format(v)) + v = list(filter(lambda ms: ms_has_master_xfp(ms[2]), v)) + + ADD('setting.' + k, v) + + rv.write('\n# EOF\n') + + return rv.getvalue() + + +async def restore_from_dict(vals): + # Restore from a dict of values. Already JSON decoded. + # Reboot on success, return stringg on failure + from common import pa, dis, settings, system + from pincodes import SE_SECRET_LEN + + # print("Restoring from: %r" % vals) + + # Step 1: the private key + # - prefer raw_secret over other values + try: + system.turbo(True) + chain = chains.get_chain(vals.get('chain', 'BTC')) + + assert 'raw_secret' in vals + raw = bytearray(SE_SECRET_LEN) + rs = vals.pop('raw_secret') + if len(rs) % 2: + rs += '0' + x = a2b_hex(rs) + raw[0:len(x)] = x + + # check we can decode this right (might be different firweare) + opmode, bits, node = stash.SecretStash.decode(raw) + assert node + + # verify against xprv value (if we have it) + if 'xprv' in vals: + check_xprv = chain.serialize_private(node) + if check_xprv != vals['xprv']: + system.turbo(False) + return 'The xprv in the backup file does not match the xprv derived from the raw secret.' + + except Exception as e: + system.turbo(False) + return ('Unable to restore the seed value from the backup.\n\n\n' + str(e)) + + dis.fullscreen("Saving Wallet...") + system.progress_bar(0) + + # clear (in-memory) settings and change also nvram key + # - also captures xfp, xpub at this point + pa.change(new_secret=raw) + + # force the right chain + await pa.new_main_secret(raw, chain) # updates xfp/xpub + + # NOTE: don't fail after this point... they can muddle thru w/ just right seed + + # restore settings from backup file + for idx, k in enumerate(vals): + system.progress_bar(int(idx * 100 / len(vals))) + if not k.startswith('setting.'): + continue + + if k == 'xfp' or k == 'xpub': + continue + + # TODO: If we implement partial restore, merge in accounts and multisigs if some already exist + + settings.set(k[8:], vals[k]) + + system.turbo(False) + + # write out + # await settings.save() + + await ux_show_story('Everything has been successfully restored. ' + 'Passport will now reboot to finalize the ' + 'updated settings and seed.', title='Success', left_btn='RESTART', right_btn='OK', center=True, center_vertically=True) + + from machine import reset + reset() + +def get_ms_wallet_by_id(id): + matches = list(filter(lambda ms: ms['id'] == multisig_id, multisig)) + return matches[0] if len(matches) == 1 else None + +def find_acct(deriv_path, bip39_hash): + from common import settings + accounts = get_accounts() + # print('find_acct: deriv_path={} bip39_hash={}'.format(deriv_path, bip39_hash)) + matches = list(filter(lambda acct: (acct.get('deriv_path') == deriv_path and acct.get('bip39_hash') == bip39_hash), accounts)) + # print('matches={}'.format(matches)) + return matches[0] if len(matches) == 1 else None + +def get_restorable_accounts(vals): + # Make a list of accounts and their corresponding multisigs, if any + restorable = [] + # print('get_restorable_accounts: vals={}'.format(to_str(vals))) + accounts = vals.get('setting.accounts', []) + multisig = vals.get('setting.multisig', []) + + # print('get_restorable_accounts: accounts={}'.format(accounts)) + # print('get_restorable_accounts: multisig={}'.format(multisig)) + for account in accounts: + multisig_id = account.get('multisig_id') + # print('multisig_id={}'.format(multisig_id)) + ms_wallet = None + if multisig_id: + # Find matching multisig entry + ms_wallet = get_ms_wallet_by_id(multisig_id) + # print('ms_wallet={}'.format(ms_wallet)) + + # See if this account exists already (based on derivation path and bip39_hash + existing_acct = find_acct(account.get('deriv_path'), account.get('bip39_hash', '')) + # Only allow restoration of non-active accounts + if existing_acct != None and existing_acct.get('status') != 'a': + restorable.append((account, ms_wallet)) + + return restorable + +async def restore_from_dict_partial(vals): + from common import pa, dis, settings, system + from uasyncio import sleep_ms + + restorable = get_restorable_accounts(vals) + if len(restorable) == 0: + await ux_show_story('All accounts in this backup already exist on this Passport.', title='Info', + center=True, center_vertically=True) + return + + acct_to_restore = None + + def account_chooser(): + choices = ['Restore All'] + values = ['all'] + for entry in restorable: + (account, _) = entry + choices.append(account.get('name')) + values.append(entry) + + def select_account(index, text): + nonlocal acct_to_restore + acct_to_restore = values[index] + + return 0, choices, select_account + + # Ask user to select account to restore + await run_chooser(account_chooser, 'Select Acct.', show_checks=False) + # print('acct_to_restore={}'.format(to_str(acct_to_restore))) + if acct_to_restore == None: + # print('No account selected!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!') + # User wants to go back without selecting an account + return + + # Actually restore the account(s) now + entries = restorable if acct_to_restore == 'all' else [acct_to_restore] + curr_accounts = get_accounts() + curr_multisig = settings.get('multisig', []) + + for entry in entries: + (account, multisig) = entry + # print('Handling account.name={}'.format(account.get('name'))) + + existing_acct = find_acct(account.get('deriv_path'), account.get('bip39_hash', '')) + if existing_acct: + if existing_acct['status'] == 'r': + result = await ux_confirm('''An archived account with the following details already exists \ +on this Passport: + +Archived Name: +{} + +Backup Name: +{} + +Derivation Path: +{} + +Do you want to replace it? + +If you select NO, this account will not be restored and you can use the Advanced > Archived Accts. \ +feature to recover it later.'''.format(existing_acct.get('name'), account.get('name'), account.get('deriv_path')), + title='Duplicate Acct.') + if result == 'x': + continue + else: + # Remove the old entry and append the new one + curr_accounts.remove(existing_acct) + elif existing_acct['status'] == 'd': + # Remove the old entry since deleted entries are missing many account details and can't be recovered + curr_accounts.remove(existing_acct) + + # Restore the account + curr_accounts.append(account) + + # Add in the multisig entry, if any + if multisig: + curr_multisig.append(multisig) + + # Save all the accounts that were restored + settings.set('accounts', curr_accounts) + settings.set('multisig', curr_multisig) + + await settings.save() + + dis.fullscreen('Restore Successful') + await sleep_ms(1000) + + +# Pick password based on the device hash and secret (entropy) so that it's always the same -- let's you make repeated backups +# without having to write down a new password each time. +def make_backup_password(): + from common import system, pa + from utils import bytes_to_hex_str + + device_hash = bytearray(32) + system.get_device_hash(device_hash) + secret = pa.fetch() + # print('secret: {}'.format(bytes_to_hex_str(secret))) + + pw = trezorcrypto.sha256() + pw.update(device_hash) + pw.update(secret) + password_hash = pw.digest() + + # print('password_hash: {}'.format(bytes_to_hex_str(password_hash))) + + words = get_bytewords_for_buf(password_hash[:NUM_PW_WORDS]) + # print('words: {}'.format(words)) + return words + +async def make_complete_backup(): + from noise_source import NoiseSource + from common import system, dis, settings + from uasyncio import sleep_ms + from seed_check_ux import SeedCheckUX + + backup_quiz_passed = settings.get('backup_quiz', False) + + cancel_msg = "Are you sure you want to cancel the backup?\n\nWithout a microSD backup or the seed phrase, you won't be able to recover your funds." + + while True: + if backup_quiz_passed: + ch = await ux_show_story('''Passport is about to create an updated microSD backup. + +The password is the same as what you previously recorded. + +Please insert a microSD card.''', title='Backup', right_btn='CONTINUE', scroll_label='MORE') + if ch == 'x': + # Warn user that the wallet is not backed up (but they HAVE seen the words already) + if await ux_confirm('Are you sure you want to cancel the backup?'): + return + else: + break + else: + ch = await ux_show_story('''Passport is about to create your first encrypted microSD backup. \ +The next screen will show you the password that is REQUIRED to access the backup. + +We recommend storing the backup password in cloud storage or a password manager. We consider this safe since physical access \ +to the microSD card is required to access the backup.''', title='Backup', right_btn='CONTINUE', scroll_label='MORE') + if ch == 'x': + # Warn user that the wallet is not backed up (and they haven't seen seed words yet!) + if await ux_confirm(cancel_msg): + return + else: + break + + words = make_backup_password() + + while True: + if not backup_quiz_passed: + msg = 'Backup Password (%d):\n' % len(words) + msg += '\n'.join('%2d: %s' % (i+1, w) for i, w in enumerate(words)) + # print('Backup Password: {}'.format(' '.join(words))) + + ch = await ux_show_story(msg, sensitive=True, right_btn='NEXT') + stash.blank_object(msg) + if ch == 'x': + if await ux_confirm('Are you sure you want to cancel password verification?'): + return + else: + continue + + # Quiz + cancel_msg = '''Are you sure you want to cancel password verification? + +The backup will still be saved, but you will be unable to access it without the correct password.''' + + phrase_check = SeedCheckUX(seed_words=words, title="Verify Password", cancel_msg=cancel_msg) + settings.set('backup_quiz', True) + if not await phrase_check.show(): + continue # Show words again + + # Write out the backup, possibly more than once + await write_complete_backup(words, is_first_backup=not backup_quiz_passed) + return + + +async def offer_backup(): + # Try auto-backup first + if await auto_backup(): + # Success! No need to bother user. + return + + result = await ux_confirm('The account configuration has been modified. Do you want to make a new microSD backup?', + title='Backup') + if result: + await make_complete_backup() + +# Check to see if there is an SD card in the slot with a 'backups' folder and, if so, write a backup. +async def auto_backup(): + from files import CardSlot + + backup_started = False + # See if the card is there + try: + with CardSlot() as card: + backups_path = get_backups_folder_path(card) + if not is_dir(backups_path): + # print('{} is not a directory'.format(backups_path)) + return False + + # microSD is inserted and has a backups folder -- it's AutoBackup time! + backup_started = True + words = make_backup_password() + await write_complete_backup(words, auto_backup=True) + return True + + except Exception as e: + from uasyncio import sleep_ms + # print('auto_backup() e={}'.format(e)) + + if backup_started: + from common import dis + dis.fullscreen('Unable to Backup') + await sleep_ms(1000) + return False + +async def view_backup_password(*a): + from common import system + words = make_backup_password() + + msg = 'Backup Password (%d):\n' % len(words) + msg += '\n'.join('%2d: %s' % (i+1, w) for i, w in enumerate(words)) + + ch = await ux_show_story(msg, title='Password', sensitive=True, right_btn='OK') + stash.blank_object(msg) + +def get_backups_folder_path(card): + # from common import settings + # xfp = xfp2str(settings.get('xfp', 0)) + # return '{}/backups-{}'.format(card.get_sd_root(), xfp) + return '{}/backups'.format(card.get_sd_root()) + +async def write_complete_backup(words, auto_backup=False, is_first_backup=False): + # Just do the writing + from common import dis, pa, settings, system + from files import CardSlot, CardMissingError + from uasyncio import sleep_ms + + # Show progress: + dis.fullscreen('AutoBackup...' if auto_backup else 'Encrypting...' if words else 'Generating...') + + body = render_backup_contents().encode() + + backup_num = settings.get('backup_num', 1) + # print('backup_num={}'.format(backup_num)) + + gc.collect() + + if words: + # NOTE: Takes a few seconds to do the key-stretching, but little actual + # time to do the encryption. + + pw = ' '.join(words) + #print('pw={}'.format(words)) + zz = compat7z.Builder(password=pw, progress_fcn=system.progress_bar) + zz.add_data(body) + + hdr, footer = zz.save('passport-backup.txt') + + filesize = len(body) + MAX_BACKUP_FILE_SIZE + + del body + + gc.collect() + else: + # cleartext dump + zz = None + filesize = len(body)+10 + + while True: + try: + with CardSlot() as card: + backups_path = get_backups_folder_path(card) + ensure_folder_exists(backups_path) + + # Make a unique filename + while True: + fname = '{}/passport-backup-{}.7z'.format(backups_path, backup_num) + + # Ensure filename doesn't already exist + if not file_exists(fname): + break + + # Ooops...that exists, so increment and try again + backup_num += 1 + # print('backup_num={}'.format(backup_num)) + + # print('Saving to fname={}'.format(fname)) + + # Do actual write + with open(fname, 'wb') as fd: + if zz: + fd.write(hdr) + fd.write(zz.body) + fd.write(footer) + else: + fd.write(body) + + except Exception as e: + # includes CardMissingError + import sys + sys.print_exception(e) + # catch any error + if not auto_backup: + ch = await ux_show_story('Unable to write backup. Please insert a formatted microSD card.\n\n' + + str(e), title='Error', center=True, center_vertically=True, right_btn='RETRY') + if ch == 'x': + return + else: + # Retry the write + continue + else: + return + + # Update backup counter + backup_num += 1 + settings.set('backup_num', backup_num) + + if not auto_backup: + dis.fullscreen('Backup Successful!') + await sleep_ms(2000) + + if await ux_confirm('Do you want to make an additional backup?\n\nIf so, insert another microSD card.', + title='Backup'): + continue + + if is_first_backup: + dis.fullscreen('Setup Complete!') + await sleep_ms(2000) + + return + + +async def verify_backup_file(fname_or_fd): + # read 7z header, and measure checksums + # - no password is wanted/required + # - really just checking CRC32, but that's enough against truncated files + from files import CardSlot, CardMissingError + from actions import needs_microsd + prob = None + fd = None + + # filename already picked, open it. + try: + with CardSlot() as card: + prob = 'Unable to open backup file.' + with (open(fname_or_fd, 'rb') if isinstance( + fname_or_fd, str) else fname_or_fd) as fd: + + prob = 'Unable to read backup file headers. Might be truncated.' + compat7z.check_file_headers(fd) + + prob = 'Unable to verify backup file contents.' + zz = compat7z.Builder() + files = zz.verify_file_crc(fd, MAX_BACKUP_FILE_SIZE) + + assert len(files) == 1 + fname, fsize = files[0] + + except CardMissingError: + await needs_microsd() + return + except Exception as e: + await ux_show_story(prob + '\n\n' + str(e), title='Error', center=True, center_vertically=True) + return + + await ux_show_story("""Backup file appears to be valid. + +Please note this is only a check to ensure the file has not been modified or damaged.""") + + +async def restore_complete(fname_or_fd, partial_restore): + from ux import the_ux + from seed_entry_ux import SeedEntryUX + + fake_it = False + if fake_it: + words = ['glow', 'rich', 'veto', 'diet', 'ramp', 'away'] + else: + seed_entry = SeedEntryUX(title='Encryption Words', seed_len=6, validate_checksum=False, word_list='bytewords') + await seed_entry.show() + if not seed_entry.is_seed_valid: + return + words = seed_entry.words + + prob = await restore_complete_doit(fname_or_fd, words, partial_restore) + + if prob: + await ux_show_story(prob, title='Error') + + +async def restore_complete_doit(fname_or_fd, words, partial_restore): + # Open file, read it, maybe decrypt it; return string if any error + # - some errors will be shown, None return in that case + # - no return if successful (due to reboot) + from common import dis, system, pa + from files import CardSlot, CardMissingError + from actions import needs_microsd + + # Show progress bar while decrypting + def progress_fn(p): + # print('p={}'.format(p)) + system.progress_bar(int(p * 100)) + + # build password + password = ' '.join(words) + + prob = None + + try: + with CardSlot() as card: + # filename already picked, taste it and maybe consider using its data. + try: + fd = open(fname_or_fd, 'rb') if isinstance( + fname_or_fd, str) else fname_or_fd + except: + return 'Unable to open backup file.\n\n' + str(fname_or_fd) + + try: + if not words: + contents = fd.read() + else: + try: + compat7z.check_file_headers(fd) + except Exception as e: + return 'Unable to read backup file. The backup may have been modified.\n\nError: ' \ + + str(e) + + dis.fullscreen("Decrypting...") + try: + zz = compat7z.Builder() + fname, contents = zz.read_file(fd, password, MAX_BACKUP_FILE_SIZE, + progress_fcn=progress_fn) + + # simple quick sanity check + assert contents[0:1] == b'#' and contents[-1:] == b'\n' + + except Exception as e: + # assume everything here is "password wrong" errors + # print("pw wrong? %s" % e) + return ('Unable to decrypt backup file. The password is incorrect.' + '\n\nYou entered:\n\n' + password) + + finally: + fd.close() + except CardMissingError: + await needs_microsd() + return + + vals = {} + for line in contents.decode().split('\n'): + if not line: + continue + if line[0] == '#': + continue + + try: + k, v = line.split(' = ', 1) + #print("%s = %s" % (k, v)) + + vals[k] = ujson.loads(v) + except: + # print("Unable to decode line: %r" % line) + # but keep going! + pass + + # this leads to reboot if it works, else errors shown, etc. + # print('vals = {}'.format(to_str(vals))) + + if partial_restore: + # Check that the seed of this backup is the same as the current one or that the seed is blank + with stash.SensitiveValues() as sv: + curr_mnemonic = trezorcrypto.bip39.from_data(sv.raw) + + backup_mnemonic = vals.get('mnemonic') + # print('pa.is_secret_blank()={}\ncurr_mnemonic={}\backup_mnemonic={}'.format(pa.is_secret_blank(), curr_mnemonic, backup_mnemonic)) + + if not pa.is_secret_blank() and curr_mnemonic != backup_mnemonic: + # ERROR! Can't import between different seeds + return "Can't restore accounts from a backup that is based on a different seed phrase than the current wallet." + + return await restore_from_dict_partial(vals) + else: + return await restore_from_dict(vals) + + +def generate_public_contents(): + # Generate public details about wallet. + # + # simple text format: + # key = value + # or #comments + # but value is JSON + from common import settings + from public_constants import AF_CLASSIC + + num_rx = 5 + + chain = chains.current_chain() + + with stash.SensitiveValues() as sv: + + yield ('''\ +# Passport Summary File +## For wallet with master key fingerprint: {xfp} + +Wallet operates on blockchain: {nb} + +For BIP44, this is coin_type '{ct}', and internally we use +symbol {sym} for this blockchain. + +## IMPORTANT WARNING + +Do **not** deposit to any address in this file unless you have a working +wallet system that is ready to handle the funds at that address! + +## Top-level, 'master' extended public key ('m/'): + +{xpub} + +What follows are derived public keys and payment addresses, as may +be needed for different systems. +'''.format(nb=chain.name, xpub=chain.serialize_public(sv.node), + sym=chain.ctype, ct=chain.b44_cointype, xfp=xfp2str(sv.node.my_fingerprint()))) + + for name, path, addr_fmt in chains.CommonDerivations: + + if '{coin_type}' in path: + path = path.replace('{coin_type}', str(chain.b44_cointype)) + + if '{' in name: + name = name.format(core_name=chain.core_name) + + show_slip132 = ('Core' not in name) + + yield ('''## For {name}: {path}\n\n'''.format(name=name, path=path)) + yield ('''First %d receive addresses (account=0, change=0):\n\n''' % num_rx) + + submaster = None + for i in range(num_rx): + subpath = path.format(account=0, change=0, idx=i) + + # find the prefix of the path that is hardneded + if "'" in subpath: + hard_sub = subpath.rsplit("'", 1)[0] + "'" + else: + hard_sub = 'm' + + if hard_sub != submaster: + # dump the xpub needed + + if submaster: + yield "\n" + + node = sv.derive_path(hard_sub, register=False) + yield ("%s => %s\n" % (hard_sub, chain.serialize_public(node))) + if show_slip132 and addr_fmt != AF_CLASSIC and (addr_fmt in chain.slip132): + yield ("%s => %s ##SLIP-132##\n" % ( + hard_sub, chain.serialize_public(node, addr_fmt))) + + submaster = hard_sub + # TODO: Add blank() back into trezor? + # node.blank() + del node + + # show the payment address + node = sv.derive_path(subpath, register=False) + yield ('%s => %s\n' % (subpath, chain.address(node, addr_fmt))) + + # TODO: Do we need to do this? node.blank() + del node + + yield ('\n\n') + + # from multisig import MultisigWallet + # if MultisigWallet.exists(): + # yield '\n# Your Multisig Wallets\n\n' + # from uio import StringIO + # + # for ms in MultisigWallet.get_all(): + # fp = StringIO() + # + # ms.render_export(fp) + # print("\n---\n", file=fp) + # + # yield fp.getvalue() + # del fp + + +async def write_text_file(fname_pattern, body, title, total_parts=72): + # - total_parts does need not be precise + from common import dis, pa, settings, system + from files import CardSlot, CardMissingError + from actions import needs_microsd + + # choose a filename + try: + with CardSlot() as card: + fname, nice = card.pick_filename(fname_pattern) + + # do actual write + with open(fname, 'wb') as fd: + for idx, part in enumerate(body): + system.progress_bar((idx * 100) // total_parts) + fd.write(part.encode()) + + except CardMissingError: + await needs_microsd() + return + except Exception as e: + await ux_show_story('Unable to write!\n\n\n'+str(e)) + return + + msg = '''%s file written:\n\n%s''' % (title, nice) + await ux_show_story(msg) + + +async def make_summary_file(fname_pattern='public.txt'): + from common import dis + + # record **public** values and helpful data into a text file + dis.fullscreen('Generating...') + + # generator function: + body = generate_public_contents() + + await write_text_file(fname_pattern, body, 'Summary') + + + +def make_bitcoin_core_wallet(account_num=0): + from common import dis, settings + import ustruct + xfp = xfp2str(settings.get('xfp')) + + # make the data + examples = [] + payload = ujson.dumps( + list(generate_bitcoin_core_wallet(examples, account_num))) + + body = '''\ +# Bitcoin Core Wallet Import File +# Exported by Passport + +## For wallet with master key fingerprint: {xfp} + +Wallet operates on blockchain: {nb} + +## Bitcoin Core RPC + +The following command can be entered after opening Window -> Console +in Bitcoin Core, or using bitcoin-cli: + +importmulti '{payload}' + +## Resulting Addresses (first 3) + +'''.format(payload=payload, xfp=xfp, nb=chains.current_chain().name) + + body += '\n'.join('%s => %s' % t for t in examples) + + body += '\n' + + return body + + +def generate_bitcoin_core_wallet(example_addrs, account_num): + # Generate the data for an RPC command to import keys into Bitcoin Core + # - yields dicts for json purposes + from descriptor import append_checksum + from common import settings + import ustruct + + from public_constants import AF_P2WPKH + + chain = chains.current_chain() + + derive = "84'/{coin_type}'/{account}'".format( + account=account_num, coin_type=chain.b44_cointype) + + with stash.SensitiveValues() as sv: + prefix = sv.derive_path(derive) + xpub = chain.serialize_public(prefix) + + for i in range(3): + sp = '0/%d' % i + node = sv.derive_path(sp, master=prefix) + a = chain.address(node, AF_P2WPKH) + example_addrs.append(('m/%s/%s' % (derive, sp), a)) + + xfp = settings.get('xfp') + txt_xfp = xfp2str(xfp).lower() + + chain = chains.current_chain() + + _, vers, _ = version.get_mpy_version() + + for internal in [False, True]: + desc = "wpkh([{fingerprint}/{derive}]{xpub}/{change}/*)".format( + derive=derive.replace("'", "h"), + fingerprint=txt_xfp, + coin_type=chain.b44_cointype, + account=0, + xpub=xpub, + change=(1 if internal else 0)) + + yield { + 'desc': append_checksum(desc), + 'range': [0, 1000], + 'timestamp': 'now', + 'internal': internal, + 'keypool': True, + 'watchonly': True + } + + +def generate_wasabi_wallet(): + # Generate the data for a JSON file which Wasabi can open directly as a new wallet. + from common import settings + import ustruct + import version + + # bitcoin (xpub) is used, even for testnet case (ie. no tpub) + # - altho, doesn't matter; the wallet operates based on it's own settings for test/mainnet + # regardless of the contents of the wallet file + btc = chains.BitcoinMain + + with stash.SensitiveValues() as sv: + xpub = btc.serialize_public(sv.derive_path("84'/0'/0'")) + + xfp = settings.get('xfp') + txt_xfp = xfp2str(xfp) + + chain = chains.current_chain() + assert chain.ctype in {'BTC', 'TBTC'}, "Only Bitcoin supported" + + _, vers, _ = version.get_mpy_version() + + return dict(MasterFingerprint=txt_xfp, + ColdCardFirmwareVersion=vers, + ExtPubKey=xpub) + +def generate_generic_export(account_num=0): + # Generate data that other programers will use to import from (single-signer) + from common import settings + from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH + + chain = chains.current_chain() + + rv = dict(chain=chain.ctype, + xpub = settings.get('xpub'), + xfp = xfp2str(settings.get('xfp')), + account = account_num, + ) + + with stash.SensitiveValues() as sv: + # each of these paths would have /{change}/{idx} in usage (not hardened) + for name, deriv, fmt, atype in [ + ( 'bip44', "m/44'/{ct}'/{acc}'", AF_CLASSIC, 'p2pkh' ), + ( 'bip49', "m/49'/{ct}'/{acc}'", AF_P2WPKH_P2SH, 'p2sh-p2wpkh' ), # was "p2wpkh-p2sh" + ( 'bip84', "m/84'/{ct}'/{acc}'", AF_P2WPKH, 'p2wpkh' ), + ]: + dd = deriv.format(ct=chain.b44_cointype, acc=account_num) + node = sv.derive_path(dd) + xfp = xfp2str(node.my_fingerprint()) + xp = chain.serialize_public(node, AF_CLASSIC) + zp = chain.serialize_public(node, fmt) if fmt != AF_CLASSIC else None + + # bonus/check: first non-change address: 0/0 + node.derive(0) + node.derive(0) + + rv[name] = dict(deriv=dd, xpub=xp, xfp=xfp, first=chain.address(node, fmt), name=atype) + if zp: + rv[name]['_pub'] = zp + + return rv + +def generate_electrum_wallet(addr_type, account_num=0): + # Generate line-by-line JSON details about wallet. + # + # Much reverse engineering of Electrum here. It's a complex legacy file format. + from common import settings + from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH + + chain = chains.current_chain() + + xfp = settings.get('xfp') + + # Must get the derivation path, and the SLIP32 version bytes right! + if addr_type == AF_CLASSIC: + mode = 44 + elif addr_type == AF_P2WPKH: + mode = 84 + elif addr_type == AF_P2WPKH_P2SH: + mode = 49 + else: + raise ValueError(addr_type) + + derive = "m/{mode}'/{coin_type}'/{account}'".format(mode=mode, + account=account_num, coin_type=chain.b44_cointype) + + with stash.SensitiveValues() as sv: + top = chain.serialize_public(sv.derive_path(derive), addr_type) + + # most values are nicely defaulted, and for max forward compat, don't want to set + # anything more than I need to + + rv = dict(seed_version=17, use_encryption=False, wallet_type='single-sig') + + lab = 'Passport Import %s' % xfp2str(xfp) + if account_num: + lab += ' Acct#%d' % account_num + + # the important stuff. + rv['keystore'] = dict(ckcc_xfp=xfp, + ckcc_xpub=settings.get('xpub'), + hw_type='passport', type='hardware', + label=lab, derivation=derive, xpub=top) + + return rv + + +async def make_json_wallet(label, generator, fname_pattern='new-wallet.json'): + # Record **public** values and helpful data into a JSON file + + from common import dis, pa, settings + from files import CardSlot, CardMissingError + from actions import needs_microsd + + dis.fullscreen('Generating...') + + body = generator() + + # choose a filename + + try: + with CardSlot() as card: + fname, nice = card.pick_filename(fname_pattern) + + # do actual write + with open(fname, 'wt') as fd: + ujson.dump(body, fd) + + except CardMissingError: + await needs_microsd() + return + except Exception as e: + await ux_show_story('Unable to write!\n\n\n'+str(e)) + return + + msg = '''%s file written:\n\n%s''' % (label, nice) + await ux_show_story(msg) + +# EOF diff --git a/ports/stm32/boards/Passport/modules/files.py b/ports/stm32/boards/Passport/modules/files.py index 401b291..879ab7e 100644 --- a/ports/stm32/boards/Passport/modules/files.py +++ b/ports/stm32/boards/Passport/modules/files.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2018 Coinkite, Inc. +# SPDX-FileCopyrightText: 2018 Coinkite, Inc. # SPDX-License-Identifier: GPL-3.0-only # # (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard @@ -48,11 +48,13 @@ def _try_microsd(bad_fs_ok=False): return False -def wipe_microsd_card(): - # Erase and re-format SD card. Not secure erase, because that is too slow. +def format_microsd_card(): + # Erase and re-format SD card. Not a fully secure erase, because that is too slow. import callgate import pyb - from common import dis + from common import dis, system + + system.turbo(True) try: os.umount('/sd') @@ -69,26 +71,40 @@ def wipe_microsd_card(): sd.power(0) sd.power(1) - dis.fullscreen('Part Erase...') cutoff = 1024 # arbitrary blk = bytearray(512) + # Just get one block of random data and write the same block + callgate.fill_random(blk) + + dis.fullscreen('Erasing microSD...') + for bnum in range(cutoff): - callgate.fill_random(blk) sd.writeblocks(bnum, blk) - dis.progress_bar_show(bnum/cutoff) + system.progress_bar(int((bnum/cutoff) * 100)) - dis.fullscreen('Formating...') + system.progress_bar(100) - # remount, with newfs option - os.mount(sd, '/sd', readonly=0, mkfs=1) + system.show_busy_bar() - # done, cleanup - os.umount('/sd') + # Create a new FAT file system + from os import VfsFat + dis.fullscreen('Formatting FAT...') + VfsFat.mkfs(sd) + + # # # remount + # # dis.fullscreen('Remounting microSD...') + # # os.mount(sd, '/sd', readonly=0) + # + # # done, cleanup + # os.umount('/sd') # important: turn off power sd = pyb.SDCard() sd.power(0) + system.hide_busy_bar() + + system.turbo(False) class CardMissingError(RuntimeError): diff --git a/ports/stm32/boards/Passport/modules/flash_cache.py b/ports/stm32/boards/Passport/modules/flash_cache.py new file mode 100644 index 0000000..21b13e1 --- /dev/null +++ b/ports/stm32/boards/Passport/modules/flash_cache.py @@ -0,0 +1,359 @@ +# SPDX-FileCopyrightText: 2021 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# SPDX-FileCopyrightText: 2018 Coinkite, Inc. +# SPDX-License-Identifier: GPL-3.0-only +# +# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard +# and is covered by GPLv3 license found in COPYING. +# +# flash_cache.py - Manage a cache of values in flash - similar to settings, but in external flash and much larger +# +# Notes: +# - Working memory is a 256K block of flash at the end of the 2MB extermal flash block +# - Cache size is 16K of JSON-encoded data +# - There are 16 cache blocks available which we rotate through randomly for flash wear leveling +# - All data is encrypted with an AES encryption key is derived from actual wallet secret +# - A 32-byte SHA is appended to the end of the cache as a checksum +# +import os, ujson, trezorcrypto, ustruct, gc +from uasyncio import sleep_ms +from uio import BytesIO +from sffile import SFFile +from utils import bytes_to_hex_str, to_str +from constants import ( + SPI_FLASH_SECTOR_SIZE, + FLASH_CACHE_START, + FLASH_CACHE_END, + FLASH_CACHE_TOTAL_SIZE, + FLASH_CACHE_BLOCK_SIZE, + FLASH_CACHE_CHECKSUM_SIZE, + FLASH_CACHE_MAX_JSON_LEN) + +# Setup address offsets for the cache slotsk of the slots in external flash +# 256K total cache size at the end of the SPI flash split into 16 blocks of 16K each +SLOTS = range(FLASH_CACHE_START, FLASH_CACHE_END, FLASH_CACHE_BLOCK_SIZE) + +# Working buffer from SRAM4 +from sram4 import flash_cache_buf + +class FlashCache: + + def __init__(self, loop=None): + self.loop = loop + self.is_dirty = 0 + self.my_pos = 0 + + self.aes_key = b'\0'*32 + self.current = self.default_values() + + # NOTE: We don't load the FlashCache initially since we don't have the AES key until + # the user logs in successfully. + # self.load() + + def get_aes(self, pos): + # Build AES key for en/decrypt of specific block. + # Include the slot number as part of the initial counter (CTR) + return trezorcrypto.aes(trezorcrypto.aes.CTR, self.aes_key, ustruct.pack('<4I', 4, 3, 2, pos)) + + def set_key(self, new_secret=None): + from common import pa + from stash import blank_object + + key = None + mine = False + + if not new_secret: + if pa.is_successful() or pa.is_secret_blank(): + # read secret and use it. + new_secret = pa.fetch() + mine = True + + if new_secret: + # print('====> new_secret={}'.format(new_secret)) + # hash up the secret... without decoding it or similar + assert len(new_secret) >= 32 + + s = trezorcrypto.sha256(new_secret) + + for round in range(5): + s.update('pad') + + s = trezorcrypto.sha256(s.digest()) + + key = s.digest() + + if mine: + blank_object(new_secret) + + # for restore from backup case, or when changing (created) the seed + self.aes_key = key + # print('====> aes_key={}'.format(self.aes_key)) + + def load(self): + # Search all slots for any we can read, decrypt that, + # and pick the newest one (in unlikely case of dups) + from common import sf + + # reset + self.current.clear() + self.my_pos = 0 + self.is_dirty = 0 + + # 4k, but last 32 bytes are a SHA (itself encrypted) + global flash_cache_buf + + buf = bytearray(4) + empty = 0 + for pos in SLOTS: + # print('pos={}'.format(pos)) + gc.collect() + + sf.read(pos, buf) + if buf[0] == buf[1] == buf[2] == buf[3] == 0xff: + # print('probably an empty page') + # erased (probably) + empty += 1 + continue + + # check if first 2 bytes makes sense for JSON + aes = self.get_aes(pos) + chk = aes.decrypt(b'{"') + # print('x5') + + if chk != buf[0:2]: + # print('Doesn\'t look like JSON') + # doesn't look like JSON meant for me + continue + + # probably good, read it + aes = self.get_aes(pos) + + chk = trezorcrypto.sha256() + expect = None + + with SFFile(pos, length=FLASH_CACHE_BLOCK_SIZE, pre_erased=True) as fd: + for i in range(FLASH_CACHE_BLOCK_SIZE/32): + enc = fd.read(32) + b = aes.decrypt(enc) + + # print('i={}: {}'.format(i, bytes_to_hex_str(b))) + if i != (FLASH_CACHE_BLOCK_SIZE/32 - 1): + flash_cache_buf[i*32:(i*32)+32] = b + chk.update(b) + else: + expect = b + + try: + + # verify checksum in last 32 bytes + actual = chk.digest() + # print(' Expected: {}'.format(expect)) + # print(' Actual: {}'.format(actual)) + if expect != actual: + # print('ERROR: Checksum doesn\'t match!') + continue + + # loads() can't work from a byte array, and converting to + # bytes here would copy it; better to use file emulation. + fd = BytesIO(flash_cache_buf) + d = ujson.load(fd) + except: + # One in 65k or so chance to come here w/ garbage decoded, so + # not an error. + continue + + got_version = d.get('_revision', 0) + if got_version > self.current.get('_revision', -1): + # print('Possible winner: _version={}'.format(got_version)) + # likely winner + self.current = d + self.my_pos = pos + # print("flash_cache: data @ %d w/ version=%d" % (pos, got_version)) + else: + # print('Cleaning up stale data') + # stale data seen; clean it up. + assert self.current['_revision'] > 0 + #rint("flash_cache: cleanup @ %d" % pos) + self.erase_cache_entry(pos) + + # 16k is a large object, sigh, for us right now. cleanup + gc.collect() + + # done, if we found something + if self.my_pos: + # print('Flash cache Load successful!: current={}'.format(to_str(self.current))) + return + + # print('Nothing found...fall back to defaults') + # nothing found. + self.my_pos = 0 + self.current = self.default_values() + + if empty == len(SLOTS): + # Whole thing is blank. Bad for plausible deniability. Write 3 slots + # with garbage. They will be wasted space until it fills. + blks = list(SLOTS) + trezorcrypto.random.shuffle(blks) + + for pos in blks[0:3]: + for i in range(0, FLASH_CACHE_BLOCK_SIZE, 256): + h = trezorcrypto.random.bytes(256) + sf.wait_done() + sf.write(pos+i, h) + + def get(self, kn, default=None): + return self.current.get(kn, default) + + def changed(self): + self.is_dirty += 1 + if self.is_dirty < 2 and self.loop: + self.loop.call_later_ms(250, self.write_out()) + + def set(self, kn, v): + self.current[kn] = v + self.changed() + + def remove(self, kn): + self.current.pop(kn, None) + self.changed() + + def clear(self): + # could be just: + # self.current = {} + # but accomodating the simulator here + rk = [k for k in self.current if k[0] != '_'] + for k in rk: + del self.current[k] + + self.changed() + + async def write_out(self): + # delayed write handler + if not self.is_dirty: + # someone beat me to it + return + + # Was sometimes running low on memory in this area: recover + try: + gc.collect() + self.save() + except MemoryError: + self.loop.call_later_ms(250, self.write_out()) + + def find_spot(self, not_here=0): + # search for a blank sector to use + # - check randomly and pick first blank one (wear leveling, deniability) + # - we will write and then erase old slot + # - if "full", blow away a random one + from common import sf + + options = [s for s in SLOTS if s != not_here] + trezorcrypto.random.shuffle(options) + + buf = bytearray(16) + for pos in options: + sf.read(pos, buf) + if set(buf) == {0xff}: + # blank + return sf, pos + + victim = options[0] + + # Nowhere to write! (probably a bug because we have lots of slots) + # ... so pick a random slot and kill what it had + # print('ERROR: flash_cache full? Picking random slot to blow away...victim={}'.format(victim)) + + self.erase_cache_entry(victim) + + return sf, victim + + def erase_cache_entry(self, start_pos): + from common import sf + sf.wait_done() + for i in range(FLASH_CACHE_BLOCK_SIZE // SPI_FLASH_SECTOR_SIZE): + addr = start_pos + (i*SPI_FLASH_SECTOR_SIZE) + # print('erasing addr={}'.format(addr)) + sf.sector_erase(addr) + sf.wait_done() + + def save(self): + # render as JSON, encrypt and write it. + + self.current['_revision'] = self.current.get('_revision', 1) + 1 + + sf, pos = self.find_spot(self.my_pos) + # print('save(): sf={}, pos={}'.format(sf, pos)) + + aes = self.get_aes(pos) + + with SFFile(pos, pre_erased=True, max_size=FLASH_CACHE_BLOCK_SIZE) as fd: + chk = trezorcrypto.sha256() + + # first the json data + d = ujson.dumps(self.current) + # print('data: {}'.format(bytes_to_hex_str(d))) + + # pad w/ zeros + data_len = len(d) + pad_len = FLASH_CACHE_MAX_JSON_LEN - data_len + if pad_len < 0: + print('ERROR: JSON data is too big!') + return + + fd.write(aes.encrypt(d)) + chk.update(d) + del d + + # print('data_len={} pad_len={}'.format(data_len, pad_len)) + + while pad_len > 0: + here = min(32, pad_len) + + pad = bytes(here) + fd.write(aes.encrypt(pad)) + chk.update(pad) + # print('pad: {}'.format(bytes_to_hex_str(pad))) + + pad_len -= here + + # print('fd.tell()={}'.format(fd.tell())) + + digest = chk.digest() + # print('Saving with digest={}'.format(digest)) + enc_digest = aes.encrypt(digest) + # print('Encrypted digest={}'.format(enc_digest)) + fd.write(enc_digest) + # print('fd.tell()={} FLASH_CACHE_BLOCK_SIZE={}'.format(fd.tell(), FLASH_CACHE_BLOCK_SIZE)) + assert fd.tell() == FLASH_CACHE_BLOCK_SIZE + + # erase old copy of data + if self.my_pos and self.my_pos != pos: + self.erase_cache_entry(self.my_pos) + + self.my_pos = pos + self.is_dirty = 0 + + def merge(self, prev): + # take a dict of previous values and merge them into what we have + self.current.update(prev) + + def blank(self): + # erase current copy of values in flash cache; older ones may exist still + # - use when clearing the seed value + + if self.my_pos: + self.erase_cache_entry(self.my_pos) + self.my_pos = 0 + + # act blank too, just in case. + self.current.clear() + self.is_dirty = 0 + + @staticmethod + def default_values(): + # Please try to avoid defaults here... It's better to put into code + # where value is used, and treat undefined as the default state. + return dict(_revision=0, _schema=1) + +# EOF diff --git a/ports/stm32/boards/Passport/modules/flow.py b/ports/stm32/boards/Passport/modules/flow.py index 1b1a28c..1c5729c 100644 --- a/ports/stm32/boards/Passport/modules/flow.py +++ b/ports/stm32/boards/Passport/modules/flow.py @@ -1,7 +1,7 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: GPL-3.0-or-later # -# SPDX-FileCopyrightText: 2018 Coinkite, Inc. +# SPDX-FileCopyrightText: 2018 Coinkite, Inc. # SPDX-License-Identifier: GPL-3.0-only # # (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard @@ -15,149 +15,174 @@ from actions import * from choosers import * from common import settings from menu import MenuItem -# from multisig import make_multisig_menu - -def has_secrets(): - from common import pa - return not pa.is_secret_blank() - - -ExportWalletMenu = [ - # Alphabetical order - MenuItem('BlueWallet', f=xpub_qr), - MenuItem('BTCPay', f=electrum_skeleton), - MenuItem('Casa', f=electrum_skeleton), - MenuItem('Electrum', f=electrum_skeleton), - MenuItem('Fully Noded', f=electrum_skeleton), - MenuItem('Gordian', f=electrum_skeleton), - MenuItem('Lily', f=electrum_skeleton), - MenuItem('Sparrow', f=electrum_skeleton), - MenuItem('Specter', f=electrum_skeleton), - MenuItem('Wasabi', f=wasabi_skeleton), - MenuItem('Other', f=electrum_skeleton), -] - -UpdateMenu = [ - MenuItem('Update Firmware', f=microsd_upgrade), +from public_constants import AF_P2WPKH +from multisig import make_multisig_menu +from wallets.utils import has_export_mode +from export import view_backup_password +from utils import is_new_wallet_in_progress, get_accounts +from new_wallet import pair_new_wallet +from ie import show_browser + +FirmwareMenu = [ + MenuItem('Update Firmware', f=update_firmware), MenuItem('Current Version', f=show_version), ] SDCardMenu = [ - #MenuItem("Verify Backup", f=verify_backup), - #MenuItem("Backup System", f=backup_everything), - MenuItem("Dump Summary", f=dump_summary), - MenuItem('Export Wallet', menu=ExportWalletMenu), - #MenuItem('Sign Text File', predicate=has_secrets, f=sign_message_on_sd), - #MenuItem('Upgrade From SD', f=microsd_upgrade), - MenuItem('Format Card', f=wipe_sd_card), + MenuItem('Format Card', f=format_sd_card), MenuItem('List Files', f=list_files), + MenuItem('Export Summary', f=export_summary), ] +def archived_accounts_exist(): + accounts = get_accounts() + for account in accounts: + if account.get('status') == 'r': + return True + return False + +def has_secrets(): + from common import pa + return not pa.is_secret_blank() + AdvancedMenu = [ MenuItem('Change PIN', f=change_pin), - MenuItem("MicroSD Settings", menu=SDCardMenu), - MenuItem("List Addresses", f=coming_soon), - MenuItem('View Seed Words', f=view_seed_words, - predicate=lambda: settings.get('words', True)), - MenuItem("Erase Wallet", f=clear_seed), - - # TODO: Don't we want to allow for this? - # MenuItem('Lock Down Seed', f=convert_bip39_to_bip32, - # predicate=lambda: settings.get('words', True)), + MenuItem('Passphrase', menu_title='Passphrase', chooser=enable_passphrase_chooser), + MenuItem('Sign Text File', predicate=has_secrets, f=sign_message_on_sd), + MenuItem('MicroSD Settings', menu=SDCardMenu), + MenuItem('View Seed Words', f=view_seed_words, predicate=lambda: settings.get('words', True)), + MenuItem('Developer PubKey', f=import_user_firmware_pubkey), + MenuItem('Erase Passport', f=erase_wallet, arg=True) ] BackupMenu = [ - MenuItem("Create Backup", menu=coming_soon), #f=backup_everything), - MenuItem("Verify Backup", menu=coming_soon), #f=verify_backup), - MenuItem("Restore Backup", menu=coming_soon), #f=restore_everything), + MenuItem('Create Backup', f=make_microsd_backup), + MenuItem('Verify Backup', f=verify_microsd_backup), + MenuItem('View Password', f=view_backup_password), + MenuItem('Restore Backup', f=restore_microsd_backup), ] SettingsMenu = [ - MenuItem("About", f=view_ident), - MenuItem('Pair External Wallet', menu=ExportWalletMenu, menu_title='Pair Wallet'), - MenuItem('Sign Text File', predicate=has_secrets, f=sign_message_on_sd), - MenuItem("Update Firmware", menu=UpdateMenu), - MenuItem('Backup Passport', menu=BackupMenu), - MenuItem('Multisig Settings', menu=coming_soon), # make_multisig_menu), - MenuItem('Screen Brightness', chooser=brightness_chooser), - MenuItem('Auto Shutdown', chooser=idle_timeout_chooser), - MenuItem('Advanced Settings', menu=AdvancedMenu, menu_title='Advanced') + MenuItem('About', f=about_info), + MenuItem('Firmware', menu=FirmwareMenu), + MenuItem('Backup', menu=BackupMenu), + # MenuItem('Screen Brightness', chooser=brightness_chooser), + MenuItem('Auto Shutdown', chooser=shutdown_timeout_chooser), + MenuItem('Multisig', menu=make_multisig_menu, arg='Multisig'), + MenuItem('Accounts', menu=make_accounts_menu, arg='Accounts'), + MenuItem('Advanced', menu=AdvancedMenu, menu_title='Advanced') ] NoWalletSettingsMenu = [ - MenuItem("About", f=view_ident), - MenuItem("Update Firmware", menu=UpdateMenu), - MenuItem('Screen Brightness', chooser=brightness_chooser), - MenuItem('Auto Shutdown', chooser=idle_timeout_chooser), + MenuItem('About', f=about_info), + MenuItem('Firmware', menu=FirmwareMenu), + # MenuItem('Screen Brightness', chooser=brightness_chooser), + MenuItem('Auto Shutdown', chooser=shutdown_timeout_chooser), MenuItem('Change PIN', f=change_pin), ] -# User has not entered a PIN yet - Need to be able to update firmware -NoPINMenu = [ - MenuItem('Select PIN', f=initial_pin_setup), - MenuItem('Update Firmware', menu=UpdateMenu), +# ManageAcctMenu = [ +# MenuItem('About', f=account_info), +# MenuItem('Export by QR', f=export_wallet_qr, predicate=lambda:has_export_mode('qr')), +# MenuItem('Export by microSD', f=export_wallet_microsd, predicate=lambda: has_export_mode('microsd')), +# MenuItem('Rename', f=rename_account), +# MenuItem('Archive', f=archive_account) +# ] + +def not_account_zero(): + return common.active_account.get('acct_num') > 0 + +AccountMenu = [ + MenuItem('Rename', f=rename_account), + MenuItem('Delete', f=delete_account, predicate=not_account_zero), ] -ImportMenu = [ - MenuItem("24 Words", menu=start_seed_import, arg=24), - MenuItem("18 Words", menu=start_seed_import, arg=18), - MenuItem("12 Words", menu=start_seed_import, arg=12), - MenuItem("Import XPRV", f=import_xprv), - MenuItem("Dice Rolls", f=import_from_dice), +SeedLengthMenu = [ + MenuItem('24-Word Seed', f=restore_wallet_from_seed, arg=24), + MenuItem('18-Word Seed', f=restore_wallet_from_seed, arg=18), + MenuItem('12-Word Seed', f=restore_wallet_from_seed, arg=12), ] -# has PIN, but no secret seed yet -NoWalletMenu = [ - MenuItem('New Wallet', f=create_new_wallet), - MenuItem('Import Wallet', f=import_wallet, arg=24), - MenuItem('Settings', menu=NoWalletSettingsMenu), +# Has PIN, but no secret seed yet +NoSeedMenu = [ + MenuItem('Create New Seed', f=create_new_seed), + MenuItem('Restore Seed', menu=SeedLengthMenu, menu_title='Seed Length'), + MenuItem('Restore Backup', f=restore_microsd_backup), + MenuItem('Settings', menu=NoWalletSettingsMenu, menu_title='Settings'), ] +from noise_source import NoiseSource + DeveloperMenu = [ - MenuItem('Battery Monitor', f=battery_mon), - MenuItem('Pair External Wallet', menu=ExportWalletMenu, menu_title='Pair Wallet'), - MenuItem('New Wallet', f=create_new_wallet), - MenuItem('Import Wallet', f=import_wallet, arg=24), - MenuItem('View Seed Words', f=view_seed_words, - predicate=lambda: settings.get('words', True)), + # MenuItem('Settings Error 2', f=generate_settings_error2), + # MenuItem('Settings Error', f=generate_settings_error), + MenuItem('Clear OVC', f=clear_ovc), + MenuItem('Test UR1', f=test_ur1), + MenuItem('Reset Device', f=reset_device), + MenuItem('Test Battery Calcs', f=test_battery_calcs), + # MenuItem('Test Folder', f=test_folders), + # MenuItem('Test Enter Number', f=test_num_entry), + MenuItem('Settings', menu=SettingsMenu), + MenuItem('Clear Accts/Multisig', f=clear_accts), + MenuItem('Dump Settings', menu=dump_settings), + MenuItem('Dump Flash Cache', menu=dump_flash_cache), + MenuItem('Toggle Battery Mon', f=toggle_battery_mon), + MenuItem('Toggle Screenshot', f=toggle_screenshot_mode), + MenuItem('Toggle Snapshot', f=toggle_snapshot_mode), + MenuItem('Write Flash Cache', f=test_write_flash_cache), + MenuItem('Read Flash Cache', f=test_read_flash_cache), + MenuItem('Supply Chain Test', f=supply_chain_challenge), + # MenuItem('Address Explorer', f=address_explore), + MenuItem('Import User PubKey', f=import_user_firmware_pubkey), + MenuItem('Read User PubKey', f=read_user_firmware_pubkey), + MenuItem('Test Derive Addrs', f=test_derive_addresses), + MenuItem('Test Seed Check', f=test_seed_check), + MenuItem('Enter Passphrase', f=enter_passphrase, arg='Passphrase'), + MenuItem('Random: All', f=gen_random, arg=NoiseSource.ALL), + # MenuItem('Random: All Except SE', f=gen_random, arg=NoiseSource.AVALANCHE | NoiseSource.MCU | NoiseSource.AMBIENT_LIGHT_SENSOR), + MenuItem('Random: Avalanche', f=gen_random, arg=NoiseSource.AVALANCHE), + MenuItem('Read Ambient', f=read_ambient), + # MenuItem('Battery Monitor', f=battery_mon), + MenuItem('Create New Seed', f=create_new_seed), + MenuItem('Restore SD Card', f=restore_microsd_backup), + MenuItem('View Seed Words', f=view_seed_words, predicate=lambda: settings.get('words', True)), MenuItem('Select PIN', f=initial_pin_setup), MenuItem('Login', f=block_until_login), MenuItem('Update XPUB/XFP', f=update_xpub), - MenuItem('Update Firmware', f=microsd_upgrade), - MenuItem('Format SD Card', f=wipe_sd_card), + MenuItem('Update Firmware', f=update_firmware), + MenuItem('Format SD Card', f=format_sd_card), MenuItem('Enter 12-Word Seed', f=enter_seed_phrase, arg=12), MenuItem('Enter 24-Word Seed', f=enter_seed_phrase, arg=24), - MenuItem('Sign with QR Code', f=sign_tx_from_qr, arg="Scan QR Code"), - MenuItem('Dump Settings', menu=dump_settings), - MenuItem('Get Serial', f=se_get_version), + MenuItem('Sign with QR Code', f=magic_scan, arg='Scan QR Code'), MenuItem('Get Config.', f=se_get_config), - MenuItem('Gen. Random', f=gen_random), MenuItem('Power Mon.', f=show_power_monitor), MenuItem('Board Rev.', f=show_board_rev), - MenuItem("UR Unit Tests", f=test_ur), - MenuItem("Test UR Encoder", f=test_ur_encoder), - MenuItem('Factory Setup', f=factory_setup), + MenuItem('UR Unit Tests', f=test_ur), + MenuItem('Test UR Encoder', f=test_ur_encoder), # Run these three to do a "factory reset" MenuItem('Erase User Settings', f=erase_user_settings), - MenuItem("Erase Wallet", f=clear_seed_no_reset), - MenuItem('Set Blank PIN',f=set_blank_pin), + MenuItem('Erase Passport', f=erase_wallet, arg=False), + # MenuItem('Set Blank PIN', f=set_blank_pin), - MenuItem('Erase ROM Secrets', f=erase_rom_secrets), MenuItem('Test UR1.0', f=test_ur1), ] MainMenu = [ - MenuItem('9 Developer Menu', menu=DeveloperMenu), - MenuItem('Sign with QR Code', f=sign_tx_from_qr, arg="Scan QR Code"), + # MenuItem('Developer Menu', menu=DeveloperMenu), + # MenuItem('Start/Stop Demo', f=toggle_demo), + MenuItem('Sign with QR Code', f=magic_scan, arg='Scan QR Code'), MenuItem('Sign with microSD', f=sign_tx_from_sd), - MenuItem('Verify Address', f=coming_soon, arg="Verify Address"), - MenuItem('Enter Passphrase', f=enter_passphrase, arg="Passphrase"), - MenuItem('Settings', menu=SettingsMenu), + MenuItem('Verify Address', f=verify_address, arg='Verify Address'), + # Show Resume or Pair Wallet menu depending on status + MenuItem('Resume Pair Wallet', f=pair_new_wallet, predicate=is_new_wallet_in_progress), + MenuItem('Pair Wallet', f=pair_new_wallet, predicate=lambda: not is_new_wallet_in_progress(), arg='Pair Wallet'), + MenuItem('Settings', menu=SettingsMenu, menu_title='Settings'), ] -GamesMenu = [ - MenuItem('Developer Menu', menu=DeveloperMenu), +ExtrasMenu = [ + # MenuItem('Developer Menu', menu=DeveloperMenu), MenuItem('Snakamoto', f=play_snake), - MenuItem('StackSats', f=play_stacksats) + MenuItem('Stacking Sats', f=play_stacking_sats), + MenuItem('Internet Browser', f=show_browser) ] diff --git a/ports/stm32/boards/Passport/modules/graphics.py b/ports/stm32/boards/Passport/modules/graphics.py index 2e71423..dfe35b3 100644 --- a/ports/stm32/boards/Passport/modules/graphics.py +++ b/ports/stm32/boards/Passport/modules/graphics.py @@ -1,3 +1,9 @@ +# SPDX-FileCopyrightText: 2021 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# SPDX-FileCopyrightText: 2018 Coinkite, Inc. +# SPDX-License-Identifier: GPL-3.0-only +# # autogenerated; don't edit # class Graphics: @@ -5,7 +11,7 @@ class Graphics: scroll = (3, 61, 1, 0, b'@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@\x00\x00@@\xe0@') - scrollbar = (5, 276, 1, 0, b'\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P\xa8P') + scrollbar = (8, 276, 1, 0, b'\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU\xaaU') space = (13, 3, 2, 0, b'\x80\x08\x80\x08\xff\xf8') @@ -19,46 +25,60 @@ class Graphics: arrow_up = (7, 11, 1, 0, b'\x108|\xfe\x10\x10\x10\x10\x10\x10\x10') - selected = (15, 12, 2, 0, b'\x00\x00\x00\x00\x00\x06\x00\x0c\x00\x18\x0000`\x18\xc0\r\x80\x07\x00\x02\x00\x00\x00') - more_left = (9, 17, 2, 0, b'\x00\x80\x01\x80\x03\x80\x07\x80\x0f\x80\x1f\x80?\x80\x7f\x80\xff\x80\x7f\x80?\x80\x1f\x80\x0f\x80\x07\x80\x03\x80\x01\x80\x00\x80') + fruit = (10, 10, 2, 0, b'\x07\x80\x04\xc0\x1f\x00?\x80\x7f\xc0\x7f@\x7f@~\xc09\x80\x1f\x00') + tetris_pattern_1 = (12, 12, 2, 0, b':\xc0z\xe0\xc0p\xef\xb0\xef\xb0\xe0p\xef\xb0\xef\xb0\xef\xb0\xc0pz\xe0:\xc0') tetris_pattern_6 = (12, 12, 2, 0, b':\xc0z\xe0\xc0p\xef\xb0\xef\xb0\xe0p\xef\xb0\xef\xb0\xef\xb0\xc0pz\xe0:\xc0') + pw_pressed_box_sm = (14, 20, 2, 0, b'?\xf0\x7f\xf8\xe0\x1c\xc0\x0c\xc0\x0c\xc0\x0c\xc0\x0c\xc0\x0c\xc3\x0c\xc7\x8c\xc7\x8c\xc3\x0c\xc0\x0c\xc0\x0c\xc0\x0c\xc0\x0c\xc0\x0c\xe0\x1c\x7f\xf8?\xf0') + wedge = (11, 11, 2, 0, b'\xc0\x00\xf0\x00|\x00\x1f\x00\x07\xc0\x01\xe0\x07\xc0\x1f\x00|\x00\xf0\x00\xc0\x00') - fruit = (10, 10, 2, 0, b'\x07\x80\x04\xc0\x1f\x00?\x80\x7f\xc0\x7f@\x7f@~\xc09\x80\x1f\x00') + x = (15, 15, 2, 0, b'\x00\x00`\x0cp\x1c88\x1cp\x0e\xe0\x07\xc0\x03\x80\x07\xc0\x0e\xe0\x1cp88p\x1c`\x0c\x00\x00') + + pw_filled_box_sm = (14, 20, 2, 0, b'?\xf0\x7f\xf8\xe0\x1c\xc0\x0c\xc0\x0c\xc0\x0c\xc7\x8c\xcf\xcc\xdf\xec\xdf\xec\xdf\xec\xdf\xec\xcf\xcc\xc7\x8c\xc0\x0c\xc0\x0c\xc0\x0c\xe0\x1c\x7f\xf8?\xf0') tetris_pattern_3 = (12, 12, 2, 0, b':\xc0z\xe0\xc0p\xef\xb0\xef\xb0\xe0p\xef\xb0\xef\xb0\xef\xb0\xc0pz\xe0:\xc0') splash = (172, 129, 22, 0, b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1e\x00\x0f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1e\x00\x0f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>\x00\x0f\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00~\x00\x0f\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00~\x00\x0f\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfe\x00\x0f\xe0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfe\x00\x0f\xe0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xfe\x00\x0f\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xfe\x00\x0f\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\xfe\x00\x0f\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xfe\x00\x0f\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xfe\x00\x0f\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\xfe\x00\x0f\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\xfe\x00\x0f\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\xfe\x00\x0f\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00?\xfe\x00\x0f\xff\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00?\xfe\x00\x0f\xff\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xfe\x00\x0f\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00`\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xe0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xe0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00?\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xff\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xff\xe0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xff\xe0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xff\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xff\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\xff\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xff\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\xff\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xff\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xff\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xff\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xff\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xff\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xff\xe0\x00\x7f\xfe\x00\x0f\xff\xc0\x00\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\xff\xc0\x00?\xfc\x00\x07\xff\x80\x00\x7f\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00?\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\x00\x00\x00\x00\x00\x7f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xc0\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xe0\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xe0\x00\x00\x00\x00\x00\x01\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xf0\x00\x00\x00\x00\x00\x01\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xf0\x00\x00\x00\x00\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xf8\x00\x00\x00\x00\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xf8\x00\x00\x00\x00\x00\x07\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfc\x00\x00\x00\x00\x00\x0f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x0f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x1f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\x80\x0f\x80\x0e\x0e\x01\xc0p\x1f\xe0\x00\x10\x03\xff\x80p\x00\xf8\x01\xc0p\xff\x80\x1f\xc0\x0e\x0e\x01\xc0p\x1f\xf0\x008\x03\xff\x80p\x01\xfc\x01\xc0p\xff\x80?\xe0\x0e\x0e\x01\xe0p\x1f\xf8\x008\x03\xff\x80p\x03\xfe\x01\xe0p\xe0\x00x\xf0\x0e\x0e\x01\xf0p\x1c<\x00|\x008\x00p\x07\x8f\x01\xf0p\xe0\x00\xf0x\x0e\x0e\x01\xf8p\x1c\x1e\x00|\x008\x00p\x0f\x07\x81\xf8p\xfe\x00\xe08\x0e\x0e\x01\xfcp\x1c\x0e\x00\xfe\x008\x00p\x0e\x03\x81\xfcp\xfe\x00\xe08\x0e\x0e\x01\xdep\x1c\x0e\x00\xee\x008\x00p\x0e\x03\x81\xdep\xfe\x00\xe08\x0e\x0e\x01\xcfp\x1c\x0e\x01\xef\x008\x00p\x0e\x03\x81\xcfp\xe0\x00\xf0x\x0e\x0e\x01\xc7\xf0\x1c\x1e\x01\xc7\x008\x00p\x0f\x07\x81\xc7\xf0\xe0\x00x\xf0\x0f\x1e\x01\xc3\xf0\x1c<\x03\xc7\x808\x00p\x07\x8f\x01\xc3\xf0\xe0\x00?\xe0\x07\xfc\x01\xc1\xf0\x1f\xf8\x03\x83\x808\x00p\x03\xfe\x01\xc1\xf0\xe0\x00\x1f\xc0\x07\xfc\x01\xc0\xf0\x1f\xf0\x07\x83\xc08\x00p\x01\xfc\x01\xc0\xf0\xe0\x00\x0f\x80\x03\xf8\x01\xc0p\x1f\xe0\x07\x01\xc08\x00p\x00\xf8\x01\xc0p') - tetris_pattern_0 = (12, 12, 2, 0, b':\xc0z\xe0\xc0p\xef\xb0\xef\xb0\xe0p\xef\xb0\xef\xb0\xef\xb0\xc0pz\xe0:\xc0') - battery_100 = (29, 15, 4, 0, b'\xff\xff\xff\xc0\xff\xff\xff\xc0\xc0\x00\x00\xc0\xc0\x00\x00\xc0\xces\x9c\xf8\xces\x9c\xf8\xces\x9c\x18\xces\x9c\x18\xces\x9c\x18\xces\x9c\xf8\xces\x9c\xf8\xc0\x00\x00\xc0\xc0\x00\x00\xc0\xff\xff\xff\xc0\xff\xff\xff\xc0') wordmark = (172, 13, 22, 0, b'\xff\x80\x0f\x80\x0e\x0e\x01\xc0p\x1f\xe0\x00\x10\x03\xff\x80p\x00\xf8\x01\xc0p\xff\x80\x1f\xc0\x0e\x0e\x01\xc0p\x1f\xf0\x008\x03\xff\x80p\x01\xfc\x01\xc0p\xff\x80?\xe0\x0e\x0e\x01\xe0p\x1f\xf8\x008\x03\xff\x80p\x03\xfe\x01\xe0p\xe0\x00x\xf0\x0e\x0e\x01\xf0p\x1c<\x00|\x008\x00p\x07\x8f\x01\xf0p\xe0\x00\xf0x\x0e\x0e\x01\xf8p\x1c\x1e\x00|\x008\x00p\x0f\x07\x81\xf8p\xfe\x00\xe08\x0e\x0e\x01\xfcp\x1c\x0e\x00\xfe\x008\x00p\x0e\x03\x81\xfcp\xfe\x00\xe08\x0e\x0e\x01\xdep\x1c\x0e\x00\xee\x008\x00p\x0e\x03\x81\xdep\xfe\x00\xe08\x0e\x0e\x01\xcfp\x1c\x0e\x01\xef\x008\x00p\x0e\x03\x81\xcfp\xe0\x00\xf0x\x0e\x0e\x01\xc7\xf0\x1c\x1e\x01\xc7\x008\x00p\x0f\x07\x81\xc7\xf0\xe0\x00x\xf0\x0f\x1e\x01\xc3\xf0\x1c<\x03\xc7\x808\x00p\x07\x8f\x01\xc3\xf0\xe0\x00?\xe0\x07\xfc\x01\xc1\xf0\x1f\xf8\x03\x83\x808\x00p\x03\xfe\x01\xc1\xf0\xe0\x00\x1f\xc0\x07\xfc\x01\xc0\xf0\x1f\xf0\x07\x83\xc08\x00p\x01\xfc\x01\xc0\xf0\xe0\x00\x0f\x80\x03\xf8\x01\xc0p\x1f\xe0\x07\x01\xc08\x00p\x00\xf8\x01\xc0p') - box = (30, 43, 4, 0, b'?\xff\xff\xf0\x7f\xff\xff\xf8\xe0\x00\x00\x1c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xe0\x00\x00\x1c\x7f\xff\xff\xf8?\xff\xff\xf0') + fcc_ce_logos = (157, 40, 20, 0, b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00?\x00\x00\x01\x80\x00\xf8\x00\x18\x00\x00\x01\xfc\x00\x00\x0f\xf0\x7f\xff\x01\xff\xc0\x00\x00\xc0\x0f\xff\x000\x00\x00\x0f\xfc\x00\x00?\xf0\x7f\xfe\x07\xff\xf8\x00\x00`?\x8f\xc0`\x00\x00?\xfc\x00\x00\x7f\xf0\x7f\xfc\x0f\xff\xfe\x00\x000p\xf8\xf0\xc0\x00\x00\xff\xfc\x00\x01\xff\xf0\x7f\xf8?\xc0\xff\x00\x00\x18\xc0\x009\x80\x00\x01\xff\xfc\x00\x03\xff\xf0x\x00~\x00?\x80\x00\r\xff\xff\xfb\x00\x00\x03\xff\x80\x00\x07\xfe\x00x\x00\xfc\x00\x0f\xc0\x00\x06\xff\xff\xf6\x00\x00\x07\xfe\x00\x00\x0f\xf8\x00x\x01\xf0\x00\x03\xe0\x00\x03`\x00l\x00\x00\x07\xf8\x00\x00\x1f\xe0\x00x\x03\xe0?\x01\xc0\x00\x01\xe0\x00|\x00\x00\x0f\xe0\x00\x00?\xc0\x00x\x03\xc0\xff\xc0\x80\x00\x00\xe0\x00l\x00\x00\x1f\xc0\x00\x00?\x80\x00x\x07\x83\xff\xf0\x00\x00\x00p\x00\xfc\x00\x00\x1f\x80\x00\x00?\x00\x00x\x07\x83\xff\xf8\x00\x00\x000\xf9\xf8\x00\x00\x1f\x80\x00\x00~\x00\x00x\x0f\x0f\xc0\xf0\x00\x00\x008\xfb\xc0\x00\x00?\x00\x00\x00~\x00\x00x\x0f\x0f\x80`\x00\x00\x00<\x06\xc0\x00\x00?\x00\x00\x00~\x00\x00x\x0f\x1f\x00\x00\x00\x00\x006\x0c\xc0\x00\x00~\x00\x00\x00\xfc\x00\x00x\x1e\x1e\x00\x00\x00\x00\x003\x18\xc0\x00\x00~\x00\x00\x00\xff\xff\x00\x7f\xfe\x1e\x00\x00\x00\x00\x001\xb0\xc0\x00\x00~\x00\x00\x00\xff\xff\x00\x7f\xfe\x1c\x00\x00\x00\x00\x000\xe0\xc0\x00\x00~\x00\x00\x00\xff\xff\x00\x7f\xfe\x1c\x00\x00\x00\x00\x000\xe0\xc0\x00\x00~\x00\x00\x00\xff\xff\x00\x7f\xfe\x1e\x00\x00\x00\x00\x001\xb0\xc0\x00\x00~\x00\x00\x00\xff\xff\x00x\x1e\x1e\x00\x00\x00\x00\x003\x18\xc0\x00\x00~\x00\x00\x00\xff\xff\x00x\x0f\x0f\x00\x00\x00\x00\x00\x1e\x0c\xc0\x00\x00~\x00\x00\x00\xfc\x00\x00x\x0f\x0f\x80p\x00\x00\x00\x1c\x07\x80\x00\x00?\x00\x00\x00~\x00\x00x\x0f\x07\xc0\xf8\x00\x00\x00\x18\x03\x80\x00\x00?\x00\x00\x00~\x00\x00x\x07\x87\xff\xf0\x00\x00\x008\x01\xc0\x00\x00\x1f\x80\x00\x00~\x00\x00x\x07\x83\xff\xf0\x00\x00\x00x\x01\xe0\x00\x00\x1f\x80\x00\x00?\x00\x00x\x03\xc0\xff\xc0\xc0\x00\x00\xd8\x01\xb0\x00\x00\x1f\xc0\x00\x00?\x80\x00x\x03\xe0?\x01\xe0\x00\x01\x98\x03\xd8\x00\x00\x0f\xe0\x00\x00?\xc0\x00x\x01\xf0\x00\x03\xe0\x00\x03\x18\x06l\x00\x00\x07\xf8\x00\x00\x1f\xe0\x00x\x00\xf8\x00\x0f\xc0\x00\x06\x18\x05\xa6\x00\x00\x07\xfe\x00\x00\x0f\xf8\x00x\x00\xfe\x00?\x80\x00\x0c\x1f\xfd\xa3\x00\x00\x03\xff\x80\x00\x07\xfe\x00x\x00?\xc0\xff\x00\x00\x18\x0e\x06a\x80\x00\x01\xff\xfc\x00\x03\xff\xf0x\x00\x1f\xff\xfe\x00\x000\x0e\x03\xc0\xc0\x00\x00\xff\xfc\x00\x01\xff\xf0x\x00\x07\xff\xf8\x00\x00`\x00\x00\x00`\x00\x00?\xfc\x00\x00\x7f\xf0\x00\x00\x01\xff\xc0\x00\x00\xc0\x00\x00\x000\x00\x00\x0f\xfc\x00\x00?\xf0\x00\x00\x00?\x00\x00\x00\x80\x00\x00\x00\x10\x00\x00\x01\xfc\x00\x00\x0f\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') + + selected = (15, 15, 2, 0, b'\x00\x0c\x00\x1c\x008\x000\x00p\x00`\x00\xe0`\xc0q\xc09\x80\x1f\x80\x0f\x00\x07\x00\x02\x00\x00\x00') + + pw_empty_box_sm = (14, 20, 2, 0, b'?\xf0\x7f\xf8\xe0\x1c\xc0\x0c\xc0\x0c\xc0\x0c\xc0\x0c\xc0\x0c\xc0\x0c\xc0\x0c\xc0\x0c\xc0\x0c\xc0\x0c\xc0\x0c\xc0\x0c\xc0\x0c\xc0\x0c\xe0\x1c\x7f\xf8?\xf0') tetris_pattern_4 = (12, 12, 2, 0, b':\xc0z\xe0\xc0p\xef\xb0\xef\xb0\xe0p\xef\xb0\xef\xb0\xef\xb0\xc0pz\xe0:\xc0') + pw_pressed_box_lg = (30, 43, 4, 0, b'?\xff\xff\xf0\x7f\xff\xff\xf8\xe0\x00\x00\x1c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x07\x80\x0c\xc0\x0f\xc0\x0c\xc0\x1f\xe0\x0c\xc0?\xf0\x0c\xc0?\xf0\x0c\xc0?\xf0\x0c\xc0?\xf0\x0c\xc0\x1f\xe0\x0c\xc0\x0f\xc0\x0c\xc0\x07\x80\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xe0\x00\x00\x1c\x7f\xff\xff\xf8?\xff\xff\xf0') + battery_50 = (29, 15, 4, 0, b'\xff\xff\xff\xc0\xff\xff\xff\xc0\xc0\x00\x00\xc0\xc0\x00\x00\xc0\xcep\x00\xf8\xcep\x00\xf8\xcep\x00\x18\xcep\x00\x18\xcep\x00\x18\xcep\x00\xf8\xcep\x00\xf8\xc0\x00\x00\xc0\xc0\x00\x00\xc0\xff\xff\xff\xc0\xff\xff\xff\xc0') + pw_filled_box_lg = (30, 43, 4, 0, b'?\xff\xff\xf0\x7f\xff\xff\xf8\xe0\x00\x00\x1c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x07\x80\x0c\xc0\x1f\xe0\x0c\xc0?\xf0\x0c\xc0\x7f\xf8\x0c\xc0\x7f\xf8\x0c\xc0\xff\xfc\x0c\xc0\xff\xfc\x0c\xc0\xff\xfc\x0c\xc0\xff\xfc\x0c\xc0\x7f\xf8\x0c\xc0\x7f\xf8\x0c\xc0?\xf0\x0c\xc0\x1f\xe0\x0c\xc0\x07\x80\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xe0\x00\x00\x1c\x7f\xff\xff\xf8?\xff\xff\xf0') + tetris_pattern_2 = (12, 12, 2, 0, b':\xc0z\xe0\xc0p\xef\xb0\xef\xb0\xe0p\xef\xb0\xef\xb0\xef\xb0\xc0pz\xe0:\xc0') battery_25 = (29, 15, 4, 0, b'\xff\xff\xff\xc0\xff\xff\xff\xc0\xc0\x00\x00\xc0\xc0\x00\x00\xc0\xce\x00\x00\xf8\xce\x00\x00\xf8\xce\x00\x00\x18\xce\x00\x00\x18\xce\x00\x00\x18\xce\x00\x00\xf8\xce\x00\x00\xf8\xc0\x00\x00\xc0\xc0\x00\x00\xc0\xff\xff\xff\xc0\xff\xff\xff\xc0') - battery_low = (29, 15, 4, 0, b'\xff\xff\xff\xc0\xff\xff\xff\xc0\xc0\x00\x00\xc0\xc0\x00\x00\xc0\xc8\x00\x00\xf8\xc8\x00\x00\xf8\xc8\x00\x00\x18\xc8\x00\x00\x18\xc8\x00\x00\x18\xc8\x00\x00\xf8\xc8\x00\x00\xf8\xc0\x00\x00\xc0\xc0\x00\x00\xc0\xff\xff\xff\xc0\xff\xff\xff\xc0') + pw_empty_box_lg = (30, 43, 4, 0, b'?\xff\xff\xf0\x7f\xff\xff\xf8\xe0\x00\x00\x1c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xe0\x00\x00\x1c\x7f\xff\xff\xf8?\xff\xff\xf0') - xbox = (30, 43, 4, 0, b'?\xff\xff\xf0\x7f\xff\xff\xf8\xe0\x00\x00\x1c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x07\x80\x0c\xc0\x1f\xe0\x0c\xc0?\xf0\x0c\xc0\x7f\xf8\x0c\xc0\x7f\xf8\x0c\xc0\xff\xfc\x0c\xc0\xff\xfc\x0c\xc0\xff\xfc\x0c\xc0\xff\xfc\x0c\xc0\x7f\xf8\x0c\xc0\x7f\xf8\x0c\xc0?\xf0\x0c\xc0\x1f\xe0\x0c\xc0\x07\x80\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xe0\x00\x00\x1c\x7f\xff\xff\xf8?\xff\xff\xf0') + tetris_pattern_0 = (12, 12, 2, 0, b':\xc0z\xe0\xc0p\xef\xb0\xef\xb0\xe0p\xef\xb0\xef\xb0\xef\xb0\xc0pz\xe0:\xc0') + + battery_low = (29, 15, 4, 0, b'\xff\xff\xff\xc0\xff\xff\xff\xc0\xc0\x00\x00\xc0\xc0\x00\x00\xc0\xc8\x00\x00\xf8\xc8\x00\x00\xf8\xc8\x00\x00\x18\xc8\x00\x00\x18\xc8\x00\x00\x18\xc8\x00\x00\xf8\xc8\x00\x00\xf8\xc0\x00\x00\xc0\xc0\x00\x00\xc0\xff\xff\xff\xc0\xff\xff\xff\xc0') - tbox = (30, 43, 4, 0, b'?\xff\xff\xf0\x7f\xff\xff\xf8\xe0\x00\x00\x1c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x07\x80\x0c\xc0\x0f\xc0\x0c\xc0\x1f\xe0\x0c\xc0?\xf0\x0c\xc0?\xf0\x0c\xc0?\xf0\x0c\xc0?\xf0\x0c\xc0\x1f\xe0\x0c\xc0\x0f\xc0\x0c\xc0\x07\x80\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xc0\x00\x00\x0c\xe0\x00\x00\x1c\x7f\xff\xff\xf8?\xff\xff\xf0') + passphrase_icon = (21, 21, 3, 0, b'\x00p\x00\x07\xff\x00?\xff\xe0?\xff\xe0>\x03\xe0>\x01\xe0>y\xe0>y\xe0>y\xe0>y\xe0>\x01\xe0>\x03\xe0>\x7f\xe0>\x7f\xe0\x1e\x7f\xc0\x1e\x7f\xc0\x0f\xff\x80\x07\xff\x00\x03\xfe\x00\x01\xfc\x00\x00p\x00') passport = (124, 12, 16, 0, b'\xfc\x00\x10\x00\x1e\x00\x1e\x00~\x00\x0f\x80\x0f\xc0\x0f\xf0\xfe\x008\x00?\x00?\x00\x7f\x00\x1f\xc0\x0f\xe0\x0f\xf0\xc7\x008\x00s\x00s\x00c\x808\xe0\x0cp\x01\x80\xc3\x00|\x00`\x00`\x00a\x80pp\x0c0\x01\x80\xc3\x00l\x00p\x00p\x00a\x80`0\x0c0\x01\x80\xc7\x00\xee\x00<\x00<\x00c\x80`0\x0cp\x01\x80\xfe\x00\xc6\x00\x1e\x00\x1e\x00\x7f\x00`0\x0f\xe0\x01\x80\xfc\x01\xc7\x00\x07\x00\x07\x00~\x00`0\x0f\xc0\x01\x80\xc0\x01\x83\x00\x03\x00\x03\x00`\x00pp\x0c\xc0\x01\x80\xc0\x03\x83\x80g\x00g\x00`\x008\xe0\x0c\xe0\x01\x80\xc0\x03\x01\x80~\x00~\x00`\x00\x1f\xc0\x0cp\x01\x80\xc0\x07\x01\xc0<\x00<\x00`\x00\x0f\x80\x0c0\x01\x80') + ie_logo = (150, 148, 19, 0, b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\xff\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xff\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xff\xff\xff\xff\xe0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\xff\xff\xff\xff\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xff\xff\xff\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\xff\xff\xff\xff\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00?\xff\xff\xff\xf8\x1f\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xff\xff\xff\xff\x00\x01\xff\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xff\xff\xff\xff\xf0\x00\x00\x7f\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x0f\xff\xff\xff\xff\xff\x80\x00\x00\x1f\xc0\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xfe\x00\x00\x00\x0f\xc0\x00\x00\x00\x00\x00\x00\x00\x0f\xff\xff\xff\xff\xff\xf0\x00\x00\x00\x0f\xe0\x00\x00\x00\x00\x00\x00\x00?\xff\xff\xff\xff\xff\xf0\x00\x00\x00\x03\xe0\x00\x00\x00\x00\x00\x00\x01\xff\xff\xff\xff\xff\xff\xfc\x00\x00\x00\x03\xe0\x00\x00\x00\x00\x00\x00\x07\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x01\xe0\x00\x00\x00\x00\x00\x00\x0f\xff\xff\xff\xff\xff\xff\xff\xc0\x00\x00\x01\xe0\x00\x00\x00\x00\x00\x00\x7f\xff\xff\xff\xff\xff\xff\xff\xf0\x00\x00\x01\xf0\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xf8\x00\x00\x00\xf0\x00\x00\x00\x00\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xfe\x00\x00\x00\xf0\x00\x00\x00\x00\x00\x07\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\x00\xf0\x00\x00\x00\x00\x00\x1f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xc0\x00\x00p\x00\x00\x00\x00\x00?\xff\xff\xff\xff\xff\xff\xff\xff\xff\xe0\x00\x00p\x00\x00\x00\x00\x00\x7f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xf0\x00\x00p\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfc\x00\x00\xf0\x00\x00\x00\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\x00\x00\xe0\x00\x00\x00\x00\x07\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\xe0\x00\x00\x00\x00\x0f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\xe0\x00\x00\x00\x00\x1f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xc0\x00\xe0\x00\x00\x00\x00?\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xe0\x00\xe0\x00\x00\x00\x00\x7f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xf0\x01\xc0\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xf8\x01\xc0\x00\x00\x00\x01\xff\xff\xef\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfc\x01\xc0\x00\x00\x00\x01\xff\xff\x1f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\x03\x80\x00\x00\x00\x03\xff\xfe\x7f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\x03\x80\x00\x00\x00\x07\xff\xf8\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x03\x80\x00\x00\x00\x0f\xff\xf1\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x87\x00\x00\x00\x00\x1f\xff\xe3\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xc7\x00\x00\x00\x00\x1f\xff\x87\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xc7\x00\x00\x00\x00?\xff\x0f\xff\xff\xff\xf0\x00?\xff\xff\xff\xff\xff\xef\x00\x00\x00\x00?\xfc\x1f\xff\xff\xff\x80\x00\x03\xff\xff\xff\xff\xff\xee\x00\x00\x00\x00\x7f\xf8?\xff\xff\xfc\x00\x00\x00\x7f\xff\xff\xff\xff\xfc\x00\x00\x00\x00\xff\xf0?\xff\xff\xf0\x00\x00\x00?\xff\xff\xff\xff\xfc\x00\x00\x00\x00\xff\xc0\x7f\xff\xff\xe0\x00\x00\x00\x0f\xff\xff\xff\xff\xfc\x00\x00\x00\x01\xff\x80\xff\xff\xff\x80\x00\x00\x00\x03\xff\xff\xff\xff\xfc\x00\x00\x00\x01\xff\x01\xff\xff\xff\x00\x00\x00\x00\x01\xff\xff\xff\xff\xfe\x00\x00\x00\x03\xfe\x03\xff\xff\xfc\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00\x00\x00\x03\xfc\x07\xff\xff\xf8\x00\x00\x00\x00\x00\x7f\xff\xff\xff\xfe\x00\x00\x00\x07\xf8\x0f\xff\xff\xf8\x00\x00\x00\x00\x00?\xff\xff\xff\xff\x00\x00\x00\x07\xf0\x1f\xff\xff\xf0\x00\x00\x00\x00\x00\x1f\xff\xff\xff\xff\x00\x00\x00\x0f\xe0\x1f\xff\xff\xe0\x00\x00\x00\x00\x00\x0f\xff\xff\xff\xff\x80\x00\x00\x0f\xc0?\xff\xff\xc0\x00\x00\x00\x00\x00\x07\xff\xff\xff\xff\x80\x00\x00\x0f\x80\x7f\xff\xff\x80\x00\x00\x00\x00\x00\x07\xff\xff\xff\xff\xc0\x00\x00\x1f\x00\xff\xff\xff\x80\x00\x00\x00\x00\x00\x03\xff\xff\xff\xff\xc0\x00\x00\x1e\x01\xff\xff\xff\x00\x00\x00\x00\x00\x00\x01\xff\xff\xff\xff\xc0\x00\x00<\x01\xff\xff\xff\x00\x00\x00\x00\x00\x00\x01\xff\xff\xff\xff\xe0\x00\x008\x03\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x01\xff\xff\xff\xff\xe0\x00\x000\x07\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xe0\x00\x00 \x0f\xff\xff\xfc\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xf0\x00\x00`\x0f\xff\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x7f\xff\xff\xff\xf0\x00\x00@\x1f\xff\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x7f\xff\xff\xff\xf0\x00\x00\x00?\xff\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x7f\xff\xff\xff\xf0\x00\x00\x00\x7f\xff\xff\xf8\x00\x00\x00\x00\x00\x00\x00\x7f\xff\xff\xff\xf8\x00\x00\x00\x7f\xff\xff\xf8\x00\x00\x00\x00\x00\x00\x00\x7f\xff\xff\xff\xf8\x00\x00\x00\xff\xff\xff\xf8\x00\x00\x00\x00\x00\x00\x00?\xff\xff\xff\xf8\x00\x00\x01\xff\xff\xff\xf8\x00\x00\x00\x00\x00\x00\x00?\xff\xff\xff\xf8\x00\x00\x01\xff\xff\xff\xf8\x00\x00\x00\x00\x00\x00\x00?\xff\xff\xff\xf8\x00\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfc\x00\x00\x07\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfc\x00\x00\x07\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfc\x00\x00\x0f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfc\x00\x00\x1f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfc\x00\x00\x1f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfc\x00\x00?\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfc\x00\x00?\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfc\x00\x00\x7f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfc\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfc\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfc\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfc\x00\x01\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfc\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfc\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfc\x00\x07\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfc\x00\x0f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfc\x00\x0f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfc\x00\x1f\xff\xff\xff\xff\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\xff\xff\xff\xff\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00?\xff\xff\xff\xff\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00?\xff\xff\xff\xff\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xff\xff\xff\xff\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xff\xff\xff\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xff\xff\xff\xff\xff\xfc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xff\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\xff\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\xff\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xff\xff\xff\xff\xff\xff\x80\x00\x00\x00\x00\x00\x01\xff\xff\xff\xff\xc0\x0f\xff\xcf\xff\xff\xff\xff\xc0\x00\x00\x00\x00\x00\x03\xff\xff\xff\xff\xc0\x0f\xff\xcf\xff\xff\xff\xff\xe0\x00\x00\x00\x00\x00\x07\xff\xff\xff\xff\x80\x0f\xff\x8f\xff\xff\xff\xff\xe0\x00\x00\x00\x00\x00\x0f\xff\xff\xff\xff\x80\x1f\xff\x87\xff\xff\xff\xff\xf0\x00\x00\x00\x00\x00\x0f\xff\xff\xff\xff\x00\x1f\xff\x07\xff\xff\xff\xff\xf8\x00\x00\x00\x00\x00\x1f\xff\xff\xff\xff\x00\x1f\xff\x03\xff\xff\xff\xff\xfc\x00\x00\x00\x00\x00?\xff\xff\xff\xfe\x00\x1f\xff\x03\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x00\xff\xff\xff\xff\xfe\x00?\xfe\x01\xff\xff\xff\xff\xff\x00\x00\x00\x00\x01\xff\xff\xff\xff\xfe\x00?\xfe\x01\xff\xff\xff\xff\xff\xc0\x00\x00\x00\x03\xff\xff\xff\xff\xfc\x00?\xfc\x00\xff\xff\xff\xff\xff\xe0\x00\x00\x00\x0f\xff\xff\xff\xff\xf8\x00?\xfc\x00\xff\xff\xff\xff\xff\xf8\x00\x00\x00\x1f\xff\xff\xff\xff\xf8\x00?\xfc\x00\x7f\xff\xff\xff\xff\xfe\x00\x00\x00\x7f\xff\xff\xff\xff\xf0\x00\x7f\xf8\x00?\xff\xff\xff\xff\xff\xc0\x00\x03\xff\xff\xff\xff\xff\xf0\x00\x7f\xf8\x00?\xff\xff\xff\xff\xff\xfc\x00?\xff\xff\xff\xff\xff\xe0\x00\x7f\xf8\x00\x1f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xc0\x00\x7f\xf8\x00\x0f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xc0\x00\xff\xf0\x00\x0f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\xff\xf0\x00\x07\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\xff\xf0\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\x00\x00\xff\xf0\x00\x01\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\x00\x00\xff\xe0\x00\x01\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfc\x00\x00\xff\xe0\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xf8\x00\x00\xff\xe0\x00\x00\x7f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xf0\x00\x00\xff\xe0\x00\x00?\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xe0\x00\x00\xff\xe0\x00\x00\x1f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xc0\x00\x00\xff\xe0\x00\x00\x0f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\x00\xff\xe0\x00\x00\x07\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\xff\xe0\x00\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\x00\x00\x00\xff\xe0\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfc\x00\x00\x00\xff\xe0\x00\x00\x00\x7f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xf8\x00\x00\x00\xff\xe0\x00\x00\x00?\xff\xff\xff\xff\xff\xff\xff\xff\xff\xe0\x00\x00\x00\xff\xe0\x00\x00\x00\x1f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xc0\x00\x00\x00\x7f\xe0\x00\x00\x00\x07\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x7f\xf0\x00\x00\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xfe\x00\x00\x00\x00\x7f\xf0\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xf8\x00\x00\x00\x00?\xf8\x00\x00\x00\x01\xff\xff\xff\xff\xff\xff\xff\xff\xf0\x00\x00\x00\x00?\xf8\x00\x00\x00\x07\xff\xff\xff\xff\xff\xff\xff\xff\xc0\x00\x00\x00\x00?\xfe\x00\x00\x00\x1f\xf7\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x1f\xff\x00\x00\x00\x7f\xc0\xff\xff\xff\xff\xff\xff\xfc\x00\x00\x00\x00\x00\x0f\xff\x80\x00\x07\xff\x00?\xff\xff\xff\xff\xff\xe0\x00\x00\x00\x00\x00\x0f\xff\xe0\x00\x1f\xfc\x00\x07\xff\xff\xff\xff\xff\x80\x00\x00\x00\x00\x00\x07\xff\xff\x83\xff\xe0\x00\x01\xff\xff\xff\xff\xf8\x00\x00\x00\x00\x00\x00\x03\xff\xff\xff\xff\x80\x00\x00\x0f\xff\xff\xff\xc0\x00\x00\x00\x00\x00\x00\x01\xff\xff\xff\xfc\x00\x00\x00\x00\xff\xff\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xff\xff\xe0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\xff\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\xff\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') + battery_75 = (29, 15, 4, 0, b'\xff\xff\xff\xc0\xff\xff\xff\xc0\xc0\x00\x00\xc0\xc0\x00\x00\xc0\xces\x80\xf8\xces\x80\xf8\xces\x80\x18\xces\x80\x18\xces\x80\x18\xces\x80\xf8\xces\x80\xf8\xc0\x00\x00\xc0\xc0\x00\x00\xc0\xff\xff\xff\xc0\xff\xff\xff\xc0') tetris_pattern_5 = (12, 12, 2, 0, b':\xc0z\xe0\xc0p\xef\xb0\xef\xb0\xe0p\xef\xb0\xef\xb0\xef\xb0\xc0pz\xe0:\xc0') diff --git a/ports/stm32/boards/Passport/modules/history.py b/ports/stm32/boards/Passport/modules/history.py new file mode 100644 index 0000000..09b9589 --- /dev/null +++ b/ports/stm32/boards/Passport/modules/history.py @@ -0,0 +1,187 @@ +# SPDX-FileCopyrightText: 2021 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# SPDX-FileCopyrightText: 2020 Coinkite, Inc. +# SPDX-License-Identifier: GPL-3.0-only +# +# (c) Copyright 2020 by Coinkite Inc. This file is part of Coldcard +# and is covered by GPLv3 license found in COPYING. +# +# history.py - store some history about past transactions and/or outputs they involved +# +import trezorcrypto, gc, chains +from utils import B2A +from ustruct import pack, unpack +from exceptions import IncorrectUTXOAmount +from ubinascii import b2a_base64, a2b_base64 +from serializations import COutPoint, uint256_from_str +from common import flash_cache + +# Limited space in external flash, so we compress as much as possible: +# - would be bad for privacy to store these **UTXO amounts** in plaintext +# - result is stored in a JSON serialization, so needs to be text encoded +# - using base64, in two parts, concatenated +# - 15 bytes are hash over txnhash:out_num => base64 => 20 chars text +# - 8 bytes exact satoshi value => base64 (pad trimmed) => 11 chars +# - stored satoshi value is XOR'ed with LSB from prevout txn hash, which isn't stored +# - result is a 31 character string for each history entry, plus 4 overhead => 35 each +# + +# We have 16K of space, but that will be shared with address cache +# We limit here to 128 entries (128 * 35 = 4480 bytes) +HISTORY_SAVED = const(128) +HISTORY_MAX_MEM = const(256) + +# length of hashed & encoded key only (base64(15 bytes) => 20) +ENCKEY_LEN = const(20) + +class OutptValueCache: + # storing a list in flash_cache + # - maps from hash of txid:n to expected sats there + # - stored as b64 key concatenated w/ int + KEY = 'ovc' + + # we keep extra entries here during the current power-up + # as defense against using very large txn in the attack + runtime_cache = [] + _cache_loaded = False + + @classmethod + def clear(cls): + cls.runtime_cache.clear() + cls._cache_loaded = True + flash_cache.remove(cls.KEY) + flash_cache.save() + + @classmethod + def load_cache(cls): + # first time: read saved value, but rest of time; use what's in memory + if not cls._cache_loaded: + saved = flash_cache.get(cls.KEY) or [] + cls.runtime_cache.extend(saved) + cls._cache_loaded = True + + @classmethod + def encode_key(cls, prevout): + # hash up the txid and output number, truncate, and encode as base64 + # - truncating at (mod3) bytes so no padding on b64 output + # - expects a COutPoint + md = trezorcrypto.sha256('OutputValueCache') + md.update(prevout.serialize()) + return b2a_base64(md.digest()[:15])[:-1].decode() + + @classmethod + def encode_value(cls, prevout, amt): + # XOR stored value with 64 LSB of original txnhash + xor = pack(' 0.8: + # depth //= 2 + + # also limit in-memory use + cls.load_cache() + if len(cls.runtime_cache) >= HISTORY_MAX_MEM: + del cls.runtime_cache[0] + + # save new addition + assert len(key) == ENCKEY_LEN + assert amount > 0 + entry = key + cls.encode_value(prevout, amount) + cls.runtime_cache.append(entry) + + # update what we're going to save long-term + saved_entries = cls.runtime_cache[-depth:] + # print('Saving cache: {}'.format(saved_entries)) + flash_cache.set(cls.KEY, saved_entries) + +# As we build a new transaction, track what we need to capture +new_outpts = [] + +def add_segwit_utxos(out_idx, amount): + # After signing and finalization, we would know all change outpoints + # (but not the txid yet) + global new_outpts + new_outpts.append((out_idx, amount)) + +def add_segwit_utxos_finalize(txid): + # Once we know the final txid, assume this txn will be broadcast, mined, + # and capture the future UTXO outputs it will represent at that point. + global new_outpts + + # might not have any change, or they may not be segwit + if not new_outpts: + # print('No new outputs!') + return + + # add it to the cache + prevout = COutPoint(uint256_from_str(txid), 0) + for oi, amount in new_outpts: + prevout.n = oi + OutptValueCache.add(prevout, amount) + + new_outpts.clear() + +# shortcut +verify_amount = lambda *a: OutptValueCache.verify_amount(*a) + + +# EOF diff --git a/ports/stm32/boards/Passport/modules/ie.py b/ports/stm32/boards/Passport/modules/ie.py new file mode 100644 index 0000000..c24e8ed --- /dev/null +++ b/ports/stm32/boards/Passport/modules/ie.py @@ -0,0 +1,42 @@ +# SPDX-FileCopyrightText: 2021 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# ie.py - Web browser +# + +from uasyncio import sleep_ms + +async def show_browser(*a): + from common import dis, system + from display import Display, FontSmall + + system.turbo(True) + + # Show the logo and the loading screen + dis.clear() + dis.draw_header('IE9') + logo_w, logo_h = dis.icon_size('ie_logo') + dis.icon(None, Display.HALF_HEIGHT - logo_h//2 - 5, 'ie_logo') + + y = Display.HEIGHT - 68 + dis.text(None, y, 'Loading browser...', font=FontSmall) + + dis.show() + + for i in range(100): + system.progress_bar(i) + await sleep_ms(30) + + # Just kidding! + dis.clear() + dis.draw_header('IE9') + dis.icon(None, Display.HALF_HEIGHT - logo_h//2 - 5, 'ie_logo') + dis.text(None, y, 'Just kidding!', font=FontSmall) + dis.show() + await sleep_ms(2000) + + system.turbo(False) + +# This doesn't do anything obviously since Passport is airgapped! +async def handle_http(url): + await show_browser() diff --git a/ports/stm32/boards/Passport/modules/keypad.py b/ports/stm32/boards/Passport/modules/keypad.py index 7c19ed4..b7224fa 100644 --- a/ports/stm32/boards/Passport/modules/keypad.py +++ b/ports/stm32/boards/Passport/modules/keypad.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: GPL-3.0-or-later # # keypad.py @@ -9,6 +9,9 @@ from foundation import Keypad as _Keypad import utime +import common +from utils import save_qr_code_image + class Keypad: def __init__(self): @@ -34,12 +37,36 @@ class Keypad: 105: '#', } self.last_event_time = utime.ticks_ms() + self.injected_keys = [] def get_event(self): - keycode = self.keypad.get_keycode() + if len(self.injected_keys) > 0: + keycode = self.injected_keys.pop(0) + else: + keycode = self.keypad.get_keycode() + if keycode == None: return None, None + + # Update activity time to defer idle timeout + common.last_activity_time = utime.ticks_ms() + event = self.keycode_to_event(keycode) + + # Handle screenshots and snapshots + if common.screenshot_mode_enabled: + (key, is_down) = event + if key == '#' and is_down: + # print('SCREENSHOT!') + common.dis.screenshot() + elif common.snapshot_mode_enabled: + (key, is_down) = event + if key == '#' and is_down: + # print('SNAPSHOT! SAY CHEESE!') + common.dis.snapshot() + save_qr_code_image(common.qr_buf) + + self.last_event_time = utime.ticks_ms() return event @@ -52,3 +79,28 @@ class Keypad: def get_last_event_time(self): return self.last_event_time + + def inject(self, key, is_down=None): + for code,val in self.key_id_dict.items(): + if key == val: + if is_down == None: + # Inject both down and up events + self.injected_keys.append(code | 0x80) # down + self.injected_keys.append(code) # up + elif is_down == True: + self.injected_keys.append(code | 0x80) # down + else: + self.injected_keys.append(code) # up + return + + # If not found, just do nothing + + def clear_keys(self): + # Clear out any injected keys + self.injected_keys = [] + + # Read keys until nothing left + while True: + keycode = self.keypad.get_keycode() + if keycode == None: + return diff --git a/ports/stm32/boards/Passport/modules/log.py b/ports/stm32/boards/Passport/modules/log.py new file mode 100644 index 0000000..660c5e8 --- /dev/null +++ b/ports/stm32/boards/Passport/modules/log.py @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: 2021 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# log.py - Log file functionality that writes to the console via print() and, if available, also writes to the SD card +# appending to a file named log.log. +# + +def log(msg): + from files import CardSlot, CardMissingError + + # Always write to console + print(msg) + + # Then try the microSD card, but it's fine if not present + try: + with CardSlot() as card: + fname, nice = card.get_file_path('log.log') + with open(fname, 'a') as fd: + fd.write(msg + '\n') + except Exception: + pass diff --git a/ports/stm32/boards/Passport/modules/login_ux.py b/ports/stm32/boards/Passport/modules/login_ux.py index 780cc37..0b05e53 100644 --- a/ports/stm32/boards/Passport/modules/login_ux.py +++ b/ports/stm32/boards/Passport/modules/login_ux.py @@ -1,325 +1,288 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: GPL-3.0-or-later # -# SPDX-FileCopyrightText: 2018 Coinkite, Inc. -# SPDX-License-Identifier: GPL-3.0-only -# -# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard -# and is covered by GPLv3 license found in COPYING. -# -# login.py - UX related to PIN code entry/login. +# login_ux.py - UX related to PIN code entry/login. # # NOTE: Mark3 hardware does not support secondary wallet concept. # -import pincodes + import version -from callgate import show_logout -from display import Display, FontLarge, FontSmall, FontTiny -from common import dis, pa +from display import Display, FontSmall, FontTiny +from common import dis, pa, system, settings from uasyncio import sleep_ms -from utils import pretty_delay, UXStateMachine -from ux import (KeyInputHandler, ux_show_story, ux_show_word_list, ux_enter_pin, ux_shutdown) +from utils import UXStateMachine +from ux import KeyInputHandler, ux_show_story, ux_show_word_list, ux_enter_pin, ux_shutdown, ux_confirm, ux_enter_text +from pincodes import BootloaderError import utime -MAX_PIN_PART_LEN = 6 -MIN_PIN_PART_LEN = 2 +# Separate PIN state machines to keep the logic cleaner in each and make it easier to change messaging in each - -# Separate state machines to keep class LoginUX(UXStateMachine): def __init__(self): # States - self.ENTER_PIN1 = 1 - self.VERIFY_ANTI_PHISHING_WORDS = 2 - self.VERIFY_ANTI_PHISHING_WORDS_FAILED = 3 - self.ENTER_PIN2 = 4 - self.CHECK_PIN = 5 - self.PIN_ATTEMPT_FAILED = 6 - self.SHOW_BRICK_MESSAGE = 7 - - print('LoginUX init: pa={}'.format(pa)) - super().__init__(self.ENTER_PIN1) + self.ENTER_PIN = 1 + self.CHECK_PIN = 2 + self.PIN_ATTEMPT_FAILED = 3 + self.SHOW_BRICK_MESSAGE = 4 + self.ENTER_PASSPHRASE = 5 # Different initial state if we are a brick - # TODO: Why does this say no attempts left? - # if not pa.attempts_left: - # self.state = self.SHOW_BRICK_MESSAGE + if pa.attempts_left == 0: + initial_state = self.SHOW_BRICK_MESSAGE + else: + initial_state = self.ENTER_PIN - self.pin1 = None - self.pin2 = None - self.pin = None + # print('LoginUX init: pa={}'.format(pa)) + super().__init__(initial_state) + self.pin = None async def show(self): while True: - print('show: state={}'.format(self.state)) - if self.state == self.ENTER_PIN1: - self.pin1 = await ux_enter_pin(title='Security Code', heading='Enter Security Code') - if self.pin1 != None and len(self.pin1) >= MIN_PIN_PART_LEN: - self.goto(self.VERIFY_ANTI_PHISHING_WORDS) - - elif self.state == self.VERIFY_ANTI_PHISHING_WORDS: - # TODO: Wrap this function with foundation.busy_bar.show() .hide() sinve this takes a while - words = pincodes.PinAttempt.anti_phishing_words(self.pin1.encode()) - - result = await ux_show_word_list('Security Words', words, heading1='Do you recognize', heading2='these words?') - if result == 'y': - self.goto(self.ENTER_PIN2) + # print('show: state={}'.format(self.state)) + if self.state == self.ENTER_PIN: + self.pin = await ux_enter_pin(title='Login', heading='Enter PIN', left_btn='SHUTDOWN') + if self.pin != None: + self.goto(self.CHECK_PIN) else: - self.goto(self.VERIFY_ANTI_PHISHING_WORDS_FAILED) - - elif self.state == self.VERIFY_ANTI_PHISHING_WORDS_FAILED: - result = await ux_show_story('''\ - If the Security Words do not match, then you either entered the incorrect PIN or your Passport may have been tampered with.''', left_btn='SHUTDOWN', right_btn='RETRY') - if result == 'x': await ux_shutdown() - elif result == 'y': - self.pin1 = None - self.goto(self.ENTER_PIN1) - - elif self.state == self.ENTER_PIN2: - self.pin2 = await ux_enter_pin(title='Login PIN', heading='Enter Login PIN') - if self.pin2 != None and len(self.pin2) >= MIN_PIN_PART_LEN: - self.pin = self.pin1 + self.pin2 - self.goto(self.CHECK_PIN) elif self.state == self.CHECK_PIN: - pa.setup(self.pin) try: - # TODO: Wrap this function with foundation.busy_bar.show() .hide() sinve this takes a while - # Put the hide() in a finally block + from common import dis + dis.fullscreen('Verifying PIN...') + system.show_busy_bar() + pa.setup(self.pin) if pa.login(): # PIN is correct! # NOTE: We never return from this function unless the PIN is correct. - return + enable_passphrase = settings.get('enable_passphrase', False) + if enable_passphrase: + self.goto(self.ENTER_PASSPHRASE) + else: + return except RuntimeError as err: - # TODO: This means the device is bricked - add appropriate handling + system.hide_busy_bar() self.goto(self.PIN_ATTEMPT_FAILED) except BootloaderError as err: + system.hide_busy_bar() self.goto(self.PIN_ATTEMPT_FAILED) + except Exception as err: + # print('Exception err={}'.format(err)) + self.goto(self.PIN_ATTEMPT_FAILED) + finally: + system.hide_busy_bar() elif self.state == self.PIN_ATTEMPT_FAILED: - if pa.attempts_left <= 10: - # TODO: Should we display the PIN here like coldcard did? Seems sketch. - result = await ux_show_story( - 'You have {} attempts remaining before this Passport IS BRICKED FOREVER.\n\nCheck and double-check your entry:\n\n {}'.format(pa.attempts_left, self.pin), - title="WARNING", - left_btn='SHUTDOWN', - right_btn='RETRY', - center_vertically=True, - center=True) - else: - result = await ux_show_story( - 'You have {} attempts remaining.'.format(pa.attempts_left), - title="INCORRECT PIN", - left_btn='SHUTDOWN', - right_btn='RETRY', - center_vertically=True, - center=True) + # Switch to bricked view if no more attempts + if pa.attempts_left == 0: + self.goto(self.SHOW_BRICK_MESSAGE) + continue + + result = await ux_show_story( + 'You have {} attempts remaining.'.format(pa.attempts_left), + title="Wrong PIN", + left_btn='SHUTDOWN', + right_btn='RETRY', + center_vertically=True, + center=True) + if result == 'y': - self.pin1 = None - self.pin2 = None self.pin = None - self.goto(self.ENTER_PIN1) + self.goto(self.ENTER_PIN) elif result == 'x': - self.goto(self.CONFIRM_SHUTDOWN) - - pass + await ux_shutdown() elif self.state == self.SHOW_BRICK_MESSAGE: - msg = '''After %d failed PIN attempts this Passport is locked forever. \ -By design, there is no way to recover the device, and its contents \ -are now forever inaccessible. + msg = '''After %d failed PIN attempts, this Passport is now permanently disabled. -Restore your seed words onto a new Passport.''' % pa.num_fails +Restore a microSD backup or seed phrase onto a new Passport to recover your funds.''' % pa.num_fails - result = await ux_show_story(msg, title='I Am Brick!', left_btn='SHUTDOWN', right_btn='-') + result = await ux_show_story(msg, title='Error', left_btn='SHUTDOWN', right_btn='RESTART') if result == 'x': - self.goto(self.CONFIRM_SHUTDOWN) - else: - while True: - print('ERROR: Should never hit this else case!') - from uasyncio import sleep_ms - await sleep_ms(1000) + await ux_shutdown() + else: + import machine + machine.reset() + + elif self.state == self.ENTER_PASSPHRASE: + import sys + from seed import set_bip39_passphrase + + passphrase = await ux_enter_text('Passphrase', label='Enter Passphrase', left_btn='NONE', right_btn='APPLY') + # print("Entered passphrase = {}".format(passphrase)) + + # if not await ux_confirm('Are you sure you want to apply the passphrase:\n\n{}'.format(passphrase)): + # return + + if passphrase != None and len(passphrase) > 0: + # Applying the passphrase takes a bit of time so show message + from common import dis + dis.fullscreen("Applying Passphrase...") + + system.show_busy_bar() + + try: + err = set_bip39_passphrase(passphrase) + if err: + await ux_show_story('Unable to apply passphrase.') + return -class EnterNewPinUX(UXStateMachine): + except BaseException as exc: + sys.print_exception(exc) + + system.hide_busy_bar() + return + + +class EnterInitialPinUX(UXStateMachine): def __init__(self): # States - self.ENTER_PIN1 = 1 - self.SHOW_ANTI_PHISHING_WORDS = 2 - self.ENTER_PIN2 = 3 + self.ENTER_PIN = 1 - super().__init__(self.ENTER_PIN1) - self.pin1 = [None, None] - self.pin2 = [None, None] + # print('EnterInitialPinUX init: pa={}'.format(pa)) + super().__init__(self.ENTER_PIN) + self.pins = [None, None] # PIN is entered twice for confirmation self.round = 0 - def is_verifying(self): + def is_confirming(self): return self.round == 1 - def pin1_matches(self): - return self.pin1[0] == self.pin1[1] - - def pin2_matches(self): - return self.pin2[0] == self.pin2[1] + def pins_match(self): + return self.pins[0] == self.pins[1] def is_pin_valid(self, pin): - return pin and len(pin) >= MIN_PIN_PART_LEN + return pin != None async def show(self): while True: - if self.state == self.ENTER_PIN1: - heading = '{} Security Code'.format('Reenter' if self.is_verifying() else 'Enter') - self.pin1[self.round] = await ux_enter_pin(title='Security Code', heading=heading) - if self.is_pin_valid(self.pin1[self.round]): - if self.is_verifying(): - if self.pin1_matches(): - self.goto(self.ENTER_PIN2) - #self.goto(self.SHOW_ANTI_PHISHING_WORDS) - else: - result = await ux_show_story('Security Code did not match. Please try again', title="PIN Mismatch", left_btn="SHUTDOWN", right_btn="RETRY") - if result == 'y': - self.pin1[self.round] = None - elif result == 'x': - await ux_shutdown() - - else: - self.goto(self.SHOW_ANTI_PHISHING_WORDS) - else: - # TODO: Error message that PIN is too short - Can we disable the right button until it's long enough? - pass - - elif self.state == self.SHOW_ANTI_PHISHING_WORDS: - words = pincodes.PinAttempt.anti_phishing_words(self.pin1[self.round].encode()) - result = await ux_show_word_list('Security Words', words, heading1='Remember these', heading2='Security Words:', left_btn='BACK', right_btn='OK') - if result == 'x': - self.pin1[self.round] = None - self.goto(self.ENTER_PIN1) - else: - self.goto(self.ENTER_PIN2) - - elif self.state == self.ENTER_PIN2: - heading = '{} Login PIN'.format('Reenter' if self.is_verifying() else 'Enter') - self.pin2[self.round] = await ux_enter_pin(title='Login PIN', heading=heading) - if self.pin2[self.round] == None: - self.goto(self.ENTER_PIN1) + if self.state == self.ENTER_PIN: + heading = '{} PIN'.format('Confirm' if self.is_confirming() else 'Enter') + self.pins[self.round] = await ux_enter_pin(title='Set PIN', heading=heading, left_btn='SHUTDOWN', hide_attempt_counter=True, is_new_pin=True) + if self.pins[self.round] == None: + await ux_shutdown() continue - if self.is_pin_valid(self.pin2[self.round]): - if self.is_verifying(): - if self.pin2_matches(): - self.pin = self.pin1[0] + self.pin2[0] - print('Entered and verified PIN is {}'.format(self.pin)) - return - else: - result = await ux_show_story('Login PIN did not match. Please try again', title="PIN Mismatch", left_btn="SHUTDOWN", right_btn="RETRY") - if result == 'y': - self.pin2[self.round] = None - elif result == 'x': - await ux_shutdown() + if self.is_confirming(): + if self.pins_match(): + self.pin = self.pins[0] + # print('Entered and confirmed PIN is {}'.format(self.pin)) + return else: - # Go back to have the user reenter both PINs and verify that they match - self.round = 1 - self.goto(self.ENTER_PIN1) + result = await ux_show_story('PINs do not match. Please try again.', title="PIN Mismatch", left_btn="SHUTDOWN", right_btn="RETRY", center=True, center_vertically=True) + if result == 'y': + # Reset to initial state so PIN needs to be entered and confirmed again + # since we don't know if they messed up the first entry or the second one. + self.round = 0 + self.pins[0] = None + self.pins[1] = None + continue + elif result == 'x': + await ux_shutdown() else: - # TODO: Error message that PIN is too short - Can we disable the right button until it's long enough? - pass - - else: - while True: - print('ERROR: Should never hit this else case!') - from uasyncio import sleep_ms - await sleep_ms(1000) + # Have the user re-enter the PIN to confirm + self.round = 1 -class ChangePINUX(UXStateMachine): +class ChangePinUX(UXStateMachine): def __init__(self): # States - self.ENTER_PIN1 = 1 - self.ENTER_PIN2 = 2 - self.SHOW_ANTI_PHISHING_WORDS = 3 - self.CHANGE_PIN = 4 - self.CHANGE_FAILED = 5 - self.CHANGE_SUCCESS = 6 + self.ENTER_OLD_PIN = 1 + self.ENTER_NEW_PIN = 2 + self.CHANGE_PIN = 3 + self.CHANGE_FAILED = 4 + self.CHANGE_SUCCESS = 5 - print('LoginUX init: pa={}'.format(pa)) - super().__init__(self.ENTER_PIN1) + # print('ChangePinUX init: pa={}'.format(pa)) + super().__init__(self.ENTER_OLD_PIN) - # Different initial state if we are a brick - # TODO: Why does this say no attempts left? - # if not pa.attempts_left: - # self.state = self.SHOW_BRICK_MESSAGE + self.reset() - self.pin1 = [None, None] - self.pin2 = [None, None] + def reset(self): + self.pins = [None, None] + self.old_pin = None self.round = 0 # Ask for old PIN first, then new + def is_confirming(self): + return self.round == 1 + + def pins_match(self): + return self.pins[0] == self.pins[1] + + def is_pin_valid(self, pin): + return pin != None async def show(self): + from common import system while True: - print('show: state={}'.format(self.state)) - if self.state == self.ENTER_PIN1: - self.pin1[self.round] = await ux_enter_pin(title='Security Code', heading='{} Security Code'.format('Old' if self.round == 0 else 'New')) - if self.pin1[self.round] != None and len(self.pin1[self.round]) >= MIN_PIN_PART_LEN: - if self.round == 1: - self.goto(self.SHOW_ANTI_PHISHING_WORDS) - else: - self.goto(self.ENTER_PIN2) + # print('show: state={}'.format(self.state)) + if self.state == self.ENTER_OLD_PIN: + pin = await ux_enter_pin(title='Change PIN', heading='Enter Current PIN') + if not self.is_pin_valid(pin): + return - elif self.state == self.SHOW_ANTI_PHISHING_WORDS: - start = utime.ticks_us() - words = pincodes.PinAttempt.anti_phishing_words(self.pin1[self.round].encode()) - end = utime.ticks_us() - result = await ux_show_word_list('Security Words', words, heading1='Remember these', heading2='Security Words:', left_btn='BACK', right_btn='OK') - if result == 'x': - self.pin1[self.round] = None - self.goto(self.ENTER_PIN1) - else: - self.goto(self.ENTER_PIN2) - - elif self.state == self.ENTER_PIN2: - self.pin2[self.round] = await ux_enter_pin(title='Login PIN', heading='{} Login PIN'.format('Old' if self.round == 0 else 'New')) - if self.pin2[self.round] != None and len(self.pin2[self.round]) >= MIN_PIN_PART_LEN: - if self.round == 0: - self.round = 1 - self.goto(self.ENTER_PIN1) - else: + self.old_pin = pin + self.goto(self.ENTER_NEW_PIN) + + elif self.state == self.ENTER_NEW_PIN: + pin = await ux_enter_pin( + title='Change PIN', + heading='{} New PIN'.format('Enter' if self.round == 0 else 'Confirm'), + left_btn='BACK', + is_new_pin=not self.is_confirming()) + if not self.is_pin_valid(pin): + self.goto_prev() + + self.pins[self.round] = pin + if self.is_confirming(): + if self.pins_match(): self.goto(self.CHANGE_PIN) + else: + result = await ux_show_story('PINs do not match. Please try again.', title="PIN Mismatch", left_btn="SHUTDOWN", right_btn="RETRY", center=True, center_vertically=True) + if result == 'y': + # Reset to initial state so PIN needs to be entered and confirmed again + # since we don't know if they messed up the first entry or the second one. + self.round = 0 + self.pins[0] = None + self.pins[1] = None + self.goto(self.ENTER_OLD_PIN) + elif result == 'x': + await ux_shutdown() + else: + self.round = 1 + continue elif self.state == self.CHANGE_PIN: try: - print('pin1={} pin2={}'.format(self.pin1, self.pin2)) + # print('Change PIN: old pin={}, new pin={}'.format(self.old_pin, self.pins[0])) args = {} - args['old_pin'] = (self.pin1[0] + self.pin2[0]).encode() - args['new_pin'] = (self.pin1[1] + self.pin2[1]).encode() - print('pa.change: args={}'.format(args)) + args['old_pin'] = (self.old_pin).encode() + args['new_pin'] = (self.pins[0]).encode() + # print('pa.change: args={}'.format(args)) + system.show_busy_bar() pa.change(**args) self.goto(self.CHANGE_SUCCESS) except Exception as err: - print('err={}'.format(err)) + # print('err={}'.format(err)) self.goto(self.CHANGE_FAILED) + finally: + system.hide_busy_bar() elif self.state == self.CHANGE_FAILED: - result = await ux_show_story('Unable to change PIN. The old PIN you entered was incorrect.', title='PIN Error', right_btn='RETRY') + result = await ux_show_story('Unable to change PIN. The current PIN is incorrect.', + center=True, center_vertically=True, title='PIN Error', right_btn='RETRY') if result == 'y': - self.pin1 = [None, None] - self.pin2 = [None, None] - self.round = 0 - self.goto(self.ENTER_PIN1) + self.reset() + self.goto(self.ENTER_OLD_PIN) else: return elif self.state == self.CHANGE_SUCCESS: - dis.fullscreen('PIN changed') - utime.sleep(1) + dis.fullscreen('PIN changed', line2='Restarting...') + utime.sleep(2) + system.reset() return - - else: - while True: - print('ERROR: Should never hit this else case!') - from uasyncio import sleep_ms - await sleep_ms(1000) diff --git a/ports/stm32/boards/Passport/modules/main.py b/ports/stm32/boards/Passport/modules/main.py index 062f825..fff243f 100644 --- a/ports/stm32/boards/Passport/modules/main.py +++ b/ports/stm32/boards/Passport/modules/main.py @@ -1,7 +1,7 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: GPL-3.0-or-later # -# SPDX-FileCopyrightText: 2018 Coinkite, Inc. +# SPDX-FileCopyrightText: 2018 Coinkite, Inc. # SPDX-License-Identifier: GPL-3.0-only # # (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard @@ -13,24 +13,19 @@ import utime import uasyncio.core as asyncio from uasyncio import sleep_ms +from periodic import update_ambient_screen_brightness, update_battery_level, check_auto_shutdown, demo_loop +from schema_evolution import handle_schema_evolutions +# # Show REPL welcome message print("Entered main.py") import gc -print('1: Available RAM = {}'.format(gc.mem_free())) - -# camera = Camera() -# start = utime.ticks_ms() -# camera.copy_capture(bytearray(10), bytearray(10)) -# camera.copy_capture(bytearray(10), bytearray(10)) -# camera.copy_capture(bytearray(10), bytearray(10)) -# end = utime.ticks_ms() -# print('Camera copy and conversion took {}ms'.format(end - start)) +print('Available RAM = {}'.format(gc.mem_free())) SETTINGS_FLASH_START = 0x81E0000 SETTINGS_FLASH_SIZE = 0x20000 -# We run main in a separate task so that the startup loop's variable can be released +# We run main in a separate task so that the startup loop's variables can be released async def main(): from ux import the_ux @@ -39,86 +34,111 @@ async def main(): await sleep_ms(10) await the_ux.interact() -# Setup a new task for the main execution - async def startup(): + import common + from common import pa, loop + from actions import goto_top_menu, validate_passport_hw, initial_pin_setup, start_login_sequence + print("startup()") + common.system.hide_busy_bar() + + import uctypes + buf = uctypes.bytearray_at(0x38000000, 1) - from actions import accept_terms, validate_passport_hw - await accept_terms() + # Check for self-test + if buf[0] == 1: + from self_test_ux import SelfTestUX + self_test = SelfTestUX() + await self_test.show() + + from accept_terms_ux import AcceptTermsUX + accept_terms = AcceptTermsUX() + await accept_terms.show() - # We will come here again if the device is shutdown, but the validation - # words have not been confirmed (assuming the terms were accepted). await validate_passport_hw() - # Setup first PIN if it's blank - from common import pa - from actions import initial_pin_setup + # Setup initial PIN if it's blank if pa.is_blank(): await initial_pin_setup() - # Prompt for PIN and then pick appropriate top-level menu, - # based on contents of secure chip (ie. is there - # a wallet defined) - from actions import start_login_sequence + # Prompt for PIN and then pick appropriate top-level menu await start_login_sequence() - # from actions import test_normal_menu - # await test_normal_menu() - from actions import goto_top_menu - goto_top_menu() + # Set the key for the flash cache (cache is inaccessible prior to user logging in) + common.system.show_busy_bar() + common.flash_cache.set_key() + common.flash_cache.load() + + # Trigger a get here so that the XFP & XPUB are captured + common.settings.get('xfp') + + # See if an update was just performed -- we may need to run a schema evolution script + update_from_to = common.settings.get('update') + # print('update_from_to={}'.format(update_from_to)) + if update_from_to: + await handle_schema_evolutions(update_from_to) + + common.system.hide_busy_bar() + + await goto_top_menu() - from common import loop loop.create_task(main()) -def go(operation='', field='chain', value='BTC'): +def go(): import common from sram4 import viewfinder_buf - print('2: Available RAM = {}'.format(gc.mem_free())) + + # Initialize the common objects # Avalanche noise source from foundation import Noise common.noise = Noise() + # Initialize the seed of the PRNG in MicroPython with a real random number + # We only use the PRNG for non-critical randomness that just needs to be fast + import random + from utils import randint + random.seed(randint(0, 2147483647)) + + # Power monitor + from foundation import Powermon + common.powermon = Powermon() + # Get the async event loop to pass in where needed common.loop = asyncio.get_event_loop() # System from foundation import System common.system = System() + common.system.show_busy_bar() - print('2.75: Available RAM = {}'.format(gc.mem_free())) # Initialize the keypad from keypad import Keypad common.keypad = Keypad() - print('3: Available RAM = {}'.format(gc.mem_free())) # Initialize SD card from files import CardSlot CardSlot.setup() - print('3.5: Available RAM = {}'.format(gc.mem_free())) # External SPI Flash from sflash import SPIFlash common.sf = SPIFlash() - # Initialize NV settings + # Initialize internal flash settings from settings import Settings common.settings = Settings(common.loop) - print('4: Available RAM = {}'.format(gc.mem_free())) + # Initialize the external flash cache + from flash_cache import FlashCache + common.flash_cache = FlashCache(common.loop) # Initialize the display and show the splash screen from display import Display - print("disp 1") common.dis = Display() - print("disp 2") common.dis.set_brightness(common.settings.get('screen_brightness', 100)) - print("disp 3") common.dis.splash() - print('5: Available RAM = {}'.format(gc.mem_free())) # Allocate buffers for camera from constants import VIEWFINDER_WIDTH, VIEWFINDER_HEIGHT, CAMERA_WIDTH, CAMERA_HEIGHT @@ -127,41 +147,19 @@ def go(operation='', field='chain', value='BTC'): import uctypes common.qr_buf = uctypes.bytearray_at(0x20000000, CAMERA_WIDTH * CAMERA_HEIGHT) # common.qr_buf = bytearray(CAMERA_WIDTH * CAMERA_HEIGHT) - print('6: Available RAM = {}'.format(gc.mem_free())) # Viewfinder buf 1s 1 bit per pixel and we round the screen width up to 240 - # so it's a multiple of 8 bits. The screen height of 303 minus 31 for the + # so it's a multiple of 8 bits. The screen height of 303 minus 31 for the # header and 31 for the footer gives 241 pixels, which we round down to 240 # to give one blank (white) line before the footer. common.viewfinder_buf = bytearray((VIEWFINDER_WIDTH * VIEWFINDER_HEIGHT) // 8) - print('7: Available RAM = {}'.format(gc.mem_free())) - # Show REPL welcome message print("Passport by Foundation Devices Inc. (C) 2020.\n") - print('8: Available RAM = {}'.format(gc.mem_free())) - from foundation import SettingsFlash f = SettingsFlash() - if operation == 'dump': - print('Settings = {}'.format(common.settings.curr_dict)) - print('addr = {}'.format(common.settings.addr)) - elif operation == 'erase': - f.erase() - elif operation == 'set': - common.settings.set(field, value) - elif operation == 'stress': - for f in range(35): - print("Round {}:".format(f)) - print(' Settings = {}'.format(common.settings.curr_dict)) - common.settings.set('field_{}'.format(f), f) - common.settings.save() - - print('\nFinal Settings = {}'.format(common.settings.curr_dict)) - - # This "pa" object holds some state shared w/ bootloader about the PIN try: from pincodes import PinAttempt @@ -169,12 +167,28 @@ def go(operation='', field='chain', value='BTC'): common.pa.setup(b'') except RuntimeError as e: print("Secure Element Problem: %r" % e) - print('9: Available RAM = {}'.format(gc.mem_free())) # Setup the startup task common.loop.create_task(startup()) + # Setup check for automatic screen brightness control + # Not used at this time + # common.loop.create_task(update_ambient_screen_brightness()) + + # Setup check to read battery level and put it in common.battery_level + common.loop.create_task(update_battery_level()) + + # Setup check for auto shutdown + common.loop.create_task(check_auto_shutdown()) + + # Setup check to read battery level and put it in common.battery_level + common.loop.create_task(demo_loop()) + + gc.collect() + + print('Available RAM after init = {}'.format(gc.mem_free())) + run_loop() @@ -209,22 +223,5 @@ def run_loop(): except Exception as exc2: sys.print_exception(exc2) - # securely die (wipe memory) - # TODO: Add this back in! - # try: - # import callgate - # callgate.show_logout(1) - # except: pass - - -# Initialization -# - NV storage / options -# - PinAttempt class -# - - -# Required to port -# - pincodes.py - need to simplify -# - go() diff --git a/ports/stm32/boards/Passport/modules/menu.py b/ports/stm32/boards/Passport/modules/menu.py index 7963aec..0baa291 100644 --- a/ports/stm32/boards/Passport/modules/menu.py +++ b/ports/stm32/boards/Passport/modules/menu.py @@ -1,7 +1,7 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: GPL-3.0-or-later # -# SPDX-FileCopyrightText: 2018 Coinkite, Inc. +# SPDX-FileCopyrightText: 2018 Coinkite, Inc. # SPDX-License-Identifier: GPL-3.0-only # # (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard @@ -10,13 +10,14 @@ # menu.py - Implement an interactive menu system. # import gc +import utime from display import Display, FontSmall from uasyncio import sleep_ms from ux import KeyInputHandler, the_ux, ux_shutdown -def start_chooser(chooser, title='Select'): +def start_chooser(chooser, title='Select', show_checks=True): # get which one to show as selected, list of choices, and fcn to call after selected, choices, setter = chooser() @@ -29,12 +30,12 @@ def start_chooser(chooser, title='Select'): the_ux.pop() # make a new menu, just for the choices - m = MenuSystem([MenuItem(c, f=picked) for c in choices], - chooser_mode=True, chosen=selected, title=title) # TODO + m = MenuSystem([MenuItem(c, f=picked, has_submenu=show_checks) for c in choices], + chooser_mode=show_checks, chosen=selected, title=title) the_ux.push(m) class MenuItem: - def __init__(self, label, menu=None, f=None, chooser=None, arg=None, predicate=None, menu_title='Passport'): + def __init__(self, label, menu=None, f=None, chooser=None, arg=None, predicate=None, menu_title='Passport', action=None, has_submenu=True): self.label = label self.arg = arg self.menu_title = menu_title @@ -46,13 +47,21 @@ class MenuItem: self.chooser = chooser if predicate: self.predicate = predicate + if action: + self.action = action + self.has_submenu = has_submenu # Used to determine whether to show the > wedge on the right side async def activate(self, menu, idx): if getattr(self, 'chooser', None): - start_chooser(self.chooser) + start_chooser(self.chooser, title=self.menu_title) else: + # Run action if any (this is some side effect, like setting the current account when entering a menu) + action = getattr(self, 'action', None) + if action: + action(label=self.label, arg=self.arg, menu_title=self.menu_title, index=idx) + # nesting menus, and functions and so on. f = getattr(self, 'next_function', None) if f: @@ -73,18 +82,19 @@ class MenuItem: if m: the_ux.push(m) - class MenuSystem: def __init__(self, menu_items, chooser_mode=False, chosen=None, should_cont=None, space_indicators=False, title="Passport"): self.should_continue = should_cont or (lambda: True) + self.original_items = menu_items self.replace_items(menu_items) self.space_indicators = space_indicators self.chooser_mode = chooser_mode self.chosen = chosen self.title = title - self.input = KeyInputHandler(down='rlduxy', up='xy') + self.input = KeyInputHandler(down='udxy', up='udxy', repeat_delay=250, repeat_speed=10) self.shutdown_btn_enabled = False + self.turbo = None # We rely on this being 3 states: None, False, True # Setup font self.font = FontSmall @@ -103,29 +113,32 @@ class MenuSystem: def early_draw(self, dis): pass + # Submenus can override this def update_contents(self): - # something changed in system state; maybe re-construct menu contents - pass + self.replace_items(self.original_items, True) def replace_items(self, menu_items, keep_position=False): # only safe to keep position if you know number of items isn't changing if not keep_position: self.cursor = 0 self.ypos = 0 + self.items = [m for m in menu_items if not getattr( m, 'predicate', None) or m.predicate()] self.count = len(self.items) + # If we removed items, make sure the cursor is still visible + while self.cursor >= self.count: + self.cursor -= 1 + def show(self): - from common import dis + from common import dis, system # # Redraw the menu. # dis.clear() - # print('cursor=%d ypos=%d' % (self.cursor, self.ypos)) - # subclass hook self.early_draw(dis) @@ -145,26 +158,37 @@ class MenuSystem: for n in range(self.ypos + self.max_lines + 1): if n+self.ypos >= self.count: break - msg = self.items[n+self.ypos].label + menu_item = self.items[n+self.ypos] + msg = menu_item.label is_sel = (self.cursor == n+self.ypos) if is_sel: wedge_w, wedge_h = dis.icon_size('wedge') dis.dis.fill_rect(0, y, Display.WIDTH, menu_item_height - 1, 1) - if not self.chooser_mode: - wedge_offset = 12 if show_scrollbar else 6 - dis.icon(dis.WIDTH - wedge_w - wedge_offset, y + - (menu_item_height - wedge_h) // 2, 'wedge', invert=1) - # TODO: Font was adjusted down by 1px here + dis.text(x, y + 2, msg, font=self.font, invert=1) + + if not self.chooser_mode and menu_item.has_submenu: + wedge_offset = 12 if show_scrollbar else 6 + icon_x = dis.WIDTH - wedge_w - wedge_offset + dis.dis.fill_rect( + icon_x - 2, + y, + Display.WIDTH - (icon_x - 2), + menu_item_height - 1, + 1) + dis.icon( + icon_x, + y + (menu_item_height - wedge_h) // 2, + 'wedge', + invert=1) else: - # TODO: Font was adjusted down by 1px here dis.text(x, y + 2, msg, font=self.font) if msg[0] == ' ' and self.space_indicators: dis.icon(x-2, y + 11, 'space', invert=is_sel) - if self.chosen is not None and (n+self.ypos) == self.chosen: - dis.icon(0, y + 4, 'selected', invert=is_sel) + if self.chooser_mode and self.chosen is not None and (n+self.ypos) == self.chosen: + dis.icon(2, y + 6, 'selected', invert=is_sel) y += menu_item_height if y > Display.HEIGHT - Display.FOOTER_HEIGHT: @@ -183,6 +207,12 @@ class MenuSystem: dis.show() + # We only want to turn it off once rather than whenever it's False, so we + # set to None to avoid turning turbo off again. + if self.turbo == False: + system.turbo(False) + self.turbo = None + def down(self): if self.cursor < self.count-1: self.cursor += 1 @@ -206,10 +236,6 @@ class MenuSystem: self.ypos = max(self.cursor - n, 0) def goto_idx(self, n): - # print('type of n is ', type(n)) - # print('type of self.count is ', type(self.count)) - # print('value of n is ', n) - # print('value of self.count is ', self.count) # skip to any item, force cursor near middle of screen # NOTE: If we get a string error here, it probably means we have # passed the title to a MenuSystem() call as the second parameter instead of as a named parameter @@ -259,6 +285,7 @@ class MenuSystem: gc.collect() await self.activate(ch) + async def wait_choice(self): # Wait until a menu choice is picked; let them move around # the menu, keep redrawing it and so on. @@ -266,8 +293,12 @@ class MenuSystem: key = None while 1: + # Give the menu predicates another chance to run in case they changed + self.update_contents() + self.show() + start = utime.ticks_ms() event = None while True: event = await self.input.get_event() @@ -275,10 +306,23 @@ class MenuSystem: if event != None: break + # Redraw the display if no menu input has occurred + # for a while. Gives the battery icon a chance to update. + end = utime.ticks_ms() + if end - start >= 60000: + event = (None, None) + break + key, event_type = event # print('key={} event_type={}'.format(key, event_type)) if event_type == 'down' or event_type == 'repeat': + + if event_type == 'down': + from common import system + system.turbo(True) + self.turbo = True + if not self.input.kcode_imminent(): if key == 'u': self.up() @@ -286,11 +330,12 @@ class MenuSystem: self.down() if event_type == 'up': + self.turbo = False # We set to False here, but actually turn off after rendering if self.input.kcode_complete(): self.input.kcode_reset(); - print('SHOW SECRET GAMES MENU!') - from flow import GamesMenu - menu_item = MenuItem('Games', GamesMenu, menu_title='Games') + # print('SHOW SECRET EXTRAS MENU!') + from flow import ExtrasMenu + menu_item = MenuItem('Extras', ExtrasMenu, menu_title='Extras') await menu_item.activate(self, 0) return -1 # So that the caller does nothing elif not self.input.kcode_imminent(): @@ -301,5 +346,4 @@ class MenuSystem: # abort/nothing selected/back out? return None - # EOF diff --git a/ports/stm32/boards/Passport/modules/multisig.py b/ports/stm32/boards/Passport/modules/multisig.py index 5428c66..54262b6 100644 --- a/ports/stm32/boards/Passport/modules/multisig.py +++ b/ports/stm32/boards/Passport/modules/multisig.py @@ -1,7 +1,7 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: GPL-3.0-or-later # -# SPDX-FileCopyrightText: 2018 Coinkite, Inc. +# SPDX-FileCopyrightText: 2018 Coinkite, Inc. # SPDX-License-Identifier: GPL-3.0-only # # (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard @@ -9,25 +9,24 @@ # # multisig.py - support code for multisig signing and p2sh in general. # -import sys - -import chains -import stash -import uio -import ure -import ustruct -from actions import needs_microsd -from files import CardMissingError, CardSlot +import stash, chains, ustruct, ure, uio, sys +import trezorcrypto +from ubinascii import hexlify as b2a_hex +from utils import xfp2str, str2xfp, cleanup_deriv_path, keypath_to_str, str_to_keypath +from ux import ux_show_story, ux_confirm, ux_enter_text +from files import CardSlot, CardMissingError +from public_constants import AF_P2SH, AF_P2WSH_P2SH, AF_P2WSH, AFC_SCRIPT, MAX_PATH_DEPTH +from constants import MAX_MULTISIG_NAME_LEN from menu import MenuSystem, MenuItem from opcodes import OP_CHECKMULTISIG -from public_constants import (AF_P2SH, AF_P2WSH, AF_P2WSH_P2SH, AFC_SCRIPT, - MAX_PATH_DEPTH) -#from ubinascii import hexlify as b2a_hex -from utils import cleanup_deriv_path, str2xfp, swab32, xfp2str -from ux import ux_confirm, ux_dramatic_pause, ux_show_story +from actions import needs_microsd +from exceptions import FatalPSBTIssue +from data_codecs.qr_type import QRType +import common # Bitcoin limitation: max number of signatures in CHECK_MULTISIG # - 520 byte redeem script limit <= 15*34 bytes per pubkey == 510 bytes +# - serializations of M/N in redeem scripts assume this range MAX_SIGNERS = const(15) # PSBT Xpub trust policies @@ -35,11 +34,9 @@ TRUST_VERIFY = const(0) TRUST_OFFER = const(1) TRUST_PSBT = const(2) - class MultisigOutOfSpace(RuntimeError): pass - def disassemble_multisig_mn(redeem_script): # pull out just M and N from script. Simple, faster, no memory. @@ -51,7 +48,6 @@ def disassemble_multisig_mn(redeem_script): return M, N - def disassemble_multisig(redeem_script): # Take apart a standard multisig's redeem/witness script, and return M/N and public keys # - only for multisig scripts, not general purpose @@ -99,6 +95,26 @@ def disassemble_multisig(redeem_script): return M, N, pubkeys +def make_redeem_script(M, nodes, subkey_idx): + # take a list of BIP32 nodes, and derive Nth subkey (subkey_idx) and make + # a standard M-of-N redeem script for that. Always applies BIP67 sorting. + N = len(nodes) + assert 1 <= M <= N <= MAX_SIGNERS + + pubkeys = [] + for n in nodes: + copy = n.clone() + copy.derive(subkey_idx, True) + # 0x21 = 33 = len(pubkey) = OP_PUSHDATA(33) + pubkeys.append(b'\x21' + copy.public_key()) + + pubkeys.sort() + + # serialize redeem script + pubkeys.insert(0, bytes([80 + M])) + pubkeys.append(bytes([80 + N, OP_CHECKMULTISIG])) + + return b''.join(pubkeys) class MultisigWallet: # Capture the info we need to store long-term in order to participate in a @@ -114,22 +130,29 @@ class MultisigWallet: FORMAT_NAMES = [ (AF_P2SH, 'p2sh'), (AF_P2WSH, 'p2wsh'), - (AF_P2WSH_P2SH, 'p2wsh-p2sh'), + (AF_P2WSH_P2SH, 'p2sh-p2wsh'), # new name + (AF_P2WSH_P2SH, 'p2wsh-p2sh'), # old name ] - def __init__(self, name, m_of_n, xpubs, addr_fmt=AF_P2SH, common_prefix=None, chain_type='BTC'): + def __init__(self, name, m_of_n, xpubs, id, addr_fmt=AF_P2SH, chain_type='BTC', deriv=None): self.storage_idx = -1 - self.name = name + self.name = name[:MAX_MULTISIG_NAME_LEN] assert len(m_of_n) == 2 self.M, self.N = m_of_n self.chain_type = chain_type or 'BTC' - self.xpubs = xpubs # list of (xfp(int), xpub(str)) - self.common_prefix = common_prefix # example: "45'" for BIP45 .. no m/ prefix - self.addr_fmt = addr_fmt # not clear how useful that is. + assert len(xpubs[0]) == 3 + self.xpubs = xpubs # list of (xfp(int), deriv, xpub(str)) + self.id = id # Unique id to associate multisig info with an account + self.addr_fmt = addr_fmt # address format for wallet + self.my_deriv = deriv - # useful cache value - self.xfps = sorted(k for k, v in self.xpubs) + # calc useful cache value: numeric xfp+subpath, with lookup + self.xfp_paths = {} + for xfp, deriv, _ in self.xpubs: + self.xfp_paths[xfp] = str_to_keypath(xfp, deriv) + + assert len(self.xfp_paths) == self.N, 'dup XFP' # not supported @classmethod def render_addr_fmt(cls, addr_fmt): @@ -138,19 +161,6 @@ class MultisigWallet: return v.upper() return '?' - def serialize(self): - # return a JSON-able object - - opts = dict() - if self.addr_fmt != AF_P2SH: - opts['ft'] = self.addr_fmt - if self.chain_type != 'BTC': - opts['ch'] = self.chain_type - if self.common_prefix: - opts['pp'] = self.common_prefix - - return (self.name, (self.M, self.N), self.xpubs, opts) - @property def chain(self): return chains.get_chain(self.chain_type) @@ -159,95 +169,162 @@ class MultisigWallet: def get_trust_policy(cls): from common import settings - which = settings.get('pms', None) + which = settings.get('multisig_policy', None) if which is None: which = TRUST_VERIFY if cls.exists() else TRUST_OFFER return which + def serialize(self): + # return a JSON-able object + from common import noise + from noise_source import NoiseSource + + opts = dict() + if self.addr_fmt != AF_P2SH: + opts['ft'] = self.addr_fmt + if self.chain_type != 'BTC': + opts['ch'] = self.chain_type + + # Data compression: most legs will all use same derivation. + # put a int(0) in place and set option 'pp' to be derivation + # (used to be common_prefix assumption) + pp = list(sorted(set(d for _,d,_ in self.xpubs))) + if len(pp) == 1: + # generate old-format data, to preserve firmware downgrade path + xp = [(a, c) for a,deriv,c in self.xpubs] + opts['pp'] = pp[0] + else: + # allow for distinct deriv paths on each leg + opts['d'] = pp + xp = [(a, pp.index(deriv),c) for a,deriv,c in self.xpubs] + + return (self.name, (self.M, self.N), xp, opts, self.id, self.my_deriv) + @classmethod def deserialize(cls, vals, idx=-1): # take json object, make instance. - name, m_of_n, xpubs, opts = vals + name, m_of_n, xpubs, opts, id, deriv = vals + + # TODO: This looks like CC legacy code - we can probably remove + if len(xpubs[0]) == 2: + # promote from old format to new: assume common prefix is the derivation + # for all of them + # PROBLEM: we don't have enough info if no common prefix can be assumed + common_prefix = opts.get('pp', None) + if not common_prefix: + # TODO: this should raise a warning, not supported anymore + common_prefix = 'm' + xpubs = [(a, common_prefix, b) for a,b in xpubs] + else: + # new format decompression + if 'd' in opts: + derivs = opts.get('d', None) + xpubs = [(a, derivs[b], c) for a,b,c in xpubs] - rv = cls(name, m_of_n, xpubs, addr_fmt=opts.get('ft', AF_P2SH), - common_prefix=opts.get('pp', None), - chain_type=opts.get('ch', 'BTC')) + rv = cls(name, m_of_n, xpubs, id, addr_fmt=opts.get('ft', AF_P2SH), + chain_type=opts.get('ch', 'BTC'), deriv=deriv) rv.storage_idx = idx return rv @classmethod - def find_match(cls, M, N, fingerprints): - # Find index of matching wallet. Don't de-serialize everything. - # - returns index, or -1 if not found - # - fingerprints are iterable of uint32's: may not be unique! - # - M, N must be known. + def iter_wallets(cls, M=None, N=None, not_idx=None, addr_fmt=None): + # yield MS wallets we know about, that match at least right M,N if known. + # - this is only place we should be searching this list, please!! from common import settings lst = settings.get('multisig', []) - fingerprints = sorted(fingerprints) - assert N == len(fingerprints) - for idx, rec in enumerate(lst): - name, m_of_n, xpubs, opts = rec - if tuple(m_of_n) != (M, N): + if idx == not_idx: + # ignore one by index continue - if sorted(f for f, _ in xpubs) != fingerprints: - continue - return idx - return -1 + if M or N: + # peek at M/N + has_m, has_n = tuple(rec[1]) + if M is not None and has_m != M: continue + if N is not None and has_n != N: continue - @classmethod - def find_candidates(cls, fingerprints): - # Find index of matching wallet and M value. Don't de-serialize everything. - # - returns set of matches, each with M value - # - fingerprints are iterable of uint32's - from common import settings - lst = settings.get('multisig', []) + if addr_fmt is not None: + opts = rec[3] + af = opts.get('ft', AF_P2SH) + if af != addr_fmt: continue - fingerprints = sorted(fingerprints) - N = len(fingerprints) - rv = [] + yield cls.deserialize(rec, idx) - for idx, rec in enumerate(lst): - name, m_of_n, xpubs, opts = rec - if m_of_n[1] != N: - continue - if sorted(f for f, _ in xpubs) != fingerprints: - continue + def get_xfp_paths(self): + # return list of lists [xfp, *deriv] + return list(self.xfp_paths.values()) - rv.append(idx) + @classmethod + def find_match(cls, M, N, xfp_paths, addr_fmt=None): + # Find index of matching wallet + # - xfp_paths is list of lists: [xfp, *path] like in psbt files + # - M and N must be known + # - returns instance, or None if not found + for rv in cls.iter_wallets(M, N, addr_fmt=addr_fmt): + if rv.matching_subpaths(xfp_paths): + return rv - return rv + return None - def assert_matching(self, M, N, fingerprints): + @classmethod + def find_candidates(cls, xfp_paths, addr_fmt=None, M=None): + # Return a list of matching wallets for various M values. + # - xpfs_paths hsould already be sorted + # - returns set of matches, of any M value + + # we know N, but not M at this point. + N = len(xfp_paths) + + matches = [] + for rv in cls.iter_wallets(M=M, addr_fmt=addr_fmt): + if rv.matching_subpaths(xfp_paths): + matches.append(rv) + + return matches + + def matching_subpaths(self, xfp_paths): + # Does this wallet use same set of xfp values, and + # the same prefix path per-each xfp, as indicated + # xfp_paths (unordered)? + # - could also check non-prefix part is all non-hardened + for x in xfp_paths: + if x[0] not in self.xfp_paths: + return False + prefix = self.xfp_paths[x[0]] + + if len(x) < len(prefix): + # PSBT specs a path shorter than wallet's xpub + #print('path len: %d vs %d' % (len(prefix), len(x))) + return False + + comm = len(prefix) + if tuple(prefix[:comm]) != tuple(x[:comm]): + # xfp => maps to wrong path + #print('path mismatch:\n%r\n%r\ncomm=%d' % (prefix[:comm], x[:comm], comm)) + return False + + return True + + def assert_matching(self, M, N, xfp_paths): # compare in-memory wallet with details recovered from PSBT + # - xfp_paths must be sorted already assert (self.M, self.N) == (M, N), "M/N mismatch" - assert len(fingerprints) == N, "XFP count" - assert sorted(fingerprints) == self.xfps, "wrong XFPs" + assert len(xfp_paths) == N, "XFP count" + assert self.matching_subpaths(xfp_paths), "wrong XFP/derivs" @classmethod def quick_check(cls, M, N, xfp_xor): - # quicker USB method. - from common import settings - lst = settings.get('multisig', []) - + # quicker? USB method. rv = [] - for rec in lst: - name, m_of_n, xpubs, opts = rec - if m_of_n[0] != M: - continue - if m_of_n[1] != N: - continue - + for ms in cls.iter_wallets(M, N): x = 0 - for xfp, _ in xpubs: + for xfp in ms.xfp_paths.keys(): x ^= xfp - if x != xfp_xor: - continue + if x != xfp_xor: continue return True @@ -256,12 +333,7 @@ class MultisigWallet: @classmethod def get_all(cls): # return them all, as a generator - from common import settings - - lst = settings.get('multisig', []) - - for idx, v in enumerate(lst): - yield cls.deserialize(v, idx) + return cls.iter_wallets() @classmethod def exists(cls): @@ -269,9 +341,15 @@ class MultisigWallet: from common import settings return bool(settings.get('multisig', False)) + @classmethod + def get_count(cls): + from common import settings + lst = settings.get('multisig', []) + return len(lst) + @classmethod def get_by_idx(cls, nth): - # instance from index number + # instance from index number (used in menu) from common import settings lst = settings.get('multisig', []) try: @@ -281,10 +359,41 @@ class MultisigWallet: return cls.deserialize(obj, nth) - def commit(self): + @classmethod + def get_by_id(cls, id): + # instance from unique id + from common import settings + + lst = settings.get('multisig', []) + # print('get_by_id(): settings.multisig={}'.format(lst)) + + for idx, v in enumerate(lst): + if v[4] == id: + return cls.deserialize(v, idx) + + return None + + @classmethod + def delete_by_id(cls, id): + from utils import to_str + from common import settings + + lst = settings.get('multisig', []) + # print('delete_by_id(): BEFORE: settings.multisig={}'.format(to_str(lst))) + + for idx, v in enumerate(lst): + if v[4] == id: + del lst[idx] + # print('delete_by_id(): AFTER: settings.multisig={}'.format(to_str(lst))) + settings.set('multisig', lst) + # Assumes caller will call save if it's important to do immediately + # We do this as part of updating 'accounts' too, so we only want one save call. + + async def commit(self): # data to save # - important that this fails immediately when nvram overflows from common import settings + from export import auto_backup obj = self.serialize() @@ -295,72 +404,156 @@ class MultisigWallet: self.storage_idx = len(v) v.append(obj) else: - # update: no provision for changing fingerprints - assert sorted(k for k, v in v[self.storage_idx][2]) == self.xfps + # update in place v[self.storage_idx] = obj settings.set('multisig', v) + # Hacky way to communicate back to the New Account flow + common.new_multisig_wallet = self + # print('new_multisig_wallet={}'.format(self)) # save now, rather than in background, so we can recover # from out-of-space situation try: - settings.save() + await settings.save() except: # back out change; no longer sure of NVRAM state try: settings.set('multisig', orig) - settings.save() + await settings.save() + # Shouldn't need to do this since we are going back to the previous values + # await auto_backup() except: pass # give up on recovery raise MultisigOutOfSpace - def has_dup(self): + def has_similar(self): # check if we already have a saved duplicate to this proposed wallet - # - also, flag if it's a dangerous/fraudulent attempt to replace it. + # - return (name_change, diff_items, count_similar) where: + # - name_change is existing wallet that has exact match, different name + # - diff_items: text list of similarity/differences + # - count_similar: same N, same xfp+paths + + lst = self.get_xfp_paths() + c = self.find_match(self.M, self.N, lst, addr_fmt=self.addr_fmt) + if c: + # All details are same: M/N, paths, addr fmt + if self.xpubs != c.xpubs: + return None, ['xpubs'], 0 + elif self.name == c.name: + return None, [], 1 + else: + return c, ['name'], 0 - idx = MultisigWallet.find_match(self.M, self.N, self.xfps) - if idx == -1: - # no matches - return False, 0 + similar = MultisigWallet.find_candidates(lst) + if not similar: + # no matches, good. + return None, [], 0 # See if the xpubs are changing, which is risky... other differences like # name are okay. - o = self.get_by_idx(idx) + diffs = set() + name_diff = None + for c in similar: + if c.M != self.M: + diffs.add('M differs') + if c.addr_fmt != self.addr_fmt: + diffs.add('address type') + if c.name != self.name: + diffs.add('name') + if c.xpubs != self.xpubs: + diffs.add('xpubs') + + return None, diffs, len(similar) + + async def rename(self, new_name): + from common import settings + from export import auto_backup - # Calc apx. number of xpub changes. - diffs = 0 - a = sorted(self.xpubs) - b = sorted(o.xpubs) - assert len(a) == len(b) # because same N - for idx in range(self.N): - if a[idx] != b[idx]: - diffs += 1 + # safety check + existing = self.find_match(self.M, self.N, self.get_xfp_paths()) + assert existing + assert existing.storage_idx == self.storage_idx - return o, diffs + new_name = new_name[:MAX_MULTISIG_NAME_LEN] + lst = settings.get('multisig', []) + self.name = new_name + # Can't modify tuple in place to make it a list, modify, then make a new tuple + w = lst[self.storage_idx] + w = list(w) + w[0] = new_name + w = tuple(w) + lst[self.storage_idx] = w + settings.set('multisig', lst) + await settings.save() + await auto_backup() - def delete(self): + self.storage_idx = -1 + + async def delete(self): # remove saved entry # - important: not expecting more than one instance of this class in memory from common import settings + from export import auto_backup assert self.storage_idx >= 0 # safety check - expect_idx = self.find_match(self.M, self.N, self.xfps) - assert expect_idx == self.storage_idx + existing = self.find_match(self.M, self.N, self.get_xfp_paths()) + assert existing + assert existing.storage_idx == self.storage_idx lst = settings.get('multisig', []) del lst[self.storage_idx] settings.set('multisig', lst) - settings.save() + await settings.save() + await auto_backup() self.storage_idx = -1 def xpubs_with_xfp(self, xfp): # return set of indexes of xpubs with indicated xfp - return set(xp_idx for xp_idx, (wxfp, _) in enumerate(self.xpubs) - if wxfp == xfp) + return set(xp_idx for xp_idx, (wxfp, _, _) in enumerate(self.xpubs) + if wxfp == xfp) + + def yield_addresses(self, start_idx, count, change_idx=0): + # Assuming a suffix of /0/0 on the defined prefix's, yield + # possible deposit addresses for this wallet. Never show + # user the resulting addresses because we cannot be certain + # they are valid and could be signed. And yet, dont blank too many + # spots or else an attacker could grid out a suitable replacement. + ch = self.chain + + assert self.addr_fmt, 'no addr fmt known' + + # setup + nodes = [] + paths = [] + for xfp, deriv, xpub in self.xpubs: + # print('xfp={}'.format(xfp)) + # print('deriv={}'.format(deriv)) + # print('xpub={}'.format(xpub)) + # load bip32 node for each cosigner, derive /0/ based on change idx + node = ch.deserialize_node(xpub, AF_P2SH) + node.derive(change_idx, True) + nodes.append(node) + + # indicate path used (for UX) + path = "(m=%s)/%s/%d/{idx}" % (xfp2str(xfp), deriv, change_idx) + paths.append(path) + + idx = start_idx + while count: + # make the redeem script, convert into address + script = make_redeem_script(self.M, nodes, idx) # idx is the address index + addr = ch.p2sh_address(self.addr_fmt, script) + # addr = addr[0:12] + '___' + addr[12+3:] + + yield idx, [p.format(idx=idx) for p in paths], addr, script + + idx += 1 + count -= 1 def validate_script(self, redeem_script, subpaths=None, xfp_paths=None): # Check we can generate all pubkeys in the redeem script, raise on errors. @@ -369,14 +562,13 @@ class MultisigWallet: # redeem_script: what we expect and we were given # subpaths: pubkey => (xfp, *path) # xfp_paths: (xfp, *path) in same order as pubkeys in redeem script - from psbt import path_to_str subpath_help = [] used = set() ch = self.chain M, N, pubkeys = disassemble_multisig(redeem_script) - assert M == self.M and N == self.N, 'wrong M/N in script' + assert M==self.M and N == self.N, 'wrong M/N in script' for pk_order, pubkey in enumerate(pubkeys): check_these = [] @@ -384,14 +576,13 @@ class MultisigWallet: if subpaths: # in PSBT, we are given a map from pubkey to xfp/path, use it # while remembering it's potentially one-2-many + # TODO: this could be simpler now assert pubkey in subpaths, "unexpected pubkey" xfp, *path = subpaths[pubkey] - for xp_idx, (wxfp, xpub) in enumerate(self.xpubs): - if wxfp != xfp: - continue - if xp_idx in used: - continue # only allow once + for xp_idx, (wxfp, _, xpub) in enumerate(self.xpubs): + if wxfp != xfp: continue + if xp_idx in used: continue # only allow once check_these.append((xp_idx, path)) else: # Without PSBT, USB caller must provide xfp+path @@ -401,20 +592,22 @@ class MultisigWallet: xfp, *path = xfp_paths[pk_order] for xp_idx in self.xpubs_with_xfp(xfp): - if xp_idx in used: - continue # only allow once + if xp_idx in used: continue # only allow once check_these.append((xp_idx, path)) here = None too_shallow = False for xp_idx, path in check_these: # matched fingerprint, try to make pubkey that needs to match - xpub = self.xpubs[xp_idx][1] + # print('xpubs={}'.format(self.xpubs)) + xpub = self.xpubs[xp_idx][-1] - node = ch.deserialize_node(xpub, AF_P2SH) - assert node + node = ch.deserialize_node(xpub, AF_P2SH); assert node dp = node.depth() + #print("%s => deriv=%s dp=%d len(path)=%d path=%s" % + # (xfp2str(xfp), self.xpubs[xp_idx][1], dp, len(path), path)) + if not (0 <= dp <= len(path)): # obscure case: xpub isn't deep enough to represent # indicated path... not wrong really. @@ -423,7 +616,7 @@ class MultisigWallet: for sp in path[dp:]: assert not (sp & 0x80000000), 'hard deriv' - node.derive(sp) # works in-place + node.derive(sp, True) # works in-place found_pk = node.public_key() @@ -432,13 +625,13 @@ class MultisigWallet: # part of the path from fingerprint to here. here = '(m=%s)\n' % xfp2str(xfp) if dp != len(path): - here += 'm' + ('/_'*dp) + path_to_str(path[dp:], '/', 0) + here += 'm' + ('/_'*dp) + keypath_to_str(path[dp:], '/', 0) if found_pk != pubkey: # Not a match but not an error by itself, since might be # another dup xfp to look at still. - # print('pk mismatch: %s => %s != %s' % ( + #print('pk mismatch: %s => %s != %s' % ( # here, b2a_hex(found_pk), b2a_hex(pubkey))) continue @@ -458,11 +651,9 @@ class MultisigWallet: if pk_order: # verify sorted order - assert bytes(pubkey) > bytes( - pubkeys[pk_order-1]), 'BIP67 violation' + assert bytes(pubkey) > bytes(pubkeys[pk_order-1]), 'BIP67 violation' - assert len(used) == self.N, 'not all keys used: %d of %d' % ( - len(used), self.N) + assert len(used) == self.N, 'not all keys used: %d of %d' % (len(used), self.N) return subpath_help @@ -473,6 +664,8 @@ class MultisigWallet: # where label is: # name: nameforwallet # policy: M of N + # format: p2sh (+etc) + # derivation: m/45'/0 (common prefix) # (8digithex): xpub of cosigner # # quick checks: @@ -483,12 +676,12 @@ class MultisigWallet: from common import settings my_xfp = settings.get('xfp') - common_prefix = None + deriv = None xpubs = [] - path_tops = set() M, N = -1, -1 - has_mine = False + has_mine = 0 addr_fmt = AF_P2SH + my_deriv = None expect_chain = chains.current_chain().ctype lines = config.split('\n') @@ -496,23 +689,29 @@ class MultisigWallet: for ln in lines: # remove comments comm = ln.find('#') - if comm != -1: - ln = ln[0:comm] + if comm == 0: + if ':' in ln: # Could be a derivation path in a comment + # Strip off the comment and let the line get trimmed/parsed below + ln = ln[1:] + else: + continue + elif comm != -1: + if not ln[comm+1:comm+2].isdigit(): + ln = ln[0:comm] ln = ln.strip() if ':' not in ln: if 'pub' in ln: - # optimization: allow bare xpub if we can calc xfp + # pointless optimization: allow bare xpub if we can calc xfp label = '0'*8 value = ln else: # complain? - if ln: - print("no colon: " + ln) + #if ln: print("no colon: " + ln) continue else: - label, value = ln.split(':') + label, value = ln.split(':', 1) label = label.lower() value = value.strip() @@ -531,11 +730,10 @@ class MultisigWallet: raise AssertionError('bad policy line') elif label == 'derivation': - # reveal the **common** path derivation for all keys + # reveal the path derivation for following key(s) try: - cp = cleanup_deriv_path(value) - # - not storing "m/" prefix, nor 'm' case which doesn't add any info - common_prefix = None if cp == 'm' else cp[2:] + assert value, 'blank' + deriv = cleanup_deriv_path(value) except BaseException as exc: raise AssertionError('bad derivation line: ' + str(exc)) @@ -557,14 +755,14 @@ class MultisigWallet: continue # deserialize, update list and lots of checks - xfp = cls.check_xpub( - xfp, value, expect_chain, xpubs, path_tops) - - if xfp == my_xfp: - # not conclusive, but enough for error catching. - has_mine = True + is_mine = cls.check_xpub(xfp, value, deriv, expect_chain, my_xfp, xpubs) + if is_mine: + # HACK: We need to know which deriv path is for our XPUB when creating a new account + # This is ugly, but avoids + my_deriv = deriv # Use the last-parsed (pattern is Derivation, then XFP: XPUB + has_mine += 1 - assert len(xpubs), 'need xpubs' + assert len(xpubs), 'No XPUBS found.' if M == N == -1: # default policy: all keys @@ -578,7 +776,7 @@ class MultisigWallet: name = str(name, 'ascii') assert 1 <= len(name) <= 20 except: - raise AssertionError('name must be ascii, 1..20 long') + raise AssertionError('Name must be ascii, 1..20 long') assert 1 <= M <= N <= MAX_SIGNERS, 'M/N range' assert N == len(xpubs), 'wrong # of xpubs, expect %d' % N @@ -586,58 +784,90 @@ class MultisigWallet: # check we're included... do not insert ourselves, even tho we # have enough info, simply because other signers need to know my xpubkey anyway - assert has_mine, 'my key not included' + assert has_mine != 0, 'File does not include a key owned by this Passport' + assert has_mine == 1 # 'my key included more than once' - if not common_prefix and len(path_tops) == 1: - # fill in the common prefix iff we can deduce it from xpubs - common_prefix = path_tops.pop() + from common import noise + from noise_source import NoiseSource + # Hacky way to give the wallet a unique ID and pass it back to the New Account flow for correlation + unique_id = bytearray(8) + noise.random_bytes(unique_id, NoiseSource.MCU) + unique_id = b2a_hex(unique_id).decode('utf-8') # done. have all the parts - return cls(name, (M, N), xpubs, addr_fmt=addr_fmt, - chain_type=expect_chain, common_prefix=common_prefix) + return cls(name, (M, N), xpubs, unique_id, addr_fmt=addr_fmt, chain_type=expect_chain, deriv=my_deriv) @classmethod - def check_xpub(cls, xfp, xpub, expect_chain, xpubs, path_tops): + def check_xpub(cls, xfp, xpub, deriv, expect_chain, my_xfp, xpubs): # Shared code: consider an xpub for inclusion into a wallet, if ok, append - # to list: xpubs, and path_tops + # to list: xpubs with a tuple: (xfp, deriv, xpub) + # return T if it's our own key + # - deriv can be None, and in very limited cases can recover derivation path + # - could enforce all same depth, and/or all depth >= 1, but + # seems like more restrictive than needed, so "m" is allowed try: # Note: addr fmt detected here via SLIP-132 isn't useful node, chain, _ = import_xpub(xpub) except: - print(xpub) raise AssertionError('unable to parse xpub') - assert node.private_key() == None, 'no privkeys plz' - assert chain.ctype == expect_chain, 'wrong chain' + # print('node={}'.format(node)) + # print('node.private_key()={}'.format(node.private_key())) + # print('xfp={}'.format(xfp)) + # print('xpub={}'.format(xpub)) + # print('expect_chain={}'.format(expect_chain)) + # print('my_xfp={}'.format(my_xfp)) + # print('xpubs={}'.format(xpubs)) + + # assert node.private_key() == None # 'no privkeys plz' + assert chain.ctype == expect_chain # 'wrong chain' + + depth = node.depth() - # NOTE: could enforce all same depth, and/or all depth >= 1, but - # seems like more restrictive than needed. - if node.depth() == 1: + if depth == 1: if not xfp: # allow a shortcut: zero/omit xfp => use observed parent value - xfp = swab32(node.fingerprint()) + xfp = node.fingerprint() else: - # generally cannot check fingerprint values, but if we can, do. - assert swab32(node.fingerprint()) == xfp, 'xfp depth=1 wrong' + # generally cannot check fingerprint values, but if we can, do so. + assert node.fingerprint() == xfp, 'xfp depth=1 wrong' - assert xfp, 'need fingerprint' + assert xfp, 'need fingerprint' # happens if bare xpub given - # detect, when possible, if it follows BIP45 ... find the path - path_top = None - if node.depth() == 1: - cn = node.child_num() - path_top = str(cn & 0x7fffffff) - if cn & 0x80000000: - path_top += "'" + # In most cases, we cannot verify the derivation path because it's hardened + # and we know none of the private keys involved. + if depth == 1: + # but derivation is implied at depth==1 + guess = keypath_to_str([node.child_num()], skip=0) - path_tops.add(path_top) + if deriv: + assert guess == deriv, '%s != %s' % (guess, deriv) + else: + deriv = guess # reachable? doubt it + + assert deriv, 'empty deriv' # or force to be 'm'? + assert deriv[0] == 'm' + + # path length of derivation given needs to match xpub's depth + p_len = deriv.count('/') + assert p_len == depth, 'deriv %d != %d xpub depth (xfp=%s)' % ( + p_len, depth, xfp2str(xfp)) + + if xfp == my_xfp: + # its supposed to be my key, so I should be able to generate pubkey + # - might indicate collision on xfp value between co-signers, + # and that's not supported + with stash.SensitiveValues() as sv: + chk_node = sv.derive_path(deriv) + assert node.public_key() == chk_node.public_key(), \ + "(m=%s)/%s wrong pubkey" % (xfp2str(xfp), deriv[2:]) # serialize xpub w/ BIP32 standard now. # - this has effect of stripping SLIP-132 confusion away - xpubs.append((xfp, chain.serialize_public(node, AF_P2SH))) + xpubs.append((xfp, deriv, chain.serialize_public(node, AF_P2SH))) - return xfp + return (xfp == my_xfp) def make_fname(self, prefix, suffix='txt'): rv = '%s-%s.%s' % (prefix, self.name, suffix) @@ -645,38 +875,38 @@ class MultisigWallet: async def export_electrum(self): # Generate and save an Electrum JSON file. - from backups import make_json_wallet + from export import make_json_wallet def doit(): rv = dict(seed_version=17, use_encryption=False, - wallet_type='%dof%d' % (self.M, self.N)) + wallet_type='%dof%d' % (self.M, self.N)) ch = self.chain # the important stuff. - for idx, (xfp, xpub) in enumerate(self.xpubs): + for idx, (xfp, deriv, xpub) in enumerate(self.xpubs): + node = None if self.addr_fmt != AF_P2SH: # CHALLENGE: we must do slip-132 format [yz]pubs here when not p2sh mode. - node = ch.deserialize_node(xpub, AF_P2SH) - assert node + node = ch.deserialize_node(xpub, AF_P2SH); assert node xp = ch.serialize_public(node, self.addr_fmt) else: xp = xpub rv['x%d/' % (idx+1)] = dict( - hw_type='passport', type='hardware', - ckcc_xfp=xfp, - label='Passport %s' % xfp2str(xfp), - derivation='m/'+self.common_prefix, xpub=xp) + hw_type='passport', type='hardware', + ckcc_xfp=xfp, + label='Passport %s' % xfp2str(xfp), + derivation=deriv, xpub=xp) return rv await make_json_wallet('Electrum multisig wallet', doit, - fname_pattern=self.make_fname('el', 'json')) + fname_pattern=self.make_fname('el', 'json')) async def export_wallet_file(self, mode="exported from", extra_msg=None): - # create a text file with the details; ready for import to next Passport + # create a text file with the details; ready for import to next Coldcard from common import settings my_xfp = xfp2str(settings.get('xfp')) @@ -688,8 +918,7 @@ class MultisigWallet: # do actual write with open(fname, 'wt') as fp: - print("# Passport Multisig setup file (%s %s)\n#" % - (mode, my_xfp), file=fp) + # print("# Passport Multisig setup file (%s %s)\n#" % (mode, my_xfp), file=fp) self.render_export(fp) msg = '''Passport multisig setup file written:\n\n%s''' % nice @@ -702,41 +931,70 @@ class MultisigWallet: await needs_microsd() return except Exception as e: - await ux_show_story('Failed to write!\n\n\n'+str(e)) + await ux_show_story('Unable to write!\n\n\n'+str(e)) return def render_export(self, fp): - print("Name: %s\nPolicy: %d of %d" % - (self.name, self.M, self.N), file=fp) - - if self.common_prefix: - print("Derivation: m/%s" % self.common_prefix, file=fp) + # print("Name: %s\nPolicy: %d of %d" % (self.name, self.M, self.N), file=fp) if self.addr_fmt != AF_P2SH: - print("Format: " + self.render_addr_fmt(self.addr_fmt), file=fp) + pass + # print("Format: " + self.render_addr_fmt(self.addr_fmt), file=fp) + + last_deriv = None + for xfp, deriv, val in self.xpubs: + if last_deriv != deriv: + # print("\nDerivation: %s\n" % deriv, file=fp) + last_deriv = deriv + + # print('%s: %s' % (xfp2str(xfp), val), file=fp) + + @classmethod + def guess_addr_fmt(cls, npath): + # Assuming the bips are being respected, what address format will be used, + # based on indicated numeric subkey path observed. + # - return None if unsure, no errors + # + #( "m/45'", 'p2sh', AF_P2SH), + #( "m/48'/{coin}'/0'/1'", 'p2sh_p2wsh', AF_P2WSH_P2SH), + #( "m/48'/{coin}'/0'/2'", 'p2wsh', AF_P2WSH) + + top = npath[0] & 0x7fffffff + if top == npath[0]: + # non-hardened top? rare/bad + return + + if top == 45: + return AF_P2SH + + if top == 48: + if len(npath) < 4: return + + last = npath[3] & 0x7fffffff + if last == 1: + return AF_P2WSH_P2SH + if last == 2: + return AF_P2WSH - print("", file=fp) - for xfp, val in self.xpubs: - print('%s: %s' % (xfp2str(xfp), val), file=fp) @classmethod def import_from_psbt(cls, M, N, xpubs_list): - # given the raw data fro, PSBT global header, offer the user + # given the raw data fro PSBT global header, offer the user # the details, and/or bypass that all and just trust the data. # - xpubs_list is a list of (xfp+path, binary BIP32 xpub) # - already know not in our records. from common import settings - import tcc trust_mode = cls.get_trust_policy() + # print('import_from_psbt(): trust_mode = {}'.format(trust_mode)) if trust_mode == TRUST_VERIFY: # already checked for existing import and wasn't found, so fail - raise AssertionError( - "XPUBs in PSBT do not match any existing wallet") + raise FatalPSBTIssue("XPUBs in PSBT do not match any existing wallet") # build up an in-memory version of the wallet. + # - capture address format based on path used for my leg (if standards compliant) assert N == len(xpubs_list) assert 1 <= M <= N <= MAX_SIGNERS, 'M/N range' @@ -744,55 +1002,108 @@ class MultisigWallet: expect_chain = chains.current_chain().ctype xpubs = [] - has_mine = False - path_tops = set() + has_mine = 0 for k, v in xpubs_list: - xfp, *path = ustruct.unpack_from('<%dI' % (len(k)/4), k, 0) - xpub = tcc.codecs.b58_encode(v) - xfp = cls.check_xpub(xfp, xpub, expect_chain, xpubs, path_tops) - if xfp == my_xfp: - has_mine = True + xfp, *path = ustruct.unpack_from('<%dI' % (len(k)//4), k, 0) + xpub = trezorcrypto.codecs.b58_encode(v) + is_mine = cls.check_xpub(xfp, xpub, keypath_to_str(path, skip=0), + expect_chain, my_xfp, xpubs) + if is_mine: + has_mine += 1 + addr_fmt = cls.guess_addr_fmt(path) - assert has_mine, 'my key not included' + assert has_mine == 1 # 'my key not included' name = 'PSBT-%d-of-%d' % (M, N) - - prefix = path_tops.pop() if len(path_tops) == 1 else None - - ms = cls(name, (M, N), xpubs, chain_type=expect_chain, - common_prefix=prefix) + ms = cls(name, (M, N), xpubs, chain_type=expect_chain, addr_fmt=addr_fmt or AF_P2SH) # may just keep just in-memory version, no approval required, if we are # trusting PSBT's today, otherwise caller will need to handle UX w.r.t new wallet return ms, (trust_mode != TRUST_PSBT) + def validate_psbt_xpubs(self, xpubs_list): + # The xpubs provided in PSBT must be exactly right, compared to our record. + # But we're going to use our own values from setup time anyway. + # Check: + # - chain codes match what we have stored already + # - pubkey vs. path will be checked later + # - xfp+path already checked when selecting this wallet + # - some cases we cannot check, so count those for a warning + # Any issue here is a fraud attempt in some way, not innocent. + # But it would not have tricked us and so the attack targets some other signer. + assert len(xpubs_list) == self.N + + for k, v in xpubs_list: + xfp, *path = ustruct.unpack_from('<%dI' % (len(k)//4), k, 0) + xpub = trezorcrypto.codecs.b58_encode(v) + + # cleanup and normalize xpub + tmp = [] + self.check_xpub(xfp, xpub, keypath_to_str(path, skip=0), self.chain_type, 0, tmp) + (_, deriv, xpub_reserialized) = tmp[0] + assert deriv # because given as arg + + # find in our records. + for (x_xfp, x_deriv, x_xpub) in self.xpubs: + if x_xfp != xfp: continue + # found matching XFP + assert deriv == x_deriv + + assert xpub_reserialized == x_xpub, 'xpub wrong (xfp=%s)' % xfp2str(xfp) + break + else: + assert False # not reachable, since we picked wallet based on xfps + + def get_deriv_paths(self): + # List of unique derivation paths being used. Often length one. + # - also a rendered single-value summary + derivs = sorted(set(d for _,d,_ in self.xpubs)) + + if len(derivs) == 1: + dsum = derivs[0] + else: + dsum = 'Varies (%d)' % len(derivs) + + return derivs, dsum + async def confirm_import(self): + from common import dis + from uasyncio import sleep_ms + # prompt them about a new wallet, let them see details and then commit change. M, N = self.M, self.N if M == N == 1: - exp = 'The one signer must approve spends.' + exp = 'The one signer must approve transactions.' if M == N: - exp = 'All %d co-signers must approve spends.' % N + exp = 'All %d co-signers must approve transactions.' % N elif M == 1: - exp = 'Any signature from %d co-signers will approve spends.' % N + exp = 'Any signature from %d co-signers will approve transactions.' % N else: - exp = '{M} signatures, from {N} possible co-signers, will be required to approve spends.'.format( - M=M, N=N) + exp = '{M} signatures, from {N} possible co-signers, will be required to approve transactions.'.format(M=M, N=N) - # Look for duplicate case. - is_dup, diff_count = self.has_dup() + # Look for duplicate stuff + name_change, diff_items, num_dups = self.has_similar() - if not is_dup: - story = 'Create new multisig wallet?' - elif diff_count: + is_dup = False + if name_change: + story = 'Update only the name of existing multisig config?' + if diff_items: + # Concern here is overwrite when similar, but we don't overwrite anymore, so + # more of a warning about funny business. story = '''\ -CAUTION: This updated wallet has %d different XPUB values, but matching fingerprints \ -and same M of N. Perhaps the derivation path has changed legitimately, otherwise, much \ -DANGER!''' % diff_count +WARNING: This new wallet is similar to an existing wallet, but will NOT replace it. Consider deleting previous wallet first. Differences: \ +''' + ', '.join(diff_items) + is_dup = True + elif num_dups: + story = 'Duplicate wallet. All details are the same as existing.' + is_dup = True else: - story = 'Update existing multisig wallet?' + story = 'Create new multisig wallet?' + + derivs, dsum = self.get_deriv_paths() + story += '''\n Wallet Name: {name} @@ -801,109 +1112,112 @@ Policy: {M} of {N} {exp} +Addresses: + {at} + Derivation: - m/{deriv} + {dsum} -Press (1) to see extended public keys, \ -OK to approve, X to cancel.'''.format(M=M, N=N, name=self.name, exp=exp, - deriv=self.common_prefix or 'unknown') +Press 1 to see extended public keys.'''.format(M=M, N=N, name=self.name, exp=exp, dsum=dsum, + at=self.render_addr_fmt(self.addr_fmt)) # ux_clear_keys(True) while 1: ch = await ux_show_story(story, escape='1') - if ch == '1': - # Show the xpubs; might be 2k or more rendered. - msg = uio.StringIO() - - for idx, (xfp, xpub) in enumerate(self.xpubs): - if idx: - msg.write('\n\n') - - # Not showing index numbers here because order - # is non-deterministic both here, our storage, and in usage. - msg.write('%s:\n%s' % (xfp2str(xfp), xpub)) - - await ux_show_story(msg, title='%d of %d' % (self.M, self.N)) + common.is_new_wallet_a_duplicate = is_dup + # print('self.is_new_wallet_a_duplicate={}'.format(common.is_new_wallet_a_duplicate)) + if ch == '1': + await self.show_detail(verbose=False) continue - if ch == 'y': + if ch == 'y' and not is_dup: # save to nvram, may raise MultisigOutOfSpace - if is_dup: - is_dup.delete() - self.commit() - await ux_dramatic_pause("Saved.", 2) + if name_change: + await name_change.delete() + + assert self.storage_idx == -1 + await self.commit() + await fullscreen("Saved") + await sleep_ms(1000) break return ch + async def show_detail(self, verbose=True): + # Show the xpubs; might be 2k or more rendered. + msg = uio.StringIO() -async def no_ms_yet(*a): - # action for 'no wallets yet' menu item - await ux_show_story("You don't have any multisig wallets yet.") + if verbose: + msg.write(''' +Policy: {M} of {N} +Blockchain: {ctype} -def psbt_xpubs_policy_chooser(): - # Chooser for trust policy - ch = ['Verify Only', 'Offer Import', 'Trust PSBT'] +Addresses: + {at}\n\n'''.format(M=self.M, N=self.N, ctype=self.chain_type, + at=self.render_addr_fmt(self.addr_fmt))) - def xset(idx, text): - from common import settings - settings.set('pms', idx) + # concern: the order of keys here is non-deterministic + for idx, (xfp, deriv, xpub) in enumerate(self.xpubs): + if idx: + msg.write('\n----------\n\n') - return MultisigWallet.get_trust_policy(), ch, xset + msg.write('%s:\n %s\n\n%s\n' % (xfp2str(xfp), deriv, xpub)) + if self.addr_fmt != AF_P2SH: + # SLIP-132 format [yz]pubs here when not p2sh mode. + # - has same info as proper bitcoin serialization, but looks much different + node = self.chain.deserialize_node(xpub, AF_P2SH) + xp = self.chain.serialize_public(node, self.addr_fmt) -async def trust_psbt_menu(*a): - # show a story then go into chooser - from menu import start_chooser + msg.write('\nSLIP-132 equiv:\n%s\n' % xp) - ch = await ux_show_story('''\ -This setting controls what Passport does \ -with the co-signer public keys (XPUB) that may \ -be provided inside a PSBT file. Three choices: + return await ux_show_story(msg, title=self.name) -- Verify Only. Do not import the xpubs found, but do \ -verify the correct wallet already exists on Passport. +async def no_ms_yet(*a): + # action for 'no wallets yet' menu item + await ux_show_story("You don't yet have any multisig accounts.", title='Multisig', center=True, center_vertically=True) -- Offer Import. If it's a new multisig wallet, offer to import \ -the details and store them as a new wallet in Passport. +def psbt_xpubs_policy_chooser(): + from multisig import TRUST_OFFER, TRUST_VERIFY, TRUST_PSBT -- Trust PSBT. Use the wallet data in the PSBT as a temporary, -multisig wallet, and do not import it. This permits some \ -deniability and additional privacy. + # Chooser for trust policy + ch = [ 'Ask to Import', 'Require Existing', 'Skip Verification'] + values = [TRUST_OFFER, TRUST_VERIFY, TRUST_PSBT] + + def set_policy(idx, text): + from common import settings + settings.set('multisig_policy', values[idx]) -When the XPUB data is not provided in the PSBT, regardless of the above, \ -we require the appropriate multisig wallet to already exist \ -on Passport. Default is to 'Offer' unless a multisig wallet already \ -exists, otherwise 'Verify'.''') + return values.index(MultisigWallet.get_trust_policy()), ch, set_policy - if ch == 'x': - return - start_chooser(psbt_xpubs_policy_chooser, title='PSBT Trust Policy') +async def multisig_policy_menu(*a): + # show a story then go into chooser + from menu import start_chooser + start_chooser(psbt_xpubs_policy_chooser, title='Multisig Policy') class MultisigMenu(MenuSystem): @classmethod def construct(cls): # Dynamic menu with user-defined names of wallets shown - #from menu import MenuSystem, MenuItem - from actions import import_multisig + # from menu import MenuSystem, MenuItem + from actions import import_multisig_from_sd, import_multisig_from_qr if not MultisigWallet.exists(): - rv = [MenuItem('(none setup yet)', f=no_ms_yet)] + rv = [MenuItem('(None setup yet)', f=no_ms_yet)] else: rv = [] for ms in MultisigWallet.get_all(): rv.append(MenuItem('%d/%d: %s' % (ms.M, ms.N, ms.name), - menu=make_ms_wallet_menu, arg=ms.storage_idx)) + menu=make_ms_wallet_menu, arg=ms.storage_idx)) - rv.append(MenuItem('Import from SD', f=import_multisig)) - rv.append(MenuItem('Export XPUB', f=export_multisig_xpubs)) - rv.append(MenuItem('Create Airgapped', f=create_ms_step1)) - rv.append(MenuItem('Trust PSBT?', f=trust_psbt_menu)) + rv.append(MenuItem('Import from SD', f=import_multisig_from_sd)) + rv.append(MenuItem('Import from QR', f=import_multisig_from_qr)) + rv.append(MenuItem('Multisig Policy', f=multisig_policy_menu)) return rv @@ -911,88 +1225,96 @@ class MultisigMenu(MenuSystem): # Reconstruct the list of wallets on this dynamic menu, because # we added or changed them and are showing that same menu again. tmp = self.construct() - self.replace_items(tmp) + self.replace_items(tmp, True) - -async def make_multisig_menu(*a): +async def make_multisig_menu(menu, label, item): # list of all multisig wallets, and high-level settings/actions from common import pa if pa.is_secret_blank(): - await ux_show_story("You must have wallet seed before creating multisig wallets.") + await ux_show_story("You must have a wallet seed before creating multisig wallets.") return rv = MultisigMenu.construct() - return MultisigMenu(rv) + return MultisigMenu(rv, title=item.arg) async def make_ms_wallet_menu(menu, label, item): # details, actions on single multisig wallet ms = MultisigWallet.get_by_idx(item.arg) - if not ms: - return + if not ms: return rv = [ - MenuItem('"%s"' % ms.name, f=ms_wallet_detail, arg=ms), + # MenuItem('"%s"' % ms.name, f=ms_wallet_detail, arg=ms), MenuItem('View Details', f=ms_wallet_detail, arg=ms), + MenuItem('Rename', f=ms_wallet_rename, arg=ms), MenuItem('Delete', f=ms_wallet_delete, arg=ms), - MenuItem('Passport Export', f=ms_wallet_ckcc_export, arg=ms), - MenuItem('Electrum Wallet', f=ms_wallet_electrum_export, arg=ms), + + # Not needed + # MenuItem('Passport Export', f=ms_wallet_ckcc_export, arg=ms), + # MenuItem('Electrum Wallet', f=ms_wallet_electrum_export, arg=ms), ] - return rv + return MenuSystem(rv, title='%d/%d: %s' % (ms.M, ms.N, ms.name)) + +async def ms_wallet_rename(menu, label, item): + ms = item.arg + + # Get new name + new_name = await ux_enter_text('Rename', label="Enter multisig name", initial_text=ms.name, + right_btn='RENAME', max_length=MAX_MULTISIG_NAME_LEN) + + if new_name == None: + return + + await ms.rename(new_name) + from ux import the_ux + # pop stack + the_ux.pop() + + # m = the_ux.top_of_stack() + # m.update_contents() async def ms_wallet_delete(menu, label, item): + from uasyncio import sleep_ms + from common import dis ms = item.arg # delete - if not await ux_confirm("Delete this multisig wallet (%s)?\n\nFunds may be impacted." - % ms.name): + if not await ux_confirm("Delete this multisig wallet (%s)?\n\nFunds may be impacted." % ms.name): return - ms.delete() - await ux_dramatic_pause('Deleted.', 3) - - # update/hide from menu - # menu.update_contents() + await ms.delete() + dis.fullscreen('Deleted') + await sleep_ms(1000) from ux import the_ux # pop stack the_ux.pop() - m = the_ux.top_of_stack() - m.update_contents() - - async def ms_wallet_ckcc_export(menu, label, item): # create a text file with the details; ready for import to next Passport ms = item.arg await ms.export_wallet_file() - async def ms_wallet_electrum_export(menu, label, item): # create a JSON file that Electrum can use. Challenges: - # - file contains a derivation path that we don't really know. + # - file contains derivation paths for each co-signer to use # - electrum is using BIP43 with purpose=48 (purpose48_derivation) to make paths like: # m/48'/1'/0'/2' # - other signers might not be Passports (we don't know) # solution: - # - (much earlier) when exporting, include all the paths needed. # - when building air-gap, pick address type at that point, and matching path to suit - # - require a common prefix path here # - could check path prefix and addr_fmt make sense together, but meh. ms = item.arg from actions import electrum_export_story - prefix = ms.common_prefix - if not prefix: - return await ux_show_story("We don't know the common derivation path for " - "these keys, so cannot create Electrum wallet.") + derivs, dsum = ms.get_deriv_paths() msg = 'The new wallet will have derivation path:\n %s\n and use %s addresses.\n' % ( - prefix, MultisigWallet.render_addr_fmt(ms.addr_fmt)) + dsum, MultisigWallet.render_addr_fmt(ms.addr_fmt) ) if await ux_show_story(electrum_export_story(msg)) != 'y': return @@ -1001,38 +1323,39 @@ async def ms_wallet_electrum_export(menu, label, item): async def ms_wallet_detail(menu, label, item): - # show details of single multisig wallet, offer to delete - import chains + # show details of single multisig wallet ms = item.arg - msg = uio.StringIO() - msg.write(''' -Policy: {M} of {N} -Blockchain: {ctype} -Addresses: - {at} -'''.format(M=ms.M, N=ms.N, ctype=ms.chain_type, - at=MultisigWallet.render_addr_fmt(ms.addr_fmt))) + return await ms.show_detail() - if ms.common_prefix: - msg.write('''\ -Derivation: - m/{der} -'''.format(der=ms.common_prefix)) +def generate_multisig_xpub_json(): + from common import settings + xfp = xfp2str(settings.get('xfp', 0)) + chain = chains.current_chain() + fp = uio.StringIO() - msg.write('\n') + fp.write('{\n') + with stash.SensitiveValues() as sv: + for deriv, name, fmt in [ + ("m/45'", 'p2sh', AF_P2SH), + ("m/48'/{coin}'/0'/1'", 'p2wsh_p2sh', AF_P2WSH_P2SH), + ("m/48'/{coin}'/0'/2'", 'p2wsh', AF_P2WSH) + ]: - # concern: the order of keys here is non-deterministic - for idx, (xfp, xpub) in enumerate(ms.xpubs): - if idx: - msg.write('\n') - msg.write('%s:\n%s\n' % (xfp2str(xfp), xpub)) + dd = deriv.format(coin=chain.b44_cointype) + node = sv.derive_path(dd) + xp = chain.serialize_public(node, fmt) + fp.write(' "%s_deriv": "%s",\n' % (name, dd)) + fp.write(' "%s": "%s",\n' % (name, xp)) - await ux_show_story(msg, title=ms.name) + fp.write(' "xfp": "%s"\n}\n' % xfp) + result = fp.getvalue() + # print('xpub json = {}'.format(result)) + return result -async def export_multisig_xpubs(*a): +async def export_multisig_xpubs_to_sd(*a): # WAS: Create a single text file with lots of docs, and all possible useful xpub values. # THEN: Just create the one-liner xpub export value they need/want to support BIP45 # NOW: Export JSON with one xpub per useful address type and semi-standard derivation path @@ -1043,7 +1366,7 @@ async def export_multisig_xpubs(*a): xfp = xfp2str(settings.get('xfp', 0)) chain = chains.current_chain() - fname_pattern = 'ccxp-%s.json' % xfp + fname_pattern = 'passport-%s.json' % xfp msg = '''\ This feature creates a small file containing \ @@ -1054,17 +1377,16 @@ The public keys exported are: BIP45: m/45' -P2WSH-P2SH: +P2SH-P2WSH: m/48'/{coin}'/0'/1' P2WSH: m/48'/{coin}'/0'/2' OK to continue. X to abort. -'''.format(coin=chain.b44_cointype) +'''.format(coin = chain.b44_cointype) resp = await ux_show_story(msg) - if resp != 'y': - return + if resp != 'y': return try: with CardSlot() as card: @@ -1074,12 +1396,12 @@ OK to continue. X to abort. fp.write('{\n') with stash.SensitiveValues() as sv: for deriv, name, fmt in [ - ("m/45'", 'p2sh', AF_P2SH), - ("m/48'/{coin}'/0'/1'", 'p2wsh_p2sh', AF_P2WSH_P2SH), - ("m/48'/{coin}'/0'/2'", 'p2wsh', AF_P2WSH) + ( "m/45'", 'p2sh', AF_P2SH), + ( "m/48'/{coin}'/0'/1'", 'p2sh_p2wsh', AF_P2WSH_P2SH), + ( "m/48'/{coin}'/0'/2'", 'p2wsh', AF_P2WSH) ]: - dd = deriv.format(coin=chain.b44_cointype) + dd = deriv.format(coin = chain.b44_cointype) node = sv.derive_path(dd) xp = chain.serialize_public(node, fmt) fp.write(' "%s_deriv": "%s",\n' % (name, dd)) @@ -1091,21 +1413,18 @@ OK to continue. X to abort. await needs_microsd() return except Exception as e: - await ux_show_story('Failed to write!\n\n\n'+str(e)) + await ux_show_story('Unable to write!\n\n\n'+str(e)) return msg = '''BIP45 multisig xpub file written:\n\n%s''' % nice await ux_show_story(msg) - def import_xpub(ln): # read an xpub/ypub/etc and return BIP32 node and what chain it's on. # - can handle any garbage line # - returns (node, chain, addr_fmt) # - people are using SLIP132 so we need this - import trezorcrypto - import chains - import ure + import chains, ure pat = ure.compile(r'.pub[A-Za-z0-9]+') @@ -1119,8 +1438,7 @@ def import_xpub(ln): for kk in ch.slip132: if found[0] == ch.slip132[kk].hint: try: - node = trezorcrypto.bip32.deserialize( - found, ch.slip132[kk].pub, ch.slip132[kk].priv) + node = trezorcrypto.bip32.deserialize(found, ch.slip132[kk].pub, ch.slip132[kk].priv) chain = ch addr_fmt = kk return (node, ch, kk) @@ -1130,178 +1448,4 @@ def import_xpub(ln): # looked like one, but fail. return None - -async def ondevice_multisig_create(mode='p2wsh', addr_fmt=AF_P2WSH): - # collect all xpub- exports on current SD card (must be > 1) - # - ask for M value - # - create wallet, save and also export - # - also create electrum skel to go with that - # - only expected to work with our ccxp-foo.json export files. - from actions import file_picker - import uos - import ujson - from utils import get_filesize - from common import settings - - chain = chains.current_chain() - my_xfp = settings.get('xfp') - - xpubs = [] - files = [] - has_mine = False - deriv = None - try: - with CardSlot() as card: - for path in card.get_paths(): - for fn, ftype, *var in uos.ilistdir(path): - if ftype == 0x4000: - # ignore subdirs - continue - - if not fn.startswith('ccxp-') or not fn.endswith('.json'): - # wrong prefix/suffix: ignore - continue - - full_fname = path + '/' + fn - - # Conside file size - # sigh, OS/filesystem variations - file_size = var[1] if len( - var) == 2 else get_filesize(full_fname) - - if not (0 <= file_size <= 1000): - # out of range size - continue - - try: - with open(full_fname, 'rt') as fp: - vals = ujson.load(fp) - - ln = vals.get(mode) - - # value in file is BE32, but we want LE32 internally - xfp = str2xfp(vals['xfp']) - if not deriv: - deriv = vals[mode+'_deriv'] - else: - assert deriv == vals[mode + - '_deriv'], "wrong derivation" - - node, _, _ = import_xpub(ln) - - if xfp == my_xfp: - has_mine = True - - xpubs.append( - (xfp, chain.serialize_public(node, AF_P2SH))) - files.append(fn) - - except CardMissingError: - raise - - except Exception as exc: - # show something for coders, but no user feedback - sys.print_exception(exc) - continue - - except CardMissingError: - await needs_microsd() - return - - # remove dups; easy to happen if you double-tap the export - delme = set() - for i in range(len(xpubs)): - for j in range(len(xpubs)): - if j in delme: - continue - if i == j: - continue - if xpubs[i] == xpubs[j]: - delme.add(j) - if delme: - xpubs = [x for idx, x in enumerate(xpubs) if idx not in delme] - - if not xpubs or len(xpubs) == 1 and has_mine: - await ux_show_story("Unable to find any Passport exported keys on this card. Must have filename: ccxp-....json") - return - - # add myself if not included already - if not has_mine: - with stash.SensitiveValues() as sv: - node = sv.derive_path(deriv) - xpubs.append((my_xfp, chain.serialize_public(node, AF_P2SH))) - - N = len(xpubs) - - if N > MAX_SIGNERS: - await ux_show_story("Too many signers, max is %d." % MAX_SIGNERS) - return - - # pick useful M value to start - assert N >= 2 - M = (N - 1) if N < 4 else ((N//2)+1) - - while 1: - msg = '''How many need to sign?\n %d of %d - -Press (7 or 9) to change M value, or OK \ -to continue. - -If you expected more or less keys (N=%d #files=%d), \ -then check card and file contents. - -Passport multisig setup file and an Electrum wallet file will be created automatically.\ -''' % (M, N, N, len(files)) - - # TODO: Update key handling - ch = await ux_show_story(msg, escape='123479') - - if ch in '1234': - M = min(N, int(ch)) # undocumented shortcut - elif ch == 'x': - await ux_dramatic_pause('Aborted.', 2) - return - elif ch == 'y': - break - - # create appropriate object - assert 1 <= M <= N <= MAX_SIGNERS - - name = 'CC-%d-of-%d' % (M, N) - ms = MultisigWallet(name, (M, N), xpubs, chain_type=chain.ctype, - common_prefix=deriv[2:], addr_fmt=addr_fmt) - - from auth import NewEnrollRequest, UserAuthorizedAction - - UserAuthorizedAction.active_request = NewEnrollRequest( - ms, auto_export=True) - - # menu item case: add to stack - from ux import the_ux - the_ux.push(UserAuthorizedAction.active_request) - - -async def create_ms_step1(*a): - # Show story, have them pick address format. - - ch = await ux_show_story('''\ -Insert SD card with exported XPUB files from at least one other \ -Passport. A multisig wallet will be constructed using those keys and \ -this device. - -Default is P2WSH addresses (segwit), but press (1) for P2WSH-P2SH or (2) for P2SH (legacy) instead. -''', escape='12') - - if ch == 'y': - n, f = 'p2wsh', AF_P2WSH - elif ch == '1': - n, f = 'p2wsh_p2sh', AF_P2WSH_P2SH - elif ch == '2': - n, f = 'p2sh', AF_P2SH - else: - return - - return await ondevice_multisig_create(n, f) - - # EOF diff --git a/ports/stm32/boards/Passport/modules/new_wallet.py b/ports/stm32/boards/Passport/modules/new_wallet.py new file mode 100644 index 0000000..ab2e385 --- /dev/null +++ b/ports/stm32/boards/Passport/modules/new_wallet.py @@ -0,0 +1,843 @@ +# SPDX-FileCopyrightText: 2021 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# new_wallet.py - Single sig and multisig accounts feature for better organization and privacy/isolation +# + +import common +from common import settings, system, dis +from wallets.sw_wallets import supported_software_wallets +from wallets.utils import ( + get_deriv_path_from_address_and_acct, + get_addr_type_from_address, + get_deriv_path_from_address_and_acct, + get_deriv_path_from_addr_type_and_acct, + get_addr_type_from_deriv) +from ux import ux_show_story, ux_confirm, ux_show_text_as_ur, ux_scan_qr_code +from multisig import MultisigWallet +from utils import ( + UXStateMachine, + to_str, + random_hex, + is_valid_address, + run_chooser, + scan_for_address, + save_next_addr, + make_account_name_num, + get_accounts) +from wallets.constants import * +from uasyncio import sleep_ms +from constants import DEFAULT_ACCOUNT_ENTRY + +def find_wallet_by_label(label, default_value): + for _, entry in enumerate(supported_software_wallets): + if entry.get('label') == label: + return entry + + return default_value + +def find_sig_type_by_id(sw_wallet, id, default_value): + if not sw_wallet: + return default_value + + for entry in sw_wallet.get('sig_types', []): + if entry.get('id') == id: + return entry + + return default_value + +def find_export_mode_by_id(sw_wallet, id, default_value): + if not sw_wallet: + return default_value + + for entry in sw_wallet.get('export_modes', []): + if entry.get('id') == id: + return entry + + return default_value + +def wallet_supports_sig_type(sw_wallet, sig_type): + if not sw_wallet: + return False + + for entry in sw_wallet.get('sig_types', []): + if entry.get('id') == sig_type: + return True + + return False + + +async def pair_new_wallet(*a): + pair_new_wallet_ux = NewWalletUX() + await pair_new_wallet_ux.show() + +def derive_address(deriv_path, addr_idx, addr_type, ms_wallet): + import stash + # print('deriv_path={} addr_idx={} ms_wallet={} ms_wallet={}'.format(deriv_path, addr_idx, ms_wallet, ms_wallet)) + + with stash.SensitiveValues() as sv: + if ms_wallet: + # This "loop" runs once to get the value from the generator + for (curr_idx, paths, curr_address, script) in ms_wallet.yield_addresses(addr_idx, 1): + addr_path = '{}/0/{}'.format(deriv_path, curr_idx) + return (addr_path, curr_address) + + else: + addr_path = '0/{}'.format(addr_idx) # Zero for non-change address + full_path = '{}/{}'.format(deriv_path, addr_path) + # print('full_path={}'.format(full_path)) + node = sv.derive_path(full_path) + address = sv.chain.address(node, addr_type) + # print('address = {}'.format(address)) + return (addr_path, address) + +def get_addresses_in_range(start, end, addr_type, acct_num, ms_wallet): + # print('addr_type={} acct_num={} ms_wallet={}'.format(addr_type, acct_num, ms_wallet)) + + entries = [] + for i in range(start, end): + fmt = get_deriv_path_from_addr_type_and_acct(addr_type, acct_num, ms_wallet != None) + deriv_path = fmt.format(acct_num) + entry = derive_address(deriv_path, i, addr_type, ms_wallet) + entries.append(entry) + return entries + +class NewWalletUX(UXStateMachine): + def __init__(self): + # States + self.SELECT_ACCOUNT = 1 + self.SELECT_SW_WALLET = 2 + self.SELECT_SIG_TYPE = 3 + self.SELECT_ADDR_TYPE = 4 + self.SELECT_EXPORT_MODE = 5 + self.PAIRING_MESSAGE = 6 + self.EXPORT_TO_QR = 7 + self.EXPORT_TO_MICROSD = 8 + self.IMPORT_MULTISIG_CONFIG_FROM_QR = 9 + self.IMPORT_MULTISIG_CONFIG_FROM_MICROSD = 10 + self.SCAN_RX_ADDRESS_VERIFICATION_INTRO = 11 + self.SCAN_RX_ADDRESS = 12 + self.SHOW_RX_ADDRESSES_VERIFICATION_INTRO = 13 + self.SHOW_RX_ADDRESSES = 14 + self.CONFIRMATION = 15 + self.RESUME_PROGRESS = 16 + + self.acct_num = None + self.sw_wallet = None + self.sig_type = None + self.export_mode = None + self.acct_info = None # Info from the create_wallet() call + self.verified = False + self.deriv_path = None # m/84'/0'/123' Used to derive the HDNode + self.multisig_wallet = None + self.exported = False + self.next_addr = 0 + self.addr_type = None + self.progress_made = False + + # print('NewWalletUX()') + + first_state = self.restore_from_progress() + super().__init__(first_state) + + def restore_from_progress(self): + progress = settings.get('wallet_prog', None) + if progress != None: + # Reload the previous progress + self.sw_wallet = find_wallet_by_label(progress.get('sw_wallet'), None) + self.sig_type = find_sig_type_by_id(self.sw_wallet, progress.get('sig_type'), None) + self.export_mode = find_export_mode_by_id(self.sw_wallet, progress.get('export_mode'), None) + self.acct_info = progress.get('acct_info') + self.acct_num = progress.get('acct_num') + self.verified = progress.get('verified', False) + self.deriv_path = progress.get('deriv_path') + self.multisig_wallet = MultisigWallet.get_by_id(progress.get('multisig_id')) + self.exported = progress.get('exported', False) + self.next_addr = progress.get('next_addr', 0) + self.addr_type = progress.get('addr_type', None) + + if self.acct_num != None: + self.progress_made = True + + return self.RESUME_PROGRESS + else: + return self.SELECT_ACCOUNT + + def __repr__(self): + return """NewWalletUX: sw_wallet={}, sig_type={}, export_mode={}, + acct_info={}, acct_num={}, verified={}, + deriv_path={}, addr_type={}, multisig_wallet={}, + exported={}, next_addr={}""".format( + self.sw_wallet, + self.sig_type, + self.export_mode, + self.acct_info, + self.acct_num, + self.verified, + self.deriv_path, + self.addr_type, + self.multisig_wallet, + self.exported, + self.next_addr) + + async def confirm_abort(self): + # If user hasn't made any progress at all, then no need to confirm anything. Just back out. + if not self.progress_made: + return True + + result = await ux_confirm('Are you sure you want to cancel pairing the new wallet?\n\nAll progress will be lost.') + if result: + self.abort_wallet_progress() + return result + + def get_account_description(self, acct_num): + accounts = get_accounts() + for acct in accounts: + curr_acct_num = acct.get('acct_num') + if curr_acct_num == acct_num: + return make_account_name_num(acct.get('name'), acct_num) + return 'Unknown Acct ({})'.format(acct_num) + + # Account chooser + def account_chooser(self): + choices = [] + values = [] + + accounts = get_accounts() + accounts.sort(key=lambda a: a.get('acct_num', 0)) + + for acct in accounts: + acct_num = acct.get('acct_num') + account_name_num = make_account_name_num(acct.get('name'), acct_num) + choices.append(account_name_num) + values.append(acct_num) + + def select_account(index, text): + self.acct_num = values[index] + + return 0, choices, select_account + + # Local choosers for wallet configuration + def sw_wallet_chooser(self): + choices = [] + values = [] + for w in supported_software_wallets: + # Include in the list if this is account 0, or (if not account 0), then if it supports single-sig + # In the flow, we will automatically pick single-sig in this case. + if self.acct_num == 0 or wallet_supports_sig_type(w, 'single-sig'): + choices.append(w['label']) + values.append(w) + + def select_sw_wallet(index, text): + self.sw_wallet = values[index] + + return 0, choices, select_sw_wallet + + def sig_type_chooser(self): + choices = [] + values = [] + + for sig_type in self.sw_wallet['sig_types']: + choices.append(sig_type['label']) + values.append(sig_type) + + def select_sig_type(index, text): + self.sig_type = values[index] + + return 0, choices, select_sig_type + + def export_mode_chooser(self): + choices = [] + values = [] + + for export_mode in self.sw_wallet['export_modes']: + choices.append(export_mode['label']) + values.append(export_mode) + + def select_export_mode(index, text): + self.export_mode = values[index] + # print('self.export_mode[\'id\']={}, self.export_mode={}'.format(self.export_mode['id'], self.export_mode)) + + return 0, choices, select_export_mode + + def singlesig_addr_type_chooser(self): + from public_constants import AF_P2WPKH, AF_P2WPKH_P2SH, AF_CLASSIC + choices = ['Native Segwit', 'P2SH-Segwit', 'Legacy (P2PKH)'] + values = [AF_P2WPKH, AF_P2WPKH_P2SH, AF_CLASSIC] + + def select_addr_type(index, text): + self.addr_type = values[index] + + return 0, choices, select_addr_type + + def get_custom_text(self, field, default_text): + if self.sw_wallet: + ct = self.sw_wallet.get('custom_text') + if ct: + return ct.get(field, default_text) + + return default_text + + def infer_wallet_info(self, address=None, ms_wallet=None): + # Ensure we have an addr_type, if possible yet + if not self.addr_type: + if self.sig_type['addr_type']: + self.addr_type = self.sig_type['addr_type'] + elif self.acct_info and len(self.acct_info) == 1: + self.addr_type = self.acct_info[0]['fmt'] + + # If we now have the necessary parts, build the deriv_path + if self.addr_type != None: + self.deriv_path = get_deriv_path_from_addr_type_and_acct(self.addr_type, self.acct_num, self.is_multisig()) + + # If we didn't figure out the deriv_path yet, try to do it now + if not self.deriv_path: + if self.acct_info and len(self.acct_info) == 1: + self.deriv_path = self.acct_info[0]['deriv'] + + elif address: + # We can derive it from the address now + if not self.addr_type: # Should be a redundant condition + self.addr_type = get_addr_type_from_address(address, self.is_multisig()) + + self.deriv_path = get_deriv_path_from_address_and_acct(address, self.acct_num, self.is_multisig()) + + if ms_wallet != None: + assert self.deriv_path == ms_wallet.my_deriv + + elif ms_wallet: + # If the address was skipped, but we have the multisig wallet, get the derivation from it directly + self.deriv_path = ms_wallet.my_deriv + + # If we still don't have the addr_type, we should be able to infer it from the deriv_path + if not self.addr_type: + self.addr_type = get_addr_type_from_deriv(self.deriv_path) + + def prepare_to_export(self): + system.show_busy_bar() + + self.infer_wallet_info() + + # We know the wallet type and sig type now so we can create the export data + (data, self.acct_info) = self.sig_type['create_wallet']( + sw_wallet=self.sw_wallet, + addr_type=self.addr_type, + acct_num=self.acct_num, + multisig=self.is_multisig(), + legacy=self.sig_type.get('legacy', False)) + + self.infer_wallet_info() + + system.hide_busy_bar() + + # print('prepared data={} self.acct_info={}'.format(to_str(data), self.acct_info)) + # print('self.acct_info={}'.format(self.acct_info)) + return data + + async def import_multisig_config(self, data): + from utils import show_top_menu, problem_file_line + from auth import maybe_enroll_xpub + + try: + maybe_enroll_xpub(config=data) + await show_top_menu() + + return not common.is_new_wallet_a_duplicate + + except Exception as e: + await ux_show_story('Invalid multisig configuration data.\n\n{}\n{}'.format(e, problem_file_line(e)), title='Error') + return False + + def save_new_wallet_progress(self): + progress = { + 'sw_wallet': self.sw_wallet['label'] if self.sw_wallet else None, + 'sig_type': self.sig_type['id'] if self.sig_type else None, + 'export_mode': self.export_mode['id'] if self.export_mode else None, + 'acct_info': self.acct_info, + 'acct_num': self.acct_num, + 'deriv_path': self.deriv_path, + 'verified': self.verified, + 'multisig_id': self.multisig_wallet.id if self.multisig_wallet else None, + 'exported': self.exported, + 'next_addr': self.next_addr, + 'addr_type': self.addr_type + } + settings.set('wallet_prog', progress) + # print('Saving progress={}'.format(to_str(progress))) + + def abort_wallet_progress(self): + # Make sure to delete the multisig wallet if it was created and we are now aborting + if self.multisig_wallet: + MultisigWallet.delete_by_id(self.multisig_wallet.id) + settings.remove('wallet_prog') + common.new_multisig_wallet = None + common.is_new_wallet_a_duplicate = False + + def reset_wallet_progress(self): + settings.remove('wallet_prog') + common.new_multisig_wallet = None + common.is_new_wallet_a_duplicate = False + + def is_multisig(self): + return self.sig_type['id'] == 'multisig' + + def goto_address_verification_method(self, save_curr=True): + method = self.sw_wallet.get('address_validation_method', 'scan_rx_address') + if method == 'scan_rx_address': + self.goto(self.SCAN_RX_ADDRESS_VERIFICATION_INTRO, save_curr=save_curr) + elif method == 'show_addresses': + self.goto(self.SHOW_RX_ADDRESSES_VERIFICATION_INTRO, save_curr=save_curr) + + async def show(self): + while True: + # print('show: state={}'.format(self.state)) + if self.state == self.SELECT_ACCOUNT: + self.acct_num = None + accounts = get_accounts() + if len(accounts) == 1: + self.acct_num = 0 + self.goto(self.SELECT_SW_WALLET, save_curr=False) + continue + + await run_chooser(self.account_chooser, 'Account', show_checks=False) + if self.acct_num == None: + if await self.confirm_abort(): + return + else: + continue + + self.goto(self.SELECT_SW_WALLET) + self.progress_made = True + + elif self.state == self.SELECT_SW_WALLET: + # Choose a wallet from the available list + self.sw_wallet = None + + await run_chooser(self.sw_wallet_chooser, 'Pair Wallet', show_checks=False) + if self.sw_wallet == None: + if not self.goto_prev(): + return + else: + continue + + # Save the progress so that we can resume later + self.save_new_wallet_progress() + + self.goto(self.SELECT_SIG_TYPE) + + elif self.state == self.SELECT_SIG_TYPE: + save_curr = True + # We can skip this step if there is only one sig type + if self.acct_num > 0: + # print('Non-zero accounts only support single-sig...skipping') + self.sig_type = find_sig_type_by_id(self.sw_wallet, 'single-sig', None) + save_curr = False + elif len(self.sw_wallet['sig_types']) == 1: + # print('Only 1 sig type...skipping') + self.sig_type = self.sw_wallet['sig_types'][0] + save_curr = False + else: + # Choose a wallet from the available list + self.sig_type = None + + await run_chooser(self.sig_type_chooser, 'Type', show_checks=False) + if self.sig_type == None: + if not self.goto_prev(): + return + continue + + # See what we can infer so far + self.infer_wallet_info() + + # Save the progress so that we can resume later + self.save_new_wallet_progress() + + # print('self.sig_type={}'.format(self.sig_type)) + + # NOTE: Nothing uses this option at the moment, but leaving it here in case we need it for a + # new wallet later. + if self.sw_wallet.get('options', {}).get('select_addr_type', False): + self.goto(self.SELECT_ADDR_TYPE, save_curr=save_curr) + else: + self.goto(self.SELECT_EXPORT_MODE, save_curr=save_curr) + + elif self.state == self.SELECT_ADDR_TYPE: + # This step is normally only included for custom wallets or low-level wallets (e.g., electrum, bitcoin core) + await run_chooser(self.singlesig_addr_type_chooser, 'Address Type', show_checks=False) + if self.addr_type == None: + if not self.goto_prev(): + return + continue + + # See what we can infer so far + self.infer_wallet_info() + + # Save the progress so that we can resume later + self.save_new_wallet_progress() + + self.goto(self.SELECT_EXPORT_MODE) + + elif self.state == self.SELECT_EXPORT_MODE: + save_curr = True + # We can skip this step if there is only a single export mode + if len(self.sw_wallet['export_modes']) == 1: + # print('Only 1 export mode...skipping') + self.export_mode = self.sw_wallet['export_modes'][0] + # print('self.export_mode[\'id\']={}, self.export_mode={}'.format(self.export_mode['id'], self.export_mode)) + save_curr = False + else: + self.export_mode = None + await run_chooser(self.export_mode_chooser, 'Export By', show_checks=False) + if self.export_mode == None: + if not self.goto_prev(): + return + continue + + # Save the progress so that we can resume later + self.save_new_wallet_progress() + + self.goto(self.PAIRING_MESSAGE, save_curr=save_curr) + + elif self.state == self.PAIRING_MESSAGE: + # Get the right message - use default if no custom value provided + if self.export_mode['id'] == EXPORT_MODE_QR: + msg = self.get_custom_text('pairing_qr', 'Next, scan the QR code on the following screen into {}.'.format(self.sw_wallet['label'])) + elif self.export_mode['id'] == EXPORT_MODE_MICROSD: + ext = self.export_mode.get('ext_multisig', '.json') if self.is_multisig() else self.export_mode.get('ext', '.json') + msg = self.get_custom_text('pairing_microsd', 'Next, Passport will save a {} file to your microSD card to use with {}.'.format(ext, self.sw_wallet['label'])) + + # Show pairing help text to the user + result = await ux_show_story(msg, title='Pairing', scroll_label='MORE', center=True, center_vertically=True) + if result == 'x': + if not self.goto_prev(): + return + continue + + # Save the progress so that we can resume later + self.save_new_wallet_progress() + + # Next state + if self.export_mode['id'] == EXPORT_MODE_QR: + self.goto(self.EXPORT_TO_QR) + elif self.export_mode['id'] == EXPORT_MODE_MICROSD: + self.goto(self.EXPORT_TO_MICROSD) + + elif self.state == self.EXPORT_TO_QR: + data = self.prepare_to_export() + + # TODO: Do we need to encode the data to text for QR here? Some formats might not be text. + + qr_type = self.export_mode['qr_type'] + await ux_show_text_as_ur(title='Export QR', qr_text=data, qr_type=qr_type, left_btn='DONE') + # Only way to get out is DONE, so no need to check result + + # Save the progress so that we can resume later + self.exported = True + self.save_new_wallet_progress() + + # If multisig, we need to import the quorum/config info first, else go right to validating the first + # receive address from the wallet. + if self.is_multisig(): + self.goto(self.IMPORT_MULTISIG_CONFIG_FROM_QR, save_curr=False) + else: + self.goto_address_verification_method(save_curr=False) + + elif self.state == self.EXPORT_TO_MICROSD: + from files import CardSlot + + data = self.prepare_to_export() + data_hash = bytearray(32) + system.sha256(data, data_hash) + + # Write the data to SD with the filename the wallet prefers + filename_pattern = self.export_mode['filename_pattern_multisig'] if self.is_multisig() else self.export_mode['filename_pattern'] + try: + with CardSlot() as card: + # Make a filename with the option of injecting the sd path, hash of the data, acct num, random number + fname = filename_pattern.format(sd=card.get_sd_root(), hash=data_hash, acct=self.acct_num, random=random_hex(8)) + # print('Saving to fname={}'.format(fname)) + + # Write the data + with open(fname, 'wb') as fd: + fd.write(data) + + except Exception as e: + # includes CardMissingError + import sys + sys.print_exception(e) + # catch any error + ch = await ux_show_story('Unable to export wallet file. Please insert a formatted microSD card.\n\n' + + str(e), title='Error', right_btn='RETRY', center=True, center_vertically=True) + if ch == 'x': + return + + # Wrap around and try again + continue + + # Save the progress so that we can resume later + self.exported = True + self.save_new_wallet_progress() + + dis.fullscreen('Saved to microSD') + await sleep_ms(1000) + + # If multisig, we need to import the quorum/config info first, else go right to validating the first + # receive address from the wallet. + if self.is_multisig(): + self.goto(self.IMPORT_MULTISIG_CONFIG_FROM_MICROSD, save_curr=False) + else: + self.goto_address_verification_method(save_curr=False) + + + elif self.state == self.IMPORT_MULTISIG_CONFIG_FROM_QR: + while True: + msg = self.get_custom_text('multisig_import_qr', 'Next, import the multisig configuration from {} via QR code.'.format(self.sw_wallet['label'])) + result = await ux_show_story(msg, title='Import Multisig', scroll_label="MORE", center=True, center_vertically=True) + if result == 'x': + if not self.goto_prev(): + return + break + + # Import the config info and save to settings + common.new_multisig_wallet = None + common.is_new_wallet_a_duplicate = False + + data = await self.sig_type['import_qr']() + if data == None: + continue + + # Now try to import the config data + result = await self.import_multisig_config(data) + if result == False: + self.abort_wallet_progress() + dis.fullscreen('Not Imported') + await sleep_ms(1000) + return + + # Success + self.multisig_wallet = common.new_multisig_wallet + # print('**********************************************************************') + # print('multisig_wallet={}'.format(to_str(self.multisig_wallet))) + # print('**********************************************************************') + + # See what we can infer so far + self.infer_wallet_info(ms_wallet=self.multisig_wallet) + + # Save the progress so that we can resume later + self.save_new_wallet_progress() + + self.goto_address_verification_method() + break + + elif self.state == self.IMPORT_MULTISIG_CONFIG_FROM_MICROSD: + while True: + msg = self.get_custom_text('multisig_import_microsd', 'Next, import the multisig configuration from {} via microSD card.'.format(self.sw_wallet['label'])) + result = await ux_show_story(msg, title='Import Multisig', scroll_label="MORE", center=True, center_vertically=True) + if result == 'x': + if not self.goto_prev(): + return + break + + # Import the config info and save to settings + common.new_multisig_wallet = None + common.is_new_wallet_a_duplicate = False + + data = await self.sig_type['import_microsd']() + if data == None: + continue + + # Now try to import the config data + result = await self.import_multisig_config(data) + if result == False: + self.abort_wallet_progress() + dis.fullscreen('Not Imported') + await sleep_ms(1000) + return + + # Success + self.multisig_wallet = common.new_multisig_wallet + + self.infer_wallet_info(ms_wallet=self.multisig_wallet) + + # Save the progress so that we can resume later + self.save_new_wallet_progress() + + self.goto_address_verification_method() + + # Regardless of whether they used QR or microSD for import, they need to use QR for address validation + break + + elif self.state == self.SCAN_RX_ADDRESS_VERIFICATION_INTRO: + msg = self.get_custom_text('scan_receive_addr', '''Next, let's check that the wallet was paired successfully. + +Generate a new receive address in {} and scan the QR code on the next page.'''.format(self.sw_wallet['label'])) + result = await ux_show_story(msg, title='Verify Address', scroll_label="MORE", center=True, center_vertically=True) + if result == 'x': + if not self.goto_prev(): + return + continue + + self.goto(self.SCAN_RX_ADDRESS) + + elif self.state == self.SCAN_RX_ADDRESS: + # Scan the address to be verified - should be a normal QR code + system.turbo(True); + address = await ux_scan_qr_code('Verify Address') + + if address == None: + # User backed out without scanning an address + result = await ux_confirm('No address was scanned. Do you want to skip address verification?') + if result: + # Skipping address scan + self.infer_wallet_info(ms_wallet=self.multisig_wallet) + self.save_new_wallet_progress() + self.goto(self.CONFIRMATION) + else: + result = await ux_confirm('Retry address verification?', negative_btn='BACK', positive_btn='RETRY') + if not result: + self.goto_prev() + continue + + # Ensure lowercase + address = address.lower() + + # Strip prefix if present + if address.startswith('bitcoin:'): + address = address[8:] + + if not is_valid_address(address): + result = await ux_show_story('That is not a valid Bitcoin address.', title='Error', left_btn='BACK', + right_btn='SCAN', center=True, center_vertically=True) + if result == 'x': + if not self.goto_prev(): + return + continue + + # Use address to nail down deriv_path and addr_type, if not yet known + self.infer_wallet_info(address=address) + + # Scan addresses to see if it's valid + addr_idx = await scan_for_address(self.acct_num, address, self.addr_type, self.deriv_path, self.multisig_wallet) + if addr_idx >= 0: + # Found it! + self.verified = True + + # Remember where to start from next time + save_next_addr(self.acct_num, self.addr_type, addr_idx) + + dis.fullscreen('Address Verified') + await sleep_ms(1000) + self.goto(self.CONFIRMATION) + continue + else: + result = await ux_show_story('Do you want to SKIP address verification or SCAN another address?', title='Not Found', left_btn='SKIP', + right_btn='SCAN', center=True, center_vertically=True) + if result == 'x': + # Skipping address scan + self.infer_wallet_info(ms_wallet=self.multisig_wallet) + self.goto(self.CONFIRMATION) + + # else loop around and scan again + + elif self.state == self.SHOW_RX_ADDRESSES_VERIFICATION_INTRO: + msg = self.get_custom_text('show_receive_addr', '''Next, let's check that {name} was paired successfully. + +{name} should display a list of addresses associated with this wallet. + +Compare them with the addresses shown on the next screen to make sure they match.'''.format(name=self.sw_wallet['label'])) + result = await ux_show_story(msg, title='Verify Address', scroll_label="MORE", center=True, center_vertically=True) + if result == 'x': + if not self.goto_prev(): + return + continue + + self.goto(self.SHOW_RX_ADDRESSES) + + elif self.state == self.SHOW_RX_ADDRESSES: + from display import FontTiny + NUM_ADDRESSES = 3 + system.show_busy_bar() + dis.fullscreen('Generating Addresses...') + addresses = get_addresses_in_range(0, NUM_ADDRESSES, self.addr_type, self.acct_num, self.multisig_wallet) + system.hide_busy_bar() + + msg = 'First {} Addresses'.format(NUM_ADDRESSES) + + for entry in addresses: + deriv_path, address = entry + msg += '\n\n{}\n{}'.format(deriv_path, address) + + await ux_show_story(msg, title='Verify', center=True, font=FontTiny) + if result == 'x': + if not self.goto_prev(): + return + else: + self.goto(self.CONFIRMATION) + + elif self.state == self.CONFIRMATION: + # Reset so we don't offer to resume again later + self.reset_wallet_progress() + + # Offer to backup if a multisig wallet was added + if self.multisig_wallet: + from export import offer_backup + await offer_backup() + + dis.fullscreen('Pairing Complete') + await sleep_ms(1000) + + return + + elif self.state == self.RESUME_PROGRESS: + msg = 'Passport was in the middle of creating a new account with the following selections:\n\n' + + if self.acct_num != None: + msg += '- {}\n'.format(self.get_account_description(self.acct_num)) + + if self.sw_wallet: + msg += '- {}\n'.format(self.sw_wallet['label']) + + if self.sig_type: + msg += '- {}\n'.format(self.sig_type['label']) + + if self.export_mode: + msg += '- {}\n'.format(self.export_mode['label']) + + msg += '\nWould you like to RESUME creating this new account from where you left off, or CANCEL and lose all progress?' + + result = await ux_show_story(msg, title='Resume?', left_btn='CANCEL', right_btn='RESUME', scroll_label="MORE") + + if result == 'x': + if await self.confirm_abort(): + return + else: + continue + + # print('Resuming New Wallet flow: self={}'.format(to_str(self))) + + # Resume based on where the user left off before + if not self.sw_wallet: + self.goto(self.SELECT_SW_WALLET) + continue + elif not self.sig_type: + self.goto(self.SELECT_SIG_TYPE) + continue + elif not self.export_mode: + self.goto(self.SELECT_EXPORT_MODE) + continue + elif not self.exported: + self.goto(self.PAIRING_MESSAGE) + continue + + if self.is_multisig(): + if not self.multisig_wallet: + # Need to import the multisig wallet + if self.export_mode['id'] == 'qr': + self.goto(self.IMPORT_MULTISIG_CONFIG_FROM_QR) + else: + self.goto(self.IMPORT_MULTISIG_CONFIG_FROM_MICROSD) + continue + + if not self.verified: + self.goto(self.SCAN_RX_ADDRESS) + continue diff --git a/ports/stm32/boards/Passport/modules/noise_source.py b/ports/stm32/boards/Passport/modules/noise_source.py new file mode 100644 index 0000000..d8191e6 --- /dev/null +++ b/ports/stm32/boards/Passport/modules/noise_source.py @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# noises_sources.py + +# Matches up with the C definitions +# #define AVALANCHE_SOURCE 1 +# #define MCU_RNG_SOURCE 2 +# #define SE_RNG_SOURCE 4 +# #define ALS_SOURCE 8 + +class NoiseSource: + AVALANCHE = 1 + MCU = 2 + SE = 4 + AMBIENT_LIGHT_SENSOR = 8 + ALL = AVALANCHE | MCU | SE | AMBIENT_LIGHT_SENSOR diff --git a/ports/stm32/boards/Passport/modules/opcodes.py b/ports/stm32/boards/Passport/modules/opcodes.py index 98c6077..366a2a2 100644 --- a/ports/stm32/boards/Passport/modules/opcodes.py +++ b/ports/stm32/boards/Passport/modules/opcodes.py @@ -1,7 +1,7 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: GPL-3.0-or-later # -# SPDX-FileCopyrightText: 2018 Coinkite, Inc. +# SPDX-FileCopyrightText: 2018 Coinkite, Inc. # SPDX-License-Identifier: GPL-3.0-only # # A very limited subset of the opcodes we might need diff --git a/ports/stm32/boards/Passport/modules/passport_fonts.py b/ports/stm32/boards/Passport/modules/passport_fonts.py index c284b52..6bac1ac 100644 --- a/ports/stm32/boards/Passport/modules/passport_fonts.py +++ b/ports/stm32/boards/Passport/modules/passport_fonts.py @@ -1,7 +1,7 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: GPL-3.0-or-later # -# SPDX-FileCopyrightText: 2018 Coinkite, Inc. +# SPDX-FileCopyrightText: 2018 Coinkite, Inc. # SPDX-License-Identifier: GPL-3.0-only # # Passport wallet font definitions @@ -832,414 +832,3 @@ class FontSmall(FontBase): 0x84,0xCE,0x7C,0x38,0x78,0xEC,0xC4, ]) - -class FontLarge(FontBase): - height = 20 - advance = 12 - ascent = 20 - descent = 5 - leading = 31 - code_range = range(32, 216) - - bboxes = [None, - (0, 0, 0, 0, 6, 0), - (1, 0, 4, 15, 6, 15), - (1, 9, 7, 6, 9, 6), - (0, 0, 15, 15, 15, 30), - (1, -2, 12, 19, 13, 38), - (1, 0, 17, 15, 18, 45), - (1, 0, 13, 15, 15, 30), - (1, 9, 3, 6, 5, 6), - (2, -4, 5, 20, 7, 20), - (0, -4, 6, 20, 7, 20), - (0, 7, 9, 9, 9, 18), - (1, 2, 10, 10, 12, 20), - (1, -3, 4, 7, 5, 7), - (1, 4, 6, 3, 8, 3), - (1, 0, 4, 4, 5, 4), - (-1, -2, 10, 20, 8, 40), - (1, 0, 12, 15, 14, 30), - (0, 0, 6, 15, 8, 15), - (0, 0, 12, 15, 12, 30), - (1, 0, 13, 15, 14, 30), - (1, 0, 12, 15, 13, 30), - (1, 0, 11, 15, 13, 30), - (0, 0, 12, 15, 13, 30), - (1, 0, 4, 11, 5, 11), - (1, -4, 4, 15, 5, 15), - (1, 3, 10, 8, 12, 16), - (0, 0, 11, 15, 12, 30), - (1, -4, 20, 19, 22, 57), - (0, 0, 16, 15, 16, 30), - (2, 0, 13, 15, 16, 30), - (1, 0, 14, 15, 15, 30), - (2, 0, 14, 15, 17, 30), - (2, 0, 11, 15, 14, 30), - (2, 0, 11, 15, 13, 30), - (1, 0, 14, 15, 16, 30), - (2, 0, 13, 15, 17, 30), - (2, 0, 3, 15, 7, 15), - (0, 0, 10, 15, 11, 30), - (2, 0, 13, 15, 15, 30), - (2, 0, 10, 15, 13, 30), - (2, 0, 16, 15, 20, 30), - (1, 0, 16, 15, 18, 30), - (2, 0, 12, 15, 15, 30), - (1, -3, 16, 18, 18, 36), - (0, 0, 13, 15, 13, 30), - (1, 0, 23, 15, 24, 45), - (0, 0, 14, 15, 15, 30), - (0, 0, 14, 15, 14, 30), - (2, -4, 5, 20, 8, 20), - (0, -4, 6, 20, 8, 20), - (1, 3, 10, 9, 13, 18), - (0, -2, 10, 2, 10, 4), - (2, 13, 6, 3, 13, 3), - (1, 0, 10, 11, 13, 22), - (1, 0, 13, 16, 14, 32), - (1, 0, 11, 11, 12, 22), - (1, 0, 12, 16, 14, 32), - (1, 0, 11, 11, 13, 22), - (0, 0, 9, 16, 8, 32), - (1, -4, 12, 15, 15, 30), - (1, 0, 4, 16, 6, 16), - (-2, -4, 7, 20, 6, 20), - (1, 0, 19, 11, 22, 33), - (1, 0, 12, 11, 14, 22), - (1, -4, 13, 15, 14, 30), - (1, -4, 12, 15, 14, 30), - (1, 0, 7, 11, 9, 11), - (1, 0, 10, 11, 11, 22), - (0, 0, 8, 14, 9, 14), - (0, 0, 12, 11, 12, 22), - (0, 0, 19, 11, 20, 33), - (0, -4, 12, 15, 12, 30), - (1, -4, 7, 20, 8, 20), - (2, -4, 3, 20, 6, 20), - (0, -4, 7, 20, 8, 20), - (1, 5, 10, 4, 12, 8), - (1, 0, 10, 13, 12, 26), - (1, 7, 7, 9, 9, 9), - (5, 13, 5, 3, 13, 3), - (2, 3, 8, 9, 12, 9), - ] - - codepoints = [ - (range(32,127), [1,2,18,25,56,95,141,172,179,200,221,240,261,269,273,278, - 319,350,366,397,428,459,490,521,552,583,614,626,642,663,680,701, - 732,790,821,852,883,914,945,976,1007,1038,1054,1085,1116,1147,1178,1209, - 1240,1271,1308,1339,1370,1401,1432,1463,1509,1540,1571,1602,1623,1664,1685,1704, - 1709,1713,1736,1769,1792,1825,1848,1881,1912,1945,1962,1983,2016,2033,2067,2090, - 2113,2144,2175,2187,2210,2225,2248,2271,2305,2328,2359,2382,2403,2424,2445,]), - (range(177,182), [2454,2481,2491,2501,2505,]), - (range(215,216), [2536,]), - ] - - bitmaps = bytes([ - 0xAA, # Dummy first entry - - 0x01, # 0020 offset = 1 - - - 0x02, # 0021 offset = 2 - 0xF0,0xF0,0xF0,0xF0,0xF0,0xF0,0x70,0x60,0x60,0x60,0x00,0x60,0xF0,0xF0,0x60, - - 0x03, # 0022 offset = 18 - 0xEE,0xEE,0xEE,0xEE,0xEE,0xEE, - - 0x04, # 0023 offset = 25 - 0x0E,0x30,0x0E,0x30,0x0E,0x30,0x0E,0x30,0x7F,0xFE,0x7F,0xFE,0x0C,0x70,0x0C,0x60,0x0C,0x60,0x1C,0x60,0xFF,0xFC,0xFF,0xFC,0x18,0xE0,0x18,0xE0,0x18,0xE0, - - 0x05, # 0024 offset = 56 - 0x06,0x00,0x06,0x00,0x1F,0x80,0x7F,0xE0,0xFF,0xE0,0xF6,0x40,0xE6,0x00,0xF6,0x00,0xFF,0x00,0x7F,0xC0,0x0F,0xE0,0x07,0xF0,0x06,0xF0,0xC6,0xF0,0xFF,0xE0,0xFF,0xC0,0x3F,0x80,0x06,0x00,0x06,0x00, - - 0x06, # 0025 offset = 95 - 0x78,0x0C,0x00,0xFC,0x18,0x00,0xCE,0x38,0x00,0xC6,0x30,0x00,0xC6,0x60,0x00,0xCE,0xE0,0x00,0xFC,0xC0,0x00,0x79,0x9E,0x00,0x03,0xBF,0x00,0x03,0x33,0x00,0x06,0x71,0x80,0x0E,0x71,0x80,0x0C,0x33,0x00,0x18,0x3F,0x00,0x38,0x1E,0x00, - - 0x07, # 0026 offset = 141 - 0x1F,0x00,0x3F,0xC0,0x39,0xC0,0x71,0xC0,0x39,0xC0,0x3F,0x80,0x1F,0x00,0x3F,0x10,0x77,0xB8,0xE3,0xF8,0xE1,0xF0,0xE0,0xF0,0xFF,0xF8,0x7F,0xF8,0x3F,0x10, - - 0x08, # 0027 offset = 172 - 0xE0,0xE0,0xE0,0xE0,0xE0,0xE0, - - 0x09, # 0028 offset = 179 - 0x38,0x70,0x70,0xF0,0xE0,0xE0,0xE0,0xE0,0xE0,0xE0,0xE0,0xE0,0xE0,0xE0,0xE0,0xE0,0xF0,0x70,0x70,0x38, - - 0x0a, # 0029 offset = 200 - 0xF0,0x70,0x78,0x38,0x38,0x38,0x3C,0x1C,0x1C,0x1C,0x1C,0x1C,0x1C,0x3C,0x38,0x38,0x38,0x78,0x70,0xF0, - - 0x0b, # 002A offset = 221 - 0x08,0x00,0x49,0x00,0x6B,0x00,0x7F,0x00,0x3E,0x00,0x3E,0x00,0xFF,0x80,0x49,0x00,0x08,0x00, - - 0x0c, # 002B offset = 240 - 0x0E,0x00,0x0E,0x00,0x0E,0x00,0xFF,0xC0,0xFF,0xC0,0xFF,0xC0,0x0E,0x00,0x0E,0x00,0x0E,0x00,0x0E,0x00, - - 0x0d, # 002C offset = 261 - 0xE0,0xF0,0xF0,0xE0,0x60,0xE0,0xC0, - - 0x0e, # 002D offset = 269 - 0xFC,0xFC,0xFC, - - 0x0f, # 002E offset = 273 - 0xE0,0xF0,0xF0,0xE0, - - 0x10, # 002F offset = 278 - 0x01,0xC0,0x01,0xC0,0x03,0x80,0x03,0x80,0x03,0x80,0x07,0x00,0x07,0x00,0x07,0x00,0x0E,0x00,0x0E,0x00,0x0E,0x00,0x1C,0x00,0x1C,0x00,0x18,0x00,0x38,0x00,0x38,0x00,0x30,0x00,0x70,0x00,0x70,0x00,0xE0,0x00, - - 0x11, # 0030 offset = 319 - 0x0F,0x80,0x3F,0xC0,0x7F,0xE0,0x70,0xF0,0xF0,0xF0,0xE0,0x70,0xE0,0x70,0xE0,0x70,0xE0,0x70,0xE0,0x70,0xF0,0xF0,0x70,0xF0,0x7F,0xE0,0x3F,0xC0,0x0F,0x80, - - 0x12, # 0031 offset = 350 - 0xFC,0xFC,0xFC,0x1C,0x1C,0x1C,0x1C,0x1C,0x1C,0x1C,0x1C,0x1C,0x1C,0x1C,0x1C, - - 0x13, # 0032 offset = 366 - 0x1F,0x80,0x7F,0xC0,0xFF,0xE0,0x61,0xE0,0x00,0xE0,0x00,0xE0,0x01,0xE0,0x03,0xC0,0x07,0x80,0x0F,0x00,0x1E,0x00,0x3C,0x00,0x7F,0xF0,0x7F,0xF0,0x7F,0xF0, - - 0x13, # 0033 offset = 397 - 0x7F,0xE0,0x7F,0xE0,0x7F,0xE0,0x03,0xC0,0x07,0x80,0x0F,0x00,0x0F,0x80,0x0F,0xE0,0x03,0xE0,0x00,0xF0,0x00,0xF0,0x40,0xE0,0xFF,0xE0,0xFF,0xC0,0x3F,0x80, - - 0x14, # 0034 offset = 428 - 0x03,0xC0,0x07,0x80,0x07,0x00,0x0F,0x00,0x1E,0x00,0x1C,0x00,0x38,0xE0,0x78,0xE0,0xF0,0xE0,0xFF,0xF8,0xFF,0xF8,0xFF,0xF8,0x01,0xE0,0x01,0xE0,0x01,0xE0, - - 0x13, # 0035 offset = 459 - 0x3F,0xE0,0x3F,0xE0,0x3F,0xE0,0x38,0x00,0x38,0x00,0x3F,0x00,0x7F,0xC0,0x7F,0xE0,0x00,0xF0,0x00,0xF0,0x00,0xF0,0x60,0xF0,0x7F,0xE0,0xFF,0xC0,0x3F,0x80, - - 0x15, # 0036 offset = 490 - 0x0F,0xC0,0x3F,0xE0,0x7F,0xE0,0x78,0x00,0xF0,0x00,0xE2,0x00,0xEF,0xC0,0xFF,0xE0,0xF0,0xF0,0xF0,0xF0,0xF0,0x70,0x70,0xF0,0x7F,0xE0,0x3F,0xC0,0x0F,0x80, - - 0x16, # 0037 offset = 521 - 0xFF,0xE0,0xFF,0xE0,0xFF,0xE0,0xE1,0xE0,0xE1,0xC0,0x03,0xC0,0x03,0x80,0x07,0x80,0x07,0x80,0x0F,0x00,0x0F,0x00,0x0E,0x00,0x1E,0x00,0x1C,0x00,0x3C,0x00, - - 0x11, # 0038 offset = 552 - 0x1F,0x80,0x7F,0xC0,0x7F,0xE0,0xF0,0xE0,0xF0,0xE0,0x70,0xE0,0x7F,0xE0,0x7F,0xC0,0xF9,0xE0,0xE0,0xF0,0xE0,0x70,0xE0,0xF0,0xFB,0xF0,0x7F,0xE0,0x1F,0x80, - - 0x17, # 0039 offset = 583 - 0x1F,0x80,0x3F,0xC0,0x7F,0xE0,0x70,0xF0,0xF0,0x70,0xF0,0x70,0x78,0xF0,0x7F,0xF0,0x3F,0xF0,0x04,0x70,0x00,0xF0,0x00,0xF0,0x3F,0xE0,0x7F,0xC0,0x3F,0x00, - - 0x18, # 003A offset = 614 - 0xE0,0xF0,0xF0,0xE0,0x00,0x00,0x00,0xE0,0xF0,0xF0,0xE0, - - 0x19, # 003B offset = 626 - 0xE0,0xF0,0xF0,0xE0,0x00,0x00,0x00,0x00,0xF0,0xF0,0xF0,0x60,0xE0,0xC0,0xC0, - - 0x0c, # 003C offset = 642 - 0x00,0xC0,0x03,0xC0,0x1F,0xC0,0xFE,0x00,0xF0,0x00,0xF8,0x00,0x7F,0x00,0x0F,0xC0,0x03,0xC0,0x00,0x40, - - 0x1a, # 003D offset = 663 - 0xFF,0xC0,0xFF,0xC0,0xFF,0xC0,0x00,0x00,0x00,0x00,0xFF,0xC0,0xFF,0xC0,0xFF,0xC0, - - 0x0c, # 003E offset = 680 - 0xC0,0x00,0xF8,0x00,0x7E,0x00,0x1F,0xC0,0x03,0xC0,0x03,0xC0,0x1F,0xC0,0xFE,0x00,0xF0,0x00,0xC0,0x00, - - 0x1b, # 003F offset = 701 - 0x1F,0x80,0x7F,0xC0,0xFF,0xE0,0x61,0xE0,0x00,0xE0,0x01,0xE0,0x03,0xC0,0x07,0x80,0x07,0x00,0x07,0x00,0x00,0x00,0x06,0x00,0x0F,0x00,0x0F,0x00,0x06,0x00, - - 0x1c, # 0040 offset = 732 - 0x03,0xF8,0x00,0x0F,0xFF,0x00,0x1E,0x07,0x80,0x38,0x01,0xC0,0x70,0x00,0xC0,0x63,0xFE,0x60,0xE7,0xFE,0x60,0xC7,0x1E,0x70,0xCE,0x0E,0x30,0xCE,0x0E,0x30,0xCE,0x0E,0x30,0xCF,0x0E,0x60,0xE7,0x9E,0x60,0x63,0xF7,0xE0,0x71,0xE3,0x80,0x38,0x00,0x00,0x1E,0x04,0x00,0x0F,0xFC,0x00,0x03,0xF8,0x00, - - 0x1d, # 0041 offset = 790 - 0x03,0xC0,0x03,0xC0,0x07,0xE0,0x07,0xE0,0x0E,0xF0,0x0E,0x70,0x1E,0x70,0x1C,0x38,0x1C,0x38,0x3F,0xFC,0x3F,0xFC,0x7F,0xFE,0x70,0x0E,0xF0,0x0F,0xE0,0x0F, - - 0x1e, # 0042 offset = 821 - 0xFF,0xC0,0xFF,0xE0,0xFF,0xF0,0xE0,0x70,0xE0,0x70,0xE0,0xF0,0xFF,0xE0,0xFF,0xE0,0xE0,0xF0,0xE0,0x78,0xE0,0x38,0xE0,0x78,0xFF,0xF8,0xFF,0xF0,0xFF,0xE0, - - 0x1f, # 0043 offset = 852 - 0x07,0xE0,0x1F,0xF0,0x3F,0xF8,0x78,0x38,0xF0,0x10,0xF0,0x00,0xE0,0x00,0xE0,0x00,0xE0,0x00,0xF0,0x00,0xF0,0x10,0x78,0x38,0x3F,0xFC,0x1F,0xF8,0x07,0xE0, - - 0x20, # 0044 offset = 883 - 0xFF,0xC0,0xFF,0xF0,0xFF,0xF8,0xE0,0x78,0xE0,0x3C,0xE0,0x1C,0xE0,0x1C,0xE0,0x1C,0xE0,0x1C,0xE0,0x1C,0xE0,0x3C,0xE0,0x78,0xFF,0xF8,0xFF,0xF0,0xFF,0xC0, - - 0x21, # 0045 offset = 914 - 0xFF,0xE0,0xFF,0xE0,0xFF,0xE0,0xE0,0x00,0xE0,0x00,0xE0,0x00,0xFF,0xC0,0xFF,0xC0,0xFF,0xC0,0xE0,0x00,0xE0,0x00,0xE0,0x00,0xFF,0xE0,0xFF,0xE0,0xFF,0xE0, - - 0x22, # 0046 offset = 945 - 0xFF,0xE0,0xFF,0xE0,0xFF,0xE0,0xE0,0x00,0xE0,0x00,0xE0,0x00,0xE0,0x00,0xFF,0xC0,0xFF,0xC0,0xFF,0xC0,0xE0,0x00,0xE0,0x00,0xE0,0x00,0xE0,0x00,0xE0,0x00, - - 0x23, # 0047 offset = 976 - 0x07,0xE0,0x1F,0xF8,0x3F,0xFC,0x78,0x38,0xF0,0x00,0xF0,0x00,0xE0,0x00,0xE0,0x1C,0xE0,0x1C,0xF0,0x1C,0xF0,0x1C,0x78,0x1C,0x3F,0xFC,0x1F,0xF8,0x07,0xE0, - - 0x24, # 0048 offset = 1007 - 0xE0,0x38,0xE0,0x38,0xE0,0x38,0xE0,0x38,0xE0,0x38,0xE0,0x38,0xFF,0xF8,0xFF,0xF8,0xFF,0xF8,0xE0,0x38,0xE0,0x38,0xE0,0x38,0xE0,0x38,0xE0,0x38,0xE0,0x38, - - 0x25, # 0049 offset = 1038 - 0xE0,0xE0,0xE0,0xE0,0xE0,0xE0,0xE0,0xE0,0xE0,0xE0,0xE0,0xE0,0xE0,0xE0,0xE0, - - 0x26, # 004A offset = 1054 - 0x7F,0xC0,0x7F,0xC0,0x7F,0xC0,0x03,0xC0,0x03,0xC0,0x03,0xC0,0x03,0xC0,0x03,0xC0,0x03,0xC0,0x03,0xC0,0x03,0xC0,0x43,0x80,0xFF,0x80,0xFF,0x00,0x3E,0x00, - - 0x27, # 004B offset = 1085 - 0xE0,0x78,0xE0,0xF0,0xE1,0xE0,0xE3,0xC0,0xE7,0x80,0xEF,0x00,0xEE,0x00,0xFF,0x00,0xFF,0x80,0xFF,0x80,0xF3,0xC0,0xE1,0xE0,0xE0,0xF0,0xE0,0x70,0xE0,0x78, - - 0x28, # 004C offset = 1116 - 0xE0,0x00,0xE0,0x00,0xE0,0x00,0xE0,0x00,0xE0,0x00,0xE0,0x00,0xE0,0x00,0xE0,0x00,0xE0,0x00,0xE0,0x00,0xE0,0x00,0xE0,0x00,0xFF,0xC0,0xFF,0xC0,0xFF,0xC0, - - 0x29, # 004D offset = 1147 - 0xE0,0x07,0xE0,0x0F,0xF0,0x0F,0xF8,0x1F,0xF8,0x1F,0xFC,0x3F,0xFC,0x3F,0xEE,0x77,0xEE,0xF7,0xE7,0xE7,0xE7,0xC7,0xE3,0xC7,0xE1,0x87,0xE0,0x07,0xE0,0x07, - - 0x24, # 004E offset = 1178 - 0xE0,0x38,0xF0,0x38,0xF8,0x38,0xF8,0x38,0xFC,0x38,0xFE,0x38,0xEF,0x38,0xEF,0x38,0xE7,0xB8,0xE3,0xF8,0xE1,0xF8,0xE0,0xF8,0xE0,0xF8,0xE0,0x78,0xE0,0x38, - - 0x2a, # 004F offset = 1209 - 0x07,0xE0,0x1F,0xF8,0x3F,0xFC,0x78,0x3E,0xF0,0x1E,0xF0,0x0F,0xE0,0x0F,0xE0,0x0F,0xE0,0x0F,0xF0,0x0F,0xF0,0x1E,0x78,0x3E,0x3F,0xFC,0x1F,0xF8,0x07,0xE0, - - 0x2b, # 0050 offset = 1240 - 0xFF,0x80,0xFF,0xE0,0xFF,0xF0,0xE0,0xF0,0xE0,0x70,0xE0,0x70,0xE0,0x70,0xE0,0xF0,0xFF,0xE0,0xFF,0xC0,0xFF,0x00,0xE0,0x00,0xE0,0x00,0xE0,0x00,0xE0,0x00, - - 0x2c, # 0051 offset = 1271 - 0x07,0xE0,0x1F,0xF8,0x3F,0xFC,0x78,0x3E,0xF0,0x1E,0xF0,0x0E,0xE0,0x0F,0xE0,0x0F,0xE0,0x0F,0xF0,0x0F,0xF0,0x0E,0x78,0x1E,0x7F,0xFC,0x3F,0xF8,0x0F,0xF0,0x01,0xE2,0x00,0xFF,0x00,0x7F, - - 0x2b, # 0052 offset = 1308 - 0xFF,0x80,0xFF,0xE0,0xFF,0xF0,0xE0,0xF0,0xE0,0x70,0xE0,0x70,0xE0,0x70,0xE0,0xF0,0xFF,0xE0,0xFF,0xC0,0xFF,0xC0,0xE1,0xC0,0xE1,0xE0,0xE0,0xF0,0xE0,0x70, - - 0x15, # 0053 offset = 1339 - 0x1F,0x80,0x7F,0xE0,0xFF,0xE0,0xF0,0x40,0xE0,0x00,0xF0,0x00,0xFE,0x00,0x7F,0xC0,0x1F,0xE0,0x01,0xF0,0x00,0xF0,0xC0,0xF0,0xFF,0xE0,0xFF,0xC0,0x3F,0x80, - - 0x2d, # 0054 offset = 1370 - 0xFF,0xF8,0xFF,0xF8,0xFF,0xF8,0x07,0x00,0x07,0x00,0x07,0x00,0x07,0x00,0x07,0x00,0x07,0x00,0x07,0x00,0x07,0x00,0x07,0x00,0x07,0x00,0x07,0x00,0x07,0x00, - - 0x1e, # 0055 offset = 1401 - 0xE0,0x78,0xE0,0x78,0xE0,0x78,0xE0,0x78,0xE0,0x78,0xE0,0x78,0xE0,0x78,0xE0,0x78,0xE0,0x78,0xE0,0x78,0xE0,0x78,0xF0,0xF0,0x7F,0xF0,0x3F,0xE0,0x1F,0x80, - - 0x1d, # 0056 offset = 1432 - 0xF0,0x0F,0xF0,0x0E,0x70,0x1E,0x78,0x1C,0x38,0x1C,0x3C,0x38,0x1C,0x38,0x1E,0x78,0x1E,0x70,0x0E,0xF0,0x0F,0xE0,0x07,0xE0,0x07,0xC0,0x07,0xC0,0x03,0xC0, - - 0x2e, # 0057 offset = 1463 - 0xE0,0x38,0x1E,0xE0,0x78,0x1C,0xF0,0x7C,0x1C,0x70,0x7C,0x3C,0x70,0xFC,0x38,0x78,0xFE,0x38,0x38,0xEE,0x78,0x39,0xCE,0x70,0x3D,0xCE,0x70,0x3D,0xC7,0x70,0x1F,0x87,0xE0,0x1F,0x87,0xE0,0x1F,0x83,0xE0,0x0F,0x03,0xC0,0x0F,0x03,0xC0, - - 0x2f, # 0058 offset = 1509 - 0x78,0x3C,0x78,0x38,0x3C,0x78,0x1E,0xF0,0x1E,0xE0,0x0F,0xE0,0x07,0xC0,0x07,0xC0,0x07,0xC0,0x0F,0xE0,0x1E,0xF0,0x1C,0x70,0x3C,0x78,0x78,0x3C,0xF0,0x1C, - - 0x30, # 0059 offset = 1540 - 0xF0,0x1C,0x70,0x3C,0x78,0x38,0x38,0x70,0x3C,0xF0,0x1C,0xE0,0x1F,0xE0,0x0F,0xC0,0x07,0x80,0x07,0x80,0x07,0x80,0x07,0x80,0x07,0x80,0x07,0x80,0x07,0x80, - - 0x14, # 005A offset = 1571 - 0xFF,0xF0,0xFF,0xF0,0xFF,0xF0,0x01,0xE0,0x03,0xC0,0x03,0xC0,0x07,0x80,0x0F,0x00,0x1E,0x00,0x3C,0x00,0x3C,0x00,0x78,0x00,0xFF,0xF8,0xFF,0xF8,0xFF,0xF8, - - 0x31, # 005B offset = 1602 - 0xF8,0xF8,0xF8,0xE0,0xE0,0xE0,0xE0,0xE0,0xE0,0xE0,0xE0,0xE0,0xE0,0xE0,0xE0,0xE0,0xE0,0xF8,0xF8,0xF8, - - 0x10, # 005C offset = 1623 - 0xE0,0x00,0xE0,0x00,0x70,0x00,0x70,0x00,0x30,0x00,0x38,0x00,0x38,0x00,0x18,0x00,0x1C,0x00,0x1C,0x00,0x0E,0x00,0x0E,0x00,0x0E,0x00,0x07,0x00,0x07,0x00,0x07,0x00,0x03,0x80,0x03,0x80,0x03,0x80,0x01,0xC0, - - 0x32, # 005D offset = 1664 - 0xFC,0xFC,0xFC,0x1C,0x1C,0x1C,0x1C,0x1C,0x1C,0x1C,0x1C,0x1C,0x1C,0x1C,0x1C,0x1C,0x1C,0xFC,0xFC,0xFC, - - 0x33, # 005E offset = 1685 - 0x0E,0x00,0x1E,0x00,0x1E,0x00,0x1B,0x00,0x3B,0x00,0x33,0x80,0x71,0x80,0x61,0xC0,0xE0,0xC0, - - 0x34, # 005F offset = 1704 - 0xFF,0xC0,0xFF,0xC0, - - 0x35, # 0060 offset = 1709 - 0xF0,0x38,0x1C, - - 0x36, # 0061 offset = 1713 - 0x3F,0x00,0xFF,0x80,0x7F,0xC0,0x01,0xC0,0x3F,0xC0,0xFF,0xC0,0xE1,0xC0,0xE1,0xC0,0xF3,0xC0,0xFF,0xC0,0x3D,0xC0, - - 0x37, # 0062 offset = 1736 - 0xF0,0x00,0xF0,0x00,0xF0,0x00,0xF0,0x00,0xF0,0x00,0xF7,0xC0,0xFF,0xE0,0xFF,0xF0,0xF0,0x70,0xF0,0x78,0xF0,0x78,0xF0,0x78,0xF0,0x70,0xFF,0xF0,0xFF,0xE0,0xF7,0xC0, - - 0x38, # 0063 offset = 1769 - 0x1F,0x80,0x3F,0xC0,0x7F,0xE0,0xF0,0xC0,0xE0,0x00,0xE0,0x00,0xE0,0x00,0xF0,0xC0,0x7F,0xE0,0x3F,0xC0,0x1F,0x80, - - 0x39, # 0064 offset = 1792 - 0x00,0x70,0x00,0x70,0x00,0x70,0x00,0x70,0x00,0x70,0x1F,0x70,0x7F,0xF0,0x7F,0xF0,0xF0,0xF0,0xE0,0x70,0xE0,0x70,0xE0,0x70,0xF0,0xF0,0x7F,0xF0,0x7F,0xF0,0x1F,0x70, - - 0x3a, # 0065 offset = 1825 - 0x1F,0x00,0x7F,0xC0,0x7B,0xE0,0xE0,0xE0,0xFF,0xE0,0xFF,0xE0,0xE0,0x00,0xF0,0x00,0x7F,0xC0,0x3F,0xC0,0x1F,0x80, - - 0x3b, # 0066 offset = 1848 - 0x0F,0x80,0x1F,0x00,0x3F,0x00,0x38,0x00,0x38,0x00,0xFF,0x00,0xFF,0x00,0x3F,0x00,0x38,0x00,0x38,0x00,0x38,0x00,0x38,0x00,0x38,0x00,0x38,0x00,0x38,0x00,0x38,0x00, - - 0x3c, # 0067 offset = 1881 - 0x1F,0x70,0x7F,0xF0,0xFF,0xF0,0xF0,0xF0,0xE0,0x70,0xE0,0x70,0xE0,0x70,0xF0,0xF0,0x7F,0xF0,0x3F,0xF0,0x0E,0x70,0x40,0xF0,0x7F,0xE0,0xFF,0xE0,0x3F,0x80, - - 0x39, # 0068 offset = 1912 - 0xF0,0x00,0xF0,0x00,0xF0,0x00,0xF0,0x00,0xF0,0x00,0xF7,0xC0,0xFF,0xE0,0xFF,0xF0,0xF8,0xF0,0xF0,0x70,0xF0,0x70,0xF0,0x70,0xF0,0x70,0xF0,0x70,0xF0,0x70,0xF0,0x70, - - 0x3d, # 0069 offset = 1945 - 0x70,0xF0,0xF0,0x60,0x00,0xF0,0xF0,0xF0,0xF0,0xF0,0xF0,0xF0,0xF0,0xF0,0xF0,0xF0, - - 0x3e, # 006A offset = 1962 - 0x0E,0x1E,0x1E,0x0C,0x00,0x0E,0x0E,0x0E,0x0E,0x0E,0x0E,0x0E,0x0E,0x0E,0x0E,0x0E,0x0E,0x7E,0xFC,0xF8, - - 0x39, # 006B offset = 1983 - 0xF0,0x00,0xF0,0x00,0xF0,0x00,0xF0,0x00,0xF0,0x00,0xF0,0xF0,0xF1,0xE0,0xF3,0xC0,0xF7,0x80,0xFF,0x00,0xFF,0x00,0xFF,0x80,0xF3,0xC0,0xF1,0xE0,0xF0,0xF0,0xF0,0xF0, - - 0x3d, # 006C offset = 2016 - 0xF0,0xF0,0xF0,0xF0,0xF0,0xF0,0xF0,0xF0,0xF0,0xF0,0xF0,0xF0,0xF0,0xF0,0xF0,0xF0, - - 0x3f, # 006D offset = 2033 - 0xF7,0x87,0x80,0xFF,0xFF,0xC0,0xFF,0xFF,0xE0,0xF0,0xF0,0xE0,0xF0,0xF0,0xE0,0xF0,0xF0,0xE0,0xF0,0xF0,0xE0,0xF0,0xF0,0xE0,0xF0,0xF0,0xE0,0xF0,0xF0,0xE0,0xF0,0xF0,0xE0, - - 0x40, # 006E offset = 2067 - 0xF7,0xC0,0xFF,0xE0,0xFF,0xF0,0xF0,0xF0,0xF0,0x70,0xF0,0x70,0xF0,0x70,0xF0,0x70,0xF0,0x70,0xF0,0x70,0xF0,0x70, - - 0x40, # 006F offset = 2090 - 0x1F,0x00,0x3F,0xC0,0x7F,0xE0,0xF0,0xF0,0xE0,0x70,0xE0,0x70,0xE0,0x70,0xF0,0xF0,0x7F,0xE0,0x7F,0xC0,0x1F,0x00, - - 0x41, # 0070 offset = 2113 - 0xF7,0xC0,0xFF,0xE0,0xFF,0xF0,0xF0,0x70,0xF0,0x78,0xF0,0x78,0xF0,0x78,0xF0,0x70,0xFF,0xF0,0xFF,0xE0,0xF7,0xC0,0xF0,0x00,0xF0,0x00,0xF0,0x00,0xF0,0x00, - - 0x42, # 0071 offset = 2144 - 0x1F,0x70,0x7F,0xF0,0xFF,0xF0,0xF0,0xF0,0xE0,0x70,0xE0,0x70,0xE0,0x70,0xF0,0xF0,0xFF,0xF0,0x7F,0xF0,0x1F,0x70,0x00,0x70,0x00,0x70,0x00,0x70,0x00,0x70, - - 0x43, # 0072 offset = 2175 - 0xF6,0xFE,0xFE,0xF8,0xF0,0xF0,0xF0,0xF0,0xF0,0xF0,0xF0, - - 0x44, # 0073 offset = 2187 - 0x3F,0x00,0xFF,0x80,0xF7,0x00,0xE0,0x00,0xF0,0x00,0xFF,0x00,0x3F,0x80,0x03,0xC0,0xF7,0x80,0xFF,0x80,0x7E,0x00, - - 0x45, # 0074 offset = 2210 - 0x38,0x38,0x38,0xFF,0xFF,0x3F,0x38,0x38,0x38,0x38,0x38,0x3F,0x1F,0x0F, - - 0x40, # 0075 offset = 2225 - 0xF0,0x70,0xF0,0x70,0xF0,0x70,0xF0,0x70,0xF0,0x70,0xF0,0x70,0xF0,0xF0,0x70,0xF0,0x7F,0xF0,0x7F,0xF0,0x1E,0x70, - - 0x46, # 0076 offset = 2248 - 0xE0,0x70,0xF0,0x70,0x70,0xF0,0x78,0xE0,0x38,0xE0,0x39,0xC0,0x1D,0xC0,0x1F,0x80,0x1F,0x80,0x0F,0x80,0x0F,0x00, - - 0x47, # 0077 offset = 2271 - 0xE0,0xE0,0xE0,0x70,0xF0,0xE0,0x70,0xF0,0xE0,0x71,0xF1,0xC0,0x39,0xF9,0xC0,0x3B,0xB9,0x80,0x3B,0x9F,0x80,0x1F,0x9F,0x80,0x1F,0x1F,0x00,0x1F,0x0F,0x00,0x0E,0x0F,0x00, - - 0x46, # 0078 offset = 2305 - 0x70,0xF0,0x78,0xE0,0x3D,0xC0,0x1F,0x80,0x0F,0x80,0x0F,0x00,0x0F,0x80,0x1F,0xC0,0x3D,0xC0,0x78,0xE0,0xF0,0xF0, - - 0x48, # 0079 offset = 2328 - 0xE0,0x70,0xF0,0x70,0x70,0xF0,0x78,0xE0,0x38,0xE0,0x3D,0xC0,0x1D,0xC0,0x1F,0x80,0x0F,0x80,0x0F,0x00,0x0F,0x00,0x0F,0x00,0x7E,0x00,0xFC,0x00,0x78,0x00, - - 0x44, # 007A offset = 2359 - 0xFF,0xC0,0xFF,0xC0,0x07,0x80,0x07,0x00,0x0E,0x00,0x1C,0x00,0x3C,0x00,0x78,0x00,0xF0,0x00,0xFF,0xC0,0xFF,0xC0, - - 0x49, # 007B offset = 2382 - 0x1E,0x3E,0x3E,0x38,0x38,0x38,0x38,0x38,0xF8,0xF0,0xF8,0x38,0x38,0x38,0x38,0x38,0x38,0x3E,0x3E,0x1E, - - 0x4a, # 007C offset = 2403 - 0xE0,0xE0,0xE0,0xE0,0xE0,0xE0,0xE0,0xE0,0xE0,0xE0,0xE0,0xE0,0xE0,0xE0,0xE0,0xE0,0xE0,0xE0,0xE0,0xE0, - - 0x4b, # 007D offset = 2424 - 0xF0,0xF8,0xF8,0x38,0x38,0x38,0x38,0x38,0x3E,0x1E,0x1E,0x3C,0x38,0x38,0x38,0x38,0x38,0xF8,0xF8,0xF0, - - 0x4c, # 007E offset = 2445 - 0x78,0xC0,0xFE,0xC0,0xCF,0xC0,0xC3,0x80, - - 0x4d, # 00B1 offset = 2454 - 0x0E,0x00,0x0E,0x00,0x0E,0x00,0xFF,0xC0,0xFF,0xC0,0x0E,0x00,0x0E,0x00,0x0E,0x00,0x0E,0x00,0x00,0x00,0xFF,0xC0,0xFF,0xC0,0xFF,0xC0, - - 0x4e, # 00B2 offset = 2481 - 0x78,0xFC,0x0E,0x0E,0x1C,0x38,0x70,0xFE,0xFE, - - 0x4e, # 00B3 offset = 2491 - 0xFE,0xFE,0x1C,0x38,0x3C,0x0E,0x06,0xFE,0xFC, - - 0x4f, # 00B4 offset = 2501 - 0x38,0x70,0xE0, - - 0x42, # 00B5 offset = 2505 - 0xF0,0x70,0xF0,0x70,0xF0,0x70,0xF0,0x70,0xF0,0x70,0xF0,0x70,0xF0,0xF0,0xF0,0xF0,0xFF,0xF0,0xFF,0xF0,0xFF,0x70,0xF0,0x00,0xF0,0x00,0xF0,0x00,0xF0,0x00, - - 0x50, # 00D7 offset = 2536 - 0x40,0xE3,0xF7,0x7E,0x3C,0x3E,0x7F,0xE7,0x42, - - ]) - diff --git a/ports/stm32/boards/Passport/modules/periodic.py b/ports/stm32/boards/Passport/modules/periodic.py new file mode 100644 index 0000000..e3311cc --- /dev/null +++ b/ports/stm32/boards/Passport/modules/periodic.py @@ -0,0 +1,275 @@ +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# + +from uasyncio import sleep_ms +import random +import utime +import common +from ubinascii import hexlify as b2a_hex + +def ambient_to_brightness(ambient): + return 100 - ambient + +async def update_ambient_screen_brightness(): + first_time = True + while True: + from common import settings, dis, system + if not first_time: + await sleep_ms(5000) + first_time = False + + # Brightness of 999 means automatic + if settings.get('screen_brightness', 100) == 999: + ambient = system.read_ambient() + brightness = ambient_to_brightness(ambient) + # print(' ambient = {} brightness = {}'.format(ambient, brightness)) + dis.set_brightness(brightness) + +battery_segments = [ + {'v': 3100, 'p': 100}, + {'v': 3100, 'p': 100}, + {'v': 3025, 'p': 75}, + {'v': 2975, 'p': 50}, + {'v': 2800, 'p': 25}, + {'v': 2400, 'p': 0}, +] + +def calc_battery_percent(current, voltage): + # print('calc_battery_percent(): voltage={}'.format(voltage)) + if voltage > 3100: + voltage = 3100 + elif voltage < 2400: + voltage = 2400 + + # First find the segment we fit in + for i in range(1, len(battery_segments)): + curr = battery_segments[i] + prev = battery_segments[i - 1] + if voltage >= curr['v']: + # print('curr[{}]={}'.format(i, curr)) + + rise = curr['v'] - prev['v'] + # print('rise={}'.format(rise)) + + run = curr['p'] - prev['p'] + # print('run={}'.format(run)) + + if run == 0: + # print('zero run, so return value directly: {}'.format(curr['p'])) + return curr['p'] + + # Slope + m = rise / run + # print('m={}'.format(m)) + + # y = mx + b => x = (y - b) / m => b = y - mx + + # Calculate y intercept for this segment + b = curr['v'] - (m * curr['p']) + # print('b={}'.format(b)) + + percent = int((voltage - b) / m) + # print('Returning percent={}'.format(percent)) + return percent + + return 0 + +NUM_SAMPLES = 2 + +async def update_battery_level(): + from utils import random_filename + from files import CardSlot + header_written = False + first_time = True + battery_mon_fname = None + + while True: + # Take a reading immediately the first time through + if not first_time: + await sleep_ms(60000) + + first_time = False + + # Read the current values -- repeat this a number of times and average for better results + total_current = 0 + total_voltage = 0 + for i in range(NUM_SAMPLES): + (current, voltage) = common.powermon.read() + voltage = round(voltage * (44.7 + 22.1) / 44.7) + total_current += current + total_voltage += voltage + await sleep_ms(1) # Wait a bit before next sample + current = total_current / NUM_SAMPLES + voltage = total_voltage / NUM_SAMPLES + + # Update the battery_mon file if enabled + if common.enable_battery_mon: + try: + with CardSlot() as card: + if battery_mon_fname == None: + battery_mon_fname = random_filename(card, 'battery-mon-{}.csv') + + with open(battery_mon_fname, 'a') as fd: + # Write the header + if not header_written: + # print('Writing battery_mon header') + fd.write('Time,Current,Voltage\n') + header_written = True + + # Write the sample values + now = utime.ticks_ms() + # print('Writing battery_mon sample: current={} voltage={}'.format(current, voltage)) + fd.write('{},{},{}\n'.format(now, current, voltage)) + except Exception as e: + # includes CardMissingError + import sys + sys.print_exception(e) + + # Update the actual battery level that drives the icon + level = calc_battery_percent(current, voltage) + # print(' new battery level = {}'.format(level)) + common.battery_level = level + common.battery_voltage = voltage + + +SHUTDOWN_COUNTDOWN_MAX = 6 + +async def check_auto_shutdown(): + countdown = SHUTDOWN_COUNTDOWN_MAX + while True: + from common import settings, dis + + # Never shutdown when doing a battery test! + if common.enable_battery_mon: + return + + timeout_ms = settings.get('shutdown_timeout', 5*60) * 1000 # Convert secs to ms + + await sleep_ms(1000) # Always check again right after waking from sleep + if timeout_ms == 0: + continue + + # Give user a chance to abort + countdown -= 1 + now = utime.ticks_ms() + idle_so_far = now - common.last_activity_time + # print('idle_so_far={} timeout_ms={} countdown={}'.format(idle_so_far, timeout_ms, countdown)) + if idle_so_far >= timeout_ms: + if countdown == -1: + common.system.shutdown() # Never return from this! + else: + dis.fullscreen('Shutting down in {}'.format(countdown), line2='Press key to cancel') + else: + # Reset countdown if we haven't hit the timeout yet + countdown = SHUTDOWN_COUNTDOWN_MAX + + +ENTER = 'y1' +BACK = 'x1' +BACKSPACE = '*1' +EXIT = -1 + +main_actions = [ + 'd4', + ENTER, + 2000, + 'd8', + ENTER, + 2000, + 'd2', + ENTER, + 2000, + 'd3', + ENTER, + 2000, + ENTER, + 5000, + BACK, + 2000, + BACK, + 2000, + BACK, + 2000, + 'u2', + ENTER, + 2000, + 'd2', + ENTER, # 100% + 2000, + ENTER, + 2000, + 'u2', # Back to 50% + ENTER, + 2000, + BACK, + 'u4', + 5000, + 'd1', + ENTER, + 15000, # Camera on for 15 seconds + BACK, + 'u1', +] + +actions = [ + 1000, + 'd2', # Just a wink so you know the script has started + 1000, + 'u2', + 1000, + { + 'repeat': 1, + 'actions': main_actions + }, + 5000, + # 7200000, # 2 hour delay before next transaction +] + +async def run_actions(actions, repeat): + import common + + for i in range(repeat): + for action in actions: + if common.demo_active: + await handle_demo_action(action) + await sleep_ms(100) # Short delay to keep things from going too fast + +async def handle_demo_action(action): + import common + + if type(action) == int: + if action == -1: + # Disable the demo + common.demo_active = False + return + + # print('delay {}ms'.format(action)) + + # Delay + await sleep_ms(action) + + elif isinstance(action, dict): + # Nested script + print('Starting nested script') + repeat = action['repeat'] + actions = action['actions'] + await run_actions(actions, repeat) + + else: + key = action[0] + count = int(action[1:]) + + print('inject "{}" {} times'.format(key, count)) + for i in range(count): + common.keypad.inject(key) + +async def demo_loop(): + import common + while True: + if common.demo_active: + await run_actions(actions, 1) + common.demo_count += 1 + else: + common.demo_count = 0 + await sleep_ms(5000) diff --git a/ports/stm32/boards/Passport/modules/pincodes.py b/ports/stm32/boards/Passport/modules/pincodes.py index cdd7f17..2ff5865 100644 --- a/ports/stm32/boards/Passport/modules/pincodes.py +++ b/ports/stm32/boards/Passport/modules/pincodes.py @@ -1,7 +1,7 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: GPL-3.0-or-later # -# SPDX-FileCopyrightText: 2018 Coinkite, Inc. +# SPDX-FileCopyrightText: 2018 Coinkite, Inc. # SPDX-License-Identifier: GPL-3.0-only # # (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard @@ -13,7 +13,7 @@ import trezorcrypto import ustruct import version -from callgate import enter_dfu, get_anti_phishing_words,get_supply_chain_validation_words +from callgate import get_anti_phishing_words,get_supply_chain_validation_words from ubinascii import hexlify as b2a_hex from se_commands import * from common import system @@ -23,10 +23,7 @@ from common import system MAX_PIN_LEN = const(32) # how many bytes per secret (you don't have to use them all) -AE_SECRET_LEN = const(72) - -# on mark3 (608a) we can also store a longer secret -AE_LONG_SECRET_LEN = const(416) +SE_SECRET_LEN = const(72) # magic number for struct PA_MAGIC_V1 = const(0xc50b61a7) @@ -119,6 +116,7 @@ class PinAttempt: self.delay_required = 0 # how much will be needed? self.num_fails = 0 # for UI: number of fails PINs self.attempts_left = 0 # ignore in mk1/2 case, only valid for mk3 + self.max_attempts = 21 # Numbger of attempts allowed self.state_flags = 0 # useful readback self.private_state = 0 # opaque data, but preserve self.cached_main_pin = bytearray(32) @@ -133,8 +131,8 @@ class PinAttempt: import callgate if callgate.get_is_bricked(): # die right away if it's not going to work - print('AM I BRICKED FOR REALZ!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!') - # callgate.enter_dfu(3) + # print('I AM I BRICKED!!!') + pass def __repr__(self): return ''.format( @@ -148,9 +146,9 @@ class PinAttempt: if new_secret is not None: change_flags |= CHANGE_SECRET - assert len(new_secret) in (32, AE_SECRET_LEN) + assert len(new_secret) in (32, SE_SECRET_LEN) else: - new_secret = bytes(AE_SECRET_LEN) + new_secret = bytes(SE_SECRET_LEN) # NOTE: pins should be bytes here. @@ -234,11 +232,14 @@ class PinAttempt: if err <= -100: #print("[%d] req: %s" % (err, b2a_hex(self.buf))) if err == EPIN_I_AM_BRICK: - # don't try to continue! - pass - # enter_dfu(3) + raise RuntimeError(err) + + # Unpack the updated attempts_left and num_fails if the pin was wrong so the UI updates correctly + if err == EPIN_AUTH_FAIL: + self.unmarshal(self.buf) + # print('Unmarshalled: {}'.format(self.attempts_left)) - print('ERROR: {} ({})'.format(PA_ERROR_CODES[err], err)) + # print('ERROR: {} ({})'.format(PA_ERROR_CODES[err], err)) raise BootloaderError(PA_ERROR_CODES[err], err) elif err: raise RuntimeError(err) @@ -262,25 +263,25 @@ class PinAttempt: if err: raise RuntimeError(err) - print('pin buf={}'.format(buf)) - # Get a mnemonic from the 32 bytes in the buffer s = trezorcrypto.bip39.from_data(buf) rv = s.split() - print('anti-phishing words = {}'.format(rv[0:2])) return rv[0:2] # Only keep 2 words for anti-phishing prefix - # TODO: Add unit tests for the supply chain validation @staticmethod - def supply_chain_validation_words(validation_str): + def supply_chain_validation_words(challenge_str): # Take the validation string and turn it into 4 # bip39 words for supply chain tampering detection. - buf = bytearray(validation_str) + # print('challenge_str={}'.format(challenge_str)) + buf = bytearray(challenge_str) + + # This actually gets the HMAC bytes, not the words err = get_supply_chain_validation_words(buf) if err: raise RuntimeError(err) + # print('hmac buf = {}'.format(buf)) # Get a mnemonic from the 32 bytes in the buffer buf = buf[:32] @@ -290,7 +291,7 @@ class PinAttempt: s = trezorcrypto.bip39.from_data(buf) rv = s.split() - print('supply chain validation words = {}'.format(rv[0:4])) + # print('supply chain validation words = {}'.format(rv[0:4])) return rv[0:4] # Only keep 4 words for supply chain validation @@ -316,7 +317,7 @@ class PinAttempt: # Prepare the class for a PIN operation (first pin, login, change) def setup(self, pin, secondary=False): - print('Setting up SE hmac') + # print('Setting up SE hmac') self.pin = pin self.hmac = bytes(32) @@ -351,56 +352,23 @@ class PinAttempt: secret = self.pin_control(PIN_GET_SECRET) return secret - def ls_fetch(self): - # get the "long secret" - #assert (13 * 32) == 416 == AE_LONG_SECRET_LEN - - secret = b'' - for n in range(13): - secret += self.pin_control(PIN_LONG_SECRET, ls_offset=n)[0:32] - - return secret - - def ls_change(self, new_long_secret): - # set the "long secret" - assert len(new_long_secret) == AE_LONG_SECRET_LEN - - for n in range(13): - self.pin_control( - PIN_LONG_SECRET, ls_offset=n, new_secret=new_long_secret[n*32:(n*32)+32]) - - def greenlight_firmware(self): - # hash all of flash and commit value to 508a/608a - self.pin_control(PIN_GREENLIGHT_FIRMWARE) - - def new_main_secret(self, raw_secret, chain=None): - # Main secret has changed: reset the settings+their key, - # and capture xfp/xpub - from common import settings + async def new_main_secret(self, raw_secret, chain=None): + from common import settings, flash_cache import stash - # capture values we have already - old_values = dict(settings.curr_dict) - - print('old_values = {}'.format(old_values)) - settings.set_key(raw_secret) - settings.load() - print('after load = {}'.format(settings.curr_dict)) - - # merge in settings, including what chain to use, timeout, etc. - settings.merge(old_values) - print('after merge = {}'.format(settings.curr_dict)) - # Recalculate xfp/xpub values (depends both on secret and chain) with stash.SensitiveValues(raw_secret) as sv: if chain is not None: sv.chain = chain sv.capture_xpub() - print('before save = {}'.format(settings.curr_dict)) + # We shouldn't need to save this anymore since we are dynamically managing xfp and xpub + # await settings.save() + + # Set the key for the flash cache (cache was inaccessible prior to user logging in) + flash_cache.set_key(new_secret=raw_secret) - # Need to save values with new AES key - settings.save() - print('after save = {}'.format(settings.curr_dict)) + # Need to save out flash cache with the new secret or else we won't be able to read it back later + flash_cache.save() # EOF diff --git a/ports/stm32/boards/Passport/modules/psbt.py b/ports/stm32/boards/Passport/modules/psbt.py index c4b9391..4ea4ce2 100644 --- a/ports/stm32/boards/Passport/modules/psbt.py +++ b/ports/stm32/boards/Passport/modules/psbt.py @@ -1,7 +1,7 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: GPL-3.0-or-later # -# SPDX-FileCopyrightText: 2018 Coinkite, Inc. +# SPDX-FileCopyrightText: 2018 Coinkite, Inc. # SPDX-License-Identifier: GPL-3.0-only # # (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard @@ -9,18 +9,20 @@ # # psbt.py - understand PSBT file format: verify and generate them # -from serializations import ser_compact_size, deser_compact_size, hash160, hash256 -from serializations import CTxIn, CTxInWitness, CTxOut, SIGHASH_ALL, ser_uint256 -from serializations import ser_sig_der, uint256_from_str, ser_push_data, uint256_from_str -from serializations import ser_string from ustruct import unpack_from, unpack, pack from ubinascii import hexlify as b2a_hex -from utils import xfp2str -import trezorcrypto, stash, gc +from utils import xfp2str, B2A, keypath_to_str, problem_file_line, swab32 +import trezorcrypto, stash, gc, history, sys from uio import BytesIO from sffile import SizerFile from sram4 import psbt_tmp256 from multisig import MultisigWallet, MAX_SIGNERS, disassemble_multisig, disassemble_multisig_mn +from exceptions import FatalPSBTIssue, FraudulentChangeOutput +from serializations import ser_compact_size, deser_compact_size, hash160, hash256 +from serializations import CTxIn, CTxInWitness, CTxOut, SIGHASH_ALL, ser_uint256 +from serializations import ser_sig_der, uint256_from_str, ser_push_data, uint256_from_str +from serializations import ser_string +from common import system from public_constants import ( PSBT_GLOBAL_UNSIGNED_TX, PSBT_GLOBAL_XPUB, PSBT_IN_NON_WITNESS_UTXO, PSBT_IN_WITNESS_UTXO, @@ -30,35 +32,23 @@ from public_constants import ( PSBT_OUT_BIP32_DERIVATION, MAX_PATH_DEPTH ) -# Max miner's fee, as percentage of output value, that we will allow to be signed. -# Amounts over 5% are warned regardless. -DEFAULT_MAX_FEE_PERCENTAGE = const(-1) - -B2A = lambda x: str(b2a_hex(x), 'ascii') - # print some things -DEBUG = const(0) - -class FatalPSBTIssue(RuntimeError): - pass -class FraudulentChangeOutput(FatalPSBTIssue): - def __init__(self, out_idx, msg): - super().__init__('Output#%d: %s' % (out_idx, msg)) - -class HashNDump: - def __init__(self, d=None): - self.rv = trezorcrypto.sha256() - print('Hashing: ', end='') - if d: - self.update(d) - - def update(self, d): - print(b2a_hex(d), end=' ') - self.rv.update(d) - - def digest(self): - print(' END') - return self.rv.digest() +# DEBUG = const(1) + +# class HashNDump: +# def __init__(self, d=None): +# self.rv = trezorcrypto.sha256() +# print('Hashing: ', end='') +# if d: +# self.update(d) +# +# def update(self, d): +# print(b2a_hex(d), end=' ') +# self.rv.update(d) +# +# def digest(self): +# print(' END') +# return self.rv.digest() def read_varint(v): # read "compact sized" int from a few bytes. @@ -72,9 +62,9 @@ def read_varint(v): return unpack_from(" ll: + here = ll + rv.update(memoryview(psbt_tmp256)[0:here]) + ll -= here + + if hasher: + return + + return trezorcrypto.sha256(rv.digest()).digest() + class psbtProxy: # store offsets to values, but track the keys in-memory. @@ -117,7 +168,7 @@ class psbtProxy: def __getattr__(self, nm): if nm in self.blank_flds: return None - raise AttributeError + raise AttributeError(nm) def parse(self, fd): self.fd = fd @@ -132,18 +183,22 @@ class psbtProxy: assert vs != None, 'eof' kt = key[0] + # print('kt={}'.format(kt)) + # print('self.no_keys={} 1'.format(self.no_keys)) if kt in self.no_keys: - assert len(key) == 1 # not expectiing key + # print('not expecting key') + assert len(key) == 1 # not expecting key # storing offset and length only! Mostly. + # print('self.short_values={}'.format(self.short_values)) if kt in self.short_values: + # print('Adding xpub') actual = fd.read(vs) self.store(kt, bytes(key), actual) else: # skip actual data for now - # TODO: could this be stored more compactly? proxy = (fd.tell(), vs) fd.seek(vs, 1) @@ -179,25 +234,6 @@ class psbtProxy: self.fd.seek(pos) return self.fd.read(ll) - def get_hash256(self, val, hasher=None): - # return the double-sha256 of a value, without loading it into memory - pos, ll = val - rv = hasher or trezorcrypto.sha256() - - self.fd.seek(pos) - while ll: - here = self.fd.read_into(psbt_tmp256) - if not here: break - if here > ll: - here = ll - rv.update(memoryview(psbt_tmp256)[0:here]) - ll -= here - - if hasher: - return - - return trezorcrypto.sha256(rv.digest()).digest() - def parse_subpaths(self, my_xfp): # Reformat self.subpaths into a more useful form for us; return # of them # that are ours (and track that as self.num_our_keys) @@ -227,17 +263,16 @@ class psbtProxy: # promote to a list of ints v = self.get(self.subpaths[pk]) - here = list(unpack_from(' 0, "no ins?" self.num_inputs = num_in + # print('self.num_inputs = {}'.format(self.num_inputs )) # all the ins are in sequence starting at this position self.vin_start = _skip_n_objs(fd, num_in, 'CTxIn') # next is outputs self.num_outputs = deser_compact_size(fd) + # print('self.num_outputs = {}'.format(self.num_outputs )) self.vout_start = _skip_n_objs(fd, self.num_outputs, 'CTxOut') @@ -1013,7 +1013,7 @@ class psbtObject(psbtProxy): if rs[-1] != OP_CHECKMULTISIG: continue M, N = disassemble_multisig_mn(rs) - assert 1 <= M <= N < MAX_SIGNERS + assert 1 <= M <= N <= MAX_SIGNERS return (M, N) @@ -1024,17 +1024,27 @@ class psbtObject(psbtProxy): async def handle_xpubs(self): # Lookup correct wallet based on xpubs in globals # - only happens if they volunteered this 'extra' data + # - do not assume multisig assert not self.active_multisig - xfps = [unpack_from('= 1 + xfp_paths.append(h) + + if h[0] == self.my_xfp: + has_mine += 1 - candidates = MultisigWallet.find_candidates(xfps) + if not has_mine: + raise FatalPSBTIssue('My XFP not involved') + + candidates = MultisigWallet.find_candidates(xfp_paths) - match_idx = -1 if len(candidates) == 1: - # exact match (by xfp set) .. normal case - self.active_multisig = MultisigWallet.get_by_idx(candidates[0]) + # exact match (by xfp+deriv set) .. normal case + self.active_multisig = candidates[0] else: # don't want to guess M if not needed, but we need it M, N = self.guess_M_of_N() @@ -1045,25 +1055,39 @@ class psbtObject(psbtProxy): # - too slow to re-derive it here, so nothing more to validate at this point return - assert N == len(xfps) + assert N == len(xfp_paths) + + for c in candidates: + if c.M == M: + assert c.N == N + self.active_multisig = c + break - if candidates: - # maybe narrowed down to single match now - match_idx = MultisigWallet.find_match(M, N, xfps) - if match_idx != -1: - self.active_multisig = MultisigWallet.get_by_idx(match_idx) + del candidates if not self.active_multisig: # Maybe create wallet, for today, forever, or fail, etc. proposed, need_approval = MultisigWallet.import_from_psbt(M, N, self.xpubs) + + # print('proposed={}, need_approval={}'.format(proposed, need_approval)) + if need_approval: # do a complex UX sequence, which lets them save new wallet - ch = await proposed.confirm_import() if ch != 'y': raise FatalPSBTIssue("Refused to import new wallet") self.active_multisig = proposed + else: + # Validate good match here. The xpubs must be exactly right, but + # we're going to use our own values from setup time anyway and not trusting + # new values without user interaction. + # Check: + # - chain codes match what we have stored already + # - pubkey vs. path will be checked later + # - xfp+path already checked above when selecting wallet + # Any issue here is a fraud attempt in some way, not innocent. + self.active_multisig.validate_psbt_xpubs(self.xpubs) if not self.active_multisig: # not clear if an error... might be part-way to importing, and @@ -1071,9 +1095,6 @@ class psbtObject(psbtProxy): # we should not reach this point (ie. raise something to abort signing) return - # Could validate good match here? The xpubs must be exactly right, but - # we're going to use our own values from setup time anyway and not trusting - # these values without user interaction. async def validate(self): # Do a first pass over the txn. Raise assertions, be terse tho because @@ -1088,17 +1109,19 @@ class psbtObject(psbtProxy): assert len(self.inputs) == self.num_inputs, 'ni mismatch' # if multisig xpub details provided, they better be right and/or offer import + # print('self.xpubs={}'.format(self.xpubs)) if self.xpubs: + # print('calling self.handle_xpubs()') await self.handle_xpubs() assert self.num_outputs >= 1, 'need outs' - if DEBUG: - our_keys = sum(1 for i in self.inputs if i.num_our_keys) - - print("PSBT: %d inputs, %d output, %d fully-signed, %d ours" % ( - self.num_inputs, self.num_outputs, - sum(1 for i in self.inputs if i and i.fully_signed), our_keys)) + # if DEBUG: + # our_keys = sum(1 for i in self.inputs if i.num_our_keys) + # + # print("PSBT: %d inputs, %d output, %d fully-signed, %d ours" % ( + # self.num_inputs, self.num_outputs, + # sum(1 for i in self.inputs if i and i.fully_signed), our_keys)) def consider_outputs(self): # scan ouputs: @@ -1106,6 +1129,7 @@ class psbtObject(psbtProxy): # - mark change outputs, so perhaps we don't show them to users total_change = 0 + # print('len(self.outputs)={}'.format(len(self.outputs))) for idx, txo in self.output_iter(): self.outputs[idx].validate(idx, txo, self.my_xfp, self.active_multisig) @@ -1118,22 +1142,25 @@ class psbtObject(psbtProxy): pass # check fee is reasonable + sending_to_self = False total_non_change_out = self.total_value_out - total_change + # print('total_non_change_out={} self.total_value_out={} total_change={}'.format(total_non_change_out, self.total_value_out, total_change)) fee = self.calculate_fee() if self.total_value_out == 0: per_fee = 100 + elif total_non_change_out == 0: + sending_to_self = True else: # Calculate fee based on non-change output value per_fee = (fee / total_non_change_out) * 100 - from common import settings - fee_limit = settings.get('fee_limit', DEFAULT_MAX_FEE_PERCENTAGE) - - if fee > total_non_change_out: + if sending_to_self: + self.warnings.append(('Self-Send', 'All outputs are being sent back to this wallet.')) + elif fee > total_non_change_out: self.warnings.append(('Huge Fee', 'Network fee is larger than the amount you are sending.')) elif per_fee >= 5: - self.warnings.append(('Big Fee', 'Network fee is more than ' - '5%% of total non-change value (%.1f%%).' % per_fee)) + self.warnings.append(('Big Fee', 'Network fee is more than ' + '5%% of total non-change value (%.1f%%).' % per_fee)) # Enforce policy related to change outputs self.consider_dangerous_change(self.my_xfp) @@ -1191,25 +1218,25 @@ class psbtObject(psbtProxy): elif hard_bits(path) != hard_pattern: iss = "has different hardening pattern" elif path[0:len(path_prefix)] != path_prefix: - iss = "goes to diff path prefix" + iss = "goes to different path prefix" elif (path[-2]&0x7fffffff) not in {0, 1}: - iss = "2nd last component not 0 or 1" + iss = "second last component not 0 or 1" elif (path[-1]&0x7fffffff) > idx_max: iss = "last component beyond reasonable gap" else: # looks ok continue - probs.append("Output#%d: %s: %s not %s/{0~1}%s/{0~%d}%s expected" - % (nout, iss, path_to_str(path, skip=0), - path_to_str(path_prefix, skip=0), + probs.append("Output #%d: %s: %s not %s/{0~1}%s/{0~%d}%s expected" + % (nout, iss, keypath_to_str(path, skip=0), + keypath_to_str(path_prefix, skip=0), "'" if hard_pattern[-2] else "", idx_max, "'" if hard_pattern[-1] else "", )) break for p in probs: - self.warnings.append(('Troublesome Change Outs', p)) + self.warnings.append(('Suspicious Change Outputs', p)) def consider_inputs(self): # Look an the UTXO's that we are spending. Do we have them? Do the @@ -1240,6 +1267,11 @@ class psbtObject(psbtProxy): # - also finds appropriate multisig wallet to be used inp.determine_my_signing_key(i, utxo, self.my_xfp, self) + # iff to UTXO is segwit, then check it's value, and also + # capture that value, since it's supposed to be immutable + if inp.is_segwit: + history.verify_amount(txi.prevout, inp.amount, i) + del utxo # XXX scan witness data provided, and consider those ins signed if not multisig? @@ -1248,7 +1280,7 @@ class psbtObject(psbtProxy): # Should probably be a fatal msg; so risky... but # - maybe we aren't expected to sign that input? (coinjoin) # - assume for now, probably funny business so we should stop - raise FatalPSBTIssue('Missing UTXO(s). Cannot determine value being signed') + raise FatalPSBTIssue('Missing UTXO(s). Cannot determine value being signed.') # self.warnings.append(('Missing UTXOs', # "We don't know enough about the inputs to this transaction to be sure " # "of their value. This means the network fee could be huge, or resulting " @@ -1261,7 +1293,7 @@ class psbtObject(psbtProxy): if len(self.presigned_inputs) == self.num_inputs: # Maybe wrong for multisig cases? Maybe they want to add their # own signature, even tho N of M is satisfied?! - raise FatalPSBTIssue('Transaction looks completely signed already?') + raise FatalPSBTIssue('Transaction is already fully signed.') # We should know pubkey required for each input now. # - but we may not be the signer for those inputs, which is fine. @@ -1270,34 +1302,46 @@ class psbtObject(psbtProxy): if inp.required_key == None and not inp.fully_signed) if no_keys: # This is seen when you re-sign same signed file by accident (multisig) - self.warnings.append(('Not Signing', - 'Some inputs are signed already, or we do not know the key: %r' % list(no_keys))) + # - case of len(no_keys)==num_inputs is handled by consider_keys + self.warnings.append(('Already Signed', 'Passport has already signed this transaction. Other signatures are still required.')) if self.presigned_inputs: # this isn't really even an issue for some complex usage cases - self.warnings.append(('Partly Signed Already', - 'Some input(s) provided were already completely signed by other parties: %r' - % list(self.presigned_inputs))) + self.warnings.append(('Partially Signed Already', + 'Some input(s) provided were already signed by other parties: ' + + seq_to_str(self.presigned_inputs))) def calculate_fee(self): # what miner's reward is included in txn? if self.total_value_in is None: - print('Very odd that a txn has no total_value_in! psbt={}'.format(self)) + # print('Very odd that a txn has no total_value_in! psbt={}'.format(self)) return None + return self.total_value_in - self.total_value_out def consider_keys(self): - # check we process the right keys for the inputs + # check we possess the right keys for the inputs cnt = sum(1 for i in self.inputs if i.num_our_keys) - if not cnt: - raise FatalPSBTIssue('None of the keys involved in this transaction ' - 'belong to this Passport (need %s).' % xfp2str(self.my_xfp)) + if cnt: return + + # collect a list of XFP's given in file that aren't ours + others = set() + for inp in self.inputs: + if not inp.subpaths: continue + for path in inp.subpaths.values(): + others.add(path[0]) + + others.discard(self.my_xfp) + msg = ', '.join(xfp2str(i) for i in others) + + raise FatalPSBTIssue('None of the keys involved in this transaction ' + 'belong to this Passport (need %s, found %s).' + % (xfp2str(self.my_xfp), msg)) @classmethod def read_psbt(cls, fd): # read in a PSBT file. Captures fd and keeps it open. hdr = fd.read(5) - print("hdr={}".format(hdr)) if hdr != b'psbt\xff': raise ValueError("bad hdr") @@ -1372,26 +1416,35 @@ class psbtObject(psbtProxy): # Double check the change outputs are right. This is slow, but critical because # it detects bad actors, not bugs or mistakes. # - equivilent check already done for p2sh outputs when we re-built the redeem script - change_outs = [n for n,o in enumerate(self.outputs) - if o.is_change and not o.is_p2sh_change] + change_outs = [n for n,o in enumerate(self.outputs) if o.is_change] if change_outs: dis.fullscreen('Change Check...') for count, out_idx in enumerate(change_outs): # only expecting single case, but be general - dis.progress_bar_show(count / len(change_outs)) + system.progress_bar(int(count / len(change_outs))* 100) - for pubkey, subpath in self.outputs[out_idx].subpaths.items(): - # derive it - skp = path_to_str(subpath) + oup = self.outputs[out_idx] + + good = 0 + for pubkey, subpath in oup.subpaths.items(): + if subpath[0] != self.my_xfp and subpath[0] != swab32(self.my_xfp): + # for multisig, will be N paths, and exactly one will + # be our key. For single-signer, should always be my XFP + continue + + # derive actual pubkey from private + skp = keypath_to_str(subpath) node = sv.derive_path(skp) # check the pubkey of this BIP32 node - pu = node.public_key() - if pu != pubkey: - raise FraudulentChangeOutput(out_idx, - "Deception regarding change output. " - "BIP32 path doesn't match actual address.") + if pubkey == node.public_key(): + good += 1 + + if not good: + raise FraudulentChangeOutput(out_idx, + "Deception regarding change output. " + "BIP32 path doesn't match actual address.") # progress dis.fullscreen('Signing...') @@ -1400,7 +1453,7 @@ class psbtObject(psbtProxy): sigs = 0 success = set() for in_idx, txi in self.input_iter(): - dis.progress_bar_show(in_idx / self.num_inputs) + system.progress_bar(int(in_idx * 100 / self.num_inputs)) inp = self.inputs[in_idx] @@ -1432,7 +1485,7 @@ class psbtObject(psbtProxy): # need to consider a set of possible keys, since xfp may not be unique for which_key in inp.required_key: # get node required - skp = path_to_str(inp.subpaths[which_key]) + skp = keypath_to_str(inp.subpaths[which_key]) node = sv.derive_path(skp, register=False) # expensive test, but works... and important @@ -1449,18 +1502,18 @@ class psbtObject(psbtProxy): assert not inp.added_sig, "already done??" assert which_key in inp.subpaths, 'unk key' - if inp.subpaths[which_key][0] != self.my_xfp: + if inp.subpaths[which_key][0] != self.my_xfp and inp.subpaths[which_key][0] != swab32(self.my_xfp): # we don't have the key for this subkey # (redundant, required_key wouldn't be set) continue # get node required - skp = path_to_str(inp.subpaths[which_key]) + skp = keypath_to_str(inp.subpaths[which_key]) node = sv.derive_path(skp, register=False) # expensive test, but works... and important pu = node.public_key() - assert pu == which_key, "Path (%s) led to wrong pubkey for input#%d"%(skp, in_idx) + assert pu == which_key, "Path (%s) led to wrong pubkey for input #%d"%(skp, in_idx) # The precious private key we need pk = node.private_key() @@ -1494,7 +1547,7 @@ class psbtObject(psbtProxy): gc.collect() # done. - dis.progress_bar_show(1) + system.progress_bar(100) def make_txn_sighash(self, replace_idx, replacement, sighash_type): @@ -1532,7 +1585,7 @@ class psbtObject(psbtProxy): # locktime rv.update(pack(' +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: GPL-3.0-or-later # -# SPDX-FileCopyrightText: 2018 Coinkite, Inc. +# SPDX-FileCopyrightText: 2018 Coinkite, Inc. # SPDX-License-Identifier: GPL-3.0-only # # Constants and various "limits" shared between embedded and desktop USB protocol @@ -11,6 +11,8 @@ try: except ImportError: const = int +from constants import PSBT_MAX_SIZE + # For upload/download this is the max size of the data block. MAX_BLK_LEN = const(2048) @@ -18,13 +20,10 @@ MAX_BLK_LEN = const(2048) # - includes args for upload command MAX_MSG_LEN = const(4+4+4+MAX_BLK_LEN) -# Max PSBT txn we support (384k bytes as PSBT) +# Max PSBT txn we support (896k as PSBT) # - the max on the wire for mainnet is 100k # - but a PSBT might contain a full txn for each input -MAX_TXN_LEN = const(384*1024) - -# Max size of any upload (firmware.dfu files in particular) -MAX_UPLOAD_LEN = const(2*MAX_TXN_LEN) +MAX_TXN_LEN = PSBT_MAX_SIZE // 2 # Max length of text messages for signing MSG_SIGNING_MAX_LENGTH = const(240) diff --git a/ports/stm32/boards/Passport/modules/schema_evolution.py b/ports/stm32/boards/Passport/modules/schema_evolution.py new file mode 100644 index 0000000..e69f54b --- /dev/null +++ b/ports/stm32/boards/Passport/modules/schema_evolution.py @@ -0,0 +1,40 @@ +# SPDX-FileCopyrightText: 2021 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# schema_evolution.py +# +# Update code for converting any stored data formats when moving from an older firmware version to a newer version +# + +# NOTE: The goal should be to mostly add to existing formats in a way that doesn't require schema evolution scripts, +# but sometimes this is not possible, so this hook exists to handle those, hopefully rare, cases. +async def handle_schema_evolutions(update_from_to): + from common import settings + + parts = update_from_to.split('->') + from_version = parts[0] + to_version = parts[1] + + # print('handle_schema_evolutions(): from_version={} -> to_version={}'.format(from_version, to_version)) + + # Potentially runs multiple times to handle the case of a user skipping firmware versions with data format changes + while True: + if from_version == '0.9.83' and to_version == '0.9.84': + # Handle evolutions + from_version = to_version + continue + + elif from_version == '1.0.2' and to_version == '1.0.3': + # Handle evolutions + from_version = to_version + continue + + # We only reach here if no more evolutions are possible. + # Remove the update indicator from the settings. + # NOTE: There is a race condition here, but these evolutions should be extremely fast, and ideally + # coded in a way that is idempotent. + # print('handle_schema_evolutions() Done 1') + settings.remove('update') + await settings.save() + # print('handle_schema_evolutions() Done 1') + return diff --git a/ports/stm32/boards/Passport/modules/se_commands.py b/ports/stm32/boards/Passport/modules/se_commands.py index 3d34775..a0866be 100644 --- a/ports/stm32/boards/Passport/modules/se_commands.py +++ b/ports/stm32/boards/Passport/modules/se_commands.py @@ -1,45 +1,23 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: GPL-3.0-or-later # # se_commands.py - Constants used to identify commands for Foundation.System.dispatch() # # Would be better if these were defined in Foundation.System directly using MP to export -# them. That way the constant could be shared with C, but it was not clear if that can +# them. That way the constant could be shared with C, but it was not clear if that can # be achieved in MP. # Main commands -CMD_GET_BOOTLOADER_VERSION = const(0) -CMD_GET_FIRMWARE_HASH = const(1) -CMD_UPGRADE_FIRMWARE = const(2) -CMD_RESET = const(3) -CMD_LED_CONTROL = const(4) CMD_IS_BRICKED = const(5) CMD_READ_SE_SLOT = const(15) CMD_GET_ANTI_PHISHING_WORDS = const(16) CMD_GET_RANDOM_BYTES = const(17) CMD_PIN_CONTROL = const(18) CMD_GET_SE_CONFIG = const(20) -CMD_FIRMWARE_CONTROL = const(21) -CMD_GET_SUPPLY_CHAIN_VALIDATION_WORDS = const(22) -CMD_FACTORY_SETUP = const(-1) - - -# Subcommands for CMD_LED_CONTROL -LED_READ = const(0) -LED_SET_RED = const(1) -LED_SET_GREEN = const(2) -LED_ATTEMPT_TO_SET_GREEN = const(3) +CMD_GET_SUPPLY_CHAIN_VALIDATION_WORDS = const(21) # Subcommands for CMD_PIN_CONTROL PIN_SETUP = const(0) PIN_ATTEMPT = const(1) PIN_CHANGE = const(2) PIN_GET_SECRET = const(3) -PIN_GREENLIGHT_FIRMWARE = const(4) -PIN_LONG_SECRET = const(5) - -# Subcommands for CMD_FIRMWARE_CONTROL -GET_MIN_FIRMWARE_VERSION = const(0) -GET_IS_FIRMWARE_DOWNGRADE = const(1) # May not be used -UPDATE_HIGH_WATERMARK = const(2) -GET_HIGH_WATERMARK = const(3) # May not be used diff --git a/ports/stm32/boards/Passport/modules/seed.py b/ports/stm32/boards/Passport/modules/seed.py index 2a7b923..04d9987 100644 --- a/ports/stm32/boards/Passport/modules/seed.py +++ b/ports/stm32/boards/Passport/modules/seed.py @@ -1,7 +1,7 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: GPL-3.0-or-later # -# SPDX-FileCopyrightText: 2018 Coinkite, Inc. +# SPDX-FileCopyrightText: 2018 Coinkite, Inc. # SPDX-License-Identifier: GPL-3.0-only # # (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard @@ -22,7 +22,7 @@ from common import noise import trezorcrypto import uctypes -from pincodes import AE_LONG_SECRET_LEN, AE_SECRET_LEN +from pincodes import SE_SECRET_LEN from stash import SecretStash, SensitiveValues from ubinascii import hexlify as b2a_hex from utils import pop_count, xfp2str @@ -32,60 +32,69 @@ from ux import ux_show_story VALID_LENGTHS = (24, 18, 12) -def create_new_wallet_seed(): - # Pick a new random seed, and +async def create_new_wallet_seed(): + from noise_source import NoiseSource + from common import dis, system + from uasyncio import sleep_ms - # await ux_dramatic_pause('Generating...', 4) - # TODO: Show screen to indicate delay? + # Pick a new random seed + dis.fullscreen('Generating Seed...') + await sleep_ms(1000) # always full 24-word (256 bit) entropy seed = bytearray(32) - noise.random_bytes(seed) + noise.random_bytes(seed, NoiseSource.ALL) # hash to mitigate any potential bias in Avalanche RNG seed = trezorcrypto.sha256(seed).digest() - print('create_new_wallet(): New seed = {}'.format(b2a_hex(seed))) + # print('create_new_wallet_seed(): New seed = {}'.format(b2a_hex(seed))) return seed -def save_wallet_seed(seed_bits): - from common import dis, pa, settings +async def save_wallet_seed(seed_bits): + from common import dis, pa, settings, system + + dis.fullscreen('Saving Seed...') + + system.show_busy_bar() - print('save_wallet_seed 1') # encode it for our limited secret space nv = SecretStash.encode(seed_bits=seed_bits) - print('save_wallet_seed 2: nv={}'.format(b2a_hex(nv))) - dis.fullscreen('Saving Wallet...') pa.change(new_secret=nv) - print('save_wallet_seed 3') # re-read settings since key is now different # - also captures xfp, xpub at this point - pa.new_main_secret(nv) - print('save_wallet_seed 4') + await pa.new_main_secret(nv) # check and reload secret pa.reset() - print('save_wallet_seed 5') pa.login() - print('save_wallet_seed 6') -# TODO: PASSPHRASE + system.hide_busy_bar() + def set_bip39_passphrase(pw): # apply bip39 passphrase for now (volatile) # - return None or error msg import stash + from common import system + from utils import bytes_to_hex_str stash.bip39_passphrase = pw - # takes a bit, so show something - from common import dis - dis.fullscreen("Working...") + # Create a hash from the passphrase + if len(stash.bip39_passphrase) > 0: + digest = bytearray(32) + system.sha256(stash.bip39_passphrase, digest) + digest_hex = bytes_to_hex_str(digest) + stash.bip39_hash = digest_hex[:8] # Take first 8 characters (32-bits) + # print('stash.bip39_hash={}'.format(stash.bip39_hash)) + else: + stash.bip39_hash = '' with stash.SensitiveValues() as sv: if sv.mode != 'words': - # can't do it without original seed woods + # can't do it without original seed words return 'No BIP39 seed words' sv.capture_xpub() @@ -94,11 +103,13 @@ def set_bip39_passphrase(pw): async def remember_bip39_passphrase(): # Compute current xprv and switch to using that as root secret. import stash - from common import dis, pa + from common import dis, pa, system dis.fullscreen('Check...') with stash.SensitiveValues() as sv: + # GIT: https://github.com/Coldcard/firmware/commit/7e97d93153aee1a6878702145410ff9a6106119a + # If this message is deemed unnecessary, we could consider if the above commit fixes it if sv.mode != 'words': # not a BIP39 derived secret, so cannot work. await ux_show_story('''The wallet secret was not based on a seed phrase, so we cannot add a BIP39 passphrase at this time.''', title='Failed') @@ -109,35 +120,46 @@ async def remember_bip39_passphrase(): # Important: won't write new XFP to nvram if pw still set stash.bip39_passphrase = '' + system.show_busy_bar() + dis.fullscreen('Saving...') pa.change(new_secret=nv) # re-read settings since key is now different # - also captures xfp, xpub at this point - pa.new_main_secret(nv) + await pa.new_main_secret(nv) + + system.hide_busy_bar() # check and reload secret pa.reset() pa.login() -def clear_seed(restart=True): - from common import dis, pa, settings +async def erase_wallet(restart=True): + from common import dis, pa, settings,system import utime import version - dis.fullscreen('Erasing Seed...') + dis.fullscreen('Erasing Wallet...') + + system.show_busy_bar(); - # clear settings associated with this key, since it will be no more - settings.reset() + # Remove wallet-related settings, but leave other settings alone like terms_ok, validated_ok + settings.remove('xfp') + settings.remove('xpub') + settings.remove('words') + settings.remove('multisig') + settings.remove('accounts') + settings.remove('backup_quiz') + settings.remove('enable_passphrase') - # save a blank secret (all zeros is a special case, detected by bootloader) - nv = bytes(AE_SECRET_LEN) + # save a blank secret (all zeros is a special case) + nv = bytes(SE_SECRET_LEN) pa.change(new_secret=nv) - # wipe the long secret too - nv = bytes(AE_LONG_SECRET_LEN) - pa.ls_change(nv) + await settings.save() + system.hide_busy_bar(); if restart: dis.fullscreen('Restarting...') @@ -148,5 +170,4 @@ def clear_seed(restart=True): reset() - -# EOF \ No newline at end of file +# EOF diff --git a/ports/stm32/boards/Passport/modules/seed_check_ux.py b/ports/stm32/boards/Passport/modules/seed_check_ux.py new file mode 100644 index 0000000..2020e84 --- /dev/null +++ b/ports/stm32/boards/Passport/modules/seed_check_ux.py @@ -0,0 +1,218 @@ +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# seed_check_ux.py - UX related to seed phrase entry and verification +# +import random +from display import Display, FontSmall, FontTiny +from common import dis, system +from uasyncio import sleep_ms +from utils import UXStateMachine +from ux import KeyInputHandler, ux_show_story, ux_confirm +import random + +TEXTBOX_MARGIN = 6 +PAGINATION_HEIGHT = Display.FOOTER_HEIGHT +MAX_WORDS_TO_DISPLAY = 4 +NUM_SELECTABLE_WORDS = 5 + +# Separate state machines to keep +class SeedCheckUX(UXStateMachine): + # States + SELECT_WORDS = 1 + SEED_CHECK_COMPLETE = 4 + + def __init__(self, title='Check Seed', seed_words=[], cancel_msg=None): + super().__init__(self.SELECT_WORDS) + self.title = title + self.seed_words = seed_words + self.seed_len = len(seed_words) + self.curr_word = 0 + self.is_check_valid = False + self.font = FontSmall + self.pagination_font = FontTiny + self.highlighted_word_index = 0 + self.selectable_words = None + self.show_check = False + self.show_error = False + self.cancel_msg = cancel_msg if cancel_msg != None else '''Are you sure you want to cancel the seed check? + +You will be unable to recover this wallet without the correct seed.''' + + self.input = KeyInputHandler(down='udxy', up='xy') + + # Update state based on current info + self.update() + + def render(self): + system.turbo(True) + dis.clear() + + dis.draw_header(self.title) + + # Draw the title + y = Display.HEADER_HEIGHT + TEXTBOX_MARGIN + 4 + dis.text(None, y, 'Select Word {} of {}'.format(self.curr_word + 1, self.seed_len)) + y += self.font.leading + 4 + + # Draw a bounding box around the list of selectable words + dis.draw_rect(TEXTBOX_MARGIN, y, Display.WIDTH - (TEXTBOX_MARGIN * 2), + NUM_SELECTABLE_WORDS * (self.font.leading - 1), 1, + fill_color=0, border_color=1) + + # Draw the selectable words + for i in range(len(self.selectable_words)): + if i == self.highlighted_word_index: + # Draw inverted text with rect to indicate that this word will be selected + # when user presses SELECT button. + dis.draw_rect(TEXTBOX_MARGIN, y, Display.WIDTH - (TEXTBOX_MARGIN * 2), self.font.leading, 0, fill_color=1) + dis.text(None, y+2, self.selectable_words[i], invert=1) + + # Draw a check mark if the word is correct + if self.show_check: + dis.icon(TEXTBOX_MARGIN + 6, y + 6, 'selected', invert=True) + elif self.show_error: + dis.icon(TEXTBOX_MARGIN + 6, y + 6, 'x', invert=True) + + else: + dis.text(None, y+1, self.selectable_words[i]) + + y += self.font.leading - 1 + + + # Draw the pagination + more_width, more_height = dis.icon_size('more_right') + y = Display.HEIGHT - Display.FOOTER_HEIGHT - more_height - 12 + + # Pagination constants + PGN_COUNT = min(7, self.seed_len) + PGN_MIDDLE = PGN_COUNT // 2 + PGN_SEP = 2 + PGN_W = 24 + PGN_H = 22 + x = (Display.WIDTH - ((PGN_W + PGN_SEP) * PGN_COUNT) + PGN_SEP) // 2 + y += 1 + + # Calculate the framing of the pagination so that the curr_word is in the middle + # whenever possible. + # print('PGN_MIDDLE={} PGN_COUNT={} seed_len={} curr_word={}'.format(PGN_MIDDLE, PGN_COUNT, self.seed_len, self.curr_word)) + if self.curr_word <= PGN_MIDDLE: + pgn_start = min(0, self.curr_word) + elif self.curr_word <= self.seed_len - PGN_MIDDLE - 1: + pgn_start = self.curr_word - PGN_MIDDLE + else: + pgn_start = self.seed_len - PGN_COUNT + pgn_end = pgn_start + PGN_COUNT + + # Show icons only if there is something to that side to scroll to + if pgn_start > 0: + dis.icon(TEXTBOX_MARGIN, y + 4, 'more_left') + if pgn_end < self.seed_len: + dis.icon(Display.WIDTH - TEXTBOX_MARGIN - more_width, y + 4, 'more_right') + + for i in range(pgn_start, pgn_end): + num_label = '{}'.format(i + 1) + label_width = dis.width(num_label, self.pagination_font) + tx = x + (PGN_W//2) - label_width // 2 + ty = y + 3 + invert_text = 0 + if i == self.curr_word: + # Draw with an inverted rectangle + dis.draw_rect(x, y, PGN_W, PGN_H, 0, fill_color=1) + invert_text = 1 + elif i < self.curr_word: + # Draw with a normal rectangle + dis.draw_rect(x, y, PGN_W, PGN_H, 1, fill_color=0, border_color=1) + + dis.text(tx, ty, num_label, font=self.pagination_font, invert=invert_text) + + x += PGN_W + PGN_SEP + + dis.draw_footer('BACK', 'SELECT', self.input.is_pressed('x'), self.input.is_pressed('y')) + + dis.show() + system.turbo(False) + + async def interact(self): + # Wait for key inputs + event = None + while True: + event = await self.input.get_event() + if event != None: + break + + if event != None: + key, event_type = event + + if event_type == 'down': + if key == 'u': + self.highlighted_word_index = max(0, self.highlighted_word_index - 1) + elif key == 'd': + self.highlighted_word_index = min(len(self.selectable_words) - 1, self.highlighted_word_index + 1) + + if event_type == 'up': + if key == 'x': + abort = await ux_confirm(self.cancel_msg) + if abort: + return False + elif key == 'y': + if self.seed_words[self.curr_word] == self.selectable_words[self.highlighted_word_index]: + # Word is correct, so draw checkmark + self.show_check = True + self.render() + self.show_check = False + await sleep_ms(500) + + # Next word? + if self.curr_word < self.seed_len - 1: + self.curr_word += 1 + self.highlighted_word_index = 0 + self.selectable_words = None + else: + # All words have been validated + self.goto(self.SEED_CHECK_COMPLETE) + else: + self.show_error = True + + return True + + def update(self): + if self.selectable_words == None: + system.turbo(True) + self.selectable_words = [] + # Choose random words and the seed word + indexes = [] + for i in range(len(self.seed_words)): + if i != self.curr_word: + indexes.append(i) + + for i in range(NUM_SELECTABLE_WORDS): + r = random.randint(0, len(indexes) - 1) + seed_index = indexes[r] + self.selectable_words.append(self.seed_words[seed_index]) + indexes.remove(seed_index) + + # Replace one index at random with the expected word + r = random.randint(0, NUM_SELECTABLE_WORDS - 1) + self.selectable_words[r] = self.seed_words[self.curr_word] + system.turbo(False) + + async def show(self): + while True: + # print('show: state={}'.format(self.state)) + if self.state == self.SELECT_WORDS: + self.render() + self.show_error = False + if await self.interact() == False: + return False + self.update() + + elif self.state == self.SEED_CHECK_COMPLETE: + self.is_check_valid = True + return True + + else: + while True: + # print('ERROR: Should never hit this else case!') + from uasyncio import sleep_ms + await sleep_ms(1000) diff --git a/ports/stm32/boards/Passport/modules/seed_phrase_ux.py b/ports/stm32/boards/Passport/modules/seed_entry_ux.py similarity index 67% rename from ports/stm32/boards/Passport/modules/seed_phrase_ux.py rename to ports/stm32/boards/Passport/modules/seed_entry_ux.py index b210ba1..c2163f4 100644 --- a/ports/stm32/boards/Passport/modules/seed_phrase_ux.py +++ b/ports/stm32/boards/Passport/modules/seed_entry_ux.py @@ -1,13 +1,13 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: GPL-3.0-or-later # -# seed_phrase_ux.py - UX related to seed phrase entry and verification +# seed_entry_ux.py - UX related to seed phrase entry and verification # import pincodes from display import Display, FontSmall, FontTiny -from common import dis +from common import dis, system from uasyncio import sleep_ms -from utils import UXStateMachine +from utils import UXStateMachine, shuffle from ux import KeyInputHandler, ux_show_story, ux_confirm from trezorcrypto import bip39 from bip39_utils import get_words_matching_prefix, word_to_keypad_numbers @@ -16,7 +16,7 @@ TEXTBOX_MARGIN = 6 PAGINATION_HEIGHT = Display.FOOTER_HEIGHT MAX_WORDS_TO_DISPLAY = 4 -# Separate state machines to keep +# Separate state machines to keep class SeedEntryUX(UXStateMachine): # States ENTER_WORDS = 1 @@ -24,7 +24,7 @@ class SeedEntryUX(UXStateMachine): INVALID_SEED = 3 VALID_SEED = 4 - def __init__(self, title='Import Wallet', seed_len=12, verify_phrase=None): + def __init__(self, title='Import Seed', seed_len=12, verify_phrase=None, validate_checksum=True, word_list='bip39'): super().__init__(self.ENTER_WORDS) self.title = title self.seed_len = seed_len @@ -37,10 +37,12 @@ class SeedEntryUX(UXStateMachine): self.pagination_font = FontTiny self.highlighted_word_index = 0 self.last_word_lookup = '' - + self.validate_checksum = validate_checksum + self.word_list = word_list + self.selectable_words = None self.input = KeyInputHandler(down='23456789lrudxy*', up='xy') - # Initialize input and word info + # Initialize input and word information num_words = len(self.words) for w in range(self.seed_len): if w < num_words: @@ -54,6 +56,7 @@ class SeedEntryUX(UXStateMachine): self.update() def render(self): + system.turbo(True) dis.clear() dis.draw_header(self.title) @@ -94,7 +97,7 @@ class SeedEntryUX(UXStateMachine): y = Display.HEIGHT - Display.FOOTER_HEIGHT - more_height - 12 # Pagination constants - PGN_COUNT = 7 + PGN_COUNT = min(7, self.seed_len) PGN_MIDDLE = PGN_COUNT // 2 PGN_SEP = 2 PGN_W = 24 @@ -104,16 +107,13 @@ class SeedEntryUX(UXStateMachine): # Calculate the framing of the pagination so that the curr_word is in the middle # whenever possible. - print('PGN_MIDDLE={} PGN_COUNT={} seed_len={} curr_word={}'.format(PGN_MIDDLE, PGN_COUNT, self.seed_len, self.curr_word)) + # print('PGN_MIDDLE={} PGN_COUNT={} seed_len={} curr_word={}'.format(PGN_MIDDLE, PGN_COUNT, self.seed_len, self.curr_word)) if self.curr_word <= PGN_MIDDLE: pgn_start = min(0, self.curr_word) - print('case 1: pgn_start={}'.format(pgn_start)) elif self.curr_word <= self.seed_len - PGN_MIDDLE - 1: pgn_start = self.curr_word - PGN_MIDDLE - print('case 2: pgn_start={}'.format(pgn_start)) else: pgn_start = self.seed_len - PGN_COUNT - print('case 3: pgn_start={}'.format(pgn_start)) pgn_end = pgn_start + PGN_COUNT # Show icons only if there is something to that side to scroll to @@ -143,6 +143,7 @@ class SeedEntryUX(UXStateMachine): dis.draw_footer('BACK', 'SELECT', self.input.is_pressed('x'), self.input.is_pressed('y')) dis.show() + system.turbo(False) async def interact(self): # Wait for key inputs @@ -166,13 +167,21 @@ class SeedEntryUX(UXStateMachine): # No word selected anymore since we changed the input self.words[self.curr_word] = '' + self.selectable_words = None elif key == 'l': self.curr_word = max(self.curr_word - 1, 0) - print('curr_word={}'.format(self.curr_word)) + # print('curr_word={}'.format(self.curr_word)) self.highlighted_word_index = 0 + self.selectable_words = None + elif key == 'r': - if len(self.words[self.curr_word]) > 0: - self.curr_word = min(self.curr_word + 1, len(self.words) - 1) + if len(self.words[self.curr_word]) == 0: + # Assume the highlighted word is the selected one + self.words[self.curr_word] = self.selectable_words[self.highlighted_word_index] + + self.curr_word = min(self.curr_word + 1, len(self.words) - 1) + self.highlighted_word_index = 0 + self.selectable_words = None elif key == 'u': self.highlighted_word_index = max(0, self.highlighted_word_index - 1) elif key == 'd': @@ -180,30 +189,37 @@ class SeedEntryUX(UXStateMachine): elif key == '*': self.user_input[self.curr_word] = self.user_input[self.curr_word][0:-1] + # Indicate that user hasn't finalized this word yet + self.words[self.curr_word] = '' + self.selectable_words = None + if event_type == 'up': if key == 'x': - abort = ux_confirm('Are you sure you want to abort seed entry? All progress will be lost.') - if abort: + cancel = await ux_confirm('Are you sure you want to cancel seed entry? All progress will be lost.') + if cancel: self.is_seed_valid = False - return + return False elif key == 'y': self.words[self.curr_word] = self.selectable_words[self.highlighted_word_index] if self.curr_word < self.seed_len - 1: self.curr_word += 1 self.highlighted_word_index = 0 + self.selectable_words = None else: self.goto(self.VALIDATE_SEED) + return True + def update(self): # User could have moved forward or back in the pages or up and down in the selectable_words list # or typed a number. # # Update the selectable words according to the current state - print('words={} curr_word={}'.format(self.words, self.curr_word)) + # print('words={} curr_word={}'.format(self.words, self.curr_word)) if self.words[self.curr_word] != None and len(self.words[self.curr_word]) > 0: - print('single word case') - # if a word has been selected, it will be stored here, so only show that word + # print('single word case') + # if a word has been selected, it will be stored here, so show that word self.selectable_words = [self.words[self.curr_word]] # Also, set the input to the numbers for this word so the two are in sync @@ -211,53 +227,67 @@ class SeedEntryUX(UXStateMachine): self.highlighted_word_index = 0 else: - print('normal input case') - # No word has been selected yet, so lookup the matches - c = self.user_input[self.curr_word] if len(self.user_input[self.curr_word]) > 0 else '2' - print('c={}'.format(c)) - self.selectable_words = get_words_matching_prefix(c, MAX_WORDS_TO_DISPLAY) - print('selectable_words={} MAX={}'.format(self.selectable_words, MAX_WORDS_TO_DISPLAY)) - self.last_word_lookup = c - - print('selectable_words={}'.format(self.selectable_words)) + # print('normal input case') + # Only regenerate the word list when the action would cause the words to change. + # Code above indicates this by setting the list to None. + if self.selectable_words == None: + # No word has been selected yet, so lookup the matches + c = self.user_input[self.curr_word] if len(self.user_input[self.curr_word]) > 0 else '2' + # print('c={}'.format(c)) + words = get_words_matching_prefix(c, MAX_WORDS_TO_DISPLAY*2, self.word_list) + # print('words={}'.format(words)) + self.selectable_words = shuffle(words) + # Sort so that exact matches come first and don't fall off the shorter list + self.selectable_words.sort(key=lambda w: len(w)) + # print('shuffled={}'.format(words)) + self.selectable_words = self.selectable_words[:MAX_WORDS_TO_DISPLAY] + # print('selectable_words={} MAX={}'.format(self.selectable_words, MAX_WORDS_TO_DISPLAY)) + self.last_word_lookup = c + + # print('selectable_words={}'.format(self.selectable_words)) async def show(self): while True: - print('show: state={}'.format(self.state)) + # print('show: state={}'.format(self.state)) if self.state == self.ENTER_WORDS: self.render() - await self.interact() + if await self.interact() == False: + return None self.update() elif self.state == self.VALIDATE_SEED: - if len(self.words) == self.seed_len: + if not self.validate_checksum: + self.goto(self.VALID_SEED) + elif len(self.words) == self.seed_len: # Ensure that the checksum of the mnemonic words is correct mnemonic = ' '.join(self.words) - print('Checking mnemonic: "{}"'.format(mnemonic)) + # print('Checking mnemonic: "{}"'.format(mnemonic)) if bip39.check(mnemonic): self.goto(self.VALID_SEED) else: self.goto(self.INVALID_SEED) - + elif self.state == self.VALID_SEED: # Return the words to the caller - print('seed = {}'.format(self.words)) + # print('seed = {}'.format(self.words)) self.is_seed_valid = True return self.words elif self.state == self.INVALID_SEED: - # Show a story that indicates the words are wrong - ABORT to return to previous menu or RETRY to try again - result = await ux_show_story('Seed phrase checksum is invalid. One or more of your seed words is incorrect.', - title='Invalid Seed', left_btn='ABORT', right_btn='RETRY', center="True", center_vertically=True) + # Show a story that indicates the words are wrong - BACK to return to previous menu or RETRY to try again + result = await ux_show_story('Seed phrase is invalid. One or more of your seed words is incorrect.', + title='Invalid Seed', left_btn='BACK', right_btn='RETRY', center="True", center_vertically=True) if result == 'x': - self.is_seed_valid = False - return None + cancel = await ux_confirm('Are you sure you want to cancel seed entry? All progress will be lost.') + if cancel: + self.is_seed_valid = False + return False elif result == 'y': self.goto(self.ENTER_WORDS) else: while True: - print('ERROR: Should never hit this else case!') + # print('ERROR: Should never hit this else case!') from uasyncio import sleep_ms await sleep_ms(1000) diff --git a/ports/stm32/boards/Passport/modules/self_test_ux.py b/ports/stm32/boards/Passport/modules/self_test_ux.py new file mode 100644 index 0000000..50fa7fb --- /dev/null +++ b/ports/stm32/boards/Passport/modules/self_test_ux.py @@ -0,0 +1,74 @@ +# SPDX-FileCopyrightText: 2021 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# self_test_ux.py - Self test UX +# + +from common import system +from utils import UXStateMachine +from ux import ux_show_text_as_ur, ux_keypad_test, ux_scan_qr_code, ux_show_story, ux_draw_alignment_grid +from data_codecs.qr_type import QRType + +class SelfTestUX(UXStateMachine): + + def __init__(self): + # States + self.SHOW_SERIAL_NUMBER = 1 + self.KEYPAD_TEST = 2 + self.CAMERA_TEST = 3 + self.CAMERA_TEST_RESULT = 4 + self.SCREEN_ALIGNMENT = 5 + self.qr_data = None + + # print('SelfTestUX init') + super().__init__(self.SHOW_SERIAL_NUMBER) + + async def show(self): + while True: + # print('show: state={}'.format(self.state)) + if self.state == self.SHOW_SERIAL_NUMBER: + serial = system.get_serial_number() + result = await ux_show_text_as_ur(title='Serial Num.', qr_text=serial, qr_type=QRType.QR, msg=serial, + right_btn='NEXT') # If right_btn is specified, then RESIZE doesn't appear/work, which is fine here + if result == 'x': + return + else: + self.goto(self.KEYPAD_TEST) + + elif self.state == self.KEYPAD_TEST: + # print('Keypad Test!') + result = await ux_keypad_test() + if result == 'x': + self.goto(self.SHOW_SERIAL_NUMBER) + else: + self.goto(self.SCREEN_ALIGNMENT) + + elif self.state == self.SCREEN_ALIGNMENT: + result = await ux_draw_alignment_grid(title='Align Screen') + if result == 'x': + self.goto(self.KEYPAD_TEST) + else: + self.goto(self.CAMERA_TEST) + + elif self.state == self.CAMERA_TEST: + # print('Camera Test!') + system.turbo(True) + self.qr_data = await ux_scan_qr_code('Camera Test') + # print('qr_data=|{}|'.format(self.qr_data)) + system.turbo(False) + self.goto(self.CAMERA_TEST_RESULT) + + elif self.state == self.CAMERA_TEST_RESULT: + if self.qr_data == None: + result = await ux_show_story('No QR code scanned.', right_btn='RETRY') + if result == 'x': + self.goto(self.SCREEN_ALIGNMENT) + else: + self.goto(self.CAMERA_TEST) + else: + # Show the data - The QR code used in the factory starts with "Camera Test Passed!" + result = await ux_show_story(self.qr_data, right_btn='DONE') + if result == 'x': + self.goto(self.SCREEN_ALIGNMENT) + else: + return diff --git a/ports/stm32/boards/Passport/modules/serializations.py b/ports/stm32/boards/Passport/modules/serializations.py index 3e5fa1c..076224f 100644 --- a/ports/stm32/boards/Passport/modules/serializations.py +++ b/ports/stm32/boards/Passport/modules/serializations.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2018 Coinkite, Inc. +# SPDX-FileCopyrightText: 2018 Coinkite, Inc. # SPDX-License-Identifier: MIT # # SPDX-FileCopyrightText: Copyright (c) 2010 ArtForz -- public domain half-a-node @@ -10,7 +10,7 @@ # SPDX-FileCopyrightText: Copyright (c) 2010-2016 The Bitcoin Core developers # SPDX-License-Identifier: MIT # -# Additions Copyright 2018 by Coinkite Inc. +# Additions Copyright 2018 by Coinkite Inc. # Copyright (c) 2010 ArtForz -- public domain half-a-node # Copyright (c) 2012 Jeff Garzik # Copyright (c) 2010-2016 The Bitcoin Core developers @@ -34,6 +34,8 @@ from ucollections import OrderedDict import ustruct as struct import trezorcrypto from opcodes import * +from utils import bytes_to_hex_str + def sha256(s): return trezorcrypto.sha256(s).digest() @@ -47,9 +49,6 @@ def hash256(s): def hash160(s): return ripemd160(sha256(s)) -def bytes_to_hex_str(s): - return str(b2a_hex(s), 'ascii') - SIGHASH_ALL = 1 SIGHASH_NONE = 2 SIGHASH_SINGLE = 3 @@ -57,16 +56,14 @@ SIGHASH_ANYONECANPAY = 0x80 # Serialization/deserialization tools def ser_compact_size(l): - r = b"" if l < 253: - r = struct.pack("B", l) + return struct.pack("B", l) elif l < 0x10000: - r = struct.pack(" +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: GPL-3.0-or-later # -# SPDX-FileCopyrightText: 2018 Coinkite, Inc. +# SPDX-FileCopyrightText: 2018 Coinkite, Inc. # SPDX-License-Identifier: GPL-3.0-only # # (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard @@ -33,51 +33,54 @@ import trezorcrypto from uio import BytesIO from uasyncio import sleep_ms from ubinascii import hexlify as b2a_hex +from utils import to_str # Base address for internal memory-mapped flash used for settings: 0x81E0000 SETTINGS_FLASH_START = const(0x81E0000) SETTINGS_FLASH_LENGTH = const(0x20000) # 128K SETTINGS_FLASH_END = SETTINGS_FLASH_START + SETTINGS_FLASH_LENGTH - 1 -DATA_SIZE = const(4096 - 32) -BLOCK_SIZE = const(4096) +DATA_SIZE = const(8192 - 32) +BLOCK_SIZE = const(8192) # Setting values: # xfp = master xpub's fingerprint (32 bit unsigned) # xpub = master xpub in base58 # chain = 3-letter codename for chain we are working on (BTC) # words = (bool) BIP39 seed words exist (else XPRV or master secret based) -# b39skip = (bool) skip discussion about use of BIP39 passphrase -# idle_to = idle timeout period (seconds) -# _version = internal version number for data - incremented every time the data is saved +# shutdown_timeout = idle timeout period (seconds) +# _revision = internal version number for data - incremented every time the data is saved # terms_ok = customer has signed-off on the terms of sale -# tested = selftest has been completed successfully # multisig = list of defined multisig wallets (complex) -# pms = trust/import/distrust xpubs found in PSBT files -# axi = index of last selected address in explorer -# lgto = (minutes) how long to wait for Login Countdown feature -# usr = (dict) map from username to their secret, as base32 -# Stored w/ key=00 for access before login -# _skip_pin = hard code a PIN value (dangerous, only for debug) -# nick = optional nickname for this coldcard (personalization) -# rngk = randomize keypad for PIN entry +# multisig_policy = trust/import/distrust xpubs found in PSBT files +# accounts = array of accounts configured on this device +# screen_brightness = 0 to 100, 999 for automatic +# enable_passphrase = True to show Set Passphrase item in main menu, False to hide it +# backup_quiz = True if backup password quiz was passed; False if not - -# These are the data slots available to use. We have 32 slots +# These are the data slots available to use. We have 32 slots # for flash wear leveling. -SLOT_ADDRS = range(SETTINGS_FLASH_START, SETTINGS_FLASH_END - BLOCK_SIZE, BLOCK_SIZE) - +SLOT_ADDRS = range(SETTINGS_FLASH_START, SETTINGS_FLASH_END, BLOCK_SIZE) class Settings: - def __init__(self, loop=None): + def __init__(self, loop=None, serial=None): from foundation import SettingsFlash + from common import system # This is defined before Settings is created, so OK to use here + self.loop = loop self.is_dirty = 0 - self.aes_key = b'\0' * 32 + # AES key is based on the serial number now instead of the PIN + # We don't store anything critical in the settings, so this level of protection is fine, + # and avoids having 2 sets of settings (one with a zero AES key and one with the PIN-based key). + serial = system.get_serial_number() + # print('Settings: serial={}'.format(serial)) + self.aes_key = trezorcrypto.sha256(serial).digest() + # print('Settings: aes_key={}'.format(self.aes_key)) + self.curr_dict = self.default_values() self.overrides = {} # volatile overide values - + self.flash = SettingsFlash() self.load() @@ -87,48 +90,11 @@ class Settings: # Include the slot number as part of the initial counter (CTR) return trezorcrypto.aes(trezorcrypto.aes.CTR, self.aes_key, ustruct.pack('<4I', 4, 3, 2, flash_offset)) - def set_key(self, new_secret=None): - # System settings (not secrets) are stored in internal flash, encrypted with this - # key that is derived from main wallet secret. Call this method when the secret - # is first loaded, or changes for some reason. - from common import pa - from stash import blank_object - - key = None - mine = False - - if not new_secret: - if not pa.is_successful() or pa.is_secret_blank(): - # simple fixed key allows us to store a few things when logged out - key = b'\0'*32 - else: - # read secret and use it. - new_secret = pa.fetch() - mine = True - - if new_secret: - # hash up the secret... without decoding it or similar - assert len(new_secret) >= 32 - - s = trezorcrypto.sha256(new_secret) - - for round in range(5): - s.update('pad') - - s = trezorcrypto.sha256(s.digest()) - - key = s.digest() - - if mine: - blank_object(new_secret) - - # for restore from backup case, or when changing (created) the seed - self.aes_key = key - def load(self): - # Search all slots for any we can read, decrypt that, - # and pick the newest one (in unlikely case of dups) + # Search all slots for any we can read, decrypt them and pick the newest one + from common import system + system.turbo(True) try: # reset self.curr_dict.clear() @@ -137,11 +103,9 @@ class Settings: self.is_dirty = 0 for addr in SLOT_ADDRS: + # print('Trying to load at {}'.format(hex(addr))) buf = uctypes.bytearray_at(addr, 4) if buf[0] == buf[1] == buf[2] == buf[3] == 0xff: - # Save this so we can start at an empty slot when no decodable data - # is found (we can't just start at the beginning since it might - # not be erased). # print(' Slot is ERASED') # erased (probably) continue @@ -153,7 +117,7 @@ class Settings: if chk != buf[0:2]: # doesn't look like JSON, so skip it - # print(' Slot does not contain JSON') + # print(' Slot does not contain JSON') continue # probably good, so prepare to read it @@ -161,13 +125,13 @@ class Settings: chk = trezorcrypto.sha256() expect = None - # Copy the data - our flash is memory mapped, so we read directly by address + # Our flash is memory mapped, so we read directly by address buf = uctypes.bytearray_at(addr, DATA_SIZE) # Get a bytearray for the SHA256 at the end expected_sha = uctypes.bytearray_at(addr + DATA_SIZE, 32) - # Decrypt and check hash + # Decrypt and check hash b = aes.decrypt(buf) # Add the decrypted result to the SHA @@ -185,11 +149,11 @@ class Settings: except: # One in 65k or so chance to come here w/ garbage decoded, so # not an error. - # print('ERROR? Unable to decode JSON') + # print('ERROR? Unable to decode JSON') continue - curr_version = d.get('_version', 0) - if curr_version > self.curr_dict.get('_version', -1): + curr_revision = d.get('_revision', 0) + if curr_revision > self.curr_dict.get('_revision', -1): # print('Found candidate JSON: {}'.format(d)) # A newer entry was found self.curr_dict = d @@ -197,10 +161,17 @@ class Settings: # If we loaded settings, then we're done if self.addr: + # print('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx') + # print('LOADED SETTINGS! _revision={} addr={}'.format(self.curr_dict.get('_revision'), hex(addr))) + # print('values: {}'.format(to_str(self.curr_dict))) + # print('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx') + + system.turbo(False) return - # Add some che - # if self. + # print('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx') + # print(' UNABLE TO LOAD SETTINGS: key={}'.format(self.aes_key)) + # print('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx') # If no entries were found, which means this is either the first boot or we have corrupt settings, so raise an exception so we erase and set default # raise ValueError('Flash is either blank or corrupt, so me must reset to recover to avoid a crash!') @@ -209,15 +180,39 @@ class Settings: self.addr = 0 except Exception as e: - print('Exception in settings.load(): e={}'.format(e)) + # print('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx') + # print('Exception in settings.load(): e={}'.format(e)) + # print('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx') self.reset() self.is_dirty = True self.write_out() + system.turbo(False) + def get(self, kn, default=None): if kn in self.overrides: return self.overrides.get(kn) else: + # Special case for xfp and xpub -- make sure they exist and create if not + if kn not in self.curr_dict: + if kn == 'xfp' or kn == 'xpub': + try: + # Update xpub/xfp in settings after creating new wallet + import stash + from common import system + + system.show_busy_bar() + with stash.SensitiveValues() as sv: + sv.capture_xpub() + except Exception as e: + # print('ERROR: Cannot create xfp/xpub: e={}'.format(e)) + # We tried to create it, but if creation fails, just let the caller handle the error + pass + finally: + system.hide_busy_bar() + # These are overrides, so return them from there + return self.overrides.get(kn) + return self.curr_dict.get(kn, default) def changed(self): @@ -226,15 +221,25 @@ class Settings: self.loop.call_later_ms(250, self.write_out()) def set(self, kn, v): - self.curr_dict[kn] = v - print('Settings: Set {} to {}'.format(kn, v)) + if self.curr_dict.get(kn, '!~$~!') != v: # So that None can be set + self.curr_dict[kn] = v + # print('Settings: Set {} to {}'.format(kn, to_str(v))) + self.changed() + + def remove(self, kn): + self.curr_dict.pop(kn, None) + # print('Settings: Remove {}'.format(kn)) self.changed() def set_volatile(self, kn, v): self.overrides[kn] = v def reset(self): - self.erase_settings_flash() + # print('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx') + # print(' RESET SETTINGS FLASH') + # print('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx') + + self.flash.erase() self.curr_dict = self.default_values() self.overrides.clear() self.addr = 0 @@ -251,24 +256,31 @@ class Settings: # Was sometimes running low on memory in this area: recover try: - gc.collect() - self.save() - except MemoryError: - # TODO: This would be an infinite async loop if it throws an exception every time -- fix this + import common + # Don't save settings in the demo loop + if not common.demo_active: + gc.collect() + await self.save() + except MemoryError as e: + # NOTE: This would be an infinite async loop if it throws an exception every time -- be aware! self.loop.call_later_ms(250, self.write_out()) + def is_erased(self, addr): + buf = uctypes.bytearray_at(addr, 32) + for i in range(32): + if buf[i] != 0xFF: + return False + return True + + def find_first_erased_addr(self): for addr in SLOT_ADDRS: buf = uctypes.bytearray_at(addr, 4) - if buf[0] == buf[1] == buf[2] == buf[3] == 0xff: + if self.is_erased(addr): return addr return 0 - # We use chunks sequentially since there is no benefit to randomness - # here. An attacker needs the PIN to decrypt the AES, and if he has - # the PIN, first of all, it's game over for the Bitcoin, and even if - # the attacker cares about these settings, running AES on each of the - # 32 entries instead of just one is trivial. + # We use chunks sequentially since there is no benefit to randomness here. def next_addr(self): # If no entries were found on load, addr will be zero if self.addr == 0: @@ -281,22 +293,38 @@ class Settings: return addr # Go to next address - if self.addr < SETTINGS_FLASH_END - BLOCK_SIZE: + if self.addr < SETTINGS_FLASH_START + SETTINGS_FLASH_LENGTH - BLOCK_SIZE: + # Sanity check - if the block we want to write to is not erased, then + # something has gone wrong and we better erase and start again! + if not self.is_erased(self.addr + BLOCK_SIZE): + # print('===============================================================') + # print('UNERASED MEMORY FOUND AT {}'.format(hex(self.addr))) + # print('Aborting save') + # print('===============================================================') + self.flash.erase() + return SETTINGS_FLASH_START + return self.addr + BLOCK_SIZE - + # We reached the end of the bank -- we need to erase it so # the new settings can be written. + # print('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx') + # print(' ERASE WHEN WRAPPING AROUND') + # print('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx') self.flash.erase() return SETTINGS_FLASH_START - def save(self): + async def save(self): + from export import auto_backup # Render as JSON, encrypt and write it - self.curr_dict['_version'] = self.curr_dict.get('_version', 0) + 1 + self.curr_dict['_revision'] = self.curr_dict.get('_revision', 0) + 1 addr = self.next_addr() - print('===============================================================') - print('SAVING SETTINGS! _version={} addr={}'.format(self.curr_dict['_version'], hex(addr))) - print('===============================================================') + + # print('===============================================================') + # print('SAVING SETTINGS! _revision={} addr={}'.format(self.curr_dict.get('_revision'), hex(addr))) + # print('values to save: {}'.format(to_str(self.curr_dict))) + # print('===============================================================') flash_offset = (addr - SETTINGS_FLASH_START) // BLOCK_SIZE aes = self.get_aes(flash_offset) @@ -307,10 +335,12 @@ class Settings: json_buf = ujson.dumps(self.curr_dict).encode('utf8') # Ensure data is not too big - # TODO: Check that null byte at the end is handled properly (no overflow) if len(json_buf) > DATA_SIZE: - # TODO: Proper error handling - assert false, 'JSON data is larger than'.format(DATA_SIZE) + # print('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx') + # print(' JSON TOO BIG!') + # print('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx') + assert false, 'JSON data is larger than {}.'.format(DATA_SIZE) + return # Create a zero-filled byte buf padded_buf = bytearray(DATA_SIZE) @@ -327,11 +357,11 @@ class Settings: # Build the final buf for writing to flash save_buf = bytearray(BLOCK_SIZE) for i in range(len(encrypted_buf)): - save_buf[i] = encrypted_buf[i] # TODO: How to do this with slice notation so it doesn't truncate destination? + save_buf[i] = encrypted_buf[i] digest = chk.digest() for i in range(32): - save_buf[BLOCK_SIZE - 32 + i] = digest[i] + save_buf[DATA_SIZE + i] = digest[i] # print('addr={}\nbuf={}'.format(hex(addr),b2a_hex(save_buf))) self.flash.write(addr, save_buf) @@ -341,8 +371,7 @@ class Settings: self.addr = addr self.is_dirty = 0 - print("Settings.save(): wrote @ {}".format(hex(addr))) - + # print("Settings.save(): wrote @ {}".format(hex(addr))) def merge(self, prev): # take a dict of previous values and merge them into what we have @@ -352,6 +381,9 @@ class Settings: def default_values(): # Please try to avoid defaults here. It's better to put into code # where value is used, and treat undefined as the default state. - return dict(_version=0) + + # _schema indicates what version of settings "schema" is in use + # Used to help auto-update code that might run after a firmware update. + return dict(_revision=0, _schema=1) # EOF diff --git a/ports/stm32/boards/Passport/modules/sffile.py b/ports/stm32/boards/Passport/modules/sffile.py index 232b117..e91fd03 100644 --- a/ports/stm32/boards/Passport/modules/sffile.py +++ b/ports/stm32/boards/Passport/modules/sffile.py @@ -1,7 +1,7 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: GPL-3.0-or-later # -# SPDX-FileCopyrightText: 2018 Coinkite, Inc. +# SPDX-FileCopyrightText: 2018 Coinkite, Inc. # SPDX-License-Identifier: GPL-3.0-only # # (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard @@ -19,13 +19,13 @@ import trezorcrypto from uasyncio import sleep_ms from uio import BytesIO +from common import system -# We have a single block of 128K on the STM32H753 -blksize = const(131072) +blksize = const(65536) def PADOUT(n): # rounds up - return (n + blksize - 1) & ~(blksize-1) + return (n + blksize - 1) & ~(blksize - 1) class SFFile: @@ -85,7 +85,7 @@ class SFFile: if i and self.message: from common import dis - dis.progress_bar_show(i/self.max_size) + system.progress_bar((i*100)//self.max_size) # expect block erase to take up to 2 seconds while self.sf.is_busy(): @@ -100,12 +100,12 @@ class SFFile: def __exit__(self, exc_type, exc_val, exc_tb): if self.message: from common import dis - dis.progress_bar_show(1) + system.progress_bar(100) return False def wait_writable(self): - # TODO: timeouts here + # TODO: Could add some timeout handling here. while self.sf.is_busy(): pass @@ -170,13 +170,13 @@ class SFFile: if self.message and ll > 1: from common import dis - dis.progress_bar_show(self.pos / self.length) + system.progress_bar((self.pos * 100) // self.length) # altho tempting to return a bytearray (which we already have) many # callers expect return to be bytes and have those methods, like "find" return bytes(rv) - def read_into(self, b): + def readinto(self, b): # limitation: this will read past end of file, but not tell the caller actual = min(self.length - self.pos, len(b)) if actual <= 0: @@ -224,7 +224,7 @@ class SizerFile(SFFile): def read(self, ll=None): raise ValueError - def read_into(self, b): + def readinto(self, b): raise ValueError def close(self): diff --git a/ports/stm32/boards/Passport/modules/sflash.py b/ports/stm32/boards/Passport/modules/sflash.py index 9892336..de9d867 100644 --- a/ports/stm32/boards/Passport/modules/sflash.py +++ b/ports/stm32/boards/Passport/modules/sflash.py @@ -1,13 +1,13 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: GPL-3.0-or-later # -# SPDX-FileCopyrightText: 2018 Coinkite, Inc. +# SPDX-FileCopyrightText: 2018 Coinkite, Inc. # SPDX-License-Identifier: GPL-3.0-only # # (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard # and is covered by GPLv3 license found in COPYING. # -# sflash.py - SPI Flash on rev D and up boards. Simple serial SPI flash on SPI2 port. +# sflash.py - SPI Flash # # see also ../external/micropython/drivers/memory/spiflash.c # but not using that, because: @@ -15,11 +15,11 @@ # - it wants to waste 4k on a buffer # # Layout for project: -# - 384k PSBT incoming (MAX_TXN_LEN) -# - 384k PSBT outgoing (MAX_TXN_LEN) -# - 128k nvram settings (32 slots of 4k each) -# -# During firmware updates, entire flash, starting at zero may be used. +# - 768 PSBT incoming (MAX_TXN_LEN) +# - 768 PSBT outgoing (MAX_TXN_LEN) +# - The previous two regions are only used when signing PSBTs. +# - The same space is used to hold firmware updates. +# - 256k flash cache - similar to settings, but for UTXOs and wallet address cache # import machine @@ -37,9 +37,9 @@ CMD_CHIP_ERASE = const(0xc7) CMD_C4READ = const(0xeb) class SPIFlash: - # must write with this page size granulatity + # must write with this page size granularity PAGE_SIZE = 256 - # must erase with one of these size granulatity! + # must erase with one of these size granulatrty! SECTOR_SIZE = 4096 BLOCK_SIZE = 65536 @@ -122,12 +122,12 @@ class SPIFlash: from nvstore import SLOTS end = SLOTS[0] - from common import dis + from common import system dis.fullscreen("Cleanup...") for addr in range(0, end, self.BLOCK_SIZE): self.block_erase(addr) - dis.progress_bar_show(addr/end) + system.progress_bar_show((addr*100)//end) while self.is_busy(): pass diff --git a/ports/stm32/boards/Passport/modules/snake.py b/ports/stm32/boards/Passport/modules/snake.py index 5dfdb23..7e277fa 100644 --- a/ports/stm32/boards/Passport/modules/snake.py +++ b/ports/stm32/boards/Passport/modules/snake.py @@ -1,9 +1,9 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: GPL-3.0-or-later # from uasyncio import sleep_ms -from common import dis +from common import dis, system, settings from display import Display, FontSmall from settings import Settings from ux import KeyInputHandler @@ -229,16 +229,14 @@ class Game: async def snake_game(): # Game functions and settings - s = Settings() - s.load() game = Game() - game.highscore = s.get('snake_highscore', 0) + game.highscore = settings.get('snake_highscore', 0) input = KeyInputHandler(down='udplrxy', up='xy') while game.running: event = await input.get_event() - + if event != None: # Handle key event and update game state key, event_type = event @@ -268,10 +266,11 @@ async def snake_game(): elif game.state == TRYING_TO_QUIT: game.running = False + system.turbo(True) game.update(utime.ticks_ms()) game.render() + system.turbo(False) await sleep_ms(1) - s.set('snake_highscore', game.highscore) - s.save() + settings.set('snake_highscore', game.highscore) return None diff --git a/ports/stm32/boards/Passport/modules/sram4.py b/ports/stm32/boards/Passport/modules/sram4.py index edd0de0..5aca4c9 100644 --- a/ports/stm32/boards/Passport/modules/sram4.py +++ b/ports/stm32/boards/Passport/modules/sram4.py @@ -1,7 +1,7 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: GPL-3.0-or-later # -# SPDX-FileCopyrightText: 2018 Coinkite, Inc. +# SPDX-FileCopyrightText: 2018 Coinkite, Inc. # SPDX-License-Identifier: GPL-3.0-only # # (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard @@ -9,19 +9,20 @@ # # sram2.py - Jam some larger, long-lived objects into the SRAM2 area, which isn't used enough. # -# Cautions/Notes: +# Cautions/Notes: # - Total size of SRAM4 bank is 64K # - mpy heap does not include SRAM4, so doing manual memory alloc here. # - top 8k reserved for bootloader, which will wipe it on each entry # - 2k at bottom reserved for code in `flashbdev.c` to use as cache data for flash writing -# - keep this file in sync with simulated version +# - keep this file in sync with simulated version # import uctypes from constants import VIEWFINDER_WIDTH, VIEWFINDER_HEIGHT # see stm32/Passport/passport.ld where this is effectively defined SRAM4_START = const(0x38000800) -SRAM4_LENGTH = const(0x5800) +SRAM4_LENGTH = const(0x10000) +SRAM4_END = SRAM4_START + SRAM4_LENGTH _start = SRAM4_START @@ -31,9 +32,9 @@ def _alloc(ln): _start += ln return rv -settings_buf = _alloc(4096-32) +flash_cache_buf = _alloc(16 * 1024) tmp_buf = _alloc(1024) psbt_tmp256 = _alloc(256) viewfinder_buf = _alloc((VIEWFINDER_WIDTH*VIEWFINDER_HEIGHT) // 8) -assert _start <= 0x38006000 +assert _start <= SRAM4_END diff --git a/ports/stm32/boards/Passport/modules/stacksats.py b/ports/stm32/boards/Passport/modules/stacking_sats.py similarity index 69% rename from ports/stm32/boards/Passport/modules/stacksats.py rename to ports/stm32/boards/Passport/modules/stacking_sats.py index 2184b51..5c0ed58 100644 --- a/ports/stm32/boards/Passport/modules/stacksats.py +++ b/ports/stm32/boards/Passport/modules/stacking_sats.py @@ -1,11 +1,10 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: GPL-3.0-or-later # from uasyncio import sleep_ms -from common import dis, noise +from common import dis, noise, settings, system from display import Display, FontSmall -from settings import Settings from ux import KeyInputHandler import utime @@ -31,6 +30,14 @@ SPAWN_AREA = 6 SPEED_FAST = 1 SPEED_SLOW = 500 +SHAPE_S = 0 +SHAPE_T = 1 +SHAPE_L = 2 +SHAPE_Lb = 3 +SHAPE_Z = 4 +SHAPE_Zb = 5 +SHAPE_I = 6 + class Block: """ Block grid @@ -78,10 +85,10 @@ class Block: def rotateBlock(self): self.rot = (self.rot + 1) % len(self.block_map[self.type]) - def draw(self): + def draw(self, offset_x, offset_y): for i in range(0, len(self.block_map[self.type][self.rot])): - self.dis.icon(MIN_X + BLOCK_SIZE * (self.x + (self.block_map[self.type][self.rot][i] % 4)), - MAX_Y - BLOCK_SIZE * (self.y + ((self.block_map[self.type][self.rot][i] // 4) % 4)), + self.dis.icon(MIN_X + BLOCK_SIZE * (self.x + (self.block_map[self.type][self.rot][i] % 4)) + offset_x, + MAX_Y - BLOCK_SIZE * (self.y + ((self.block_map[self.type][self.rot][i] // 4) % 4)) - offset_y, 'tetris_pattern_' + str(self.type)) class Map: @@ -102,7 +109,7 @@ class Map: if self.grid[row][col] == -1: # if at any point a row has an empty block, skip to the next row break if col == (self.width - 1): # if entire column is filled shift all rows above it down. - print("deleting row") + # print("deleting row") for new_row in range(row, self.height - SPAWN_AREA): for col in range(0, self.width): self.grid[new_row][col] = self.grid[new_row + 1][col] @@ -129,31 +136,32 @@ class Map: class Game: - def __init__(self): + def __init__(self, KeyInput): self.dis = dis self.font = FontSmall self.running = True self.state = READY_TO_PLAY self.prev_time = 0 self.speed = SPEED_SLOW - self.input = KeyInputHandler(down='udplrxy', up='xy') + self.input = KeyInput self.map = Map() self.falling_block = Block(self.dis, BLOCK_FALLING_X, BLOCK_FALLING_Y) self.next_block = Block(self.dis, BLOCK_NEXT_X, BLOCK_NEXT_Y) self.temp_block = Block(self.dis, 0, 0) self.left_btn = '' self.right_btn = '' + self.isLRkeyPressed = False def blockInSpawn(self): for i in range(0, 4): if ((self.falling_block.y + ((self.falling_block.block_map[self.falling_block.type][self.falling_block.rot][i] // 4) % 4)) >= self.map.height): # or ((self.falling_block.y + ((self.falling_block.block_map[self.falling_block.type][self.falling_block.rot][i] // 4) % 4)) < 0): - print("blockInSpawn() = True") + # print("blockInSpawn() = True") return True - print("blockInSpawn() = True") + # print("blockInSpawn() = True") return False - # direction must be either 'o', 'r', 'l', 'd' - def isCollision(self, direction): + + def isCollisionWall(self, direction): self.temp_block.x = self.falling_block.x self.temp_block.y = self.falling_block.y self.temp_block.type = self.falling_block.type @@ -166,13 +174,29 @@ class Game: for i in range(4): if (direction == 'r' or direction == 'o') and ((self.temp_block.x + (self.temp_block.block_map[self.temp_block.type][self.temp_block.rot][i] % 4)) >= self.map.width): - return True + return 'r' elif (direction == 'l' or direction == 'o') and ((self.temp_block.x + (self.temp_block.block_map[self.temp_block.type][self.temp_block.rot][i] % 4)) < 0): - return True + return 'l' elif (direction == 'd' or direction == 'o') and ((self.temp_block.y + ((self.temp_block.block_map[self.temp_block.type][self.temp_block.rot][i] // 4) % 4)) < 0): - return True + return 'd' + + return False + + # direction must be either 'o', 'r', 'l', 'd' + def isCollisionMap(self, direction): + self.temp_block.x = self.falling_block.x + self.temp_block.y = self.falling_block.y + self.temp_block.type = self.falling_block.type + self.temp_block.rot = self.falling_block.rot + + if direction == 'o': + self.temp_block.rotateBlock() + else: + self.temp_block.moveBlock(direction) + + for i in range(4): # is part of block colliding with block embedded in map.grid[row][col] - elif self.map.grid[(self.temp_block.y + ((self.temp_block.block_map[self.temp_block.type][self.temp_block.rot][i] // 4) % 4))][(self.temp_block.x + (self.temp_block.block_map[self.temp_block.type][self.temp_block.rot][i] % 4))] >= 0: + if self.map.grid[(self.temp_block.y + ((self.temp_block.block_map[self.temp_block.type][self.temp_block.rot][i] // 4) % 4))][(self.temp_block.x + (self.temp_block.block_map[self.temp_block.type][self.temp_block.rot][i] % 4))] >= 0: return True return False @@ -184,10 +208,10 @@ class Game: self.dis.text(MIN_X + 27 * BLOCK_SIZE // 2 + 3, MAX_Y - 37 * BLOCK_SIZE // 2, 'Next:', invert=1) self.dis.text(MIN_X + 27 * BLOCK_SIZE // 2 + 3, 150, 'Stack', invert=1) - self.dis.text(MIN_X + 27 * BLOCK_SIZE // 2 + 3, 150 + self.font.leading, 'Sats!', invert=1) + self.dis.text(MIN_X + 27 * BLOCK_SIZE // 2 + 6, 150 + self.font.leading, 'Sats!', invert=1) def embedBlockInMap(self): - print("embedBlockInMap()") + # print("embedBlockInMap()") # Block must be checked to be entirly within map bounds before embedding using isCollision for i in range(4): # print("checkrow[" + str(i) + "] = " + str(self.falling_block.y + ((self.falling_block.block_map[self.falling_block.type][self.falling_block.rot][i] // 4) % 4))) @@ -207,6 +231,21 @@ class Game: self.map.grid[row][col] = -1 # clear game map self.state = GAME_IN_PROGRESS + def drawNextBlock(self): + if self.next_block.type == SHAPE_S: + self.next_block.draw(-3,0) + elif self.next_block.type == SHAPE_T: + self.next_block.draw(9,6) + elif self.next_block.type == SHAPE_L: + self.next_block.draw(0,6) + elif self.next_block.type == SHAPE_Lb: + self.next_block.draw(6,6) + elif self.next_block.type == SHAPE_Z: + self.next_block.draw(3,9) + elif self.next_block.type == SHAPE_Zb: + self.next_block.draw(3,9) + elif self.next_block.type == SHAPE_I: + self.next_block.draw(3,0) def render(self): self.dis.clear() @@ -220,14 +259,14 @@ class Game: elif self.state == GAME_IN_PROGRESS: self.left_btn = 'BACK' self.right_btn = 'PAUSE' - self.falling_block.draw() - self.next_block.draw() + self.falling_block.draw(0,0) + self.drawNextBlock() elif self.state == PAUSED: self.left_btn = 'BACK' self.right_btn = 'RESUME' - self.falling_block.draw() - self.next_block.draw() + self.falling_block.draw(0,0) + self.drawNextBlock() POPUP_WIDTH = 180 POPUP_HEIGHT = 100 POPUP_X = Display.HALF_WIDTH - (POPUP_WIDTH // 2) @@ -273,9 +312,9 @@ class Game: def update(self, now): if (now - self.prev_time > self.speed - (self.map.score * 5)): self.prev_time = now - + # print("isLRkeyPressed = " + str(self.isLRkeyPressed)) if self.state == GAME_IN_PROGRESS: - if self.isCollision('d'): + if (self.isCollisionWall('d') or self.isCollisionMap('d')): self.embedBlockInMap() if self.map.isGameOver(): self.state = GAME_OVER @@ -284,38 +323,76 @@ class Game: self.map.deleteFilledRows() else: self.falling_block.moveBlock('d') + if self.input.is_pressed('r'): + if self.isCollisionWall('r') == False: + if self.isCollisionMap('r') == False: + self.falling_block.moveBlock('r') + elif self.input.is_pressed('l'): + if self.isCollisionWall('l') == False: + if self.isCollisionMap('l') == False: + self.falling_block.moveBlock('l') + + if (self.input.is_pressed('r')) or (self.input.is_pressed('l')): + self.isLRkeyPressed = True + else: + self.isLRkeyPressed = False -async def stacksats_game(): +async def stacking_sats_game(): - game = Game() - s = Settings() - s.load() - game.map.highscore = s.get('stacksats_highscore', 0) - input = KeyInputHandler(down='udplrxy', up='xy') + game = Game(input) + game.map.highscore = settings.get('sats_highscore', 0) + while game.running: event = await input.get_event() if input.is_pressed('d'): game.speed = SPEED_FAST else: - game.speed = SPEED_SLOW - game.map.score + game.speed = SPEED_SLOW if event != None: key, event_type = event if event_type == 'down': if key == 'l': - if not game.isCollision(key): - game.falling_block.moveBlock(key) + if game.isCollisionWall(key) == False: + if game.isCollisionMap(key) == False: + game.falling_block.moveBlock(key) elif key == 'r': - if not game.isCollision(key): - game.falling_block.moveBlock(key) + if game.isCollisionWall(key) == False: + if game.isCollisionMap(key) == False: + game.falling_block.moveBlock(key) elif key == 'u': - if not game.isCollision('o'): - game.falling_block.rotateBlock() + collision_wall = game.isCollisionWall('o') + if collision_wall == False: + if game.isCollisionMap('o') == False: + game.falling_block.rotateBlock() + else: + pass + # print("Collision with block in map when trying to rotate") + elif collision_wall == 'l': + # print("Left wall collision when trying to rotate") + if game.isCollisionMap('r') == False: + game.falling_block.moveBlock('r') + if game.isCollisionWall('o') == False: + if game.isCollisionMap('o') == False: + game.falling_block.rotateBlock() + + elif collision_wall == 'r': + # print("Right wall collision when trying to rotate") + if game.isCollisionMap('l') == False: + game.falling_block.moveBlock('l') + if game.isCollisionWall('o') == False: + if game.isCollisionMap('o') == False: + game.falling_block.rotateBlock() + else: # corner case where a straight piece is flush with right wall and tries to rotate + if game.isCollisionMap('l') == False: + game.falling_block.moveBlock('l') + if game.isCollisionWall('o') == False: + game.falling_block.rotateBlock() elif key == 'd': pass elif key == 'x': @@ -325,7 +402,7 @@ async def stacksats_game(): elif event_type == 'up': if key == 'x': - print("x-up") + # print("x-up") if game.state == GAME_IN_PROGRESS: game.state = TRYING_TO_QUIT elif game.state == TRYING_TO_QUIT: @@ -333,7 +410,7 @@ async def stacksats_game(): elif (game.state == READY_TO_PLAY) or (game.state == GAME_OVER): game.running = False elif key == 'y': - print("y-up") + # print("y-up") if (game.state == READY_TO_PLAY) or (game.state == GAME_OVER): game.start() elif game.state == GAME_IN_PROGRESS: @@ -343,11 +420,11 @@ async def stacksats_game(): elif game.state == TRYING_TO_QUIT: game.running = False - + system.turbo(True) game.update(utime.ticks_ms()) game.render() + system.turbo(False) await sleep_ms(10) - s.set('stacksats_highscore', game.map.highscore) - s.save() + settings.set('sats_highscore', game.map.highscore) return None diff --git a/ports/stm32/boards/Passport/modules/stash.py b/ports/stm32/boards/Passport/modules/stash.py index c02e8aa..3a40432 100644 --- a/ports/stm32/boards/Passport/modules/stash.py +++ b/ports/stm32/boards/Passport/modules/stash.py @@ -1,7 +1,7 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: GPL-3.0-or-later # -# SPDX-FileCopyrightText: 2018 Coinkite, Inc. +# SPDX-FileCopyrightText: 2018 Coinkite, Inc. # SPDX-License-Identifier: GPL-3.0-only # # (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard @@ -18,7 +18,7 @@ # - 'abandon' * 11 + 'about' # import trezorcrypto, uctypes, gc -from pincodes import AE_SECRET_LEN +from pincodes import SE_SECRET_LEN def blank_object(item): # Use/abuse uctypes to blank objects until python. Will likely @@ -32,9 +32,7 @@ def blank_object(item): buf[i] = 0 elif isinstance(item, trezorcrypto.bip32.HDNode): pass - # This function was added by coinkite - # TODO: Important enough to add blank() back into trezor? - # item.blank() + # item.blank() # node.blank() elsewhere else: raise TypeError(item) @@ -47,7 +45,7 @@ class SecretStash: @staticmethod def encode(seed_bits=None, master_secret=None, xprv=None): - nv = bytearray(AE_SECRET_LEN) + nv = bytearray(SE_SECRET_LEN) if seed_bits: # typical: seed bits without checksum bits @@ -95,7 +93,7 @@ class SecretStash: # seed phrase ll = ((marker & 0x3) + 2) * 8 - # note: + # note: # - byte length > number of words # - not storing checksum assert ll in [16, 24, 32] @@ -121,11 +119,14 @@ class SecretStash: # optional global value: user-supplied passphrase to salt BIP39 seed process bip39_passphrase = '' +bip39_hash = '' class SensitiveValues: # be a context manager, and holder to secrets in-memory def __init__(self, secret=None, for_backup=False): + from common import system + if secret is None: # fetch the secret from bootloader/atecc508a from common import pa @@ -134,22 +135,27 @@ class SensitiveValues: raise ValueError('no secrets yet') self.secret = pa.fetch() + self.spots = [ self.secret ] else: # sometimes we already know it - assert set(secret) != {0} + # assert set(secret) != {0} self.secret = secret + self.spots = [] # backup during volatile bip39 encryption: do not use passphrase self._bip39pw = '' if for_backup else str(bip39_passphrase) + # print('self._bip39pw={}'.format(self._bip39pw)) + def __enter__(self): import chains self.mode, self.raw, self.node = SecretStash.decode(self.secret, self._bip39pw) - self.chain = chains.current_chain() + self.spots.append(self.node) + self.spots.append(self.raw) - self.spots = [ self.secret, self.node, self.raw ] + self.chain = chains.current_chain() return self @@ -161,7 +167,6 @@ class SensitiveValues: if hasattr(self, 'secret'): # will be blanked from above - assert self.secret == bytes(AE_SECRET_LEN) del self.secret if hasattr(self, 'node'): @@ -175,31 +180,46 @@ class SensitiveValues: gc.collect() if exc_val: - # An exception happened, but we've done cleanup already now, so + # An exception happened, but we've done cleanup already now, so # not a big deal. Cause it be raised again. return False return True + def get_xfp(self): + return self.node.my_fingerprint() + def capture_xpub(self): # track my xpubkey fingerprint & value in settings (not sensitive really) # - we share these on any USB connection + import common from common import settings - # Implicit in the values is the BIP39 encryption passphrase, - # which we might not want to actually store. - # TODO: trezorcrypto doesn't implement this, but implement fingerprint() instead, which apparently - # returns the fingerprint of the parent?!?!?!? + # # Set the master values if no account selected yet + # if common.active_account: + # # Derive xfp and xpub based on the current active account + # # The BIP39 passphrase is already taken into account by SensitiveValues + # # print('deriving from path: {}'.format(common.active_account.deriv_path)) + # if not common.active_account.deriv_path: + # return + # + # node = self.derive_path(common.active_account.deriv_path) + # + # xfp = node.my_fingerprint() + # print('capture_xpub(): xfp={}'.format(hex(xfp))) + # xpub = self.chain.serialize_public(node, common.active_account.addr_type) + # print('capture_xpub(): xpub={}'.format(xpub)) + # else: + xfp = self.node.my_fingerprint() + # print('capture_xpub(): xfp={}'.format(hex(xfp))) xpub = self.chain.serialize_public(self.node) + # print('capture_xpub(): xpub={}'.format(xpub)) - if self._bip39pw: - settings.set_volatile('xfp', xfp) - settings.set_volatile('xpub', xpub) - else: - settings.overrides.clear() - settings.set('xfp', xfp) - settings.set('xpub', xpub) + # Always store these volatile - Takes less than 1 second to recreate, and it will change whenever + # a passphrase is entered, so no need to waste flash cycles on storing it. + settings.set_volatile('xfp', xfp) + settings.set_volatile('xpub', xpub) settings.set('chain', self.chain.ctype) settings.set('words', (self.mode == 'words')) @@ -232,6 +252,6 @@ class SensitiveValues: rv.derive(here) - return rv + return rv # EOF diff --git a/ports/stm32/boards/Passport/modules/stat.py b/ports/stm32/boards/Passport/modules/stat.py new file mode 100644 index 0000000..cd9cba7 --- /dev/null +++ b/ports/stm32/boards/Passport/modules/stat.py @@ -0,0 +1,148 @@ +"""Constants/functions for interpreting results of os.stat() and os.lstat(). +Suggested usage: from stat import * +""" + +# Indices for stat struct members in the tuple returned by os.stat() + +ST_MODE = 0 +ST_INO = 1 +ST_DEV = 2 +ST_NLINK = 3 +ST_UID = 4 +ST_GID = 5 +ST_SIZE = 6 +ST_ATIME = 7 +ST_MTIME = 8 +ST_CTIME = 9 + +# Extract bits from the mode + +def S_IMODE(mode): + """Return the portion of the file's mode that can be set by + os.chmod(). + """ + return mode & 0o7777 + +def S_IFMT(mode): + """Return the portion of the file's mode that describes the + file type. + """ + return mode & 0o170000 + +# Constants used as S_IFMT() for various file types +# (not all are implemented on all systems) + +S_IFDIR = 0o040000 # directory +S_IFCHR = 0o020000 # character device +S_IFBLK = 0o060000 # block device +S_IFREG = 0o100000 # regular file +S_IFIFO = 0o010000 # fifo (named pipe) +S_IFLNK = 0o120000 # symbolic link +S_IFSOCK = 0o140000 # socket file + +# Functions to test for each file type + +def S_ISDIR(mode): + """Return True if mode is from a directory.""" + return S_IFMT(mode) == S_IFDIR + +def S_ISCHR(mode): + """Return True if mode is from a character special device file.""" + return S_IFMT(mode) == S_IFCHR + +def S_ISBLK(mode): + """Return True if mode is from a block special device file.""" + return S_IFMT(mode) == S_IFBLK + +def S_ISREG(mode): + """Return True if mode is from a regular file.""" + return S_IFMT(mode) == S_IFREG + +def S_ISFIFO(mode): + """Return True if mode is from a FIFO (named pipe).""" + return S_IFMT(mode) == S_IFIFO + +def S_ISLNK(mode): + """Return True if mode is from a symbolic link.""" + return S_IFMT(mode) == S_IFLNK + +def S_ISSOCK(mode): + """Return True if mode is from a socket.""" + return S_IFMT(mode) == S_IFSOCK + +# Names for permission bits + +S_ISUID = 0o4000 # set UID bit +S_ISGID = 0o2000 # set GID bit +S_ENFMT = S_ISGID # file locking enforcement +S_ISVTX = 0o1000 # sticky bit +S_IREAD = 0o0400 # Unix V7 synonym for S_IRUSR +S_IWRITE = 0o0200 # Unix V7 synonym for S_IWUSR +S_IEXEC = 0o0100 # Unix V7 synonym for S_IXUSR +S_IRWXU = 0o0700 # mask for owner permissions +S_IRUSR = 0o0400 # read by owner +S_IWUSR = 0o0200 # write by owner +S_IXUSR = 0o0100 # execute by owner +S_IRWXG = 0o0070 # mask for group permissions +S_IRGRP = 0o0040 # read by group +S_IWGRP = 0o0020 # write by group +S_IXGRP = 0o0010 # execute by group +S_IRWXO = 0o0007 # mask for others (not in group) permissions +S_IROTH = 0o0004 # read by others +S_IWOTH = 0o0002 # write by others +S_IXOTH = 0o0001 # execute by others + +# Names for file flags + +UF_NODUMP = 0x00000001 # do not dump file +UF_IMMUTABLE = 0x00000002 # file may not be changed +UF_APPEND = 0x00000004 # file may only be appended to +UF_OPAQUE = 0x00000008 # directory is opaque when viewed through a union stack +UF_NOUNLINK = 0x00000010 # file may not be renamed or deleted +UF_COMPRESSED = 0x00000020 # OS X: file is hfs-compressed +UF_HIDDEN = 0x00008000 # OS X: file should not be displayed +SF_ARCHIVED = 0x00010000 # file may be archived +SF_IMMUTABLE = 0x00020000 # file may not be changed +SF_APPEND = 0x00040000 # file may only be appended to +SF_NOUNLINK = 0x00100000 # file may not be renamed or deleted +SF_SNAPSHOT = 0x00200000 # file is a snapshot file + + +_filemode_table = ( + ((S_IFLNK, "l"), + (S_IFREG, "-"), + (S_IFBLK, "b"), + (S_IFDIR, "d"), + (S_IFCHR, "c"), + (S_IFIFO, "p")), + + ((S_IRUSR, "r"),), + ((S_IWUSR, "w"),), + ((S_IXUSR|S_ISUID, "s"), + (S_ISUID, "S"), + (S_IXUSR, "x")), + + ((S_IRGRP, "r"),), + ((S_IWGRP, "w"),), + ((S_IXGRP|S_ISGID, "s"), + (S_ISGID, "S"), + (S_IXGRP, "x")), + + ((S_IROTH, "r"),), + ((S_IWOTH, "w"),), + ((S_IXOTH|S_ISVTX, "t"), + (S_ISVTX, "T"), + (S_IXOTH, "x")) +) + +def filemode(mode): + """Convert a file's mode to a string of the form '-rwxrwxrwx'.""" + perm = [] + for table in _filemode_table: + for bit, char in table: + if mode & bit == bit: + perm.append(char) + break + else: + perm.append("-") + return "".join(perm) diff --git a/ports/stm32/boards/Passport/modules/uQR.py b/ports/stm32/boards/Passport/modules/uQR.py index 61c303c..d5640a9 100644 --- a/ports/stm32/boards/Passport/modules/uQR.py +++ b/ports/stm32/boards/Passport/modules/uQR.py @@ -1041,7 +1041,7 @@ class QRCode: for data in self.data_list: buffer.put(data.mode, 4) buffer.put(len(data), mode_sizes[data.mode]) - print('buffer={}'.format(buffer)) + # print('buffer={}'.format(buffer)) data.write(buffer) needed_bits = len(buffer) diff --git a/ports/stm32/boards/Passport/modules/uasyncio/__init__.py b/ports/stm32/boards/Passport/modules/uasyncio/__init__.py index ef10d50..6ab5851 100644 --- a/ports/stm32/boards/Passport/modules/uasyncio/__init__.py +++ b/ports/stm32/boards/Passport/modules/uasyncio/__init__.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2018 Coinkite, Inc. +# SPDX-FileCopyrightText: 2018 Coinkite, Inc. # SPDX-License-Identifier: GPL-3.0-only # # (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard diff --git a/ports/stm32/boards/Passport/modules/uasyncio/core.py b/ports/stm32/boards/Passport/modules/uasyncio/core.py index d344c68..a44c18f 100644 --- a/ports/stm32/boards/Passport/modules/uasyncio/core.py +++ b/ports/stm32/boards/Passport/modules/uasyncio/core.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2018 Coinkite, Inc. +# SPDX-FileCopyrightText: 2018 Coinkite, Inc. # SPDX-License-Identifier: GPL-3.0-only # # (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard diff --git a/ports/stm32/boards/Passport/modules/uasyncio/queues.py b/ports/stm32/boards/Passport/modules/uasyncio/queues.py index 9bc3990..ebeedbc 100644 --- a/ports/stm32/boards/Passport/modules/uasyncio/queues.py +++ b/ports/stm32/boards/Passport/modules/uasyncio/queues.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2018 Coinkite, Inc. +# SPDX-FileCopyrightText: 2018 Coinkite, Inc. # SPDX-License-Identifier: GPL-3.0-only # # (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard @@ -6,7 +6,7 @@ # # See also: # -from collections.deque import deque +from ucollections import deque from uasyncio.core import sleep @@ -33,7 +33,7 @@ class Queue: def __init__(self, maxsize=0): self.maxsize = maxsize - self._queue = deque() + self._queue = deque((), 20) def _get(self): return self._queue.popleft() diff --git a/ports/stm32/boards/Passport/modules/uasyncio/synchro.py b/ports/stm32/boards/Passport/modules/uasyncio/synchro.py index 6840f64..6b246e8 100644 --- a/ports/stm32/boards/Passport/modules/uasyncio/synchro.py +++ b/ports/stm32/boards/Passport/modules/uasyncio/synchro.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2018 Coinkite, Inc. +# SPDX-FileCopyrightText: 2018 Coinkite, Inc. # SPDX-License-Identifier: GPL-3.0-only # # (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard diff --git a/ports/stm32/boards/Passport/modules/ur/__init__.py b/ports/stm32/boards/Passport/modules/ur/__init__.py deleted file mode 100644 index be31177..0000000 --- a/ports/stm32/boards/Passport/modules/ur/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. -# SPDX-License-Identifier: BSD-2-Clause-Patent -# diff --git a/ports/stm32/boards/Passport/modules/ur1/bc32.py b/ports/stm32/boards/Passport/modules/ur1/bc32.py index 787d39c..aaaef9a 100644 --- a/ports/stm32/boards/Passport/modules/ur1/bc32.py +++ b/ports/stm32/boards/Passport/modules/ur1/bc32.py @@ -1,9 +1,9 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: GPL-3.0-or-later # from .bech32 import encode, decode from .bech32_version import Bech32_Version_Origin, Bech32_Version_Bis -from binascii import hexlify +from ubinascii import hexlify import gc def convert_bits(data, from_bits, to_bits, pad): @@ -16,34 +16,34 @@ def convert_bits(data, from_bits, to_bits, pad): ret = bytearray(new_size) maxv = (1 << to_bits) - 1 i = 0 - + for p in range(len(data)): value = data[p] if value < 0 or value >> from_bits != 0: return None - + acc = (acc << from_bits) | value bits += from_bits while bits >= to_bits: bits -= to_bits ret[i] = (acc >> bits) & maxv i += 1 - + if pad: if bits > 0: ret[i] = (acc << (to_bits - bits)) & maxv else: - ret = ret[:-1] # TODO: Find out if this allocates and if so, find a better way to handle padding + ret = ret[:-1] elif bits >= from_bits or (acc << (to_bits - bits)) & maxv: return None - + return ret # NOTE: Segwit functions not needed, so not ported def encode_bc32_data(data): u82u5 = convert_bits(data, 8, 5, True) - print('u82u5={}'.format(u82u5)) + # print('u82u5={}'.format(u82u5)) res = u82u5 if u82u5 == None: raise ValueError('Invalid bc32 data') diff --git a/ports/stm32/boards/Passport/modules/ur1/bech32.py b/ports/stm32/boards/Passport/modules/ur1/bech32.py index 548a913..43ebecc 100644 --- a/ports/stm32/boards/Passport/modules/ur1/bech32.py +++ b/ports/stm32/boards/Passport/modules/ur1/bech32.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: GPL-3.0-or-later # from .bech32_version import Bech32_Version_Origin, Bech32_Version_Bis diff --git a/ports/stm32/boards/Passport/modules/ur1/bech32_version.py b/ports/stm32/boards/Passport/modules/ur1/bech32_version.py index d545226..c53805c 100644 --- a/ports/stm32/boards/Passport/modules/ur1/bech32_version.py +++ b/ports/stm32/boards/Passport/modules/ur1/bech32_version.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: GPL-3.0-or-later # Bech32_Version_Origin = 1 diff --git a/ports/stm32/boards/Passport/modules/ur1/decode_ur.py b/ports/stm32/boards/Passport/modules/ur1/decode_ur.py index 0032b1d..9d45bfb 100644 --- a/ports/stm32/boards/Passport/modules/ur1/decode_ur.py +++ b/ports/stm32/boards/Passport/modules/ur1/decode_ur.py @@ -1,10 +1,10 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: GPL-3.0-or-later # -from .utils import sha256_hash from .mini_cbor import decode_simple_cbor from .bc32 import decode_bc32_data from ubinascii import unhexlify as a2b_hex, hexlify as b2a_hex +from common import system def check_and_get_sequence(sequence): pieces = sequence.upper().split('OF') @@ -21,7 +21,8 @@ def check_digest(digest, payload): raise ValueError('Unable to decode payload: {}'.format(payload)) decoded_bytes = a2b_hex(decoded) - sha = sha256_hash(decoded_bytes) + sha = bytearray(32) + system.sha256(decoded_bytes, sha) decoded_digest = b2a_hex(sha).decode() # bytearray comp_digest = decode_bc32_data(digest) if comp_digest != decoded_digest: @@ -59,9 +60,9 @@ def deal_with_multiple_workloads(workloads, type='bytes'): fragments = ['' for i in range(num_workloads)] digest = None for workload in workloads: - print('workload = {}'.format(workload)) + # print('workload = {}'.format(workload)) pieces = workload.split('/') - print('pieces = {}'.format(pieces)) + # print('pieces = {}'.format(pieces)) check_ur_header(pieces[0], type) (index, total) = check_and_get_sequence(pieces[1]) if total != num_workloads: diff --git a/ports/stm32/boards/Passport/modules/ur1/encode_ur.py b/ports/stm32/boards/Passport/modules/ur1/encode_ur.py index 774d510..9d43824 100644 --- a/ports/stm32/boards/Passport/modules/ur1/encode_ur.py +++ b/ports/stm32/boards/Passport/modules/ur1/encode_ur.py @@ -1,10 +1,11 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: GPL-3.0-or-later # from .mini_cbor import encode_simple_cbor from .bc32 import encode_bc32_data -from .utils import sha256_hash, compose3 -from binascii import hexlify +from .utils import compose3 +from ubinascii import hexlify +from common import system def compose_ur(payload, type='bytes'): return 'ur:{}/{}'.format(type, payload) @@ -33,7 +34,8 @@ def compose_headers_to_fragments(fragments, digest, type='bytes'): def encode_ur(payload, fragment_capacity=1000): cbor_payload = encode_simple_cbor(payload) bc32_payload = encode_bc32_data(cbor_payload) - digest = sha256_hash(cbor_payload) + digest = bytearray(32) + system.sha256(cbor_payload, digest) bc32_digest = encode_bc32_data(digest) fragments = [bc32_payload[i:i+fragment_capacity] for i in range(0, len(bc32_payload), fragment_capacity)] if not fragments: diff --git a/ports/stm32/boards/Passport/modules/ur1/mini_cbor.py b/ports/stm32/boards/Passport/modules/ur1/mini_cbor.py index b1acf49..a4063ea 100644 --- a/ports/stm32/boards/Passport/modules/ur1/mini_cbor.py +++ b/ports/stm32/boards/Passport/modules/ur1/mini_cbor.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: GPL-3.0-or-later # from ubinascii import hexlify diff --git a/ports/stm32/boards/Passport/modules/ur1/utils.py b/ports/stm32/boards/Passport/modules/ur1/utils.py index 9c0b3fb..13c4187 100644 --- a/ports/stm32/boards/Passport/modules/ur1/utils.py +++ b/ports/stm32/boards/Passport/modules/ur1/utils.py @@ -1,12 +1,5 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: GPL-3.0-or-later # -from hashlib import sha256 - -def sha256_hash(data): - m = sha256() - m.update(data) - return m.digest() - def compose3(f, g, h): return lambda x: f(g(h(x))) diff --git a/ports/stm32/boards/Passport/modules/ur2/__init__.py b/ports/stm32/boards/Passport/modules/ur2/__init__.py new file mode 100644 index 0000000..3574f74 --- /dev/null +++ b/ports/stm32/boards/Passport/modules/ur2/__init__.py @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-License-Identifier: BSD-2-Clause-Patent +# diff --git a/ports/stm32/boards/Passport/modules/ur/bytewords.py b/ports/stm32/boards/Passport/modules/ur2/bytewords.py similarity index 98% rename from ports/stm32/boards/Passport/modules/ur/bytewords.py rename to ports/stm32/boards/Passport/modules/ur2/bytewords.py index 6bd3889..79e1bd6 100644 --- a/ports/stm32/boards/Passport/modules/ur/bytewords.py +++ b/ports/stm32/boards/Passport/modules/ur2/bytewords.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: BSD-2-Clause-Patent # # bytewords.py diff --git a/ports/stm32/boards/Passport/modules/ur/cbor_lite.py b/ports/stm32/boards/Passport/modules/ur2/cbor_lite.py similarity index 98% rename from ports/stm32/boards/Passport/modules/ur/cbor_lite.py rename to ports/stm32/boards/Passport/modules/ur2/cbor_lite.py index 746e6d2..d00f752 100644 --- a/ports/stm32/boards/Passport/modules/ur/cbor_lite.py +++ b/ports/stm32/boards/Passport/modules/ur2/cbor_lite.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: BSD-2-Clause-Patent # # crc32.py @@ -254,7 +254,7 @@ class CBORDecoder: if tag == Tag_Major_unsignedInteger: return (value, length) elif tag == Tag_Major_negativeInteger: - # TODO: Check that this is the right way -- do we need to use struct.unpack()? + # TODO: Need to properly test negative integers (although we don't use them). Need to use struct.unpack()? return (-1 - value, length) def decodeBool(self, flags=Flag_None): diff --git a/ports/stm32/boards/Passport/modules/ur/constants.py b/ports/stm32/boards/Passport/modules/ur2/constants.py similarity index 58% rename from ports/stm32/boards/Passport/modules/ur/constants.py rename to ports/stm32/boards/Passport/modules/ur2/constants.py index c8645a7..97d256e 100644 --- a/ports/stm32/boards/Passport/modules/ur/constants.py +++ b/ports/stm32/boards/Passport/modules/ur2/constants.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: BSD-2-Clause-Patent # # constants.py diff --git a/ports/stm32/boards/Passport/modules/ur/crc32.py b/ports/stm32/boards/Passport/modules/ur2/crc32.py similarity index 88% rename from ports/stm32/boards/Passport/modules/ur/crc32.py rename to ports/stm32/boards/Passport/modules/ur2/crc32.py index 9c16084..93ea28b 100644 --- a/ports/stm32/boards/Passport/modules/ur/crc32.py +++ b/ports/stm32/boards/Passport/modules/ur2/crc32.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: BSD-2-Clause-Patent # # crc32.py diff --git a/ports/stm32/boards/Passport/modules/ur/fountain_decoder.py b/ports/stm32/boards/Passport/modules/ur2/fountain_decoder.py similarity index 82% rename from ports/stm32/boards/Passport/modules/ur/fountain_decoder.py rename to ports/stm32/boards/Passport/modules/ur2/fountain_decoder.py index 08f75eb..e0e5333 100644 --- a/ports/stm32/boards/Passport/modules/ur/fountain_decoder.py +++ b/ports/stm32/boards/Passport/modules/ur2/fountain_decoder.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: BSD-2-Clause-Patent # # fountain_decoder.py @@ -6,7 +6,7 @@ from .fountain_utils import choose_fragments, contains, is_strict_subset, set_difference from .utils import join_lists, join_bytes, crc32_int, xor_with, take_first - +from utils import bytes_to_hex_str class InvalidPart(Exception): pass @@ -36,7 +36,7 @@ class FountainDecoder: return len(self.indexes) == 1 def index(self): - # TODO: Not efficient + # TODO: Find a more efficient way of doing this (measure performance overhead first) return list(self.indexes)[0] # FountainDecoder @@ -54,7 +54,9 @@ class FountainDecoder: self.queued_parts = [] def expected_part_count(self): - return len(self.expected_part_indexes) # TODO: Handle None? + if self.expected_part_indexes == None: + return 0 + return len(self.expected_part_indexes) def is_success(self): result = self.result @@ -88,6 +90,7 @@ class FountainDecoder: # Don't continue if this part doesn't validate if not self.validate_part(encoder_part): + print('INVALID PART!') return False # Add this part to the queue @@ -101,7 +104,7 @@ class FountainDecoder: # # Keep track of how many parts we've processed # # NOTE: We should only increment this if we haven't processed this part before - print('encoder_part.seq_num={} .seq_len={} received_part_indexes={}'.format(encoder_part.seq_num, encoder_part.seq_len, self.received_part_indexes)) + # print('encoder_part.seq_num={} .seq_len={} received_part_indexes={}'.format(encoder_part.seq_num, encoder_part.seq_len, self.received_part_indexes)) if encoder_part.seq_num - 1 not in self.received_part_indexes: self.processed_parts_count += 1 @@ -189,7 +192,8 @@ class FountainDecoder: # Verify the message checksum and note success or failure checksum = crc32_int(message) if(checksum == self.expected_checksum): - self.result = bytes(message) + result = bytes(message) + self.result = result else: self.result = InvalidChecksum() @@ -201,10 +205,11 @@ class FountainDecoder: # Don't process duplicate parts for r in self.mixed_parts.values(): if r == p.indexes: + # print('Already processed/duplicate?') return # Reduce this part by all the others - p2 = p # TODO: Does this need to make a copy of p? + p2 = p for r in self.simple_parts.values(): p2 = self.reduce_part_by_part(p2, r) @@ -264,24 +269,26 @@ class FountainDecoder: else: assert(False) - def print_part(self, p): - print('part indexes: {}'.format(self.indexes_to_string(p.indexes))) - - def print_part_end(self): - expected = self.expected_part_count() if self.expected_part_indexes != None else 'None' - percent = int(round(self.estimated_percent_complete() * 100)) - print("processed: {}, expected: {}, received: {}, percent: {}%".format( - self.processed_parts_count, expected, len(self.received_part_indexes), percent)) - - def print_state(self): - parts = self.expected_part_count() if self.expected_part_indexes != None else 'None' - received = self.indexes_to_string(self.received_part_indexes) - mixed = [] - for indexes, p in self.mixed_parts.items(): - mixed.append(self.indexes_to_string(indexes)) - - mixed_s = "[{}]".format(', '.join(mixed)) - queued = len(self.queued_parts) - res = self.result_description() - print('parts: {}, received: {}, mixed: {}, queued: {}, result: {}'.format( - parts, received, mixed_s, queued, res)) + # DEBUG CODE + # + # def print_part(self, p): + # print('part indexes: {}'.format(self.indexes_to_string(p.indexes))) + # + # def print_part_end(self): + # expected = self.expected_part_count() if self.expected_part_indexes != None else 'None' + # percent = int(round(self.estimated_percent_complete() * 100)) + # print("processed: {}, expected: {}, received: {}, percent: {}%".format( + # self.processed_parts_count, expected, len(self.received_part_indexes), percent)) + # + # def print_state(self): + # parts = self.expected_part_count() if self.expected_part_indexes != None else 'None' + # received = self.indexes_to_string(self.received_part_indexes) + # mixed = [] + # for indexes, p in self.mixed_parts.items(): + # mixed.append(self.indexes_to_string(indexes)) + # + # mixed_s = "[{}]".format(', '.join(mixed)) + # queued = len(self.queued_parts) + # res = self.result_description() + # print('parts: {}, received: {}, mixed: {}, queued: {}, result: {}'.format( + # parts, received, mixed_s, queued, res)) diff --git a/ports/stm32/boards/Passport/modules/ur/fountain_encoder.py b/ports/stm32/boards/Passport/modules/ur2/fountain_encoder.py similarity index 96% rename from ports/stm32/boards/Passport/modules/ur/fountain_encoder.py rename to ports/stm32/boards/Passport/modules/ur2/fountain_encoder.py index 04b7b39..33996e0 100644 --- a/ports/stm32/boards/Passport/modules/ur/fountain_encoder.py +++ b/ports/stm32/boards/Passport/modules/ur2/fountain_encoder.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: BSD-2-Clause-Patent # # fountain_encoder.py @@ -33,7 +33,7 @@ class Part: raise InvalidHeader() (seq_num, _) = decoder.decodeUnsigned() - if seq_num > MAX_UINT64: # TODO: Do something better with this check + if seq_num > MAX_UINT64: raise InvalidHeader() (seq_len, _) = decoder.decodeUnsigned() diff --git a/ports/stm32/boards/Passport/modules/ur/fountain_utils.py b/ports/stm32/boards/Passport/modules/ur2/fountain_utils.py similarity index 94% rename from ports/stm32/boards/Passport/modules/ur/fountain_utils.py rename to ports/stm32/boards/Passport/modules/ur2/fountain_utils.py index ba3957c..7cd1b0e 100644 --- a/ports/stm32/boards/Passport/modules/ur/fountain_utils.py +++ b/ports/stm32/boards/Passport/modules/ur2/fountain_utils.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: BSD-2-Clause-Patent # # fountain_utils.py diff --git a/ports/stm32/boards/Passport/modules/ur/random_sampler.py b/ports/stm32/boards/Passport/modules/ur2/random_sampler.py similarity index 94% rename from ports/stm32/boards/Passport/modules/ur/random_sampler.py rename to ports/stm32/boards/Passport/modules/ur2/random_sampler.py index 5e46807..a71cc11 100644 --- a/ports/stm32/boards/Passport/modules/ur/random_sampler.py +++ b/ports/stm32/boards/Passport/modules/ur2/random_sampler.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: BSD-2-Clause-Patent # # random_sampler.py diff --git a/ports/stm32/boards/Passport/modules/ur/ur.py b/ports/stm32/boards/Passport/modules/ur2/ur.py similarity index 83% rename from ports/stm32/boards/Passport/modules/ur/ur.py rename to ports/stm32/boards/Passport/modules/ur2/ur.py index 36b8b7a..5a91457 100644 --- a/ports/stm32/boards/Passport/modules/ur/ur.py +++ b/ports/stm32/boards/Passport/modules/ur2/ur.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: BSD-2-Clause-Patent # # ur.py diff --git a/ports/stm32/boards/Passport/modules/ur/ur_decoder.py b/ports/stm32/boards/Passport/modules/ur2/ur_decoder.py similarity index 93% rename from ports/stm32/boards/Passport/modules/ur/ur_decoder.py rename to ports/stm32/boards/Passport/modules/ur2/ur_decoder.py index f8cf91c..bcdabbe 100644 --- a/ports/stm32/boards/Passport/modules/ur/ur_decoder.py +++ b/ports/stm32/boards/Passport/modules/ur2/ur_decoder.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: BSD-2-Clause-Patent # # ur_decoder.py @@ -85,7 +85,7 @@ class URDecoder: raise InvalidSequenceComponent() seq_num = int(comps[0]) seq_len = int(comps[1]) - print('seq_num={} seq_len={}'.format(seq_num, seq_len)) + # print('seq_num={} seq_len={}'.format(seq_num, seq_len)) if seq_num < 1 or seq_len < 1: raise InvalidSequenceComponent() return (seq_num, seq_len) @@ -129,10 +129,12 @@ class URDecoder: cbor = Bytewords.decode(Bytewords_Style_minimal, fragment) part = FountainEncoderPart.from_cbor(cbor) if seq_num != part.seq_num or seq_len != part.seq_len: + print('seq num mismatch') return False # Process the part if not self.fountain_decoder.receive_part(part): + print('Error in foundation_decoder.receive_part(): part={}'.format(part)) return False if self.fountain_decoder.is_success(): @@ -142,6 +144,7 @@ class URDecoder: return True except Exception as err: + print('ur_decoder.receive_part() err={}'.format(err)) return False def expected_type(self): diff --git a/ports/stm32/boards/Passport/modules/ur/ur_encoder.py b/ports/stm32/boards/Passport/modules/ur2/ur_encoder.py similarity index 95% rename from ports/stm32/boards/Passport/modules/ur/ur_encoder.py rename to ports/stm32/boards/Passport/modules/ur2/ur_encoder.py index d426d59..2178c13 100644 --- a/ports/stm32/boards/Passport/modules/ur/ur_encoder.py +++ b/ports/stm32/boards/Passport/modules/ur2/ur_encoder.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: BSD-2-Clause-Patent # # ur_encoder.py diff --git a/ports/stm32/boards/Passport/modules/ur/utils.py b/ports/stm32/boards/Passport/modules/ur2/utils.py similarity index 94% rename from ports/stm32/boards/Passport/modules/ur/utils.py rename to ports/stm32/boards/Passport/modules/ur2/utils.py index 4e8b3bd..39a89ee 100644 --- a/ports/stm32/boards/Passport/modules/ur/utils.py +++ b/ports/stm32/boards/Passport/modules/ur2/utils.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: BSD-2-Clause-Patent # # utils.py diff --git a/ports/stm32/boards/Passport/modules/ur/xoshiro256.py b/ports/stm32/boards/Passport/modules/ur2/xoshiro256.py similarity index 91% rename from ports/stm32/boards/Passport/modules/ur/xoshiro256.py rename to ports/stm32/boards/Passport/modules/ur2/xoshiro256.py index 96acd80..98b3a67 100644 --- a/ports/stm32/boards/Passport/modules/ur/xoshiro256.py +++ b/ports/stm32/boards/Passport/modules/ur2/xoshiro256.py @@ -1,21 +1,14 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: BSD-2-Clause-Patent # # xoshiro256.py # import sys -try: - import uhashlib as hashlib -except: - try: - import hashlib - except: - sys.exit( - "ERROR: No hashlib or uhashlib implementation found (required for sha256)") - -from ur.utils import string_to_bytes, int_to_bytes -from ur.constants import MAX_UINT64 +from trezorcrypto import sha256 + +from ur2.utils import string_to_bytes, int_to_bytes +from ur2.constants import MAX_UINT64 # Original Info: # Written in 2018 by David Blackman and Sebastiano Vigna (vigna@acm.org) @@ -67,7 +60,7 @@ class Xoshiro256: self.s[i] = v def _hash_then_set_s(self, buf): - m = hashlib.sha256() + m = sha256() m.update(buf) digest = m.digest() self._set_s(digest) diff --git a/ports/stm32/boards/Passport/modules/utils.py b/ports/stm32/boards/Passport/modules/utils.py index 73320db..0126737 100644 --- a/ports/stm32/boards/Passport/modules/utils.py +++ b/ports/stm32/boards/Passport/modules/utils.py @@ -1,7 +1,7 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: GPL-3.0-or-later # -# SPDX-FileCopyrightText: 2018 Coinkite, Inc. +# SPDX-FileCopyrightText: 2018 Coinkite, Inc. # SPDX-License-Identifier: GPL-3.0-only # # (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard @@ -10,10 +10,13 @@ # utils.py # -import gc, sys, ustruct +import gc, sys, ustruct, trezorcrypto from ubinascii import unhexlify as a2b_hex from ubinascii import hexlify as b2a_hex from ubinascii import a2b_base64, b2a_base64 +import common + +B2A = lambda x: str(b2a_hex(x), 'ascii') class imported: # Context manager that temporarily imports @@ -71,22 +74,56 @@ def get_filesize(fn): import uos return uos.stat(fn)[6] +def is_dir(fn): + from stat import S_ISDIR + import uos + mode = uos.stat(fn)[0] + # print('is_dir() mode={}'.format(mode)) + return S_ISDIR(mode) + class HexWriter: # Emulate a file/stream but convert binary to hex as they write def __init__(self, fd): self.fd = fd + self.pos = 0 + self.checksum = trezorcrypto.sha256() def __enter__(self): self.fd.__enter__() return self def __exit__(self, *a, **k): + self.fd.seek(0, 3) # go to end self.fd.write(b'\r\n') return self.fd.__exit__(*a, **k) + def tell(self): + return self.pos + def write(self, b): + self.checksum.update(b) + self.pos += len(b) + self.fd.write(b2a_hex(b)) + def seek(self, offset, whence=0): + assert whence == 0 # limited support + self.pos = offset + self.fd.seek((2*offset), 0) + + def read(self, ll): + b = self.fd.read(ll*2) + if not b: + return b + assert len(b)%2 == 0 + self.pos += len(b)//2 + return a2b_hex(b) + + def readinto(self, buf): + b = self.read(len(buf)) + buf[0:len(b)] = b + return len(b) + class Base64Writer: # Emulate a file/stream but convert binary to Base64 as they write def __init__(self, fd): @@ -138,7 +175,7 @@ def problem_file_line(exc): lines = tmp.getvalue().split('\n')[-3:] del tmp - # convert: + # convert: # File "main.py", line 63, in interact # into just: # main.py:63 @@ -199,11 +236,36 @@ def cleanup_deriv_path(bin_path, allow_star=False): try: ip = int(p, 10) except: - ip = -1 + ip = -1 assert 0 <= ip < 0x80000000 and p == str(ip), "bad component: "+p - + return 'm/' + '/'.join(parts) +def keypath_to_str(bin_path, prefix='m/', skip=1): + # take binary path, like from a PSBT and convert into text notation + rv = prefix + '/'.join(str(i & 0x7fffffff) + ("'" if i & 0x80000000 else "") + for i in bin_path[skip:]) + return 'm' if rv == 'm/' else rv + +def str_to_keypath(xfp, path): + # Take a numeric xfp, and string derivation, and make a list of numbers, + # like occurs in a PSBT. + # - no error checking here + + rv = [xfp] + for i in path.split('/'): + if i == 'm': continue + if not i: continue # trailing or duplicated slashes + + if i[-1] == "'": + here = int(i[:-1]) | 0x80000000 + else: + here = int(i) + + rv.append(here) + + return rv + def match_deriv_path(patterns, path): # check for exact string match, or wildcard match (star in last position) # - both args must be cleaned by cleanup_deriv_path() already @@ -258,87 +320,442 @@ class Base64Streamer(DecodeStreamer): def a2b(self, x): return a2b_base64(x) - -def check_firmware_hdr(hdr, binary_size=None, bad_magic_ok=False): - # Check basics of new firmware being loaded. Return text of error msg if any. - # - basic checks only: for confused customers, not attackers. - # - hdr must be a bytearray(FW_HEADER_SIZE+more) - - from sigheader import FW_HEADER_SIZE, FW_HEADER_MAGIC, FWH_PY_FORMAT - from sigheader import MK_1_OK, MK_2_OK, MK_3_OK - from ustruct import unpack_from - from version import hw_label - - try: - assert len(hdr) >= FW_HEADER_SIZE - - magic_value, timestamp, version_string, pk, fw_size, install_flags, hw_compat = \ - unpack_from(FWH_PY_FORMAT, hdr)[0:7] - - if bad_magic_ok and magic_value != FW_HEADER_MAGIC: - # it's just not a firmware file, and that's ok - return None - - assert magic_value == FW_HEADER_MAGIC, 'bad magic' - if binary_size is not None: - assert fw_size == binary_size, 'truncated' - - # TODO: maybe show the version string? Warn them that downgrade doesn't work? - - except Exception as exc: - return "That does not look like a firmware " \ - "file we would want to use: %s" % exc - - if hw_compat != 0: - # check this hardware is compatible - ok = False - if hw_label == 'mk1': - ok = (hw_compat & MK_1_OK) - elif hw_label == 'mk2': - ok = (hw_compat & MK_2_OK) - elif hw_label == 'mk3': - ok = (hw_compat & MK_3_OK) - - if not ok: - return "New firmware doesn't support this version of Coldcard hardware (%s)."%hw_label - - return None - - -def clean_shutdown(style=0): - # wipe SPI flash and shutdown (wiping main memory) - import callgate - - try: - from common import sf - sf.wipe_most() - except: pass - - callgate.show_logout(style) - - class UXStateMachine: def __init__(self, initial_state, machine_name=None): - print('UXStateMachine init: initial_state={}'.format(initial_state)) + # print('UXStateMachine init: initial_state={}'.format(initial_state)) self.state = initial_state self.prev_states = [] - def goto(self, new_state): - print('Go from {} to {}'.format(self.state, new_state)) - self.prev_states.append(self.state) + def goto(self, new_state, save_curr=True): + # print('Go from {} to {}'.format(self.state, new_state)) + if save_curr: + self.prev_states.append(self.state) self.state = new_state # Transition back to previous state def goto_prev(self): - if len(self.prev_state) > 0: - prev_state = self.prev_state.pop() - if self.machine_name != None: - print('{}: Go from {} to PREVIOUS state {}'.format(self.machine_name, self.state, prev_state)) - else: - print('Go from {} to PREVIOUS state {}'.format(self.state, prev_state)) + # print('goto_prev: prev_states={}'.format(self.prev_states)) + if len(self.prev_states) > 0: + prev_state = self.prev_states.pop() + # print('Go BACK from {} to {}'.format(self.state, prev_state)) + # if self.machine_name != None: + # print('{}: Go from {} to PREVIOUS state {}'.format(self.machine_name, self.state, prev_state)) + # else: + # print('Go from {} to PREVIOUS state {}'.format(self.state, prev_state)) self.state = prev_state + return True + else: + return False async def show(self): pass +def get_month_str(month): + if month == 1: + return "January" + elif month == 2: + return "February" + elif month == 3: + return "March" + elif month == 4: + return "April" + elif month == 5: + return "May" + elif month == 6: + return "June" + elif month == 7: + return "July" + elif month == 8: + return "August" + elif month == 9: + return "September" + elif month == 10: + return "October" + elif month == 11: + return "November" + elif month == 12: + return "December" + +def randint(a, b): + import struct + from common import noise + from noise_source import NoiseSource + + buf = bytearray(4) + noise.random_bytes(buf, NoiseSource.MCU) + num = struct.unpack_from(">I", buf)[0] + + result = a + (num % (b-a+1)) + return result + +def bytes_to_hex_str(s): + return str(b2a_hex(s), 'ascii') + +# Pass a string pattern like 'foo-{}.txt' and the {} will be replaced by a random 4 bytes hex number +def random_filename(card, pattern): + from noise_source import NoiseSource + buf = bytearray(4) + common.noise.random_bytes(buf, NoiseSource.MCU) + fn = pattern.format(b2a_hex(buf).decode('utf-8')) + return '{}/{}'.format(card.get_sd_root(), fn) + +def to_json(o): + import ujson + s = ujson.dumps(o) + parts = s.split(', ') + lines = ',\n'.join(parts) + return lines + +def to_str(o): + s = '{}'.format(o) + parts = s.split(', ') + lines = ',\n'.join(parts) + return lines + +def random_hex(num_chars): + import random + + rand = bytearray((num_chars + 1)//2) + for i in range(len(rand)): + rand[i] = random.randint(0, 255) + s = b2a_hex(rand).decode('utf-8').upper() + return s[:num_chars] + +def truncate_string_to_width(name, font, max_pixel_width): + from common import dis + if max_pixel_width <= 0: + # print('WARNING: Invalid max_pixel_width passed to truncate_string_to_width(). Must be > 0.') + return name + + while True: + actual_width = dis.width(name, font) + if actual_width < max_pixel_width: + return name + name = name[0:-1] + +# The multisig import code is implemented as a menu, and we are coming from a state machine. +# We want to be able to show the topmost menu that was pushed onto the stack here and wait for it to exit. +# This is a hack. Side effect is that the top menu shows briefly after menu exits. +async def show_top_menu(): + from ux import the_ux + c = the_ux.top_of_stack() + await c.interact() + +# TODO: For now this just checks the front bytes, but it could ensure the whole thing is valid +def is_valid_address(address): + return (len(address) > 3) and (address[0] == '1' or address[0] == '3' or (address[0] == 'b' and address[1] == 'c' and address[2] == '1')) + + +# Return array of bytewords where each byte in buf maps to a word +# There are 256 bytewords, so this maps perfectly. +def get_bytewords_for_buf(buf): + from ur2.bytewords import get_word + words = [] + for b in buf: + words.append(get_word(b)) + + return words + +# We need an async way for the chooser menu to be shown. This does a local call to interact(), which gives +# us exactly that. Once the chooser completes, the menu stack returns to the way it was. +async def run_chooser(chooser, title, show_checks=True): + from ux import the_ux + from menu import start_chooser + start_chooser(chooser, title=title, show_checks=show_checks) + c = the_ux.top_of_stack() + await c.interact() + +# Return the elements of a list in a random order in a new list +def shuffle(list): + import random + new_list = [] + list_len = len(list) + while list_len > 0: + i = random.randint(0, list_len-1) + element = list.pop(i) + new_list.append(element) + list_len = len(list) + + return new_list + +def ensure_folder_exists(path): + import uos + try: + # print('Creating folder: {}'.format(path)) + uos.mkdir(path) + except Exception as e: + # print('Folder already exists: {}'.format(e)) + return + +def file_exists(path): + try: + with open(fname, 'wb') as fd: + return True + except: + return False + +# Derive addresses from the specified path until we find the address or have tried max_to_check addresses +# If single sig, we need `path`. +# If multisig, we need `ms_wallet`, but not `path` +def find_address(path, start_address_idx, address, addr_type, ms_wallet, max_to_check=100, reverse=False): + import stash + + with stash.SensitiveValues() as sv: + if ms_wallet: + # NOTE: Can't easily reverse order here, so this is slightly less efficient + for (curr_idx, paths, curr_address, script) in ms_wallet.yield_addresses(start_address_idx, max_to_check): + # print('curr_idx={}: paths={} curr_address = {}'.format(curr_idx, paths, curr_address)) + + if curr_address == address: + return (curr_idx, paths) # NOTE: Paths are the full paths of the addresses of each signer + + else: + r = range(start_address_idx, start_address_idx + max_to_check) + if reverse: + r = reversed(r) + + for curr_idx in r: + addr_path = '{}/0/{}'.format(path, curr_idx) # Zero for non-change address + # print('addr_path={}'.format(addr_path)) + node = sv.derive_path(addr_path) + # print('node={}'.format(node)) + curr_address = sv.chain.address(node, addr_type) + # print('curr_idx={}: path={} addr_type={} curr_address = {}'.format(curr_idx, addr_path, addr_type, curr_address)) + if curr_address == address: + return (curr_idx, addr_path) + return (-1, None) + +def get_accounts(): + from common import settings + from constants import DEFAULT_ACCOUNT_ENTRY + accounts = settings.get('accounts', [DEFAULT_ACCOUNT_ENTRY]) + accounts.sort(key=lambda a: a.get('acct_num', 0)) + return accounts + +# Only call when there is an active account +def set_next_addr(new_addr): + if not common.active_account: + return + + common.active_account.next_addr = new_addr + + accounts = get_accounts() + for account in accounts: + if account('id') == common.active_account.id: + account['next_addr'] = new_addr + common.settings.set('accounts', accounts) + common.settings.save() + break + +# Only call when there is an active account +def account_exists(name): + accounts = get_accounts() + for account in accounts: + if account.get('name') == name: + return True + + return False + + +def get_next_addr(acct_num, addr_type): + from common import settings + next_addrs = settings.get('next_addrs', {}) + key = '{}/{}'.format(acct_num, addr_type) + return next_addrs.get(key, 0) + +# Save the next address to use for the specific account and address type +def save_next_addr(acct_num, addr_type, addr_idx): + from common import settings + next_addrs = settings.get('next_addrs', {}) + key = '{}/{}'.format(acct_num, addr_type) + next_addrs[key] = addr_idx + settings.set('next_addrs', next_addrs) + +def get_prev_address_range(range, max_size): + low, high = range + size = min(max_size, low) + return ((low - size, low), size) + +def get_next_address_range(range, max_size): + low, high = range + return ((high, high + max_size), max_size) + +async def scan_for_address(acct_num, address, addr_type, deriv_path, ms_wallet): + from common import system + from ux import ux_show_story + + # print('Address to verify = {}'.format(address)) + + # print('ms_wallet={}'.format(to_str(ms_wallet))) + + # We always check this many addresses, but we split them 50/50 until we reach 0 on the low end, + # then we use the rest for the high end. + NUM_TO_CHECK = 100 + + # Setup the initial ranges + a = get_next_addr(acct_num, addr_type) + + low_range, low_size = get_prev_address_range((a, a), NUM_TO_CHECK // 2) + high_range, high_size = get_next_address_range((a, a), NUM_TO_CHECK - low_size) + + while True: + # See if the address is valid + system.show_busy_bar() + + addr_idx = -1 + + # Check downwards + if low_size > 0: + # print('Check low range') + (addr_idx, path_info) = find_address( + deriv_path, + low_range[0], + address, + addr_type, + ms_wallet, + max_to_check=low_size, + reverse=True) + + if addr_idx < 0: + # Check upwards + # print('Check high range') + (addr_idx, path_info) = find_address( + deriv_path, + high_range[0], + address, + addr_type, + ms_wallet, + max_to_check=high_size) + + system.hide_busy_bar() + + # Was the address found? + if addr_idx >= 0: + return addr_idx + else: + # Address was not found in that batch of 100, so offer to keep searching + result = await ux_show_story('''The scanned address was not yet found. + +Do you want to continue searching addresses?''', title='Not Found', left_btn='BACK', right_btn='CONTINUE', + center=True, center_vertically=True) + if result == 'x': + return -1 + + # Try next batch of addresses + low_range, low_size = get_prev_address_range(low_range, NUM_TO_CHECK // 2) + high_range, high_size = get_next_address_range(high_range, NUM_TO_CHECK - low_size) + +def is_new_wallet_in_progress(): + from common import settings + ap = settings.get('wallet_prog', None) + return ap != None + +async def do_rename_account(acct_num, new_name): + from common import settings + from export import auto_backup + from constants import DEFAULT_ACCOUNT_ENTRY + + accounts = get_accounts() + for account in accounts: + if account.get('acct_num') == acct_num: + account['name'] = new_name + break + + settings.set('accounts', accounts) + await settings.save() + await auto_backup() + +async def do_delete_account(acct_num): + from common import settings + from export import auto_backup + + accounts = get_accounts() + accounts = list(filter(lambda acct: acct.get('acct_num') != acct_num, accounts)) + settings.set('accounts', accounts) + await settings.save() + await auto_backup() + +async def save_new_account(name, acct_num): + from common import settings + from export import offer_backup + from constants import DEFAULT_ACCOUNT_ENTRY + + accounts = get_accounts() + accounts.append({'name': name, 'acct_num': acct_num}) + settings.set('accounts', accounts) + await settings.save() + await offer_backup() + +def make_account_name_num(name, num): + return '{} (#{})'.format(name, num) + + +# Save the QR code image in PPM (Portable Pixel Map) -- a very simple format that doesn't need a big library to be included. +def save_qr_code_image(qr_buf): + from files import CardSlot + from utils import random_hex + from constants import CAMERA_WIDTH, CAMERA_HEIGHT + + common.system.turbo(True) + + try: + with CardSlot() as card: + # Need to use get_sd_root() here to prefix the /sd/ or we get EPERM errors + fname = '{}/qr-{}.ppm'.format(card.get_sd_root(), random_hex(4)) + # print('Saving QR code image to: {}'.format(fname)) + + # PPM file format + # http://paulbourke.net/dataformats/ppm/ + with open(fname, 'wb') as fd: + hdr = '''P6 +# Created by Passport +{} {} +255\n'''.format(CAMERA_WIDTH, CAMERA_HEIGHT) + + # Write the header + fd.write(bytes(hdr, 'utf-8')) + + line = bytearray(CAMERA_WIDTH) # One byte per pixel + pixel = bytearray(3) + + # Write the pixels + for y in range(CAMERA_HEIGHT): + # print('QR Line {}'.format(y)) + for x in range(CAMERA_WIDTH): + g = qr_buf[y*CAMERA_WIDTH + x] + pixel[0] = g + pixel[1] = g + pixel[2] = g + fd.write(pixel) + + except Exception as e: + print('EXCEPTION: {}'.format(e)) + # This method is not async, so no error or warning if you don't have an SD card inserted + + # print('QR Image saved.') + common.system.turbo(False) + +alphanumeric_chars = { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', + 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + ' ', '$', '%', '*', '+', '-', '.', '/', ':' +} + +def is_char_alphanumeric(ch): + # print('Lookup ch={}'.format(ch)) + return ch in alphanumeric_chars + +# Alphanumeric QR codes contain only the following characters: +# +# 0–9, A–Z (upper-case only), space, $, %, *, +, -, ., /, : +def is_alphanumeric_qr(buf): + for ch in buf: + is_alpha = is_char_alphanumeric(chr(ch)) + # print('is_alpha "{}" == {}'.format(ch, is_alpha)) + if not is_alpha: + return False + + return True + # EOF diff --git a/ports/stm32/boards/Passport/modules/ux.py b/ports/stm32/boards/Passport/modules/ux.py index bb0d6f7..ad9769c 100644 --- a/ports/stm32/boards/Passport/modules/ux.py +++ b/ports/stm32/boards/Passport/modules/ux.py @@ -1,7 +1,7 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: GPL-3.0-or-later # -# SPDX-FileCopyrightText: 2018 Coinkite, Inc. +# SPDX-FileCopyrightText: 2018 Coinkite, Inc. # SPDX-License-Identifier: GPL-3.0-only # # (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard @@ -11,20 +11,18 @@ # # NOTE: do not import from main at top. -from ur.ur_encoder import UREncoder -from ur.cbor_lite import CBOREncoder -from ur.ur import UR -from ur1.encode_ur import encode_ur import gc import utime -from display import Display, FontSmall +from display import Display, FontSmall, FontTiny from uasyncio import sleep_ms from uasyncio.queues import QueueEmpty -# from bip39_utils import get_words_matching_prefix +from common import system, dis +from data_codecs.qr_type import QRType +from data_codecs.qr_factory import get_qr_decoder_for_data, make_qr_encoder +from utils import is_alphanumeric_qr -DEFAULT_IDLE_TIMEOUT = const(5*60) # (seconds) 4 hours -LEFT_MARGIN = 6 +LEFT_MARGIN = 8 RIGHT_MARGIN = 6 TOP_MARGIN = 12 VERT_SPACING = 10 @@ -35,9 +33,6 @@ TEXTBOX_MARGIN = 6 # menu (or whatever) to show something new. The # stack has already been updated, but the old # top-of-stack code was waiting for a key event. -# - - class AbortInteraction(Exception): pass @@ -52,6 +47,10 @@ class UserInteraction: def top_of_stack(self): return self.stack[-1] if self.stack else None + def reset_to_root(self): + root = self.stack[0] + self.reset(root) + def reset(self, new_ux): self.stack.clear() gc.collect() @@ -90,15 +89,15 @@ def time_now_ms(): import utime return utime.ticks_ms() - -# TODO: Move this to it's own file class KeyInputHandler: - def __init__(self, down="", up="", long="", repeat=None, long_duration=2000): + def __init__(self, down="", up="", long="", repeat_delay=None, repeat_speed=None, long_duration=2000): self.time_pressed = {} self.down = down self.up = up self.long = long - self.repeat = repeat + self.repeat_delay = repeat_delay # How long until repeat mode starts + self.repeat_speed = repeat_speed # How many ms between each repeat + self.repeat_active = False self.long_duration = long_duration self.kcode_state = 0 self.kcode_last_time_pressed = 0 @@ -113,7 +112,7 @@ class KeyInputHandler: return pressed def __update_kcode_state(self, expected_keys, actual_key): - # print('kcode: state={} expected={} actual={}'.format(self.kcode_state, expected_key, actual_key)) + # print('kcode: state={} expected={} actual={}'.format(self.kcode_state, expected_key, actual_key)) if actual_key in expected_keys: self.kcode_state += 1 self.kcode_last_time_pressed = time_now_ms() @@ -165,6 +164,10 @@ class KeyInputHandler: def is_pressed(self, key): return key in self.time_pressed + def clear(self): + from common import keypad + keypad.clear_keys() + # New input function to be used in place of PressRelease and ux_press_release, ux_all_up and ux_poll_once. async def get_event(self): from common import keypad @@ -176,21 +179,27 @@ class KeyInputHandler: # See if we have a character in the queue and if so process it # Poll for an event key, is_down = keypad.get_event() - + # if key != None: # print('key={} is_down={}'.format(key, is_down)) - + if key == None: # There was nothing in the queue, so handle the time-dependent events now = time_now_ms() for k in self.time_pressed: # print('k={} self.down={} self.repeat={} self.time_pressed={}'.format(k, self.down, self.repeat, self.time_pressed)) # Handle repeats - if self.repeat != None and k in self.down: + if self.repeat_delay != None and k in self.down: elapsed = now - self.time_pressed[k] - if elapsed >= self.repeat: - self.time_pressed[k] = now - return (k, 'repeat') + if self.repeat_active == False: + if elapsed >= self.repeat_delay: + self.repeat_active = True + self.time_pressed[k] = now + return (k, 'repeat') + else: + if elapsed >= self.repeat_speed: + self.time_pressed[k] = now + return (k, 'repeat') # Handle long press expiration if k in self.long: @@ -222,6 +231,7 @@ class KeyInputHandler: else: # up # Removing this will cancel long presses of the key as well if key in self.time_pressed: + self.repeat_active = False del self.time_pressed[key] # Check to see if we are interested in this key event @@ -271,19 +281,25 @@ key_to_char_map_numbers = { # Takes KeyInputHandler events as state change events, along with elapsed time. IDLE_KEY_TIMEOUT = 500 -PLACEHOLDER_CHAR = '^' -# TODO: Move this to its own file class TextInputHandler: - def __init__(self, text=""): + def __init__(self, text="", num_only=False, max_length=None): self.text = [ch for ch in text] - self.cursor_pos = 0 + self.cursor_pos = len(self.text) # Put cursor at the end if there is any initial text self.last_key_down_time = 0 self.last_key = None self.next_map_index = 0 - self.curr_key_map = key_to_char_map_lower + self.curr_key_map = key_to_char_map_numbers if num_only else key_to_char_map_lower + self.num_only = num_only + self.max_length = max_length + if self.max_length: + # Make sure user-passed value doesn't exceed the max, if given + self.text = self.text[0:self.max_length] def _next_key_map(self): + if self.num_only: + return + if self.curr_key_map == key_to_char_map_lower: self.curr_key_map = key_to_char_map_upper elif self.curr_key_map == key_to_char_map_upper: @@ -303,7 +319,11 @@ class TextInputHandler: now = time_now_ms() key, event_type = event if event_type == 'down': - print("key={}".format(key)) + # Outer code handles these + if key in 'xy': + return + + # print("key={}".format(key)) if key in '*#rl': if key == '#': self._next_key_map() @@ -327,25 +347,31 @@ class TextInputHandler: # Check for symbols pop-up if key == '1' and self.curr_key_map != key_to_char_map_numbers: - # Show the symbols pop-up, otherwise fall through and handle '1' as a normal key press - symbol = await ux_show_symbols_popup('!') - if symbol == None: - return - - # Insert the symbol - self.text.insert(self.cursor_pos, symbol) - self.cursor_pos += 1 + if self.max_length and len(self.text) >= self.max_length: + pass + else: + # Show the symbols pop-up, otherwise fall through and handle '1' as a normal key press + symbol = await ux_show_symbols_popup('!') + if symbol == None: + return + + # Insert the symbol + self.text.insert(self.cursor_pos, symbol) + self.cursor_pos += 1 return if self.last_key == None: - print("first press of {}".format(key)) - # A new keypress, so insert the first character mapped to this key - self.text.insert( - self.cursor_pos, self.curr_key_map[key][self.next_map_index]) - self.cursor_pos += 1 - if len(self.curr_key_map[key]) == 1: - # Just immediate commit this key since there are no other possible choices to wait for - return + # print("first press of {}".format(key)) + # A new keypress, so insert the first character mapped to this key, unless max reached + if self.max_length and len(self.text) >= self.max_length: + pass + else: + self.text.insert( + self.cursor_pos, self.curr_key_map[key][self.next_map_index]) + self.cursor_pos += 1 + if len(self.curr_key_map[key]) == 1: + # Just immediate commit this key since there are no other possible choices to wait for + return elif self.last_key == key: # User is pressing the same key within the idle timeout, so cycle to the next key @@ -357,14 +383,17 @@ class TextInputHandler: 1] = self.curr_key_map[key][self.next_map_index] else: - # User pressed a different key, but before the idle timeout, so we finalize the last - # character and start the next character as tentative. - self.cursor_pos += 1 # Finalize the last character - self.next_map_index = 0 # Reset the map index + if self.max_length and len(self.text) >= self.max_length: + pass + else: + # User pressed a different key, but before the idle timeout, so we finalize the last + # character and start the next character as tentative. + self.cursor_pos += 1 # Finalize the last character + self.next_map_index = 0 # Reset the map index - # Append the new key - self.text.insert( - self.cursor_pos, self.curr_key_map[key][self.next_map_index]) + # Append the new key + self.text.insert( + self.cursor_pos, self.curr_key_map[key][self.next_map_index]) # Insert or overwrite the character # print('cursor_pos={} next_map_index={} key={} text={}'.format( @@ -380,7 +409,7 @@ class TextInputHandler: now = time_now_ms() if self.last_key_down_time != 0 and now - self.last_key_down_time >= IDLE_KEY_TIMEOUT: - print("timeout!") + # print("timeout!") # Reset for next key self.last_key_down_time = 0 self.last_key = None @@ -391,28 +420,32 @@ class TextInputHandler: return False def get_text(self): - # TODO: Remove PLACEHOLDER_CHAR from end, if present return "".join(self.text) + def get_num(self): + return int("".join(self.text)) -async def ux_enter_text(title="Enter Text", label="Text"): + +async def ux_enter_text(title="Enter Text", label="Text", initial_text='', left_btn='BACK', right_btn='CONTINUE', + num_only=False, max_length=None): from common import dis from display import FontSmall font = FontSmall - input = KeyInputHandler(down='1234567890*#rl', up='xy') - text_handler = TextInputHandler() + input = KeyInputHandler(down='1234567890*#rlxy', up='xy') + text_handler = TextInputHandler(text=initial_text, num_only=num_only, max_length=max_length) while 1: # redraw + system.turbo(True) dis.clear() dis.draw_header(title, left_text=text_handler.get_mode_description()) # Draw the title y = Display.HEADER_HEIGHT + TEXTBOX_MARGIN - dis.text(LEFT_MARGIN, y, label) + dis.text(None, y+2, label) # Draw a bounding box around the text area y += font.leading + TEXTBOX_MARGIN @@ -422,12 +455,13 @@ async def ux_enter_text(title="Enter Text", label="Text"): # Draw the text and any other stuff y += 4 dis.text_input(None, y, text_handler.get_text(), - cursor_pos=text_handler.cursor_pos, font=font, max_chars_per_line=12) + cursor_pos=text_handler.cursor_pos, font=font, max_chars_per_line=14) - dis.draw_footer('BACK', 'CONTINUE', input.is_pressed( + dis.draw_footer(left_btn, right_btn, input.is_pressed( 'x'), input.is_pressed('y')) dis.show() + system.turbo(False) # Wait for key inputs event = None @@ -452,7 +486,7 @@ async def ux_enter_text(title="Enter Text", label="Text"): if key == 'x': return None if key == 'y': - return text_handler.get_text() + return text_handler.get_num() if num_only else text_handler.get_text() symbol_rows = [ @@ -466,7 +500,7 @@ symbol_rows = [ async def ux_show_symbols_popup(title="Enter Passphrase"): from common import dis from display import FontSmall - print('ux_show_symbols_popup()') + # print('ux_show_symbols_popup()') font = FontSmall input = KeyInputHandler(down='rlduxy', up='xy') @@ -484,6 +518,8 @@ async def ux_show_symbols_popup(title="Enter Passphrase"): height = num_rows * font.leading + (2 * v_margin) while 1: + system.turbo(True) + # redraw x = Display.WIDTH // 2 - width // 2 y = Display.HEIGHT - Display.FOOTER_HEIGHT - height - 14 @@ -510,6 +546,8 @@ async def ux_show_symbols_popup(title="Enter Passphrase"): dis.show() + system.turbo(False) + # Wait for key inputs event = None while True: @@ -544,6 +582,8 @@ async def ux_show_symbols_popup(title="Enter Passphrase"): return symbol_rows[cursor_row][cursor_col] +# NOTE: This function factors in the scrollbar width even if the scrollbar might not end up being shown, +# because the line breaking code uses it BEFORE knowing if scrolling is required. def chars_per_line(font): return (Display.WIDTH - LEFT_MARGIN - Display.SCROLLBAR_WIDTH) // font.advance @@ -584,19 +624,22 @@ def word_wrap(ln, font): yield line -async def ux_show_story(msg, title='Passport', sensitive=False, font=FontSmall, left_btn='BACK', right_btn='CONTINUE', scroll_label=None, left_btn_enabled=True, right_btn_enabled=True, center_vertically=False, center=False): - # show a big long string, and wait for XY to continue - # - returns character used to get out (X or Y) - # - accepts a stream or string - from common import dis +async def ux_show_story(msg, title='Passport', sensitive=False, font=FontSmall, escape='', left_btn='BACK', + right_btn='CONTINUE', scroll_label=None, left_btn_enabled=True, right_btn_enabled=True, + center_vertically=False, center=False, overlay=None, clear_keys=False): + from common import dis, keypad + + system.turbo(True) + + # Clear the keys before starting + if clear_keys: + keypad.clear_keys() ch_per_line = chars_per_line(font) lines = [] - # if title: - # # kinda weak rendering but it works. - # lines.append('\x01' + title) + # First case is used with StringIO objects if hasattr(msg, 'readline'): msg.seek(0) for ln in msg: @@ -630,8 +673,12 @@ async def ux_show_story(msg, title='Passport', sensitive=False, font=FontSmall, Display.FOOTER_HEIGHT) // font.leading max_visible_lines = (Display.HEIGHT - Display.HEADER_HEIGHT - Display.FOOTER_HEIGHT) // font.leading - input = KeyInputHandler(down='rldu0xy', up='xy', repeat=250) + input = KeyInputHandler(down='rldu0xy', up='xy' + escape, repeat_delay=250, repeat_speed=10) + + system.turbo(False) + allow_right_btn_action = True + turbo = None # We rely on this being 3 states: None, False, True while 1: # redraw dis.clear() @@ -647,14 +694,19 @@ async def ux_show_story(msg, title='Passport', sensitive=False, font=FontSmall, text_height = len(lines) * font.leading - font.descent y += avail_height // 2 - text_height // 2 + + content_to_height_ratio = H / len(lines) + show_scrollbar= True if content_to_height_ratio < 1 else False + last_to_show = min(top+H+1, len(lines)) for ln in lines[top:last_to_show]: x = LEFT_MARGIN if not center else None - dis.text(x, y, ln, font=font) + dis.text(x, y, ln, font=font, scrollbar_visible=show_scrollbar) y += font.leading - dis.scrollbar(top / len(lines), H / len(lines)) + if show_scrollbar: + dis.scrollbar(top / len(lines), content_to_height_ratio) # Show the scroll_label if given and if we have not reached the bottom yet scroll_enable_right_btn = True @@ -667,60 +719,102 @@ async def ux_show_story(msg, title='Passport', sensitive=False, font=FontSmall, dis.draw_footer(left_btn, right_btn_label, input.is_pressed('x'), input.is_pressed('y')) + # Draw overlay image, if any + if overlay: + (x, y, image) = overlay + dis.icon(x, y, image) + dis.show() + # We only want to turn it off once rather than whenever it's False, so we + # set to None to avoid turning turbo off again. + if turbo == False: + system.turbo(False) + turbo = None + # Wait for key inputs event = None while True: - event = await input.get_event() - - if event != None: - break + while True: + event = await input.get_event() - key, event_type = event - # print('key={} event_type={}'.format(key, event_type)) + if event != None: + break - if event_type == 'down' or event_type == 'repeat': - if key == 'u': - top = max(0, top-1) - elif key == 'd': - if len(lines) > H: - top = min(len(lines) - H, top+1) + key, event_type = event + # print('key={} event_type={}'.format(key, event_type)) - if event_type == 'down': - if key == '0': - top = 0 + if event_type == 'down' or event_type == 'repeat': + system.turbo(True) + turbo = True - if event_type == 'up': - # No left_btn means don't exit on the 'x' key - if left_btn_enabled and (key == 'x'): - return key - - if key == 'y': - if scroll_enable_right_btn: - if right_btn_enabled: - return key - else: + if key == 'u': + top = max(0, top-1) + break + elif key == 'd': if len(lines) > H: top = min(len(lines) - H, top+1) + break + elif key == 'y': + if event_type == 'repeat': + allow_right_btn_action = False + elif event_type == 'down': + allow_right_btn_action = True + + if not scroll_enable_right_btn: + if len(lines) > H: + top = min(len(lines) - H, top+1) + else: + continue + break + elif key in 'xy': + # allow buttons to redraw for pressed state + break + else: + continue + if event_type == 'down': + if key == '0': + top = 0 + break + else: + continue + if event_type == 'up': + turbo = False # We set to False here, but actually turn off after rendering + if key in escape: + return key -async def ux_confirm(msg, negative_btn='NO', positive_btn='YES', center=True, center_vertically=True): - resp = await ux_show_story(msg, center=center, center_vertically=center_vertically, left_btn=negative_btn, right_btn=positive_btn) - return resp == 'y' - + # No left_btn means don't exit on the 'x' key + if left_btn_enabled and (key == 'x'): + return key -async def ux_dramatic_pause(msg, seconds): - from common import dis + if key == 'y': + if scroll_enable_right_btn: + if right_btn_enabled and allow_right_btn_action: + return key + break + else: + if len(lines) > H: + top = min(len(lines) - H, top+1) + break + else: + continue + +async def ux_confirm(msg, title='Passport', negative_btn='NO', positive_btn='YES', center=True, center_vertically=True, scroll_label=None): + resp = await ux_show_story(msg, title=title, center=center, center_vertically=center_vertically, left_btn=negative_btn, right_btn=positive_btn, scroll_label=scroll_label) + return resp == 'y' - # show a full-screen msg, with a dramatic pause + progress bar - n = seconds * 8 - dis.fullscreen(msg) - for i in range(n): - dis.progress_bar_show(i/n) - await sleep_ms(125) +# async def ux_dramatic_pause(msg, seconds): +# from common import dis, system +# +# # show a full-screen msg, with a dramatic pause + progress bar +# n = seconds * 8 +# dis.fullscreen(msg) +# for i in range(n): +# system.progress_bar((i*100)//n) +# await sleep_ms(125) def blocking_sleep(ms): start = utime.ticks_ms() @@ -782,6 +876,8 @@ def ux_show_fatal(msg): filename = 'error.log' wrote_to_sd, lines[0] = save_error_log(msg, filename) while (1): + system.turbo(True) + # Draw dis.clear() dis.draw_header('Error') @@ -793,6 +889,7 @@ def ux_show_fatal(msg): y += font.leading dis.show() + system.turbo(False) blocking_sleep(delay) @@ -823,9 +920,6 @@ def show_fatal_error(msg): # Remove lines we don't want to shorten lines = all_lines[1:-2] - # Shorten file path to only file name - # TODO: FIX THIS: lines = [line[line.index('passport-mp')+19:] for line in lines] - # Insert lines we want to add for readability or keep from the original msg lines.insert(0, "") lines.insert(1, "") @@ -836,7 +930,7 @@ def show_fatal_error(msg): def restore_menu(): - # redraw screen contents after distrupting it w/ non-ux things (usb upload) + # redraw screen contents after disrupting it w/ non-ux things (usb upload) m = the_ux.top_of_stack() if hasattr(m, 'update_contents'): @@ -847,172 +941,177 @@ def restore_menu(): def abort_and_goto(m): - # TODO: Clear out keypad buffer + import common + common.keypad.clear_keys() the_ux.reset(m) def abort_and_push(m): - # TODO: Clear out keypad buffer + common.keypad.clear_keys() the_ux.push(m) -async def show_qr_codes(addrs, is_alnum, start_n): - o = QRDisplay(addrs, is_alnum, start_n, sidebar=None) - await o.interact_bare() - - -class QRDisplay(UserInteraction): - # Show a QR code for (typically) a list of addresses. Can only work on Mk3 - - def __init__(self, addrs, is_alnum, start_n=0, sidebar=None): - self.is_alnum = is_alnum - self.idx = 0 # start with first address - self.invert = False # looks better, but neither mode is ideal - self.addrs = addrs - self.sidebar = sidebar - self.start_n = start_n - self.qr_data = None - self.left_down = False - self.right_down = False - self.input = KeyInputHandler(down='xyudlr', up='xy') - - def render_qr(self, msg): - # Version 2 would be nice, but can't hold what we need, even at min error correction, - # so we are forced into version 3 = 29x29 pixels - # - see - # - to display 29x29 pixels, we have to double them up: 58x58 - # - not really providing enough space around it - # - inverted QR (black/white swap) still readable by scanners, altho wrong - - from utils import imported - - with imported('uQR') as uqr: - if self.is_alnum: - # targeting 'alpha numeric' mode, typical len is 42 - ec = uqr.ERROR_CORRECT_Q - assert len(msg) <= 47 - else: - # has to be 'binary' mode, altho shorter msg, typical 34-36 - ec = uqr.ERROR_CORRECT_M - assert len(msg) <= 42 - - q = uqr.QRCode(version=3, box_size=1, border=0, - mask_pattern=3, error_correction=ec) - if self.is_alnum: - here = uqr.QRData(msg.upper().encode('ascii'), - mode=uqr.MODE_ALPHA_NUM, check_data=False) - else: - here = uqr.QRData(msg.encode('ascii'), - mode=uqr.MODE_8BIT_BYTE, check_data=False) - q.add_data(here) - q.make(fit=False) - - self.qr_data = q.get_matrix() - - def redraw(self): - # Redraw screen. - from common import dis - from display import FontTiny - - font = FontTiny - inv = self.invert - - # what we are showing inside the QR - msg = self.addrs[self.idx] - - # make the QR, if needed. - if not self.qr_data: - # dis.busy_bar(True) - self.render_qr(msg) - - # Draw display - if inv: - dis.dis.fill_rect(0, 0, Display.WIDTH, - Display.HEIGHT - Display.FOOTER_HEIGHT + 1, 1) - else: - dis.clear() - - y = TOP_MARGIN - - # Draw the derivation path - if len(self.addrs) > 1: - path = "Path: {}".format(self.start_n + self.idx) - dis.text(None, y, path, font, invert=inv) - y += font.leading + VERT_SPACING - - w = 29 # because version=3 - module_size = 6 # Each "dot" in a QR code is called a module - pixel_width = w * module_size - frame_width = pixel_width + (module_size * 2) - - # QR code offsets - XO = (Display.WIDTH - pixel_width) // 2 - YO = y - dis.dis.fill_rect(XO - module_size, YO - - module_size, frame_width, frame_width, 0 if inv else 1) - - # Draw the actual QR code - data = self.qr_data - for qx in range(w): - for qy in range(w): - px = data[qx][qy] - X = (qx*module_size) + XO - Y = (qy*module_size) + YO - dis.dis.fill_rect(X, Y, module_size, - module_size, px if inv else (not px)) - - # Show the data encoded by the QR code - y += w*module_size + VERT_SPACING + 3 - - sidebar, ll = self.sidebar or (msg, 20) - for i in range(0, len(sidebar), ll): - dis.text(None, y, sidebar[i:i+ll], font, inv) +# async def show_qr_codes(addrs, is_alnum, start_n): +# o = QRDisplay(addrs, is_alnum, start_n) +# await o.interact_bare() - y += font.leading - - dis.draw_footer('BACK', 'INVERT', self.input.is_pressed( - 'x'), self.input.is_pressed('y')) - dis.show() - async def interact_bare(self): - - self.redraw() - while 1: - event = await self.input.get_event() - - if event != None: - key, event_type = event - if event_type == 'down': - if key == 'u' or key == 'l': - if self.idx > 0: - self.idx -= 1 - self.qr_data = None - elif key == 'd' or key == 'r': - if self.idx != len(self.addrs)-1: - self.idx += 1 - self.qr_data = None - else: - continue - elif event_type == 'up': - if key == 'x': - self.redraw() - break - elif key == 'y': - self.invert = not self.invert - else: - continue - - self.redraw() - - async def interact(self): - await self.interact_bare() - the_ux.pop() +# class QRDisplay(UserInteraction): +# # Show a QR code for (typically) a list of addresses. Can only work on Mk3 +# +# def __init__(self, addrs, is_alnum, start_n=0,path='', account=0, change=0): +# self.is_alnum = is_alnum +# self.idx = 0 # start with first address +# self.invert = False # looks better, but neither mode is ideal +# self.addrs = addrs +# self.start_n = start_n +# self.qr_data = None +# self.left_down = False +# self.right_down = False +# self.input = KeyInputHandler(down='xyudlr', up='xy') +# +# def render_qr(self, msg): +# # Version 2 would be nice, but can't hold what we need, even at min error correction, +# # so we are forced into version 3 = 29x29 pixels +# # - see +# # - to display 29x29 pixels, we have to double them up: 58x58 +# # - not really providing enough space around it +# # - inverted QR (black/white swap) still readable by scanners, altho wrong +# +# from utils import imported +# +# with imported('uQR') as uqr: +# if self.is_alnum: +# # targeting 'alpha numeric' mode, typical len is 42 +# ec = uqr.ERROR_CORRECT_Q +# assert len(msg) <= 47 +# else: +# # has to be 'binary' mode, altho shorter msg, typical 34-36 +# ec = uqr.ERROR_CORRECT_M +# assert len(msg) <= 42 +# +# q = uqr.QRCode(version=3, box_size=1, border=0, +# mask_pattern=3, error_correction=ec) +# if self.is_alnum: +# here = uqr.QRData(msg.upper().encode('ascii'), +# mode=uqr.MODE_ALPHA_NUM, check_data=False) +# else: +# here = uqr.QRData(msg.encode('ascii'), +# mode=uqr.MODE_8BIT_BYTE, check_data=False) +# q.add_data(here) +# q.make(fit=False) +# +# self.qr_data = q.get_matrix() +# +# def redraw(self): +# # Redraw screen. +# from common import dis, system +# from display import FontTiny +# +# system.turbo(True) +# font = FontTiny +# inv = self.invert +# +# # what we are showing inside the QR +# msg, path = self.addrs[self.idx] +# +# # make the QR, if needed. +# if not self.qr_data: +# # dis.busy_bar(True) +# self.render_qr(msg) +# +# # Draw display +# if inv: +# dis.dis.fill_rect(0, 0, Display.WIDTH, +# Display.HEIGHT - Display.FOOTER_HEIGHT + 1, 1) +# else: +# dis.clear() +# +# y = TOP_MARGIN +# +# # Draw the derivation path +# if len(self.addrs) > 1: +# dis.text(None, y, path, font, invert=inv) +# y += font.leading + VERT_SPACING +# +# w = 29 # because version=3 +# module_size = 5 # Each "dot" in a QR code is called a module +# pixel_width = w * module_size +# frame_width = pixel_width + (module_size * 2) +# +# # QR code offsets +# XO = (Display.WIDTH - pixel_width) // 2 +# YO = y +# dis.dis.fill_rect(XO - module_size, YO - +# module_size, frame_width, frame_width, 0 if inv else 1) +# +# # Draw the actual QR code +# data = self.qr_data +# for qx in range(w): +# for qy in range(w): +# px = data[qx][qy] +# X = (qx*module_size) + XO +# Y = (qy*module_size) + YO +# dis.dis.fill_rect(X, Y, module_size, +# module_size, px if inv else (not px)) +# +# # Show the data encoded by the QR code +# y += w*module_size + VERT_SPACING + 3 +# +# MAX_ADDR_CHARS_PER_LINE = 16 +# for i in range(0, len(msg), MAX_ADDR_CHARS_PER_LINE): +# dis.text(None, y, msg[i:i+MAX_ADDR_CHARS_PER_LINE], font, inv) +# y += font.leading +# +# dis.draw_footer('BACK', 'INVERT', self.input.is_pressed( +# 'x'), self.input.is_pressed('y')) +# dis.show() +# system.turbo(False) +# +# async def interact_bare(self): +# +# self.redraw() +# while 1: +# event = await self.input.get_event() +# +# if event != None: +# key, event_type = event +# if event_type == 'down': +# if key == 'u' or key == 'l': +# if self.idx > 0: +# self.idx -= 1 +# self.qr_data = None +# elif key == 'd' or key == 'r': +# if self.idx != len(self.addrs)-1: +# self.idx += 1 +# self.qr_data = None +# elif key in 'xy': +# # Allow buttons to redraw in pressed state +# pass +# else: +# continue +# elif event_type == 'up': +# if key == 'x': +# self.redraw() +# break +# elif key == 'y': +# self.invert = not self.invert +# else: +# continue +# +# self.redraw() +# +# async def interact(self): +# await self.interact_bare() +# the_ux.pop() -async def ux_show_text_as_ur(title='QR Code', msg='', qr_text=''): - o = DisplayURCode(title, msg, qr_text) - await o.interact_bare() +async def ux_show_text_as_ur(title='QR Code', qr_text='', qr_type=QRType.UR2, qr_args=None, msg=None, left_btn='BACK', right_btn='RESIZE'): + # print('ux_show_text_as_ur: qr_type: {}'.format(qr_type)) + o = DisplayURCode(title, qr_text, qr_type, qr_args=qr_args, msg=msg, left_btn=left_btn, right_btn=right_btn) + result = await o.interact_bare() gc.collect() + return result def qr_get_module_size_for_version(version): # 1 -> 21 @@ -1026,69 +1125,59 @@ def qr_buffer_size_for_version(version): class DisplayURCode(UserInteraction): - + # Show a QR code or a series of codes in Blockchain Commons' UR format # Purpose is to allow a QR code to be scanned, so we make it as big as possible # given our screen size, but if it's too big, we display a series of images # instead. - def __init__(self, title, msg, qr_text): + def __init__(self, title, qr_text, qr_type, qr_args=None, msg=None, left_btn='DONE', right_btn='RESIZE', is_binary=False): self.title = title - self.msg = msg self.qr_text = qr_text - # self.qr = None self.input = KeyInputHandler(down='xy', up='xy') - self.ur_version = 1 + self.last_version = 0 + self.msg = msg + self.left_btn = left_btn + self.right_btn = right_btn + + self.num_supported_sizes = 0 self.qr_version_idx = 0 # "version" for QR codes essentially maps to the size - self.qr_versions = [22, 12, 8] self.render_id = 0 self.last_render_id = -1; + self.qr_type = qr_type + # print('DisplayURCode: qr_type: {}'.format(qr_type)) + self.qr_args = qr_args + self.is_binary = is_binary + system.turbo(True) self.generate_qr_data() - self.qr_data = None - self.curr_part = 0 + system.turbo(False) def generate_qr_data(self): + dis.fullscreen('Generating QR') + system.show_busy_bar() + # Instantiate the right type of QR encoder - always make a new one + self.qr_encoder = make_qr_encoder(self.qr_type, self.qr_args) + self.num_supported_sizes = self.qr_encoder.get_num_supported_sizes() + # We collect before and after to ensure the most available memory - self.parts = None gc.collect() - # Generate the parts - if self.ur_version == 1: - # UR 1.0 - self.parts = encode_ur(self.qr_text, fragment_capacity=self.get_ur_max_len()) - elif self.ur_version == 2: - # UR 2.0 - encoder = CBOREncoder() - encoder.encodeBytes(self.qr_text) - ur_obj = UR("bytes", encoder.get_bytes()) - self.ur_encoder = UREncoder(ur_obj, 30) - - self.parts = [] - while not self.ur_encoder.is_complete(): - part = self.ur_encoder.next_part() - print('part={}'.format(part)) - self.parts.append(part) - else: - raise ValueError('Invalid UR version') + max_len = self.qr_encoder.get_max_len(self.qr_version_idx) + self.qr_encoder.encode(self.qr_text, is_binary=self.is_binary, max_fragment_len=max_len) gc.collect() + system.hide_busy_bar() def set_next_density(self): - self.qr_version_idx = (self.qr_version_idx + 1) % len(self.qr_versions) - - # TODO: Determine best values for version, and max len - def get_ur_max_len(self): - if self.qr_version_idx == 0: - return 500 - elif self.qr_version_idx == 1: - return 200 - else: - return 60 + self.last_version = 0; + self.qr_version_idx = (self.qr_version_idx + 1) % self.num_supported_sizes def render_qr(self, data): from utils import imported + # print('data: {}'.format(data)) + if self.last_render_id != self.render_id: self.last_render_id = self.render_id @@ -1097,14 +1186,25 @@ class DisplayURCode(UserInteraction): gc.collect() # Render QR data to buffer - print('qr={}'.format(data.upper())) - encoded_data = data.upper().encode('ascii') + # print('qr={}'.format(data.upper())) + encoded_data = data.encode('ascii') + if self.qr_type != QRType.QR: + encoded_data = encoded_data.upper() ll = len(encoded_data) from foundation import QRCode qrcode = QRCode() - version = qrcode.fit_to_version(ll) + + version = qrcode.fit_to_version(ll, is_alphanumeric_qr(encoded_data)) + + # Don't go to a smaller QR code, even if it means repeated data since it looks weird + # to change the QR code size + if self.last_version > version: + version = self.last_version + else: + self.last_version = version + buf_size = qr_buffer_size_for_version(version) self.modules_count = qr_get_module_size_for_version(version) # print('fit_to_version({}) = {} buffer size = {}'.format(ll, version,buf_size)) @@ -1116,30 +1216,32 @@ class DisplayURCode(UserInteraction): self.qr_data = out_buf def redraw(self): - # Redraw screen. + # Redraw screen from common import dis from display import FontTiny - TOP_MARGIN = 9 - VERT_SPACING = 10 + system.turbo(True) + TOP_MARGIN = 7 font = FontTiny - # Make the QR, if needed - #if not self.qr_data[self.curr_part]: - # print('rendering QR code for entry {}: "{}" len={}'.format(self.curr_part, self.parts[self.curr_part], len(self.parts[self.curr_part]))) - - self.render_qr(self.parts[self.curr_part]) + data = self.qr_encoder.next_part() + # print('data={}'.format(data)) + self.render_qr(data) # Draw QR display dis.clear() - - dis.draw_header(self.title, left_text='{}/{}'.format(self.curr_part + 1, len(self.parts))) + dis.draw_header(self.title) + # dis.draw_header(self.title, left_text='{}/{}'.format(self.curr_part + 1, len(self.parts))) y = Display.HEADER_HEIGHT + TOP_MARGIN w = self.modules_count # print('modules_count={}'.format(w)) - module_pixel_width = (Display.WIDTH - 20) // w + if self.msg: + module_pixel_width = (Display.WIDTH - 60) // w + else: + module_pixel_width = (Display.WIDTH - 20) // w + # print('module_pixel_width={}'.format(module_pixel_width)) total_pixel_width = w * module_pixel_width @@ -1148,10 +1250,10 @@ class DisplayURCode(UserInteraction): # QR code offsets XO = (Display.WIDTH - total_pixel_width) // 2 - # Center vertically now that we have no label underneath + # Center vertically now that we have no label underneath YO = ((Display.HEIGHT - Display.HEADER_HEIGHT - Display.FOOTER_HEIGHT) - total_pixel_width ) // 2 + Display.HEADER_HEIGHT dis.dis.fill_rect(XO - module_pixel_width, YO - - module_pixel_width, frame_width, frame_width, 1) + module_pixel_width, frame_width, frame_width, 0) # Draw the actual QR code # print('qr_data = {}'.format(self.qr_data)) @@ -1163,45 +1265,51 @@ class DisplayURCode(UserInteraction): X = (qx*module_pixel_width) + XO Y = (qy*module_pixel_width) + YO - dis.dis.fill_rect(X, Y, module_pixel_width, module_pixel_width, not px) + dis.dis.fill_rect(X, Y, module_pixel_width, module_pixel_width, px) + + # Draw message + if self.msg != None: + dis.text(None, Display.HEIGHT - Display.FOOTER_HEIGHT - 20, self.msg, font=FontTiny) dis.draw_footer( - 'BACK', - 'RESIZE', + self.left_btn, + self.right_btn, self.input.is_pressed('x'), self.input.is_pressed('y') ) dis.show() - self.last_frame_render_time = time_now_ms() + system.turbo(False) async def interact_bare(self): self.redraw() while 1: event = await self.input.get_event() - if event != None: + # print('event={}'.format(event)) key, event_type = event if event_type == 'up': if key == 'x': self.redraw() - break + return 'x' elif key == 'y': - self.set_next_density() - self.generate_qr_data() - self.curr_part = 0 - self.render_id += 1 + if self.right_btn == 'RESIZE': + system.turbo(True) + self.set_next_density() + self.generate_qr_data() + self.render_id += 1 + system.turbo(False) + else: + # User has something else in mind + self.redraw() + return 'y' else: # Only need to check timer and advance part number if we have more than one part - if len(self.parts) > 1: - now = time_now_ms() - elapsed_time = now - self.last_frame_render_time - # print('elapsed_time={}'.format(elapsed_time)) - if elapsed_time > 1: - # Show the next part - self.curr_part = (self.curr_part + 1) % len(self.parts) - self.redraw() - self.render_id += 1 + # if len(self.parts) > 1: + # Show the next part after a short delay to control speed + await sleep_ms(200) + self.render_id += 1 + self.redraw() continue self.redraw() @@ -1211,98 +1319,75 @@ class DisplayURCode(UserInteraction): the_ux.pop() -async def ux_enter_number(prompt, max_value): - # return the decimal number which the user has entered - # - default/blank value assumed to be zero - # - clamps large values to the max - from common import dis - from display import FontTiny, FontSmall - from math import log - - # allow key repeat on X only - press = PressRelease('1234567890y') - - footer = "X to DELETE, or OK when DONE." - y = 26 - value = '' - max_w = int(log(max_value, 10) + 1) - - while 1: - dis.clear() - dis.text(0, 0, prompt) - - # text centered - if value: - bx = dis.text(None, y, value) - dis.icon(bx+1, y+11, 'space') - else: - dis.icon(64-7, y+11, 'space') - - dis.text(None, -1, footer, FontTiny) - dis.show() - - # ======================================== - # ======================================== - # ======================================== - # ======================================== - # TODO: Replace with KeyInputHandler - # ======================================== - # ======================================== - # ======================================== - # ======================================== - - ch = await press.wait() - if ch == 'y': - - if not value: - return 0 - return min(max_value, int(value)) - - elif ch == 'x': - if value: - value = value[0:-1] - else: - # quit if they press X on empty screen - return 0 - else: - if len(value) == max_w: - value = value[0:-1] + ch - else: - value += ch - - # cleanup leading zeros and such - value = str(int(value)) - - -THRESHOLD = 128 - - -def convert_to_bw(img, w, h): - dest_bytes_per_line = ((w + 7) // 8) - dest_len = dest_bytes_per_line * h - dest = bytearray(dest_len) - - for y in range(h): - for x in range(w): - src_offset = (y*w) + x - color = img[src_offset] - - dest_offset = (y*dest_bytes_per_line) + (x // 8) - # print('dest_offset=' + str(dest_offset)) - mask = 0x80 >> x % 8 - - if color < THRESHOLD: - dest[dest_offset] = dest[dest_offset] | mask - - return dest - +# async def ux_enter_number(prompt, max_value): +# # return the decimal number which the user has entered +# # - default/blank value assumed to be zero +# # - clamps large values to the max +# from common import dis +# from display import FontTiny, FontSmall +# from math import log +# +# # allow key repeat on X only +# press = PressRelease('1234567890y') +# +# footer = "X to DELETE, or OK when DONE." +# y = 26 +# value = '' +# max_w = int(log(max_value, 10) + 1) +# +# while 1: +# dis.clear() +# dis.text(0, 0, prompt) +# +# # text centered +# if value: +# bx = dis.text(None, y, value) +# dis.icon(bx+1, y+11, 'space') +# else: +# dis.icon(64-7, y+11, 'space') +# +# dis.text(None, -1, footer, FontTiny) +# dis.show() +# +# # ======================================== +# # ======================================== +# # ======================================== +# # ======================================== +# # TODO: Replace with KeyInputHandler +# # ======================================== +# # ======================================== +# # ======================================== +# # ======================================== +# +# ch = await press.wait() +# if ch == 'y': +# +# if not value: +# return 0 +# return min(max_value, int(value)) +# +# elif ch == 'x': +# if value: +# value = value[0:-1] +# else: +# # quit if they press X on empty screen +# return 0 +# else: +# if len(value) == max_w: +# value = value[0:-1] + ch +# else: +# value += ch +# +# # cleanup leading zeros and such +# value = str(int(value)) async def ux_scan_qr_code(title): + import common from common import dis, qr_buf, viewfinder_buf - from display import FontLarge, FontSmall - from ur.ur_decoder import URDecoder - from ur1.decode_ur import decode_ur, extract_single_workload, Workloads + from display import FontSmall + from utils import save_qr_code_image + import utime from constants import VIEWFINDER_WIDTH, VIEWFINDER_HEIGHT, CAMERA_WIDTH, CAMERA_HEIGHT @@ -1318,22 +1403,15 @@ async def ux_scan_qr_code(title): qr = QR(CAMERA_WIDTH, CAMERA_HEIGHT, qr_buf) qr_code = None data = None - - # Premptively create a URDecoder too - we don't know if we need it yet - ur_decoder = URDecoder() - percent_complete = 0 + error = None input = KeyInputHandler(up='xy', down='xy') fps_start = utime.ticks_us() frame_count = 0 - ur_version = 1 - workloads = Workloads() - - parts_received = 0 - total_parts = 0 - + qr_decoder = None + progress = None while True: frame_start = utime.ticks_us() @@ -1343,8 +1421,8 @@ async def ux_scan_qr_code(title): snapshot_end = utime.ticks_us() if not result: - print("ERROR: cam.copy_capture() returned False!") - # TODO: Show some error to the user!!! + # print("ERROR: cam.copy_capture() returned False!") + await ux_show_story('Unable to capture image with camera.', title='Error') return None draw_start = utime.ticks_us(); @@ -1362,21 +1440,7 @@ async def ux_scan_qr_code(title): RIGHT_X = Display.WIDTH - OFFSET * 2 Y = Display.HEADER_HEIGHT + OFFSET - # # Upper left - # dis.draw_rect(LEFT_X, Y, SIZE, THICKNESS, 0, fill_color=1) - # dis.draw_rect(LEFT_X, Y + THICKNESS, SIZE, THICKNESS, 0, fill_color=0) - - # dis.draw_rect(LEFT_X, Y, THICKNESS, SIZE, 0, fill_color=1) - # dis.draw_rect(LEFT_X + THICKNESS, Y + THICKNESS, THICKNESS, SIZE - THICKNESS, 0, fill_color=0) - - # # Upper right - # dis.draw_rect(RIGHT_X - SIZE, Y, SIZE, THICKNESS, 0, fill_color=1) - # dis.draw_rect(RIGHT_X - SIZE, Y + THICKNESS, SIZE, THICKNESS, 0, fill_color=0) - - # dis.draw_rect(RIGHT_X, Y, THICKNESS, SIZE, 0, fill_color=1) - # dis.draw_rect(RIGHT_X - THICKNESS, Y + THICKNESS, THICKNESS, SIZE - THICKNESS, 0, fill_color=0) - - right_label = '{} OF {}'.format(parts_received, total_parts) if total_parts > 0 else 'SCANNING...' + right_label = progress if progress != None else 'SCANNING...' dis.draw_footer('BACK', right_label, left_down=input.is_pressed('x'), right_down=input.is_pressed('y')) draw_end = utime.ticks_us(); @@ -1385,63 +1449,57 @@ async def ux_scan_qr_code(title): dis.show() show_end = utime.ticks_us(); - # Try to decode the data + # Look for QR codes in the image decode_start = utime.ticks_us() - qr_code = qr.find_qr_codes() + data = qr.find_qr_codes() # print('find_qr_codes() out') decode_end = utime.ticks_us() - if qr_code != None: - data = qr_code - print('qr_code={}'.format(qr_code)) + # Don't try decoding if we are in Snapshot mode...user is probably using this as a viewfinder + if not common.snapshot_mode_enabled and data != None: + # print('data={}'.format(data)) # See if this looks like a ur code ur_start = utime.ticks_us() - ur_end = 0 try: - if ur_version == 1: - workloads.add(data) - parts_received, total_parts = workloads.get_progress() + if qr_decoder == None: + # We need to find out what type of QR this is and have the factory make a decoder for us + qr_decoder = get_qr_decoder_for_data(data) - if workloads.is_complete(): - data = decode_ur(workloads.workloads) - break + # We should be guaranteed to have a qr_decoder here since basic QR accepts any data format + qr_decoder.add_data(data) - elif ur_version == 2: - import math - if ur_decoder.receive_part(qr_code) == True: - print('Part was accepted') - else: - print('Part was NOT accepted') - ur_end = utime.ticks_us() - if ur_decoder.is_success(): - result = ur_decoder.result_message() - print('Success! len={} result={}'.format( - len(result.cbor), result)) - data = result.cbor - break + # See if there was any error + error = qr_decoder.get_error() + if error != None: + # print('ERROR: error={}'.format(error)) + data = None + break - percent_complete = math.floor( - ur_decoder.estimated_percent_complete() * 100) + if qr_decoder.is_complete(): + data = qr_decoder.decode() + # print('data: |{}|'.format(data)) - except Exception as e: - ur_end = utime.ticks_us() + # Set the last QRType so that signed transactions know what to encode as + common.last_scanned_qr_type = qr_decoder.get_data_format() + # print('common.last_scanned_qr_type={}'.format(common.last_scanned_qr_type)) + break - print('Failed to parse UR!') + progress = '{} OF {}'.format(qr_decoder.received_parts(), qr_decoder.total_parts()) + + except Exception as e: + # print('Failed to parse UR!') import sys - print('Exception: {}'.format(e)) + # print('Exception: {}'.format(e)) sys.print_exception(e) - # Doesn't look like it's a UR code, so interpret as a normal QR code and return the data - data = qr_code break - print('ur decode: {}ms'.format(ur_end - ur_start)) - else: - pass - # print("******* NO QR CODE FOUND!") + ur_end = utime.ticks_us() + + # print('ur decode: {}us'.format(ur_end - ur_start)) - key_start = utime.ticks_us() # Check for key input to see if we should back out + key_start = utime.ticks_us() event = await input.get_event() if event != None: key, event_type = event @@ -1451,9 +1509,6 @@ async def ux_scan_qr_code(title): break key_end = utime.ticks_us() - # An extra sleep to avoid redrawing so much - # TODO: See if this is necessary on actual hardware - may be able to reduce the duration of the sleep - # TODO: Balance between screen refresh rate and battery drain. frame_count += 1 now = utime.ticks_us() @@ -1465,8 +1520,9 @@ async def ux_scan_qr_code(title): total_ms = snapshot_ms + draw_ms + show_ms + decode_ms + key_ms measured_frame_ms = (now - frame_start) / 1000 fps = frame_count / ((now - fps_start) / 1000000) - - if frame_count % 10 == 0: + + print_stats = False + if print_stats and frame_count % 10 == 0: print_start = utime.ticks_us() print('Frame Stats:') print(' {:>3.2f}ms Snapshot'.format(snapshot_ms)) @@ -1485,120 +1541,11 @@ async def ux_scan_qr_code(title): print(' {:03.1f}ms Print time'.format(print_ms)) - # await sleep_ms(10) - - # Turn off camera after capturing is done! - print('cam.disable() starting') cam.disable() - print('cam.disable() done') - # Test sha256 from trezor - return data - -# Keeping this for a bit as an example of HOLD TO SIGN -# async def ux_scan_qr_code(title): -# # show a big long string, and wait for XY to continue -# # - returns character used to get out (X or Y) -# # - accepts a stream or string -# from common import dis -# from display import FontLarge, FontSmall - -# from camera import Camera, CAMERA_WIDTH, CAMERA_HEIGHT -# from foundation import QR - -# font = FontSmall - -# # Create the Camera connection -# cam = Camera() -# cam.enable() - -# # Create QR decoder -# qr = QR(CAMERA_WIDTH, CAMERA_HEIGHT, cam.get_image_buffer()) -# qr_code = None - -# is_signed = False -# is_signing = False -# signing_progress = 0 -# SIGNING_DURATION = 2000 - -# input = KeyInputHandler(up='xy', down='y', long='y', -# long_duration=SIGNING_DURATION) -# while True: -# dis.clear() - -# dis.draw_header(title) - -# if not is_signing: -# img = cam.capture() -# if img == None: -# print("No image received!") -# # TODO: Show some error!!! -# return None - -# preview = convert_to_bw(img, CAMERA_WIDTH, CAMERA_HEIGHT) -# dis.image(Display.WIDTH // 2 - 120, 31, 240, 320, preview) - -# qr_code = qr.find_qr_codes(img) - -# if qr_code != None: -# break -# # print("qr_code=" + qr_code) -# # lines = [] -# # lines.extend(word_wrap(qr_code, font)) -# # y = Display.HEIGHT - (len(lines) * font.leading) -# # # print("Display.HEIGHT=" + str(Display.HEIGHT) + " len(lines)=" + str(len(lines)) + " font.height=" + str(font.height)) -# # for ln in lines: -# # dis.clear_rect(0, y, Display.WIDTH, font.leading) -# # dis.text(None, y, ln) -# # y += font.leading -# else: -# # Draw Signing UI -# if is_signed: -# dis.text(None, 100, "Signing Successful!") -# else: -# dis.text(None, 100, "Signing Transaction...") - -# dis.draw_rect(10, 140, Display.WIDTH - 20, 40, 2, 0, 1) -# dis.draw_rect(14, 144, int((Display.WIDTH - 28) -# * signing_progress), 32, 0, 1, 0) -# dis.show() -# event = await input.get_event() -# if event != None: -# key, event_type = event -# if event_type == 'up': -# if key == 'x': -# break -# if key == 'y': -# if not is_signed: -# is_signing = False -# signing_progress = 0 - -# if event_type == 'down': -# if key == 'y': -# is_signing = True - -# if event_type == 'long_press': -# if key == 'y': -# is_signed = True -# signing_progress = 1 - -# if is_signing and not is_signed: -# all_pressed = input.get_all_pressed() -# if 'y' in all_pressed: -# elapsed = all_pressed['y'] -# # Handle the elapsed time calc -# signing_progress = elapsed / SIGNING_DURATION -# # print("elapsed={} signing_progress={}".format( -# # elapsed, signing_progress)) - -# # An extra sleep to avoid redrawing so much -# await sleep_ms(100) - -# # Turn off camera after capturing is done! -# cam.disable() -# return qr_code + return data async def ux_show_story_sequence(stories): @@ -1615,7 +1562,8 @@ async def ux_show_story_sequence(stories): right_btn=s.get('right_btn', 'CONTINUE'), center=s.get('center', False), center_vertically=s.get('center_vertically', False), - scroll_label=s.get('`scroll_label`', None)) + scroll_label=s.get('scroll_label', None), + clear_keys=s.get('clear_keys', False)) if key == 'x': if story_idx == 0: @@ -1644,9 +1592,10 @@ async def ux_show_word_list(title, words, heading1='', heading2=None, left_align px_width = dis.width(word, font) if px_width > longest_word_width: longest_word_width = px_width - x = Display.HALF_WIDTH - (longest_word_width // 2) + x = Display.HALF_WIDTH - (longest_word_width // 2) while True: + system.turbo(True) dis.clear() dis.draw_header(title) @@ -1673,6 +1622,7 @@ async def ux_show_word_list(title, words, heading1='', heading2=None, left_align right_down=input.is_pressed('y')) dis.show() + system.turbo(False) while 1: event = await input.get_event() @@ -1685,56 +1635,148 @@ async def ux_show_word_list(title, words, heading1='', heading2=None, left_align if key in 'xy': return key -async def ux_enter_pin(title, heading='Enter PIN', message=None): - from common import dis +async def ux_enter_pin(title, heading='Enter PIN', left_btn='BACK', right_btn='ENTER', left_btn_function=None, + hide_attempt_counter=False, is_new_pin=False): + from common import dis, pa + import pincodes - MAX_PIN_PART_LEN = 6 - MIN_PIN_PART_LEN = 2 + SHOW_SECURITY_WORDS_AT_LEN = 4 + MIN_PIN_LEN = 6 + MAX_PIN_LEN = 12 + LARGE_BOX_LIMIT = 6 - PIN_BOX_W, PIN_BOX_H = dis.icon_size('box') - PIN_BOX_SPACING = (dis.WIDTH - PIN_BOX_W * - MAX_PIN_PART_LEN) // (MAX_PIN_PART_LEN + 1) - PIN_BOX_ADVANCE = PIN_BOX_W + PIN_BOX_SPACING + POPUP_WIDTH = Display.WIDTH - 40 font = FontSmall input = KeyInputHandler(up='xy0123456789', down='xy0123456789*') + show_security_words = False + security_words_confirmed = False + show_short_pin_message = False pin = '' pressed = False + security_label = 'NEXT' if is_new_pin else 'CONFIRM' + + def draw_popup(title, lines, y): + POPUP_HEIGHT = FontSmall.leading * 4 + popup_x = Display.HALF_WIDTH - POPUP_WIDTH // 2 + + dis.draw_rect(popup_x, y, POPUP_WIDTH, FontSmall.leading + 2, border_w=0, fill_color=1) + dis.draw_rect(popup_x, y+FontSmall.leading, POPUP_WIDTH, POPUP_HEIGHT - FontSmall.leading, border_w=2) + + dis.text(None, y+3, title, font=FontSmall, invert=True) + y += int(FontSmall.leading * 1.5) + + for line in lines: + dis.text(None, y, line, font=FontSmall) + y += FontSmall.leading + + return y + + def draw_security_words_popup(title1, title2, words, y): + POPUP_HEIGHT = FontSmall.leading * 2 + popup_x = Display.HALF_WIDTH - POPUP_WIDTH // 2 + + dis.draw_rect(popup_x, y, POPUP_WIDTH, FontSmall.leading * 2 + 2, border_w=0, fill_color=1) + dis.draw_rect(popup_x, y+FontSmall.leading*2, POPUP_WIDTH, POPUP_HEIGHT, border_w=2) + + dis.text(None, y+3, title1, font=FontSmall, invert=True) + y += int(FontSmall.leading)-1 + dis.text(None, y+3, title2, font=FontSmall, invert=True) + y += int(FontSmall.leading * 1.5) + 2 + + dis.text(None, y, words, font=FontSmall) + + return y while True: + system.turbo(True) + dis.clear() dis.draw_header(title) - filled = len(pin) - if pressed: - filled -= 1 - - y = dis.HEADER_HEIGHT + 20 + y = dis.HEADER_HEIGHT + 8 dis.text(None, y, heading, font=FontSmall) - y += FontSmall.leading + 20 + y += FontSmall.leading - 2 + + num_filled = len(pin) + # print('num_filled={} pressed={}'.format(num_filled, pressed)) + if (num_filled < LARGE_BOX_LIMIT) or (num_filled == LARGE_BOX_LIMIT and not pressed): + empty_box = 'pw_empty_box_lg' + pressed_box = 'pw_pressed_box_lg' + filled_box = 'pw_filled_box_lg' + MAX_PIN_BOXES_TO_DISPLAY = LARGE_BOX_LIMIT + y += 3 + else: + empty_box = 'pw_empty_box_sm' + pressed_box = 'pw_pressed_box_sm' + filled_box = 'pw_filled_box_sm' + MAX_PIN_BOXES_TO_DISPLAY = MAX_PIN_LEN + y += 14 + + PIN_BOX_W, PIN_BOX_H = dis.icon_size(empty_box) + PIN_BOX_SPACING = (dis.WIDTH - PIN_BOX_W * + MAX_PIN_BOXES_TO_DISPLAY) // (MAX_PIN_BOXES_TO_DISPLAY + 1) + PIN_BOX_ADVANCE = PIN_BOX_W + PIN_BOX_SPACING + + if num_filled >= 1: + total_width = (num_filled * PIN_BOX_W) + ((num_filled - 1) * PIN_BOX_SPACING) + else: + total_width = 0 + # print('total_width = {}'.format(total_width)) + + if pressed and num_filled > 0: + total_width += PIN_BOX_SPACING + PIN_BOX_W + # print('total_width increased to = {}'.format(total_width)) + + # Have a width when PIN is empty and nothing pressed + if total_width == 0: + total_width = PIN_BOX_W - num_boxes = filled - total_width = (filled * PIN_BOX_W) + ((num_boxes - 1) * PIN_BOX_SPACING) - x = Display.HALF_WIDTH - (total_width // 2) - (PIN_BOX_W // 2) - 4 - for _idx in range(filled): - dis.icon(x, y, 'xbox') + x = Display.HALF_WIDTH - (total_width // 2) + for _idx in range(num_filled): + dis.icon(x, y, filled_box) x += PIN_BOX_ADVANCE if pressed: - dis.icon(x, y, 'tbox') + # print('pressed case') + dis.icon(x, y, pressed_box) + elif len(pin) == 0: + # print('draw empty box') + dis.icon(x, y, empty_box) + + # Show remaining attempts if not hidden and if there was at least one failure + if not hide_attempt_counter and pa.attempts_left < pa.max_attempts: + dis.text(None, Display.HEIGHT - Display.HEADER_HEIGHT - FontTiny.leading + 3, '{} attempts remaining'.format(pa.attempts_left), font=FontTiny) + + # Since box size can vary, we advance a constant here and center the box in that space based on height above + y = 128 + + if show_short_pin_message: + y = draw_popup('Info', ['PIN must be at','least 6 digits'], y) + + # Show the security word list if key is held down + elif show_security_words: + if not security_words: + try: + security_words = pincodes.PinAttempt.anti_phishing_words(pin[0:SHOW_SECURITY_WORDS_AT_LEN].encode()) + except Exception as e: + security_words = ['Unable to','retrieve'] + # print("Exception getting anti-phishing words: {}".format(e)) + + if is_new_pin: + y = draw_security_words_popup('Remember these', 'Security Words', ' '.join(security_words), y) + else: + y = draw_security_words_popup('Recognize these', 'Security Words?', ' '.join(security_words), y) else: - if len(pin) != MAX_PIN_PART_LEN: - dis.icon(x, y, 'box') + # Clear them so we reload next time (numbers may have changed) + security_words = None - if message: - dis.text(None, Display.HEIGHT - Display.FOOTER_HEIGHT - - FontTiny.leading, message, FontTiny) - dis.draw_footer("BACK", "ENTER", input.is_pressed('x'), - input.is_pressed('y')) + dis.draw_footer(left_btn, security_label if show_security_words else right_btn, input.is_pressed('x'), input.is_pressed('y')) dis.show() + system.turbo(False) # Interaction while True: @@ -1746,39 +1788,234 @@ async def ux_enter_pin(title, heading='Enter PIN', message=None): key, event_type = event if event_type == 'down': + # Hide the short pin message as soon as any other key is pressed + show_short_pin_message = False + if key == '*': - # Delete one digit from the PIN - if pin: + if pin and len(pin) > 0: pin = pin[:-1] - elif key in '0123456789': - pressed = True + if len(pin) <= SHOW_SECURITY_WORDS_AT_LEN: + security_words_confirmed = False - # Add the number to the PIN or replace the last digit - if len(pin) == MAX_PIN_PART_LEN: - pin = pin[:-1] + key - else: - pin += key + elif key in '0123456789': + if not show_security_words: + if len(pin) < MAX_PIN_LEN: + pressed = True elif event_type == 'up': if key == 'x': return None elif key == 'y': - if len(pin) < MIN_PIN_PART_LEN: + if show_security_words: + show_security_words = False + security_words_confirmed = True + continue + + elif len(pin) < MIN_PIN_LEN: # they haven't given enough yet + show_short_pin_message = True continue else: - print('RETURNING PIN = {}'.format(pin)) + # print('RETURNING PIN = {}'.format(pin)) return pin + elif key in '0123456789': - pressed = False + if not show_security_words: + pressed = False + + # Add the number to the PIN or replace the last digit + if len(pin) == MAX_PIN_LEN: + pass + else: + pin += key + + # Decide whether to show the security words + if not security_words_confirmed: + show_security_words = len(pin) == SHOW_SECURITY_WORDS_AT_LEN + async def ux_shutdown(): from common import system confirm = await ux_confirm("Are you sure you want to shutdown?", center=True, center_vertically=True) if confirm: - print('SHUTTING DOWN!') - # TODO: CLEAR THE SCREEN BEFORE POWERING DOWN! + # print('SHUTTING DOWN!') system.shutdown() return + +Keypad = [ + ['1', '2', '3'], + ['4', '5', '6'], + ['7', '8', '9'], + ['*', '0', '#'], +] + +KeyState = { + '1': {'pressed': False, 'released': False}, + '2': {'pressed': False, 'released': False}, + '3': {'pressed': False, 'released': False}, + '4': {'pressed': False, 'released': False}, + '5': {'pressed': False, 'released': False}, + '6': {'pressed': False, 'released': False}, + '7': {'pressed': False, 'released': False}, + '8': {'pressed': False, 'released': False}, + '9': {'pressed': False, 'released': False}, + '0': {'pressed': False, 'released': False}, + '*': {'pressed': False, 'released': False}, + '#': {'pressed': False, 'released': False}, + 'l': {'pressed': False, 'released': False}, + 'r': {'pressed': False, 'released': False}, + 'u': {'pressed': False, 'released': False}, + 'd': {'pressed': False, 'released': False}, + 'x': {'pressed': False, 'released': False}, + 'y': {'pressed': False, 'released': False}, +} + +async def ux_keypad_test(*a): + from common import dis + from display import FontSmall + + SIDE_MARGIN = 20 + NUMKEY_HGAP = 10 + NUMKEY_VGAP = 5 + KEY_WIDTH = ((Display.WIDTH - (SIDE_MARGIN*2) - (2 * NUMKEY_HGAP)) // 3) + KEY_HEIGHT = 24 + + font = FontSmall + + # Hold x or y for one second to exit keypad test + input = KeyInputHandler(down='1234567890*#rludxy', up='1234567890*#rludxy', long='xy', + long_duration=1000) + + def draw_key(key, key_x, key_y, small=False, vertical=False): + state = KeyState.get(key) + pressed = state.get('pressed') + released = state.get('released') + w = KEY_WIDTH if not small else KEY_WIDTH // 2 + dis.draw_rect(key_x, key_y, + w, KEY_HEIGHT, 4 if pressed else 2, + fill_color=1 if released else 0 , border_color=1) + if not small: + dis.text(key_x + 22, key_y + 1, key, invert=1 if released else 0) + else: + dis.text(key_x + 9, key_y + 1, key, invert=1 if released else 0) + + while True: + # Redraw + system.turbo(True) + dis.clear() + + dis.draw_header('Keypad Test') + + # Draw the title + y = Display.HEADER_HEIGHT + TEXTBOX_MARGIN + dis.text(None, y, 'Press Each Key') + y += font.leading + + # Draw the select and back buttons + draw_key('u', Display.HALF_WIDTH - KEY_WIDTH // 4, y, small=True) + draw_key('d', Display.HALF_WIDTH - KEY_WIDTH // 4, y + NUMKEY_VGAP + KEY_HEIGHT, small=True) + draw_key('l', Display.HALF_WIDTH - KEY_WIDTH // 4 - NUMKEY_HGAP - KEY_WIDTH//2, y + (NUMKEY_VGAP + KEY_HEIGHT)//2, small=True) + draw_key('r', Display.HALF_WIDTH - KEY_WIDTH // 4 + NUMKEY_HGAP + KEY_WIDTH//2, y + (NUMKEY_VGAP + KEY_HEIGHT)//2, small=True) + draw_key('x', SIDE_MARGIN, y + (NUMKEY_VGAP + KEY_HEIGHT)//2, small=True) + draw_key('y', Display.WIDTH - SIDE_MARGIN - KEY_WIDTH//2, y + (NUMKEY_VGAP + KEY_HEIGHT)//2, small=True) + + y += 80 + + # Draw the Numeric keypad grid + for row in range(len(Keypad)): + for col in range(len(Keypad[row])): + key = Keypad[row][col] + key_x = SIDE_MARGIN + (col *(KEY_WIDTH + NUMKEY_HGAP)) + key_y = y + row * (KEY_HEIGHT + NUMKEY_VGAP) + draw_key(key, key_x, key_y) + + dis.draw_footer('BACK', 'NEXT', input.is_pressed('x'), input.is_pressed('y')) + + dis.show() + system.turbo(False) + + # Wait for key inputs + event = None + while True: + event = await input.get_event() + if event != None: + break + + if event != None: + key, event_type = event + state = KeyState.get(key) + + # Check for footer button actions first + if event_type == 'down': + state['pressed'] = True + + if event_type == 'up': + state['released'] = True + + if event_type == 'long_press': + return key + + +async def ux_draw_alignment_grid(title='Align Screen'): + from common import dis + from display import FontSmall + + NUM_VERTICAL_LINES = 7 + NUM_HORIZONTAL_LINES = 14 + GRID_WIDTH = Display.WIDTH // (NUM_VERTICAL_LINES + 1) + GRID_HEIGHT = Display.HEIGHT // (NUM_HORIZONTAL_LINES + 1) + # print('GRID_WIDTH={}'.format(GRID_WIDTH)) + # print('GRID_HEIGHT={}'.format(GRID_HEIGHT)) + + font = FontSmall + + input = KeyInputHandler(down='xy', up='xy') + pressed = {} + + while True: + # Redraw + system.turbo(True) + dis.clear() + + # dis.draw_header(title) + # dis.draw_footer('BACK', 'NEXT', input.is_pressed('x'), input.is_pressed('y')) + + # Draw an inset rectangle around the outside + dis.draw_rect(2, 2, Display.WIDTH-4, Display.HEIGHT-4, 1, fill_color=0, border_color=1) + + # Draw vertical lines + for col in range(NUM_VERTICAL_LINES): + x = (col + 1) * GRID_WIDTH + dis.vline(x) + + # Draw Horizontal lines + for row in range(NUM_HORIZONTAL_LINES): + y = (row + 1) * GRID_HEIGHT + dis.hline(y) + + dis.show() + system.turbo(False) + + # Wait for key inputs + event = None + while True: + event = await input.get_event() + if event != None: + break + + if event != None: + key, event_type = event + state = KeyState.get(key) + + # Exit if key pressed + if event_type == 'down': + pressed[key] = True + + if event_type == 'up': + # We may have come to this screen from a longpress on the keypad test screen + # in which case an up will come that we DON'T want to act on. The local input + # won't have transitioned the key to pressed state yet, so use this to differentiate. + if key in pressed: + return key diff --git a/ports/stm32/boards/Passport/modules/version.py b/ports/stm32/boards/Passport/modules/version.py index 1539d25..626c1f7 100644 --- a/ports/stm32/boards/Passport/modules/version.py +++ b/ports/stm32/boards/Passport/modules/version.py @@ -1,7 +1,7 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: GPL-3.0-or-later # -# SPDX-FileCopyrightText: 2018 Coinkite, Inc. +# SPDX-FileCopyrightText: 2018 Coinkite, Inc. # SPDX-License-Identifier: GPL-3.0-only # # (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard @@ -86,34 +86,4 @@ def is_factory_mode(): # return is_factory return False - -def is_fresh_version(): - # from sigheader import RAM_BOOT_FLAGS, RBF_FRESH_VERSION - # import stm - - # flags = stm.mem32[RAM_BOOT_FLAGS] - - # return bool(flags & RBF_FRESH_VERSION) - return False - - -def serial_number(): - # Our USB serial number, both in DFU mode (system boot ROM), and later thanks to code in - # USBD_StrDescriptor() - # - # - this is **probably** public info, since shared freely over USB during enumeration - # - import machine - i = machine.unique_id() - return "%02X%02X%02X%02X%02X%02X" % (i[11], i[10] + i[2], i[9], i[8] + i[0], i[7], i[6]) - - -def probe_system(): - # run-once code to determine what hardware we are running on - global hw_label - hw_label = 'Passport' - - -probe_system() - # EOF diff --git a/ports/stm32/boards/Passport/modules/wallets/bitcoin_core.py b/ports/stm32/boards/Passport/modules/wallets/bitcoin_core.py new file mode 100644 index 0000000..1065e34 --- /dev/null +++ b/ports/stm32/boards/Passport/modules/wallets/bitcoin_core.py @@ -0,0 +1,108 @@ +# SPDX-FileCopyrightText: 2021 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# SPDX-FileCopyrightText: 2018 Coinkite, Inc. +# SPDX-License-Identifier: GPL-3.0-only +# +# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard +# and is covered by GPLv3 license found in COPYING. +# +# bitcoin_core.py - Bitcoin Corek wallet +# + +import stash +import ujson +import chains +from common import settings +from utils import xfp2str, to_str +from .multisig_json import create_multisig_json_wallet +from .multisig_import import read_multisig_config_from_qr, read_multisig_config_from_microsd + +def create_bitcoin_core_export(sw_wallet=None, addr_type=None, acct_num=0, multisig=False, legacy=False): + import ustruct + xfp = xfp2str(settings.get('xfp')) + + # make the data + example_addrs = [] + payload = ujson.dumps(list(generate_bitcoin_core_wallet(example_addrs, acct_num))) + + body = '''\ +# Bitcoin Core Wallet Import File + +## For wallet with master key fingerprint: {xfp} + +Wallet operates on blockchain: {nb} + +## Bitcoin Core RPC + +The following command can be entered after opening Window -> Console +in Bitcoin Core, or using bitcoin-cli: + +importmulti '{payload}' + +## Resulting Addresses (first 3) + +'''.format(payload=payload, xfp=xfp, nb=chains.current_chain().name) + + body += '\n'.join('%s => %s' % addr for addr in example_addrs) + body += '\n' + + accts = [] # [ {'fmt':addr_type, 'deriv': acct_path, 'acct': acct_num} ] + return (body, accts) + + +def generate_bitcoin_core_wallet(example_addrs, acct_num): + # Generate the data for an RPC command to import keys into Bitcoin Core + # - yields dicts for json purposes + from descriptor import append_checksum + import ustruct + + from public_constants import AF_P2WPKH + + chain = chains.current_chain() + + derive = "84'/{coin_type}'/{account}'".format(account=acct_num, coin_type=chain.b44_cointype) + + with stash.SensitiveValues() as sv: + prefix = sv.derive_path(derive) + xpub = chain.serialize_public(prefix) + + for i in range(3): + sp = '0/%d' % i + node = sv.derive_path(sp, master=prefix) + a = chain.address(node, AF_P2WPKH) + example_addrs.append( ('m/%s/%s' % (derive, sp), a) ) + + xfp = settings.get('xfp') + txt_xfp = xfp2str(xfp).lower() + + chain = chains.current_chain() + + for internal in [False, True]: + desc = "wpkh([{fingerprint}/{derive}]{xpub}/{change}/*)".format( + derive=derive.replace("'", "h"), + fingerprint=txt_xfp, + coin_type=chain.b44_cointype, + account=0, + xpub=xpub, + change=(1 if internal else 0)) + + yield { + 'desc': append_checksum(desc), + 'range': [0, 1000], + 'timestamp': 'now', + 'internal': internal, + 'keypool': True, + 'watchonly': True + } + +BitcoinCoreWallet = { + 'label': 'Bitcoin Core', + 'sig_types': [ + {'id':'single-sig', 'label':'Single-sig', 'addr_type': None, 'create_wallet': create_bitcoin_core_export}, + ], + 'export_modes': [ + {'id': 'microsd', 'label': 'microSD', 'filename_pattern': '{sd}/passport-bitcoin-core.txt', 'ext': '.txt', + 'filename_pattern_multisig': '{sd}/passport-bitcoin-core-multisig.json'} + ] +} diff --git a/ports/stm32/boards/Passport/modules/wallets/bluewallet.py b/ports/stm32/boards/Passport/modules/wallets/bluewallet.py new file mode 100644 index 0000000..2504602 --- /dev/null +++ b/ports/stm32/boards/Passport/modules/wallets/bluewallet.py @@ -0,0 +1,24 @@ +# SPDX-FileCopyrightText: 2021 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# bluewallet.py - BlueWallet support +# + +from .electrum import create_electrum_export +from .multisig_json import create_multisig_json_wallet +from .multisig_import import read_multisig_config_from_qr, read_multisig_config_from_microsd +from data_codecs.qr_type import QRType +from public_constants import AF_P2WPKH + +BlueWallet = { + 'label': 'BlueWallet', + 'sig_types': [ + {'id':'single-sig', 'label':'Single-sig', 'addr_type': AF_P2WPKH, 'create_wallet': create_electrum_export}, + {'id':'multisig', 'label':'Multsig', 'addr_type': None, 'create_wallet': create_multisig_json_wallet, + 'import_qr': read_multisig_config_from_qr, 'import_microsd': read_multisig_config_from_microsd} + ], + 'export_modes': [ + {'id': 'qr', 'label': 'QR Code', 'qr_type': QRType.UR1}, + {'id': 'microsd', 'label': 'microSD', 'filename_pattern': '{sd}/passport-bluewallet.json', 'filename_pattern_multisig': '{sd}/passport-bluewallet-multisig.json'} + ] +} diff --git a/ports/stm32/boards/Passport/modules/wallets/btcpay.py b/ports/stm32/boards/Passport/modules/wallets/btcpay.py new file mode 100644 index 0000000..2c053bf --- /dev/null +++ b/ports/stm32/boards/Passport/modules/wallets/btcpay.py @@ -0,0 +1,23 @@ +# SPDX-FileCopyrightText: 2021 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# btcpay.py - BTCPay wallet support +# + +from .vault import create_vault_export +from .multisig_json import create_multisig_json_wallet +from .multisig_import import read_multisig_config_from_qr, read_multisig_config_from_microsd +from data_codecs.qr_type import QRType +from public_constants import AF_P2WPKH + +BtcPayWallet = { + 'label': 'BTCPay', + 'sig_types': [ + {'id':'single-sig', 'label':'Single-sig', 'addr_type': AF_P2WPKH, 'create_wallet': create_vault_export}, + ], + 'address_validation_method': 'show_addresses', + 'export_modes': [ + {'id': 'qr', 'label': 'QR Code', 'qr_type': QRType.QR}, + {'id': 'microsd', 'label': 'microSD', 'filename_pattern': '{sd}/passport-btcpay.json', 'filename_pattern_multisig': '{sd}/passport-btcpay-multisig.json'} + ] +} diff --git a/ports/stm32/boards/Passport/modules/wallets/caravan.py b/ports/stm32/boards/Passport/modules/wallets/caravan.py new file mode 100644 index 0000000..db962f3 --- /dev/null +++ b/ports/stm32/boards/Passport/modules/wallets/caravan.py @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: 2021 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# caravan.py - Caravan wallet support +# + +from .multisig_json import create_multisig_json_wallet +from .multisig_import import read_multisig_config_from_microsd + +CaravanWallet = { + 'label': 'Caravan', + 'sig_types': [ + {'id':'multisig', 'label':'Multsig', 'addr_type': None, 'create_wallet': create_multisig_json_wallet, + 'import_microsd': read_multisig_config_from_microsd} + ], + 'export_modes': [ + {'id': 'microsd', 'label': 'microSD', 'filename_pattern': '{sd}/passport-caravan.json', 'filename_pattern_multisig': '{sd}/passport-caravan-multisig.json'} + ] +} diff --git a/ports/stm32/boards/Passport/modules/wallets/casa.py b/ports/stm32/boards/Passport/modules/wallets/casa.py new file mode 100644 index 0000000..569dfde --- /dev/null +++ b/ports/stm32/boards/Passport/modules/wallets/casa.py @@ -0,0 +1,60 @@ +# SPDX-FileCopyrightText: 2021 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# casa.py - Casa support (preliminary) +# + +from .multisig_json import create_multisig_json_wallet +from .multisig_import import read_multisig_config_from_qr, read_multisig_config_from_microsd +import chains +import stash +from utils import xfp2str + +def create_casa_export(sw_wallet=None, addr_type=None, acct_num=0, multisig=False, legacy=False): + # Get public details about wallet. + # + # simple text format: + # key = value + # or #comments + # but value is JSON + from common import settings + from public_constants import AF_CLASSIC + + chain = chains.current_chain() + + with stash.SensitiveValues() as sv: + s = '''\ +# Passport Summary File +## For wallet with master key fingerprint: {xfp} + +Wallet operates on blockchain: {nb} + +For BIP44, this is coin_type '{ct}', and internally we use +symbol {sym} for this blockchain. + +## IMPORTANT WARNING + +Do **not** deposit to any address in this file unless you have a working +wallet system that is ready to handle the funds at that address! + +## Top-level, 'master' extended public key ('m/'): + +{xpub} +'''.format(nb=chain.name, xpub=chain.serialize_public(sv.node), + sym=chain.ctype, ct=chain.b44_cointype, xfp=xfp2str(settings.get('xfp'))) + + # print('create_casa_export() returning:\n{}'.format(s)) + return (s, None) # No 'acct_info' + + +CasaWallet = { + 'label': 'Casa', + 'sig_types': [ + {'id':'multisig', 'label':'Multsig', 'addr_type': None, 'create_wallet': create_casa_export, + 'import_microsd': read_multisig_config_from_microsd} + ], + 'export_modes': [ + {'id': 'microsd', 'label': 'microSD', 'filename_pattern': '{sd}/passport-casa.txt', 'ext': '.txt', + 'filename_pattern_multisig': '{sd}/passport-casa-multisig.json', 'ext_multisig': '.txt',} + ] +} diff --git a/ports/stm32/boards/Passport/modules/wallets/constants.py b/ports/stm32/boards/Passport/modules/wallets/constants.py new file mode 100644 index 0000000..3de6192 --- /dev/null +++ b/ports/stm32/boards/Passport/modules/wallets/constants.py @@ -0,0 +1,32 @@ +# SPDX-FileCopyrightText: 2021 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# constants.py - Software wallet constants +# + +from data_codecs.qr_type import QRType +from .bluewallet import BlueWallet + +SIG_TYPE_SINGLE = 'singlesig' +SIG_TYPE_MULTI = 'multisig' + +EXPORT_MODE_QR = 'qr' +EXPORT_MODE_MICROSD = 'microsd' + +TXN_FMT_PSBT = 'psbt' + +# Wallet export formats (xpub, ypub, zpub or some combination): +# - bitcoin_core +# - electrum +# - generic +XPUB_FMT_BITCOIN_CORE = 'bitcoin_core' +XPUB_FMT_ELECTRUM = 'electrum' +XPUB_FMT_GENERIC = 'generic' + +# Multisig import formats (single derivation path or one per signer?): +# - kv +# - kv_multi +# (Do we need to have different importers for these?) +MULTISIG_CONFIG_FMT_KV = 'kv' +MULTISIG_CONFIG_FMT_KV_MULTI = 'kv_multi' +MULTISIG_CONFIG_FMT_JSON = 'json' diff --git a/ports/stm32/boards/Passport/modules/wallets/dux_reserve.py b/ports/stm32/boards/Passport/modules/wallets/dux_reserve.py new file mode 100644 index 0000000..dd8e1e3 --- /dev/null +++ b/ports/stm32/boards/Passport/modules/wallets/dux_reserve.py @@ -0,0 +1,23 @@ +# SPDX-FileCopyrightText: 2021 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# dux_reserve.py - Dux Reserve wallet support +# + +from .generic_json_wallet import create_generic_json_wallet +from .multisig_json import create_multisig_json_wallet +from .multisig_import import read_multisig_config_from_qr, read_multisig_config_from_microsd +from data_codecs.qr_type import QRType +from public_constants import AF_P2WPKH + +DuxReserveWallet = { + 'label': 'Dux Reserve', + 'sig_types': [ + {'id':'single-sig', 'label':'Single-sig', 'addr_type': AF_P2WPKH, 'create_wallet': create_generic_json_wallet}, + {'id':'multisig', 'label':'Multsig', 'addr_type': None, 'create_wallet': create_multisig_json_wallet, + 'import_microsd': read_multisig_config_from_microsd} + ], + 'export_modes': [ + {'id': 'microsd', 'label': 'microSD', 'filename_pattern': '{sd}/passport-dux.json', 'filename_pattern_multisig': '{sd}/passport-dux-multisig.json'} + ] +} diff --git a/ports/stm32/boards/Passport/modules/wallets/electrum.py b/ports/stm32/boards/Passport/modules/wallets/electrum.py new file mode 100644 index 0000000..c07027d --- /dev/null +++ b/ports/stm32/boards/Passport/modules/wallets/electrum.py @@ -0,0 +1,141 @@ +# SPDX-FileCopyrightText: 2021 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# SPDX-FileCopyrightText: 2018 Coinkite, Inc. +# SPDX-License-Identifier: GPL-3.0-only +# +# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard +# and is covered by GPLv3 license found in COPYING. +# +# electrum.py - Electrum export +# + +import stash +import ujson +import chains +from utils import xfp2str, to_str +from .multisig_json import create_multisig_json_wallet +from .multisig_import import read_multisig_config_from_qr, read_multisig_config_from_microsd +from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH +from .utils import get_bip_num_from_addr_type + +def create_electrum_export(sw_wallet=None, addr_type=None, acct_num=0, multisig=False, legacy=False): + # Generate line-by-line JSON details about wallet. + # + # Much reverse engineering of Electrum here. It's a complex legacy file format. + from common import settings + + mode = get_bip_num_from_addr_type(addr_type, multisig) + + chain = chains.current_chain() + + with stash.SensitiveValues() as sv: + acct_path = "m/{mode}'/{coin}'/{acct}'".format( + mode=mode, + coin=chain.b44_cointype, + acct=acct_num) + + child_node = sv.derive_path(acct_path) + ckcc_xfp = settings.get('xfp') + ckcc_xpub = sv.chain.serialize_public(child_node, AF_CLASSIC) + xpub = sv.chain.serialize_public(child_node, addr_type) + # print('ckcc_xfp to export: {}'.format(ckcc_xfp)) + # print('ckcc_xpub to export: {}'.format(ckcc_xpub)) + # print('xpub to export: {}'.format(xpub)) + + rv = dict(seed_version=17, use_encryption=False, wallet_type='standard') + + label = 'Passport{} ({})'.format( + ' Acct. #%d' % acct_num if acct_num else '', + xfp2str(ckcc_xfp)) + + rv['keystore'] = dict(ckcc_xfp=ckcc_xfp, + ckcc_xpub=ckcc_xpub, + hw_type='passport', + type='hardware', + label=label, + derivation=acct_path, + xpub=xpub) + + # Return the possible account mappings that the exported wallet can choose from + # When we get the address back, we can determine the 'fmt' from the address and then look it up to + # Find the derivation path and account number. + accts = [ {'fmt':addr_type, 'deriv': acct_path, 'acct': acct_num} ] + msg = ujson.dumps(rv) + # print('msg={}'.format(to_str(msg))) + return (msg, accts) + + +# Zach found that Electrum will successfully import Passport with this format, while not also thinking that it is +# a Coldcard and expecting a USB connection. +# +# { +# "keystore": +# { +# "ckcc_xpub": "xpub6CFZUBDBeW2VBwrVDs1HKZvpueR7ceTuSY6pLkKdo7okmpw9NGrQYKLE3o4wBUS9aPeYfzxTsbuM4HXnyom8nZgmdJNVqh5mEEMrqDkVJ2g", +# "xpub": "zpub6qv65WZ1ws7StYEitaaXjk7qFai1VtSuGm9FuY7QZ8ZWt2ZbsbBXnSeW6Cz7BHjzPftAAx9anvcSprkvRCbAP33yMymM1WijmgV9cPRCgu6", +# "label": "Passport (317184B6)", +# "ckcc_xfp": 3062133041, +# "type": "bip32", +# "derivation": "m/84'/0'/0'" +# }, +# "wallet_type": "standard"} +# +def create_electrum_watch_only_export(sw_wallet=None, addr_type=None, acct_num=0, multisig=False, legacy=False): + from common import settings + + mode = get_bip_num_from_addr_type(addr_type, multisig) + + chain = chains.current_chain() + + with stash.SensitiveValues() as sv: + acct_path = "m/{mode}'/{coin}'/{acct}'".format( + mode=mode, + coin=chain.b44_cointype, + acct=acct_num) + + + print('acct_path={} addr_type={} multisig={}'.format(acct_path, addr_type, multisig)) + + child_node = sv.derive_path(acct_path) + ckcc_xfp = settings.get('xfp') + ckcc_xpub = sv.chain.serialize_public(child_node, AF_CLASSIC) + xpub = sv.chain.serialize_public(child_node, addr_type) + # print('ckcc_xfp to export: {}'.format(ckcc_xfp)) + # print('ckcc_xpub to export: {}'.format(ckcc_xpub)) + # print('xpub to export: {}'.format(xpub)) + + rv = dict(wallet_type='standard') + + label = 'Passport{} ({})'.format( + ' Acct. #%d' % acct_num if acct_num else '', + xfp2str(ckcc_xfp)) + + rv['keystore'] = dict(ckcc_xpub=ckcc_xpub, + xpub=xpub, + label=label, + ckcc_xfp=ckcc_xfp, + type='bip32', + derivation=acct_path, + ) + + # Return the possible account mappings that the exported wallet can choose from + # When we get the address back, we can determine the 'fmt' from the address and then look it up to + # Find the derivation path and account number. + accts = [ {'fmt':addr_type, 'deriv': acct_path, 'acct': acct_num} ] + msg = ujson.dumps(rv) + # print('msg={}'.format(to_str(msg))) + return (msg, accts) + + +ElectrumWallet = { + 'label': 'Electrum', + 'sig_types': [ + {'id':'single-sig', 'label':'Single-sig', 'addr_type': AF_P2WPKH, 'create_wallet': create_electrum_watch_only_export}, + {'id':'multisig', 'label':'Multsig', 'addr_type': None, 'create_wallet': create_multisig_json_wallet, + 'import_microsd': read_multisig_config_from_microsd} + ], + 'export_modes': [ + {'id': 'microsd', 'label': 'microSD', 'filename_pattern': '{sd}/passport-electrum.json', 'filename_pattern_multisig': '{sd}/passport-electrum-multisig.json'} + ] +} diff --git a/ports/stm32/boards/Passport/modules/wallets/fullynoded.py b/ports/stm32/boards/Passport/modules/wallets/fullynoded.py new file mode 100644 index 0000000..83fe608 --- /dev/null +++ b/ports/stm32/boards/Passport/modules/wallets/fullynoded.py @@ -0,0 +1,24 @@ +# SPDX-FileCopyrightText: 2021 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# fullynoded.py - FullyNoded wallet support +# + +from .electrum import create_electrum_export +from .multisig_json import create_multisig_json_wallet +from .multisig_import import read_multisig_config_from_qr, read_multisig_config_from_microsd +from data_codecs.qr_type import QRType +from public_constants import AF_P2WPKH + +FullyNodedWallet = { + 'label': 'FullyNoded', + 'sig_types': [ + {'id':'single-sig', 'label':'Single-sig', 'addr_type': AF_P2WPKH, 'create_wallet': create_electrum_export}, + {'id':'multisig', 'label':'Multsig', 'addr_type': None, 'create_wallet': create_multisig_json_wallet, + 'import_qr': read_multisig_config_from_qr, 'import_microsd': read_multisig_config_from_microsd} + ], + 'export_modes': [ + {'id': 'qr', 'label': 'QR Code', 'qr_type': QRType.UR2}, + {'id': 'microsd', 'label': 'microSD', 'filename_pattern': '{sd}/passport-fullynoded.json', 'filename_pattern_multisig': '{sd}/passport-fullynoded-multisig.json'} + ] +} diff --git a/ports/stm32/boards/Passport/modules/wallets/generic_json_wallet.py b/ports/stm32/boards/Passport/modules/wallets/generic_json_wallet.py new file mode 100644 index 0000000..3dec8cc --- /dev/null +++ b/ports/stm32/boards/Passport/modules/wallets/generic_json_wallet.py @@ -0,0 +1,62 @@ +# SPDX-FileCopyrightText: 2021 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# SPDX-FileCopyrightText: 2018 Coinkite, Inc. +# SPDX-License-Identifier: GPL-3.0-only +# +# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard +# and is covered by GPLv3 license found in COPYING. +# +# generic_json_wallet.py - Generic JSON Wallet export +# +import chains +import ujson +import stash +from utils import xfp2str, to_str +from common import settings +from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2WSH_P2SH, AF_P2WSH + +def create_generic_json_wallet(sw_wallet=None, addr_type=None, acct_num=0, multisig=False, legacy=False): + # Generate data that other programers will use to import from (single-signer) + + chain = chains.current_chain() + rv = dict(chain=chain.ctype, + # Don't include these for privacy reasons + xpub = settings.get('xpub'), + xfp = xfp2str(settings.get('xfp')), + account = acct_num, + ) + + accts = [] + + with stash.SensitiveValues() as sv: + # Each of these paths will have /{change}/{idx} in usage (not hardened) + for name, deriv, fmt, atype, is_multisig in [ + ( 'bip44', "m/44'/0'/{acct}'", AF_CLASSIC, 'p2pkh', False ), + ( 'bip49', "m/49'/0'/{acct}'", AF_P2WPKH_P2SH, 'p2sh-p2wpkh', False ), # was "p2wpkh-p2sh" + ( 'bip84', "m/84'/0'/{acct}'", AF_P2WPKH, 'p2wpkh', False ), + ( 'bip48_1', "m/48'/0'/{acct}'/1'", AF_P2WSH_P2SH, 'p2sh-p2wsh', True ), + ( 'bip48_2', "m/48'/0'/{acct}'/2'", AF_P2WSH, 'p2wsh', True ), + ]: + dd = deriv.format(acct=acct_num) + node = sv.derive_path(dd) + xfp = xfp2str(node.my_fingerprint()) + xp = chain.serialize_public(node, AF_CLASSIC) + zp = chain.serialize_public(node, fmt) if fmt != AF_CLASSIC else None + + if is_multisig: + first_address = None + else: + # Include first non-change address for single-sig wallets: 0/0 + node.derive(0) + node.derive(0) + first_address = chain.address(node, fmt) + + accts.append({'fmt':fmt, 'deriv': dd, 'acct': acct_num, 'xfp': xfp}) + + rv[name] = dict(deriv=dd, xpub=xp, xfp=xfp, first=first_address, name=atype) + if zp: + rv[name]['_pub'] = zp + + msg = ujson.dumps(rv) + return (msg, accts) diff --git a/ports/stm32/boards/Passport/modules/wallets/gordian.py b/ports/stm32/boards/Passport/modules/wallets/gordian.py new file mode 100644 index 0000000..495279d --- /dev/null +++ b/ports/stm32/boards/Passport/modules/wallets/gordian.py @@ -0,0 +1,24 @@ +# SPDX-FileCopyrightText: 2021 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# gordian.py - Gordian wallet support +# + +from .electrum import create_electrum_export +from .multisig_json import create_multisig_json_wallet +from .multisig_import import read_multisig_config_from_qr, read_multisig_config_from_microsd +from data_codecs.qr_type import QRType +from public_constants import AF_P2WPKH + +GordianWallet = { + 'label': 'Gordian', + 'sig_types': [ + {'id':'single-sig', 'label':'Single-sig', 'addr_type': AF_P2WPKH, 'create_wallet': create_electrum_export}, + {'id':'multisig', 'label':'Multsig', 'addr_type': None, 'create_wallet': create_multisig_json_wallet, + 'import_qr': read_multisig_config_from_qr, 'import_microsd': read_multisig_config_from_microsd} + ], + 'export_modes': [ + {'id': 'qr', 'label': 'QR Code', 'qr_type': QRType.UR2}, + {'id': 'microsd', 'label': 'microSD', 'filename_pattern': '{sd}/passport-gordian.json', 'filename_pattern_multisig': '{sd}/passport-gordian-multisig.json'} + ] +} diff --git a/ports/stm32/boards/Passport/modules/wallets/lily.py b/ports/stm32/boards/Passport/modules/wallets/lily.py new file mode 100644 index 0000000..17634fe --- /dev/null +++ b/ports/stm32/boards/Passport/modules/wallets/lily.py @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: 2021 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# lily.py - Lily wallet support +# + +from .electrum import create_electrum_export +from .multisig_json import create_multisig_json_wallet +from .multisig_import import read_multisig_config_from_qr, read_multisig_config_from_microsd +from data_codecs.qr_type import QRType +from public_constants import AF_P2WPKH + +LilyWallet = { + 'label': 'Lily', + 'sig_types': [ + {'id':'single-sig', 'label':'Single-sig', 'addr_type': AF_P2WPKH, 'create_wallet': create_electrum_export}, + {'id':'multisig', 'label':'Multsig', 'addr_type': None, 'create_wallet': create_multisig_json_wallet, + # 'import_qr': read_multisig_config_from_qr, + 'import_microsd': read_multisig_config_from_microsd} + ], + 'export_modes': [ + # {'id': 'qr', 'label': 'QR Code', 'qr_type': QRType.UR1}, + {'id': 'microsd', 'label': 'microSD', 'filename_pattern': '{sd}/passport-lily.json', 'filename_pattern_multisig': '{sd}/passport-lily-multisig.json'} + ] +} diff --git a/ports/stm32/boards/Passport/modules/wallets/multisig_import.py b/ports/stm32/boards/Passport/modules/wallets/multisig_import.py new file mode 100644 index 0000000..450e43d --- /dev/null +++ b/ports/stm32/boards/Passport/modules/wallets/multisig_import.py @@ -0,0 +1,58 @@ +# SPDX-FileCopyrightText: 2021 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# SPDX-FileCopyrightText: 2018 Coinkite, Inc. +# SPDX-License-Identifier: GPL-3.0-only +# +# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard +# and is covered by GPLv3 license found in COPYING. +# +# multisig_import.py - Multsig config import +# + +from ux import ux_scan_qr_code, ux_show_story +import common +from common import system + +async def read_multisig_config_from_qr(): + system.turbo(True); + data = await ux_scan_qr_code('Import Multisig') + system.turbo(False); + + if isinstance(data, (bytes, bytearray)): + data = data.decode('utf-8') + + return data + + +async def read_multisig_config_from_microsd(): + from files import CardSlot, CardMissingError + from actions import needs_microsd, file_picker + + def possible(filename): + with open(filename, 'rt') as fd: + for ln in fd: + if 'pub' in ln: + return True + + fn = await file_picker('Select multisig configuration file to import (.txt)', suffix='.txt', + min_size=100, max_size=40*200, taster=possible) + + if not fn: + return None + + system.turbo(True); + try: + with CardSlot() as card: + with open(fn, 'rt') as fp: + data = fp.read() + except CardMissingError: + system.turbo(False); + await needs_microsd() + return None + + system.turbo(False); + + # print('data={}'.format(data)) + + return data diff --git a/ports/stm32/boards/Passport/modules/wallets/multisig_json.py b/ports/stm32/boards/Passport/modules/wallets/multisig_json.py new file mode 100644 index 0000000..beacf03 --- /dev/null +++ b/ports/stm32/boards/Passport/modules/wallets/multisig_json.py @@ -0,0 +1,47 @@ +# SPDX-FileCopyrightText: 2021 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# SPDX-FileCopyrightText: 2018 Coinkite, Inc. +# SPDX-License-Identifier: GPL-3.0-only +# +# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard +# and is covered by GPLv3 license found in COPYING. +# +# multisig_json.py - Multsig export format +# + +import stash +import uio +from utils import xfp2str +from .utils import get_next_account_num +from common import settings +from public_constants import AF_P2SH, AF_P2WSH, AF_P2WSH_P2SH + + +def create_multisig_json_wallet(sw_wallet=None, addr_type=None, acct_num=0, multisig=False, legacy=False): + fp = uio.StringIO() + + fp.write('{\n') + accts = [] + with stash.SensitiveValues() as sv: + + for deriv, name, fmt in [ + ("m/45'", 'p2sh', AF_P2SH), + ("m/48'/0'/{acct}'/1'", 'p2wsh_p2sh', AF_P2WSH_P2SH), + ("m/48'/0'/{acct}'/2'", 'p2wsh', AF_P2WSH) + ]: + # Fill in the acct number + dd = deriv.format(acct=acct_num) + node = sv.derive_path(dd) + xfp = xfp2str(node.my_fingerprint()) + xpub = sv.chain.serialize_public(node, fmt) + fp.write(' "%s_deriv": "%s",\n' % (name, dd)) + fp.write(' "%s": "%s",\n' % (name, xpub)) + + accts.append( {'fmt': fmt, 'deriv': dd, 'acct': acct_num} ) # e.g., AF_P2WSH_P2SH: {'deriv':m/48'/0'/4'/1', 'acct': 4} + + xfp = xfp2str(settings.get('xfp', 0)) + fp.write(' "xfp": "%s"\n}\n' % xfp) + result = fp.getvalue() + # print('xpub json = {}'.format(result)) + return (result, accts) diff --git a/ports/stm32/boards/Passport/modules/wallets/sparrow.py b/ports/stm32/boards/Passport/modules/wallets/sparrow.py new file mode 100644 index 0000000..4b93fa4 --- /dev/null +++ b/ports/stm32/boards/Passport/modules/wallets/sparrow.py @@ -0,0 +1,23 @@ +# SPDX-FileCopyrightText: 2021 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# sparrow.py - Sparrow wallet support +# + +from .generic_json_wallet import create_generic_json_wallet +from .multisig_json import create_multisig_json_wallet +from .multisig_import import read_multisig_config_from_qr, read_multisig_config_from_microsd +from data_codecs.qr_type import QRType + +SparrowWallet = { + 'label': 'Sparrow', + 'sig_types': [ + {'id':'single-sig', 'label':'Single-sig', 'addr_type': None, 'create_wallet': create_generic_json_wallet}, + {'id':'multisig', 'label':'Multsig', 'addr_type': None, 'create_wallet': create_multisig_json_wallet, + 'import_qr': read_multisig_config_from_qr, 'import_microsd': read_multisig_config_from_microsd} + ], + 'export_modes': [ + {'id': 'qr', 'label': 'QR Code', 'qr_type': QRType.UR2}, + {'id': 'microsd', 'label': 'microSD', 'filename_pattern': '{sd}/passport-sparrow.json', 'filename_pattern_multisig': '{sd}/passport-sparrow-multisig.json'} + ] +} diff --git a/ports/stm32/boards/Passport/modules/wallets/specter.py b/ports/stm32/boards/Passport/modules/wallets/specter.py new file mode 100644 index 0000000..e0f183c --- /dev/null +++ b/ports/stm32/boards/Passport/modules/wallets/specter.py @@ -0,0 +1,24 @@ +# SPDX-FileCopyrightText: 2021 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# specter.py - Specter wallet support +# + +from .electrum import create_electrum_export +from .multisig_json import create_multisig_json_wallet +from .multisig_import import read_multisig_config_from_qr, read_multisig_config_from_microsd +from data_codecs.qr_type import QRType +from public_constants import AF_P2WPKH + +SpecterWallet = { + 'label': 'Specter', + 'sig_types': [ + {'id':'single-sig', 'label':'Single-sig', 'addr_type': AF_P2WPKH, 'create_wallet': create_electrum_export}, + {'id':'multisig', 'label':'Multsig', 'addr_type': None, 'create_wallet': create_multisig_json_wallet, + 'import_qr': read_multisig_config_from_qr, 'import_microsd': read_multisig_config_from_microsd} + ], + 'export_modes': [ + # {'id': 'qr', 'label': 'QR Code', 'qr_type': QRType.UR1}, + {'id': 'microsd', 'label': 'microSD', 'filename_pattern': '{sd}/passport-specter.json', 'filename_pattern_multisig': '{sd}/passport-specter-multisig.json'} + ] +} diff --git a/ports/stm32/boards/Passport/modules/wallets/sw_wallets.py b/ports/stm32/boards/Passport/modules/wallets/sw_wallets.py new file mode 100644 index 0000000..23f1a94 --- /dev/null +++ b/ports/stm32/boards/Passport/modules/wallets/sw_wallets.py @@ -0,0 +1,37 @@ +# SPDX-FileCopyrightText: 2021 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# sw_wallets.py - Software wallet config data for all supported wallets +# + +from data_codecs.qr_type import QRType +from .bitcoin_core import BitcoinCoreWallet +from .bluewallet import BlueWallet +from .btcpay import BtcPayWallet +from .casa import CasaWallet +from .caravan import CaravanWallet +from .dux_reserve import DuxReserveWallet +from .electrum import ElectrumWallet +# from .fullynoded import FullyNodedWallet +# from .gordian import GordianWallet +# from .lily import LilyWallet +from .sparrow import SparrowWallet +from .specter import SpecterWallet +from .wasabi import WasabiWallet + +# Array of all supported software wallets and their attributes -- used to build wallet menus and drive their behavior +supported_software_wallets = [ + BitcoinCoreWallet, + BlueWallet, + BtcPayWallet, + CaravanWallet, + CasaWallet, + DuxReserveWallet, + ElectrumWallet, + # FullyNodedWallet, + # GordianWallet, + # LilyWallet, + SparrowWallet, + SpecterWallet, + WasabiWallet, +] diff --git a/ports/stm32/boards/Passport/modules/wallets/utils.py b/ports/stm32/boards/Passport/modules/wallets/utils.py new file mode 100644 index 0000000..c99317e --- /dev/null +++ b/ports/stm32/boards/Passport/modules/wallets/utils.py @@ -0,0 +1,181 @@ +# SPDX-FileCopyrightText: 2021 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# utils.py - Wallet utils +# + +import common +from common import settings +from public_constants import AF_CLASSIC, AF_P2SH, AF_P2WPKH_P2SH, AF_P2WSH_P2SH, AF_P2WPKH, AF_P2WSH +from constants import DEFAULT_ACCOUNT_ENTRY +import stash +from log import log +from utils import get_accounts + +# Dynamic find the next account number rather than storing it - we never want to skip an account number +# since that would create gaps and potentially make recovering funds harder if we exceeded the gap limit. +def get_next_account_num(): + accts = get_accounts() + + acct_nums = [] + for acct in accts: + acct_nums.append(acct['acct_num']) + + acct_nums.sort() + curr_acct_num = 0 + + # This should normally be sequentially sorted from 0 onward, monotonically increasing by 1. + # If we find it is not then there's a hole in the sequence and we can use it. + # That should only happen if the user manually adds custom accounts that cause a gap in the range. + for i in range(len(acct_nums)): + if acct_nums[curr_acct_num] != curr_acct_num: + return curr_acct_num + curr_acct_num += 1 + + return curr_acct_num + +# TODO: Make this a data table and drive these function from it +# P2PKH / Classic 1 Single Base58 check x m/44'/0'/{acct} +# P2SH P2WPKH 3 Single Base58 check y m/49'/0'/{acct} +# P2WPKH bc1 Single Bech32 z m/84'/0'/{acct} + +def get_addr_type_from_address(address, is_multisig): + if len(address) < 26: + return None + + if address[0] == '1': + return AF_P2SH if is_multisig else AF_CLASSIC + elif address[0] == '3': + return AF_P2WSH_P2SH if is_multisig else AF_P2WPKH_P2SH + elif address[0] == 'b' and address[1] == 'c' and address[2] == '1': + return AF_P2WSH if is_multisig else AF_P2WPKH + + return None + +def get_bip_num_from_addr_type(addr_type, is_multisig): + if is_multisig: + if addr_type == AF_P2WSH_P2SH: + return 48 + elif addr_type == AF_P2WSH: + return 48 + else: + if addr_type == AF_CLASSIC: + return 44 + elif addr_type == AF_P2WPKH: + return 84 + elif addr_type == AF_P2WPKH_P2SH: + return 49 + else: + raise ValueError(addr_type) + +def get_addr_type_from_deriv(path): + type_str = get_addr_type_from_deriv_path(path) + subpath = get_part_from_deriv_path(path, 4) + + if type_str == '44': + return AF_CLASSIC + elif type_str == '49': + return AF_P2WPKH_P2SH + elif type_str == '84': + return AF_P2WPKH + elif type_str == '48': + if subpath == '1': + return AF_P2WSH_P2SH + elif subpath == '2': + return AF_P2WSH + + return None + +def get_deriv_fmt_from_address(address, is_multisig): + # print('get_deriv_fmt_from_address(): address={} is_multisig={}'.format(address, is_multisig)) + if len(address) < 26: + return None + + # Map the address prefix to a standard derivation path and insert the account number + if is_multisig: + if address[0] == '3': + return "m/48'/0'/{acct}'/1'" + elif address[0] == 'b' and address[1] == 'c' and address[2] == '1': + return "m/48'/0'/{acct}'/2'" + else: + if address[0] == '1': + return "m/44'/0'/{acct}'" + elif address[0] == '3': + return "m/49'/0'/{acct}'" + elif address[0] == 'b' and address[1] == 'c' and address[2] == '1': + return "m/84'/0'/{acct}'" + + return None + +def get_deriv_fmt_from_addr_type(addr_type, is_multisig): + # print('get_deriv_fmt_from_addr_type(): addr_type={} is_multisig={}'.format(addr_type, is_multisig)) + + # Map the address prefix to a standard derivation path and insert the account number + if is_multisig: + if addr_type == AF_P2WSH_P2SH: + return "m/48'/0'/{acct}'/1'" + elif addr_type == AF_P2WSH: + return "m/48'/0'/{acct}'/2'" + else: + if addr_type == AF_CLASSIC: + return "m/44'/0'/{acct}'" + elif addr_type == AF_P2WPKH_P2SH: + return "m/49'/0'/{acct}'" + elif addr_type == AF_P2WPKH: + return "m/84'/0'/{acct}'" + + return None + +def get_deriv_path_from_addr_type_and_acct(addr_type, acct_num, is_multisig): + # print('get_deriv_path_from_addr_type_and_acct(): addr_type={} acct={} is_multisig={}'.format(addr_type, acct_num, is_multisig)) + fmt = get_deriv_fmt_from_addr_type(addr_type, is_multisig) + if fmt != None: + return fmt.format(acct=acct_num) + + return None + +# For single sig only +def get_deriv_path_from_address_and_acct(address, acct, is_multisig): + # print('get_deriv_path_from_address_and_acct(): address={} acct={} is_multisig={}'.format(address, acct, is_multisig)) + fmt = get_deriv_fmt_from_address(address, is_multisig) + if fmt != None: + return fmt.format(acct=acct) + + return None + +def get_acct_num_from_deriv_path(path): + parts = path.split('/') + if parts[3][-1] == "'": + return int(parts[3][0:-1]) + else: + return int(parts[3]) + +def get_addr_type_from_deriv_path(path): + parts = path.split('/') + if parts[1][-1] == "'": + return int(parts[1][0:-1]) + else: + return int(parts[1]) + +def get_part_from_deriv_path(path, index): + parts = path.split('/') + if parts[index][-1] == "'": + return int(parts[index][0:-1]) + else: + return int(parts[index]) + +def has_export_mode(mode_id): + acct = common.active_account + if acct: + for mode in acct.sw_wallet['export_modes']: + if mode['id'] == mode_id: + return True + + return False + +def get_export_mode(sw_wallet, mode_id): + for mode in sw_wallet['export_modes']: + if mode['id'] == mode_id: + # log('Returning mode={}'.format(mode)) + return mode + return None diff --git a/ports/stm32/boards/Passport/modules/wallets/vault.py b/ports/stm32/boards/Passport/modules/wallets/vault.py new file mode 100644 index 0000000..8610012 --- /dev/null +++ b/ports/stm32/boards/Passport/modules/wallets/vault.py @@ -0,0 +1,30 @@ +# SPDX-FileCopyrightText: 2021 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# vault.py - Export format used by some wallets +# + +import stash +import ujson +from utils import xfp2str, to_str +from .multisig_json import create_multisig_json_wallet +from .multisig_import import read_multisig_config_from_qr, read_multisig_config_from_microsd +from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH + +def create_vault_export(sw_wallet=None, addr_type=None, acct_num=0, multisig=False, legacy=False): + from common import settings, system + + (fw_version, _, _, _) = system.get_software_info() + acct_path = "84'/0'/{acct}'".format(acct=acct_num) + master_xfp = xfp2str(settings.get('xfp')) + + with stash.SensitiveValues() as sv: + child_node = sv.derive_path(acct_path) + xpub = sv.chain.serialize_public(child_node, addr_type) + + msg = ujson.dumps(dict(ExtPubKey=xpub, MasterFingerprint=master_xfp, AccountKeyPath=acct_path, FirmwareVersion=fw_version)) + + accts = [ {'fmt':AF_P2WPKH, 'deriv': acct_path, 'acct': acct_num} ] + + print('msg={}'.format(to_str(msg))) + return (msg, accts) diff --git a/ports/stm32/boards/Passport/modules/wallets/wasabi.py b/ports/stm32/boards/Passport/modules/wallets/wasabi.py new file mode 100644 index 0000000..db4c875 --- /dev/null +++ b/ports/stm32/boards/Passport/modules/wallets/wasabi.py @@ -0,0 +1,54 @@ +# SPDX-FileCopyrightText: 2021 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# SPDX-FileCopyrightText: 2018 Coinkite, Inc. +# SPDX-License-Identifier: GPL-3.0-only +# +# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard +# and is covered by GPLv3 license found in COPYING. +# +# wasabi.py - Wasabi wallet +# + +from public_constants import AF_P2WPKH, AF_CLASSIC +import chains +import stash +import ujson +from utils import xfp2str, to_str +from common import settings, system + +def create_wasabi_export(sw_wallet=None, addr_type=None, acct_num=0, multisig=False, legacy=False): + # Generate the data for a JSON file which Wasabi can open directly as a new wallet. + + btc = chains.BitcoinMain + + with stash.SensitiveValues() as sv: + acct_path = "m/84'/0'/{acct}'".format(acct=acct_num) + node = sv.derive_path(acct_path) + xfp = xfp2str(settings.get('xfp')) + xpub = btc.serialize_public(node, AF_CLASSIC) + + chain = chains.current_chain() + assert chain.ctype in {'BTC', 'TBTC'}, "Only Bitcoin supported" + + (fw_version, _, _, _) = system.get_software_info() + + rv = dict(MasterFingerprint=xfp, + ExtPubKey=xpub, + FirmwareVersion=fw_version) + + accts = [ {'fmt': AF_P2WPKH, 'deriv': acct_path, 'acct': acct_num} ] + msg = ujson.dumps(rv) + # print('msg={}'.format(to_str(msg))) + return (msg, accts) + + +WasabiWallet = { + 'label': 'Wasabi', + 'sig_types': [ + {'id':'single-sig', 'label':'Single-sig', 'addr_type': AF_P2WPKH, 'create_wallet': create_wasabi_export}, + ], + 'export_modes': [ + {'id': 'microsd', 'label': 'microSD', 'filename_pattern': '{sd}/passport-wasabi.json', 'filename_pattern_multisig': '{sd}/passport-wasabi-multisig.json'} + ] +} diff --git a/ports/stm32/boards/Passport/mpconfigboard.h b/ports/stm32/boards/Passport/mpconfigboard.h index 66b7516..1aa838f 100644 --- a/ports/stm32/boards/Passport/mpconfigboard.h +++ b/ports/stm32/boards/Passport/mpconfigboard.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // @@ -7,9 +7,6 @@ #define MICROPY_PASSPORT -// #define MICROPY_PASSPORT_HW_REV1 -#define MICROPY_PASSPORT_HW_REV2 - #define MICROPY_HW_ENABLE_RTC (1) #define MICROPY_HW_ENABLE_RNG (1) #define MICROPY_HW_ENABLE_ADC (1) @@ -24,6 +21,11 @@ #define MICROPY_HW_ENABLE_INTERNAL_FLASH_STORAGE (0) #define MICROPY_HW_USB_MSC (0) +/* Turn off the network functionality */ +#define MICROPY_PY_NETWORK (0) +#define MICROPY_PY_USOCKET (0) +#define MICROPY_PY_UHASHLIB_SHA256 (0) + #define PASSPORT_FOUNDATION_ENABLED (1) #define MICROPY_BOARD_EARLY_INIT Passport_board_early_init @@ -51,7 +53,7 @@ void Passport_board_init(void); #define PASSPORT_KEYPAD_END_ATOMIC_SECTION(state) enable_irq(state); keypad_write(KBD_ADDR, KBD_REG_INT_STAT, 0xFF) -// The board has an 8MHz HSE, the following gives 400MHz CPU speed +// The board has an 8MHz HSE, the following gives 480MHz CPU speed #define MICROPY_HW_CLK_PLLM (1) #define MICROPY_HW_CLK_PLLN (120) //(19) #define MICROPY_HW_CLK_PLLP (2) diff --git a/ports/stm32/boards/Passport/mpconfigboard.mk b/ports/stm32/boards/Passport/mpconfigboard.mk index 682aad0..4e2215c 100644 --- a/ports/stm32/boards/Passport/mpconfigboard.mk +++ b/ports/stm32/boards/Passport/mpconfigboard.mk @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: GPL-3.0-or-later # @@ -14,9 +14,9 @@ LD_FILES = boards/Passport/passport.ld boards/common_ifs.ld TEXT0_ADDR = 0x08020800 # MicroPython settings -MICROPY_PY_LWIP = 1 -MICROPY_PY_USSL = 1 -MICROPY_SSL_MBEDTLS = 1 +MICROPY_PY_LWIP = 0 +MICROPY_PY_USSL = 0 +MICROPY_SSL_MBEDTLS = 0 FROZEN_MANIFEST = boards/Passport/manifest.py @@ -55,17 +55,25 @@ build-Passport/boards/Passport/crypto/%.o: CFLAGS_MOD += \ CFLAGS_MOD += -Iboards/$(BOARD)/trezor-firmware/core/embed/extmod/modtrezorcrypto -Iboards/$(BOARD)/trezor-firmware/core SRC_MOD += $(addprefix boards/$(BOARD)/trezor-firmware/core/embed/extmod/modtrezorcrypto/, modtrezorcrypto.c crc.c) -BL_NVROM_BASE = 0x081c0000 -BL_NVROM_SIZE = 0x20000 +BL_NVROM_BASE = 0x0801FF00 +BL_NVROM_SIZE = 0x100 CFLAGS_MOD += -DBL_NVROM_BASE=$(BL_NVROM_BASE) -DBL_NVROM_SIZE=$(BL_NVROM_SIZE) -CFLAGS_MOD += -Iboards/$(BOARD)/include +CFLAGS_MOD += -Iboards/$(BOARD)/include -Iboards/$(BOARD)/common/micro-ecc # include code common to both the bootloader and firmware SRC_MOD += $(addprefix boards/$(BOARD)/common/,\ - delay.c \ - lcd-sharp-ls018B7dh02.c \ - pprng.c \ - se.c \ - sha256.c \ - spiflash.c \ - utils.c ) + backlight.c \ + delay.c \ + display.c \ + gpio.c \ + keypad-adp-5587.c \ + lcd-sharp-ls018B7dh02.c \ + pprng.c \ + ring_buffer.c \ + se.c \ + sha256.c \ + spiflash.c \ + utils.c \ + hash.c \ + micro-ecc/uECC.c \ + passport_fonts.c ) diff --git a/ports/stm32/boards/Passport/passport.ld b/ports/stm32/boards/Passport/passport.ld index f8b727f..2e022fa 100644 --- a/ports/stm32/boards/Passport/passport.ld +++ b/ports/stm32/boards/Passport/passport.ld @@ -1,5 +1,5 @@ /* - SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. + SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. SPDX-License-Identifier: GPL-3.0-or-later GNU linker script for STM32H753 @@ -25,10 +25,10 @@ MEMORY _minimum_stack_size = 2K; _minimum_heap_size = 16K; -/* Define the stack. The stack is full descending so begins just above last byte - of RAM. Note that EABI requires the stack to be 8-byte aligned for a call. */ +/* Define the stack. The stack is full descending so begins just above last byte + of RAM. Note that EABI requires the stack to be 8-byte aligned for a call. */ _estack = ORIGIN(RAM) + LENGTH(RAM) - _estack_reserve; -_sstack = _estack - 32K; /* tunable */ +_sstack = _estack - 48K; /* tunable */ /* RAM extents for the garbage collector */ _ram_start = ORIGIN(RAM); diff --git a/ports/stm32/boards/Passport/pins.c b/ports/stm32/boards/Passport/pins.c index 9dddc65..8bfa300 100644 --- a/ports/stm32/boards/Passport/pins.c +++ b/ports/stm32/boards/Passport/pins.c @@ -1,7 +1,7 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // -// SPDX-FileCopyrightText: 2018 Coinkite, Inc. +// SPDX-FileCopyrightText: 2018 Coinkite, Inc. // SPDX-License-Identifier: GPL-3.0-only // /* @@ -26,13 +26,13 @@ #include "debug-utils.h" // Number of iterations for KDF -#define KDF_ITER_WORDS 12 +#define KDF_ITER_WORDS 2 #define KDF_ITER_PIN 8 // about ? seconds (measured in-system) // We try to keep at least this many PIN attempts available to legit users // - challenge: comparitor resolution is only 32 units (5 LSB not implemented) // - solution: adjust both the target and counter (upwards) -#define MAX_TARGET_ATTEMPTS 13 +#define MAX_TARGET_ATTEMPTS 21 // Pretty sure it doesn't matter, but adding some salt into our PIN->bytes[32] code // based on the purpose of the PIN code. @@ -41,6 +41,9 @@ #define PIN_PURPOSE_ANTI_PHISH_WORDS 0x2e6d6773 #define PIN_PURPOSE_SUPPLY_CHAIN_WORDS 0xb6c9f792 +// Keep this around after the user logs in successfully +uint8_t g_cached_main_pin[32]; + // Hash up a PIN for indicated purpose. static void pin_hash(const char *pin, int pin_len, uint8_t result[32], uint32_t purpose); @@ -71,7 +74,7 @@ static bool pin_is_blank(uint8_t keynum) // static bool is_main_pin(const uint8_t digest[32], int *pin_kn) { - int kn = KEYNUM_main_pin; + int kn = KEYNUM_pin_hash; se_reset_chip(); se_pair_unlock(); @@ -125,7 +128,7 @@ static void pin_hash(const char *pin, int pin_len, uint8_t result[32], uint32_t // static int pin_hash_attempt(uint8_t target_kn, const char *pin, int pin_len, uint8_t result[32]) { - uint8_t tmp[32]; + uint8_t tmp[32]; if (pin_len == 0) { // zero len PIN is the "blank" value: all zeros, no hashing @@ -149,7 +152,7 @@ static int pin_hash_attempt(uint8_t target_kn, const char *pin, int pin_len, uin } memcpy(tmp, result, 32); - if (target_kn == KEYNUM_main_pin) { + if (target_kn == KEYNUM_pin_hash) { se_mixin_key(KEYNUM_pin_attempt, tmp, result); } else { se_mixin_key(0, tmp, result); @@ -173,10 +176,10 @@ void pin_cache_get_key(uint8_t key[32]) // pin_cache_save() // -static void pin_cache_save(pinAttempt_t *args, uint8_t *digest) +static int pin_cache_save(pinAttempt_t *args, uint8_t *digest) { // encrypt w/ rom secret + SRAM seed value - uint8_t value[32]; + uint8_t value[32]; if (!check_all_zeros(digest, 32)) { pin_cache_get_key(value); @@ -185,20 +188,26 @@ static void pin_cache_save(pinAttempt_t *args, uint8_t *digest) memset(value, 0, 32); } -#ifdef FIXME - ASSERT(args->magic_value == PA_MAGIC_V1); -#endif /* FIXME */ + if (args->magic_value != PA_MAGIC_V1) { + return EPIN_BAD_MAGIC; + } + memcpy(args->cached_main_pin, value, 32); + + // Keep a copy around so we can do other auth'd actions later like set a user firmware pubkey + memcpy(g_cached_main_pin, value, 32); + return 0; } // pin_cache_restore() // -static void pin_cache_restore(pinAttempt_t *args, uint8_t digest[32]) +int pin_cache_restore(pinAttempt_t *args, uint8_t digest[32]) { // decrypt w/ rom secret + SRAM seed value -#ifdef FIXME - ASSERT(args->magic_value == PA_MAGIC_V1); -#endif /* FIXME */ + if (args->magic_value != PA_MAGIC_V1) { + return EPIN_BAD_MAGIC; + } + memcpy(digest, args->cached_main_pin, 32); if (!check_all_zeros(digest, 32)) { @@ -206,13 +215,15 @@ static void pin_cache_restore(pinAttempt_t *args, uint8_t digest[32]) pin_cache_get_key(key); xor_mixin(digest, key, 32); } + + return 0; } // anti_phishing_words() // -// Look up some bits... do HMAC(words secret) and return some LSB's +// Do HMAC(words secret) and return digest // -// CAUTIONS: +// CAUTIONS: // - rate-limited by the chip, since it takes many iterations of HMAC(key we dont have) // - hash generated is shown on bus (but further hashing happens after that) // @@ -225,8 +236,6 @@ int anti_phishing_words(const char *pin_prefix, int prefix_len, uint32_t *result pin_hash(pin_prefix, prefix_len, tmp, PIN_PURPOSE_ANTI_PHISH_WORDS); // Using 608a, we can do key stretching to get good built-in delays - se_setup(); - int rv = se_stretch_iter(tmp, digest, KDF_ITER_WORDS); se_reset_chip(); @@ -242,25 +251,19 @@ int anti_phishing_words(const char *pin_prefix, int prefix_len, uint32_t *result // supply_chain_validation_words() // -// TODO: Validate message is signed by us using pub key stored in ROM secrets +// Perform HMAC_SHA256 using SE and supply chain slot. This hashes the given +// data with the secret value in the SE and returns the result. // -// TODO: Hash given data with private key from the SE +// Caller will use these 32 bytes to generate words. // int supply_chain_validation_words(const char *data, int data_len, uint32_t *result) { - SHA256_CTX ctx; - - sha256_init(&ctx); - sha256_update(&ctx, (uint8_t*)data, data_len); - // TODO: Change this to hash with a private key from the SE - // sha256_update(&ctx, ???, 32)); - sha256_final(&ctx, (uint8_t*)result); - - // Double SHA - sha256_init(&ctx); - sha256_update(&ctx, (uint8_t*)result, 32); - sha256_final(&ctx, (uint8_t*)result); + se_pair_unlock(); + int rc = se_hmac32(KEYNUM_supply_chain, (uint8_t*)data, (uint8_t*)result); + if (rc < 0) { + return -1; + } return 0; } @@ -296,8 +299,8 @@ static int _validate_attempt(pinAttempt_t *args, bool first_time) _hmac_attempt(args, actual); - printf("args->hmac=%02x %02x %02x %02x\n", args->hmac[0], args->hmac[1], args->hmac[2], args->hmac[3] ); - printf("actual =%02x %02x %02x %02x\n", actual[0], actual[1], actual[2], actual[3]); + // printf("args->hmac=%02x %02x %02x %02x\n", args->hmac[0], args->hmac[1], args->hmac[2], args->hmac[3] ); + // printf("actual =%02x %02x %02x %02x\n", actual[0], actual[1], actual[2], actual[3]); if (!check_equal(actual, args->hmac, 32)) { // hmac is wrong? return EPIN_HMAC_FAIL; @@ -316,7 +319,7 @@ static int _validate_attempt(pinAttempt_t *args, bool first_time) if (args->old_pin_len > MAX_PIN_LEN) return EPIN_RANGE_ERR; if (args->new_pin_len > MAX_PIN_LEN) return EPIN_RANGE_ERR; if ((args->change_flags & CHANGE__MASK) != args->change_flags) return EPIN_RANGE_ERR; - + return 0; } @@ -348,7 +351,7 @@ static int _read_slot_as_counter(uint8_t slot, uint32_t *dest) if (se_gendig_slot(slot, (const uint8_t *)padded, tempkey)) return -1; if (!se_is_correct_tempkey(tempkey)) { - fatal_mitm(); + return -1; } *dest = padded[0]; @@ -379,16 +382,16 @@ static int __attribute__ ((noinline)) get_last_success(pinAttempt_t *args) if (se_gendig_slot(slot, (const uint8_t *)padded, tempkey)) return -2; if (!se_is_correct_tempkey(tempkey)) { - fatal_mitm(); + return -3; } // Read two values from data slots uint32_t lastgood=0, match_count=0, counter=0; - if (_read_slot_as_counter(KEYNUM_lastgood, &lastgood)) return -3; - if (_read_slot_as_counter(KEYNUM_match_count, &match_count)) return -4; + if (_read_slot_as_counter(KEYNUM_lastgood, &lastgood)) return -4; + if (_read_slot_as_counter(KEYNUM_match_count, &match_count)) return -5; // Read the monotonically-increasing counter - if (se_get_counter(&counter, 0)) return -5; + if (se_get_counter(&counter, 0)) return -6; if(lastgood > counter) { // monkey business, but impossible, right?! @@ -414,14 +417,11 @@ static int __attribute__ ((noinline)) get_last_success(pinAttempt_t *args) // static int warmup_se(void) { - // se_setup(); - for (int retry=0; retry<5; retry++) { if (se_probe() == true) { // Success break; } - printf("retrying...\n"); } if (se_pair_unlock()) return -1; @@ -437,12 +437,12 @@ static int warmup_se(void) // Get number of failed attempts on a PIN, since last success. Calculate // required delay, and setup initial struct for later attempts. // - int +int pin_setup_attempt(pinAttempt_t *args) { -#ifdef FIXME - STATIC_ASSERT(sizeof(pinAttempt_t) == PIN_ATTEMPT_SIZE_V1); -#endif /* FIXME */ + if (sizeof(pinAttempt_t) != PIN_ATTEMPT_SIZE_V1) { + return EPIN_BAD_REQUEST; + } int rv = _validate_attempt(args, true); if (rv) return rv; @@ -461,7 +461,6 @@ pin_setup_attempt(pinAttempt_t *args) args->pin_len = pin_len; memcpy(args->pin, pin_copy, pin_len); - // TODO: WTF does "warming up" the SE mean? // unlock the AE chip int result = warmup_se(); if (result) { @@ -469,11 +468,7 @@ pin_setup_attempt(pinAttempt_t *args) return EPIN_I_AM_BRICK; } - // read counters, and calc number of PIN attempts lef - - // TODO: For the case where we are setting the initial PIN, this is expected - // to fail, isn't it? We won't have "last success", etc. - + // Read counters, and calc number of PIN attempts left result = get_last_success(args); if (result) { printf("pin_setup_attempt() ERROR: 2 result = %d\n", result); @@ -487,16 +482,19 @@ pin_setup_attempt(pinAttempt_t *args) args->delay_achieved = 0; // need to know if we are blank/unused device - result = pin_is_blank(KEYNUM_main_pin); + result = pin_is_blank(KEYNUM_pin_hash); if (result) { printf("pin_setup_attempt() ERROR: 3 (BLANK PIN!): result = %d\n", result); args->state_flags |= PA_SUCCESSFUL | PA_IS_BLANK; - // We need to save this 'zero' value because it's encrypted, and/or might be - // un-initialized memory. + // We need to save this 'zero' value because it's encrypted, and/or might be + // un-initialized memory. uint8_t zeros[32] = {0}; - pin_cache_save(args, zeros); + result = pin_cache_save(args, zeros); + if (result) { + return result; + } // need legit value in here args->private_state = (rng_sample() & ~1) ^ rom_secrets->hash_cache_secret[0]; @@ -518,45 +516,39 @@ static int updates_for_good_login(uint8_t digest[32]) int rv = se_get_counter(&count, 0); if (rv) goto fail; - // Challenge: Have to update both the counter, and the target match value because - // no other way to have exact value. - + // The weird math here is because the match count slot in the SE ignores the least + // significant 5 bits, so the match count must be a multiple of 32. When a good + // login occurs, we need to update both the match count and the monotonic counter. + // + // For example, if the monotonic counter was 19 and the match count was 32, and the + // user just provided the correct PIN, you would normally just bump the match count + // to 33, but since that is not a multiple of 32, we have to bump it to 64. That + // would then give 64-19 = 45 login attempts remaining though, so further down, + // in se_add_counter(), we bump the monotonic counter in a loop until there are + // MAX_TARGET_ATTEMPTS left (match count - counter0 = MAX_TARGET_ATTEMPTS_LEFT). uint32_t mc = (count + MAX_TARGET_ATTEMPTS + 32) & ~31; -#ifdef FIXME - ASSERT(mc >= count); -#endif /* FIXME */ int bump = (mc - MAX_TARGET_ATTEMPTS) - count; -#ifdef FIXME - ASSERT(bump >= 1); - ASSERT(bump <= 32); // assuming MAX_TARGET_ATTEMPTS < 30 -#endif /* FIXME */ - // Would rather update the counter first, so that a hostile interruption can't increase - // attempts (altho the attacker knows the pin at that point?!) .. but chip won't - // let the counter go past the match value, so that has to be first. + // The SE won't let the counter go past the match count, so we have to update the + // match count first. - // set the new "match count" + // Set the new "match count" { uint32_t tmp[32/4] = {mc, mc} ; - rv = se_encrypted_write(KEYNUM_match_count, KEYNUM_main_pin, digest, (void *)tmp, 32); + rv = se_encrypted_write(KEYNUM_match_count, KEYNUM_pin_hash, digest, (void *)tmp, 32); if (rv) goto fail; } - // incr the counter a bunch to get to that-13 + // Increment the counter a bunch to get to that-13 uint32_t new_count = 0; rv = se_add_counter(&new_count, 0, bump); if (rv) goto fail; -#ifdef FIXME - ASSERT(new_count == count + bump); - ASSERT(mc > new_count); -#endif /* FIXME */ - // Update the "last good" counter { uint32_t tmp[32/4] = {new_count, 0 }; - rv = se_encrypted_write(KEYNUM_lastgood, KEYNUM_main_pin, digest, (void *)tmp, 32); + rv = se_encrypted_write(KEYNUM_lastgood, KEYNUM_pin_hash, digest, (void *)tmp, 32); if(rv) goto fail; } @@ -585,9 +577,6 @@ int pin_login_attempt(pinAttempt_t *args) return rv; } - // OBSOLETE: did they wait long enough? - // if(args->delay_achieved < args->delay_required) return EPIN_MUST_WAIT; - if (args->state_flags & PA_SUCCESSFUL) { printf("pin_login_attempt 2\n"); // already worked, or is blank @@ -626,23 +615,31 @@ int pin_login_attempt(pinAttempt_t *args) } if (!is_main_pin(digest, &pin_kn)) { - printf("pin_login_attempt 7\n"); + // Update args with latest attempts remaining + get_last_success(args); + printf("BAD PIN: attempts_left: %lu num_fails: %lu\n", args->attempts_left, args->num_fails); + // PIN code is just wrong. // - nothing to update, since the chip's done it already return EPIN_AUTH_FAIL; } - secret_kn = KEYNUM_secret; + secret_kn = KEYNUM_seed; // change the various counters, since this worked rv = updates_for_good_login(digest); if (rv) { - printf("pin_login_attempt 8: rv=%d\n", rv); + // Update args with latest attempts remaining + get_last_success(args); + printf("GOOD PIN: attempts_left: %lu num_fails: %lu\n", args->attempts_left, args->num_fails); return EPIN_SE_FAIL; } // SUCCESS! "digest" holds a working value. Save it. - pin_cache_save(args, digest); + rv = pin_cache_save(args, digest); + if (rv) { + return rv; + } // ASIDE: even if the above was bypassed, the following code will // fail when it tries to read/update the corresponding slots in the SE @@ -676,7 +673,6 @@ int pin_login_attempt(pinAttempt_t *args) _sign_attempt(args); - printf("pin_login_attempt SUCCESS!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n"); return 0; } @@ -686,49 +682,37 @@ int pin_login_attempt(pinAttempt_t *args) // int pin_change(pinAttempt_t *args) { - printf("pin_change() 1\n"); // Validate args and signature int rv = _validate_attempt(args, false); - printf("pin_change() 2\n"); if (rv) { - printf("pin_change() 3: rv=%d\n", rv); return rv; } - printf("pin_change() 4\n"); if ((args->state_flags & PA_SUCCESSFUL) != PA_SUCCESSFUL) { - printf("pin_change() 5\n"); // must come here with a successful PIN login (so it's rate limited nicely) return EPIN_WRONG_SUCCESS; } - printf("pin_change() 6\n"); if (args->state_flags & PA_IS_BLANK) { - printf("pin_change() 7\n"); // if blank, must provide blank value if (args->pin_len) return EPIN_RANGE_ERR; } // Look at change flags. const uint32_t cf = args->change_flags; - printf("pin_change() 8: change_flags=%u\n", args->change_flags); // Must be here to do something. if (cf == 0) { - printf("pin_change() 9\n"); return EPIN_RANGE_ERR; } - printf("pin_change() 10\n"); // unlock the AE chip if (warmup_se()) { - printf("pin_change() 11\n"); return EPIN_I_AM_BRICK; } - printf("pin_change() 12\n"); // what pin do they need to know to make their change? - int required_kn = KEYNUM_main_pin; + int required_kn = KEYNUM_pin_hash; // what slot (key number) are updating? int target_slot = -1; @@ -737,127 +721,103 @@ int pin_change(pinAttempt_t *args) // below the SE validates it all again. if (cf & CHANGE_WALLET_PIN) { - printf("pin_change() 13\n"); - target_slot = KEYNUM_main_pin; + target_slot = KEYNUM_pin_hash; } else if (cf & CHANGE_SECRET) { - printf("pin_change() 14\n"); - target_slot = KEYNUM_secret; + target_slot = KEYNUM_seed; } else { - printf("pin_change() 15\n"); return EPIN_RANGE_ERR; } - printf("pin_change() 16\n"); // Determine they know hash protecting the secret/pin to be changed. uint8_t required_digest[32]; // Construct hash of pin needed. - pin_hash_attempt(required_kn, args->old_pin, args->old_pin_len, required_digest); - printf("pin_change() 17\n"); - // Check the old pin provided, is right. - se_pair_unlock(); - if (se_checkmac(required_kn, required_digest)) { - // they got old PIN wrong, we won't be able to help them - se_reset_chip(); + // Use pin cache when changing the secret, otherwise it's a pin change and we rehash + if (cf & CHANGE_SECRET) { + // Restore cached version of PIN digest: faster + pin_cache_restore(args, required_digest); + } else { + pin_hash_attempt(required_kn, args->old_pin, args->old_pin_len, required_digest); - // NOTE: altho we are changing flow based on result of ae_checkmac() here, - // if the response is faked by an active bus attacker, it doesn't matter - // because the change to the dataslot below will fail due to wrong PIN. + // Check the old pin provided, is right. + se_pair_unlock(); + if (se_checkmac(required_kn, required_digest)) { + // they got old PIN wrong, we won't be able to help them + se_reset_chip(); - printf("pin_change() 18\n"); - return EPIN_OLD_AUTH_FAIL; + // NOTE: altho we are changing flow based on result of ae_checkmac() here, + // if the response is faked by an active bus attacker, it doesn't matter + // because the change to the dataslot below will fail due to wrong PIN. + + return EPIN_OLD_AUTH_FAIL; + } } - printf("pin_change() 19\n"); // Calculate new PIN hashed value: will be slow for main pin. if (cf & CHANGE_WALLET_PIN) { - printf("pin_change() 20\n"); - uint8_t new_digest[32]; + uint8_t new_digest[32]; rv = pin_hash_attempt(required_kn, args->new_pin, args->new_pin_len, new_digest); - printf("pin_change() 21\n"); if (rv) { - printf("pin_change() 22\n"); goto se_fail; } - printf("pin_change() 23\n"); - dump_buf(required_digest, 32); + // dump_buf(required_digest, 32); if (se_encrypted_write(target_slot, required_kn, required_digest, new_digest, 32)) { - printf("pin_change() 24\n"); goto se_fail; } - printf("pin_change() 25\n"); if (target_slot == required_kn) { - printf("pin_change() 26\n"); memcpy(required_digest, new_digest, 32); - printf("pin_change() 27\n"); } - printf("pin_change() 28\n"); - if (target_slot == KEYNUM_main_pin) { - printf("pin_change() 29\n"); - + if (target_slot == KEYNUM_pin_hash) { // main pin is changing; reset counter to zero (good login) and our cache - pin_cache_save(args, new_digest); - printf("pin_change() 30\n"); + rv = pin_cache_save(args, new_digest); + if (rv) { + return rv; + } updates_for_good_login(new_digest); - printf("pin_change() 31\n"); } } // Record new secret. // Note the required_digest might have just changed above. if (cf & CHANGE_SECRET) { - printf("pin_change() 32\n"); - int secret_kn = KEYNUM_secret; - printf("pin_change() 33\n"); + int secret_kn = KEYNUM_seed; bool is_all_zeros = check_all_zeros(args->secret, SE_SECRET_LEN); - printf("pin_change() 34\n"); // encrypt new secret, but only if not zeros! uint8_t tmp[SE_SECRET_LEN] = {0}; if (!is_all_zeros) { - printf("pin_change() 35\n"); xor_mixin(tmp, rom_secrets->otp_key, SE_SECRET_LEN); xor_mixin(tmp, args->secret, SE_SECRET_LEN); } - printf("pin_change() 36\n"); - dump_buf(required_digest, 32); + // dump_buf(required_digest, 32); if (se_encrypted_write(secret_kn, required_kn, required_digest, tmp, SE_SECRET_LEN)){ - printf("pin_change() 37\n"); goto se_fail; } // update the zero-secret flag to be correct. if (cf & CHANGE_SECRET) { - printf("pin_change() 38\n"); if (is_all_zeros) { - printf("pin_change() 39\n"); args->state_flags |= PA_ZERO_SECRET; } else { - printf("pin_change() 40\n"); args->state_flags &= ~PA_ZERO_SECRET; } } - printf("pin_change() 41\n"); } se_reset_chip(); - printf("pin_change() 42\n"); // need to pass back the (potentially) updated cache value and some flags. _sign_attempt(args); - printf("pin_change() 43\n"); return 0; se_fail: - printf("pin_change() 44\n"); se_reset_chip(); - printf("pin_change() 45\n"); return EPIN_SE_FAIL; } @@ -882,10 +842,13 @@ int pin_fetch_secret(pinAttempt_t *args) // - no real need to re-prove PIN knowledge. // - if they tricked us, doesn't matter as below the SE validates it all again uint8_t digest[32]; - pin_cache_restore(args, digest); + rv = pin_cache_restore(args, digest); + if (rv) { + return rv; + } - int pin_kn = KEYNUM_main_pin; - int secret_slot = KEYNUM_secret; + int pin_kn = KEYNUM_pin_hash; + int secret_slot = KEYNUM_seed; // read out the secret that corresponds to the pin rv = se_encrypted_read(secret_slot, pin_kn, digest, args->secret, SE_SECRET_LEN); @@ -902,61 +865,4 @@ int pin_fetch_secret(pinAttempt_t *args) return 0; } -// pin_long_secret() -// -// Read or write the "long" secret: an additional 416 bytes on 608a only. -// -int pin_long_secret(pinAttempt_t *args) -{ - // Validate args and signature - int rv = _validate_attempt(args, false); - if (rv) return rv; - - if ((args->state_flags & PA_SUCCESSFUL) != PA_SUCCESSFUL) { - // must come here with a successful PIN login (so it's rate limited nicely) - return EPIN_WRONG_SUCCESS; - } - - // fetch the already-hashed pin - // - no real need to re-prove PIN knowledge. - // - if they tricked us, doesn't matter as below the SE validates it all again - uint8_t digest[32]; - pin_cache_restore(args, digest); - - // which 32-byte section? -#ifdef FIXME - STATIC_ASSERT(CHANGE_LS_OFFSET == 0xf00); -#endif /* FIXME */ - int blk = (args->change_flags >> 8) & 0xf; - if (blk > 13) return EPIN_RANGE_ERR; - - // read/write exactly 32 bytes - if (!(args->change_flags & CHANGE_SECRET)) { - rv = se_encrypted_read32(KEYNUM_long_secret, blk, KEYNUM_main_pin, digest, args->secret); - if(rv) goto fail; - - if(!check_all_zeros(args->secret, 32)) { - xor_mixin(args->secret, rom_secrets->otp_key_long+(32*blk), 32); - } - } else { - // write case - uint8_t tmp[32] = {0}; - - if (!check_all_zeros(args->secret, 32)) { - xor_mixin(tmp, args->secret, 32); - xor_mixin(tmp, rom_secrets->otp_key_long+(32*blk), 32); - } - - rv = se_encrypted_write32(KEYNUM_long_secret, blk, KEYNUM_main_pin, digest, tmp); - if (rv) goto fail; - } - -fail: - se_reset_chip(); - - if (rv) return EPIN_SE_FAIL; - - return 0; -} - // EOF diff --git a/ports/stm32/boards/Passport/pins.h b/ports/stm32/boards/Passport/pins.h index e7cc164..a39d3eb 100644 --- a/ports/stm32/boards/Passport/pins.h +++ b/ports/stm32/boards/Passport/pins.h @@ -1,7 +1,7 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // -// SPDX-FileCopyrightText: 2018 Coinkite, Inc. +// SPDX-FileCopyrightText: 2018 Coinkite, Inc. // SPDX-License-Identifier: GPL-3.0-only // /* @@ -25,9 +25,6 @@ // ATECC608A limitation/feature caps this at weird 72 byte value. #define SE_SECRET_LEN 72 -// .. but on 608a, we can use this one weird data slot with more space -#define AE_LONG_SECRET_LEN 416 - // For change_flags field: choose one secret and/or one PIN only. #define CHANGE_WALLET_PIN 0x001 #define CHANGE_SECRET 0x008 @@ -43,6 +40,8 @@ #define PA_IS_BLANK 0x02 #define PA_ZERO_SECRET 0x10 +extern uint8_t g_cached_main_pin[32]; + typedef struct { uint32_t magic_value; // = PA_MAGIC_V1 char pin[MAX_PIN_LEN]; // value being attempted @@ -106,7 +105,6 @@ int anti_phishing_words(const char *pin_prefix, int prefix_len, uint32_t *result int supply_chain_validation_words(const char *data, int data_len, uint32_t *result); -// Read/write the long secret. 32 bytes at a time. -int pin_long_secret(pinAttempt_t *args); +int pin_cache_restore(pinAttempt_t *args, uint8_t digest[32]); // EOF diff --git a/ports/stm32/boards/Passport/quirc.h b/ports/stm32/boards/Passport/quirc.h index 1516303..5d324e8 100644 --- a/ports/stm32/boards/Passport/quirc.h +++ b/ports/stm32/boards/Passport/quirc.h @@ -37,7 +37,7 @@ const char *quirc_version(void); struct quirc *quirc_new(void); /* Alternate constructor function that accepts buffer pointers so that - * no malloc() or free() calls are necessary. Useful on embedded systems + * no malloc() or free() calls are necessary. Useful on embedded systems * to help avoid allocating more buffer space than might be available, or * to use a memory buffer that is statically allocated or allocated with * something other than malloc(). diff --git a/ports/stm32/boards/Passport/ring_buffer.c b/ports/stm32/boards/Passport/ring_buffer.c deleted file mode 100644 index 52a0f29..0000000 --- a/ports/stm32/boards/Passport/ring_buffer.c +++ /dev/null @@ -1,72 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. -// SPDX-License-Identifier: GPL-3.0-or-later -// - -#include "ring_buffer.h" -#include - -/** - * Code adapted from https://github.com/AndersKaloer/Ring-Buffer - */ - -int ring_buffer_init(ring_buffer_t* buffer) -{ - buffer->size = MAX_RING_BUFFER_SIZE; - buffer->size_plus1 = MAX_RING_BUFFER_SIZE + 1; - buffer->head_index = 0; - buffer->tail_index = 0; - return 0; -} - -void ring_buffer_enqueue(ring_buffer_t* buffer, uint8_t data) -{ - // printf("enqueue...H=%u T=%u Count=%u\n", buffer->head_index, buffer->tail_index, ring_buffer_num_items(buffer)); - if (ring_buffer_is_full(buffer)) { - buffer->tail_index = ((buffer->tail_index + 1) % buffer->size_plus1); - } - - buffer->buffer[buffer->head_index] = data; - buffer->head_index = ((buffer->head_index + 1) % buffer->size_plus1); -} - -uint8_t ring_buffer_dequeue(ring_buffer_t* buffer, uint8_t* data) -{ - if (ring_buffer_is_empty(buffer)) { - return 0; - } - - *data = buffer->buffer[buffer->tail_index]; - buffer->tail_index = ((buffer->tail_index + 1) % buffer->size_plus1); - return 1; -} - -uint8_t ring_buffer_peek(ring_buffer_t* buffer, uint8_t* data, ring_buffer_size_t index) -{ - if (index >= ring_buffer_num_items(buffer)) { - // __enable_irq(); - return 0; - } - - ring_buffer_size_t data_index = ((buffer->tail_index + index) % buffer->size_plus1); - *data = buffer->buffer[data_index]; - return 1; -} - -uint8_t ring_buffer_is_empty(ring_buffer_t* buffer) -{ - uint8_t result = (buffer->head_index == buffer->tail_index); - return result; -} - -uint8_t ring_buffer_is_full(ring_buffer_t* buffer) -{ - uint8_t num_items = ring_buffer_num_items(buffer); - uint8_t result = num_items == buffer->size; - return result; -} - -ring_buffer_size_t ring_buffer_num_items(ring_buffer_t* buffer) -{ - uint8_t result = (buffer->head_index + buffer->size_plus1 - buffer->tail_index) % buffer->size_plus1; - return result; -} diff --git a/ports/stm32/boards/Passport/se-atecc608a.c b/ports/stm32/boards/Passport/se-atecc608a.c index dc840ef..ea02ac8 100644 --- a/ports/stm32/boards/Passport/se-atecc608a.c +++ b/ports/stm32/boards/Passport/se-atecc608a.c @@ -1,14 +1,14 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // -// SPDX-FileCopyrightText: 2018 Coinkite, Inc. +// SPDX-FileCopyrightText: 2018 Coinkite, Inc. // SPDX-License-Identifier: GPL-3.0-only // /* * (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard * and is covered by GPLv3 license found in COPYING. * - * SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. + * SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. * SPDX-License-Identifier: GPL-3.0-or-later */ #include @@ -112,70 +112,6 @@ int se_sign(uint8_t keynum, uint8_t msg_hash[32], uint8_t signature[64]) return 0; } -// Just read a one-way counter. -// -int se_get_counter(uint32_t *result, uint8_t counter_number) -{ - int rc; - - se_write(OP_Counter, 0x0, counter_number, NULL, 0); - rc = se_read((uint8_t *)result, 4); - se_sleep(); - if (rc < 0) - return -1; - - // IMPORTANT: Always verify the counter's value because otherwise - // nothing prevents an active MitM changing the value that we think - // we just read. - uint8_t digest[32]; - rc = se_gendig_counter(counter_number, *result, digest); - if (rc < 0) - return -1; - - if (!se_is_correct_tempkey(digest)) - return -1; - - return 0; -} - -// Add-to and return a one-way counter's value. Have to go up in -// single-unit steps, but can we loop. -// -int se_add_counter(uint32_t *result, uint8_t counter_number, int incr) -{ - int rc; - int rval = 0; - - for (int i = 0; i < incr; i++) { - se_write(OP_Counter, 0x1, counter_number, NULL, 0); - rc = se_read((uint8_t *)result, 4); - if (rc < 0) - { - rval = -1; - goto out; - } - } - - // IMPORTANT: Always verify the counter's value because otherwise - // nothing prevents an active MitM changing the value that we think - // we just read. They could also stop us increamenting the counter. - - uint8_t digest[32]; - rc = se_gendig_counter(counter_number, *result, digest); - if (rc < 0) - { - rval = -1; - goto out; - } - - if (!se_is_correct_tempkey(digest)) - rval = -1; - -out: - se_sleep(); - return rval; -} - // Use old SHA256 command from 508A, but with new flags. // int se_hmac32(uint8_t keynum, uint8_t msg[32], uint8_t digest[32]) @@ -222,153 +158,6 @@ int se_get_serial(uint8_t serial[6]) return 0; } -// Construct a digest over one of the two counters. Track what we think -// the digest should be, and ask the chip to do the same. Verify we match -// using MAC command (done elsewhere). -// -int se_gendig_counter(int counter_num, const uint32_t expected_value, uint8_t digest[32]) -{ - int rc; - uint8_t num_in[20], tempkey[32]; - - rng_buffer(num_in, sizeof(num_in)); - - rc = se_pick_nonce(num_in, tempkey); - if (rc < 0) - return -1; - - //using Zone=4="Counter" => "KeyID specifies the monotonic counter ID" - se_write(OP_GenDig, 0x4, counter_num, NULL, 0); - rc = se_read1(); - se_sleep(); - if (rc != 0) - return -1; -#if 0 - se_keep_alive(); -#endif - // we now have to match the digesting (hashing) that has happened on - // the chip. No feedback at this point if it's right tho. - // - // msg = hkey + b'\x15\x02' + ustruct.pack(" data - // only reading first block of 32 bytes. ignore the rest - se_write(OP_Read, (len == 4 ? 0x00 : 0x80) | 2, (slot_num<<3), NULL, 0); - rc = se_read(data, (len == 4) ? 4 : 32); - if (rc < 0) - { - rval = -1; - goto out; - } - - if (len == 72) { - // read second block - se_write(OP_Read, 0x82, (1<<8) | (slot_num<<3), NULL, 0); - rc = se_read(data+32, 32); - if (rc < 0) - { - rval = -1; - goto out; - } - - // read third block, but only using part of it - uint8_t tmp[32]; - se_write(OP_Read, 0x82, (2<<8) | (slot_num<<3), NULL, 0); - rc = se_read(tmp, 32); - if (rc < 0) - { - rval = -1; - goto out; - } - - memcpy(data+64, tmp, 72-64); - } - -out: - se_sleep(); - return rval; -} - int se_destroy_key(int keynum) { int rc; @@ -408,9 +197,10 @@ int se_stretch_iter( int iterations ) { -#ifdef FIXME - ASSERT(start != end); // we can't work inplace -#endif + if (start == end) { + return -1; + } + memcpy(end, start, 32); for (int i = 0; i < iterations; i++) { @@ -437,9 +227,10 @@ int se_mixin_key( { int rc; -#ifdef FIXME - ASSERT(start != end); // we can't work in place -#endif + if (start == end) { + return -1; + } + rc = se_pair_unlock(); if (rc < 0) return -1; diff --git a/ports/stm32/boards/Passport/se-atecc608a.h b/ports/stm32/boards/Passport/se-atecc608a.h index 854de64..d1a7055 100644 --- a/ports/stm32/boards/Passport/se-atecc608a.h +++ b/ports/stm32/boards/Passport/se-atecc608a.h @@ -1,7 +1,7 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // -// SPDX-FileCopyrightText: 2018 Coinkite, Inc. +// SPDX-FileCopyrightText: 2018 Coinkite, Inc. // SPDX-License-Identifier: GPL-3.0-only // @@ -13,26 +13,12 @@ // Test if chip responds correctly, and do some setup; returns error string if fail. bool se_probe(); -// Read first 32 bytes out of a slot. len must be 4 or 32. -int se_read_data_slot(int slot_num, uint8_t *data, int len); - -// Read and write to slots that are encrypted (must know that before using) -// - can specific different lenghts -int se_encrypted_read(int data_slot, int read_kn, const uint8_t read_key[32], uint8_t *data, int len); - -// read/write exactly 32 bytes -int se_encrypted_read32(int data_slot, int blk, int read_kn, - const uint8_t read_key[32], uint8_t data[32]); - // Pick a fresh random number. int se_random(uint8_t randout[32]); // Roll (derive) a key using random number we forget. One way! int se_destroy_key(int keynum); -// Ask the chip to make a digest of a counter's value or data slot's contents. -int se_gendig_counter(int counter_num, const uint32_t expected_value, uint8_t digest[32]); - // Do Info(p1=2) command, and return result; p1=3 if get_gpio uint16_t se_get_info(void); @@ -52,12 +38,6 @@ uint16_t se_get_info(void); // Do a dance that unlocks various keys. Return T if it fails. int se_unlock_ip(uint8_t keynum, const uint8_t secret[32]); -// Read a one-way counter (there are 2 of these) -int se_get_counter(uint32_t *result, uint8_t counter_number); - -// Add onto a counter. Slow; has to go by one. -int se_add_counter(uint32_t *result, uint8_t counter_number, int incr); - // Perform HMAC on the chip, using a particular key. //int se_hmac(uint8_t keynum, const uint8_t *msg, uint16_t msg_len, uint8_t digest[32]); int se_hmac32(uint8_t keynum, uint8_t *msg, uint8_t digest[32]); @@ -72,9 +52,6 @@ int se_get_serial(uint8_t serial[6]); // Call this if possible mitm is detected. extern void fatal_mitm(void) __attribute__((noreturn)); -// Update the match-counter with a new number. -int se_write_match_count(uint32_t count, const uint8_t *write_key); - // Perform many key iterations and read out the result. Designed to be slow. int se_stretch_iter(const uint8_t start[32], uint8_t end[32], int iterations); diff --git a/ports/stm32/boards/Passport/stm32h7xx_hal_conf.h b/ports/stm32/boards/Passport/stm32h7xx_hal_conf.h index 3941722..76bca81 100644 --- a/ports/stm32/boards/Passport/stm32h7xx_hal_conf.h +++ b/ports/stm32/boards/Passport/stm32h7xx_hal_conf.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // // SPDX-FileCopyrightText: Copyright (c) 2019 Damien P. George diff --git a/ports/stm32/boards/Passport/strnlen.c b/ports/stm32/boards/Passport/strnlen.c index 27c8975..b2fbfd6 100644 --- a/ports/stm32/boards/Passport/strnlen.c +++ b/ports/stm32/boards/Passport/strnlen.c @@ -17,7 +17,7 @@ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) diff --git a/ports/stm32/boards/Passport/tools/add-secrets/Makefile b/ports/stm32/boards/Passport/tools/add-secrets/Makefile new file mode 100644 index 0000000..43b88bf --- /dev/null +++ b/ports/stm32/boards/Passport/tools/add-secrets/Makefile @@ -0,0 +1,78 @@ +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later + +TOP = ../.. + +SOURCES = add-secrets.c + +VPATH = + +ARCH ?= x86 + +CFLAGS = -Wall -fno-strict-aliasing +CFLAGS += -fno-omit-frame-pointer +CFLAGS += -I$(TOP)/include + +LDFLAGS = -Wl,-Map,$(MAP) + +LIBS = + +CROSS_COMPILE ?= +CC = $(CROSS_COMPILE)gcc +EXECUTABLE = add-secrets +TARGETDIR = x86 + +ifeq ($(findstring debug,$(MAKECMDGOALS)),debug) +OBJDIR = $(TARGETDIR)/debug +CFLAGS += -g -DDEBUG +LDFLAGS += -g +STRIP = +else +OBJDIR = $(TARGETDIR)/release +CFLAGS += -O2 +STRIP = $(CROSS_COMPILE)strip +endif + +PROGRAMDIR = $(OBJDIR) +INSTALL_DIR = $(HOME)/bin +PROGRAM = $(PROGRAMDIR)/$(EXECUTABLE) +MAP = $(PROGRAMDIR)/$(EXECUTABLE).map + +OBJECTS = $(addprefix $(OBJDIR)/,$(SOURCES:.c=.o)) + +RM := rm -rf + +all: $(PROGRAM) + +debug: $(PROGRAM) + +# Tool invocations +$(PROGRAM): $(OBJECTS) FORCE + @echo 'Building target: $@' + @echo 'Invoking: GCC C Linker' + @[ -d $(dir $@) ] || mkdir -p $(dir $@) + $(CC) $(LDFLAGS) -o $@ $(OBJECTS) $(LIBS) + @echo 'Finished building target: $@' + @echo ' ' + +# Other Targets +clean: FORCE + @$(RM) $(TARGETDIR) + +$(OBJDIR)/%.o: %.c + @rm -f $@ + @[ -d $(dir $@) ] || mkdir -p $(dir $@) + $(CC) $(CFLAGS) -c -MMD -MP -o $@ $< + +ifneq ($(MAKECMDGOALS),clean) +-include $(OBJECTS:.o=.d) +endif + +install: $(PROGRAM) + @echo 'Installing $(PROGRAM)...' + @cp -f $(PROGRAM) $(INSTALL_DIR) + @[ -z $(STRIP) ] || $(STRIP) $(INSTALL_DIR)/$(EXECUTABLE) + @echo 'Installation complete' + +.PHONY: all clean install FORCE +.SECONDARY: diff --git a/ports/stm32/boards/Passport/tools/add-secrets/add-secrets.c b/ports/stm32/boards/Passport/tools/add-secrets/add-secrets.c new file mode 100644 index 0000000..38ef02d --- /dev/null +++ b/ports/stm32/boards/Passport/tools/add-secrets/add-secrets.c @@ -0,0 +1,225 @@ +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-License-Identifier: GPL-3.0-or-later +// +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define EXTENSION "-secrets" + +static char *bootloader; +static char *secrets; +static bool help; +static bool debug_log_level; + +static void usage( + char *name +) +{ + printf("Usage:%s\n", name); + printf("\t-d: debug logging\n" + "\t-b : full path to bootloader binary file\n" + "\t-s : full path to secrets binary file\n" + "\t-h: this message\n" + ); + exit(1); +} + +static void process_args( + int argc, + char **argv +) +{ + int c = 0; + + while ((c = getopt(argc, argv, "dhb:s:")) != -1) + { + switch (c) + { + case 'b': + bootloader = optarg; + break; + case 's': + secrets = optarg; + break; + case 'd': + debug_log_level = true; + break; + case 'h': + help = true; + break; + default: + usage(argv[0]); + break; + } + } +} + +static size_t read_file( + char *path, + uint8_t **buffer +) +{ + uint32_t ret = 0; + struct stat info; + FILE *fp; + + fp = fopen(path, "r"); + if (fp == NULL) + { + printf("failed to open %s\n", path); + return 0; + } + + stat(path, &info); + *buffer = (uint8_t*)calloc(1, info.st_size + sizeof(ulong)); + if (*buffer == NULL) + { + printf("insufficient memory\n"); + goto out; + } + + ret = fread(*buffer, 1, info.st_size, fp); + if (ret != info.st_size) + free(*buffer); + else + ret = info.st_size; + +out: + fclose(fp); + return ret; +} + +static void add_secrets( + char *bl, + char *secrets +) +{ + size_t ret = 0; + size_t bl_len; + size_t secrets_len; + uint8_t *bl_buf = NULL; + uint8_t *secrets_buf = NULL; + FILE *fp = NULL; + char *outfile; + char *path; + char *filename; + char *file; + char *tmp; + + if (bl == NULL) + { + printf("bootloader not specified\n"); + return; + } + tmp = strdup(bl); + + filename = basename(tmp); + if (filename == NULL) + { + printf("basename() failed\n"); + return; + } + + path = dirname(tmp); + if (path == NULL) + { + printf("dirname() failed\n"); + return; + } + + file = strtok(filename, "."); + if (file == NULL) + { + printf("strtok() failed\n"); + return; + } + + outfile = (char *)calloc(1, strlen(bl) + strlen(EXTENSION) + 1); + if (outfile == NULL) + { + printf("insufficient memory\n"); + return; + } + + sprintf(outfile, "%s/%s%s.bin", path, file, EXTENSION); + + if (debug_log_level) + printf("Reading %s...", bl); + bl_len = read_file(bl, &bl_buf); + if (bl_len == 0) + { + printf("file %s has no data\n", bl); + return; + } + if (debug_log_level) + printf("done\n"); + + if (debug_log_level) + printf("Reading %s...", secrets); + secrets_len = read_file(secrets, &secrets_buf); + if (secrets_len == 0) + { + printf("file %s has no data\n", secrets); + return; + } + if (debug_log_level) + printf("done\n"); + + fp = fopen(outfile, "wb"); + if (fp == NULL) + { + printf("failed to open %s\n", outfile); + goto out; + } + + if (debug_log_level) + printf("Writing bootloader to %s - ", outfile); + ret = fwrite(bl_buf, 1, bl_len, fp); + if (ret != bl_len) + { + unlink(outfile); + printf("\n%s write failed - check disk space\n", outfile); + goto out; + } + if (debug_log_level) + printf("done\n"); + + if (debug_log_level) + printf("Writing secrets to %s - ", outfile); + ret = fwrite(secrets_buf, 1, secrets_len, fp); + if (ret != secrets_len) + { + unlink(outfile); + printf("\n%s write failed - check disk space\n", outfile); + goto out; + } + if (debug_log_level) + printf("done\n"); + + +out: + free(bl_buf); + free(secrets_buf); + free(outfile); + free(tmp); + fclose(fp); +} + +int main(int argc, char *argv[]) +{ + process_args(argc, argv); + + if (help) + usage(argv[0]); + + add_secrets(bootloader, secrets); + + exit(0); +} diff --git a/ports/stm32/boards/Passport/tools/add-secrets/x86/release/add-secrets b/ports/stm32/boards/Passport/tools/add-secrets/x86/release/add-secrets new file mode 100755 index 0000000000000000000000000000000000000000..24e89a47644c583851d3136b43e4d13818ea64d3 GIT binary patch literal 13416 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 +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. # SPDX-License-Identifier: GPL-3.0-or-later TOP = ../.. @@ -17,15 +17,17 @@ ARCH ?= x86 CFLAGS = -Wall -fno-strict-aliasing CFLAGS += -fno-omit-frame-pointer CFLAGS += -I$(TOP)/include +CFLAGS += -I/usr/local/include CFLAGS += -I$(TOP)/common/micro-ecc -DuECC_PLATFORM=uECC_x86 CFLAGS += -DPASSPORT_COSIGN_TOOL -#CFLAGS += -DUSE_CRYPTO +CFLAGS += -DUSE_CRYPTO +CFLAGS += -L/usr/local/lib LDFLAGS = -Wl,-Map,$(MAP) LIBS = -lcrypto -CROSS_COMPILE ?= +CROSS_COMPILE ?= CC = $(CROSS_COMPILE)gcc EXECUTABLE = cosign TARGETDIR = x86 diff --git a/ports/stm32/boards/Passport/tools/cosign/cosign.c b/ports/stm32/boards/Passport/tools/cosign/cosign.c index 499b34b..3982fa8 100644 --- a/ports/stm32/boards/Passport/tools/cosign/cosign.c +++ b/ports/stm32/boards/Passport/tools/cosign/cosign.c @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // #include @@ -44,9 +44,9 @@ static void usage( printf("Usage:%s\n", name); printf("\t-d: debug logging\n" "\t-f : full path to firmware file to sign\n" - "\t-h: this message" + "\t-h: this message\n" #ifdef USE_CRYPTO - "\t-k \n" + "\t-k \n" #endif /* USE_CRYPTO */ "\t-v : firmware version\n" ); @@ -129,7 +129,7 @@ out: fclose(fp); return ret; } - +#ifdef USE_CRYPTO static uint8_t *read_private_key( char *key ) @@ -324,7 +324,7 @@ int find_public_key( } return -1; } - +#endif /* USE_CRYPTO */ static void sign_firmware( char *fw, #ifdef USE_CRYPTO @@ -333,7 +333,6 @@ static void sign_firmware( char *version ) { - int rc; size_t ret = 0; size_t fwlen; uint8_t *fwbuf = NULL; @@ -348,6 +347,7 @@ static void sign_firmware( uint8_t fw_hash[HASH_LEN]; uint8_t *working_signature; #ifdef USE_CRYPTO + int rc; uint8_t working_key = 0; uint8_t *private_key; uint8_t *public_key; @@ -369,21 +369,21 @@ static void sign_firmware( { printf("could not get private key\n"); return; - } + } public_key = read_public_key(key); if (public_key == NULL) { printf("could not get public key\n"); return; - } + } rc = find_public_key(public_key); if (rc < 0) { - printf("key %s does not have a supported public key\n", key); - return; - } + printf("key %s not a supported public key...assuming user key\n", key); + working_key = FW_USER_KEY; + } else working_key = rc; #endif /* USE_CRYPTO */ @@ -438,7 +438,7 @@ static void sign_firmware( } /* - * Test for an existing header in the firwmare. I one exists that + * Test for an existing header in the firwmare. If one exists that * means that it has been signed at least once already. */ hdrptr = (passport_firmware_header_t *)fwbuf; @@ -484,8 +484,15 @@ static void sign_firmware( hdrptr = (passport_firmware_header_t *)header; /* Create a new header...this is the first signature. */ + time_t now = time(NULL); hdrptr->info.magic = FW_HEADER_MAGIC; - hdrptr->info.timestamp = time(NULL); + hdrptr->info.timestamp = now; + struct tm now_tm = *localtime(&now); + + // Make a string version of the date too for easier display in the bootloader + // where we don't have fancy date/time functions. + strftime((char*)hdrptr->info.fwdate, sizeof(hdrptr->info.fwdate), "%b %d, %Y", &now_tm); + strcpy((char *)hdrptr->info.fwversion, version); hdrptr->info.fwlength = fwlen; #ifdef USE_CRYPTO @@ -499,6 +506,7 @@ static void sign_firmware( { printf("FW header content:\n"); printf("\ttimestamp: %d\n", hdrptr->info.timestamp); + printf("\t fwdate: %s\n", hdrptr->info.fwdate); printf("\tfwversion: %s\n", hdrptr->info.fwversion); printf("\t fwlength: %d\n", hdrptr->info.fwlength); } @@ -512,6 +520,7 @@ static void sign_firmware( printf("%02x", fw_hash[i]); printf("\n"); } + #ifdef USE_CRYPTO /* Encrypt the hash here... */ rc = uECC_sign(private_key, @@ -523,13 +532,16 @@ static void sign_firmware( goto out; } - rc = uECC_verify(approved_pubkeys[working_key], - fw_hash, sizeof(fw_hash), - working_signature, uECC_secp256k1()); - if (rc == 0) + if (working_key != FW_USER_KEY) { - printf("verify signature failed\n"); - goto out; + rc = uECC_verify(approved_pubkeys[working_key], + fw_hash, sizeof(fw_hash), + working_signature, uECC_secp256k1()); + if (rc == 0) + { + printf("verify signature failed\n"); + goto out; + } } #else memset(working_signature, 0, SIGNATURE_LEN); @@ -581,6 +593,7 @@ static void dump_firmware_signature( size_t fwlen; uint8_t *fwbuf = NULL; passport_firmware_header_t *hdrptr; + uint8_t fw_hash[HASH_LEN]; if (fw == NULL) { @@ -604,6 +617,7 @@ static void dump_firmware_signature( { printf("FW header content:\n"); printf("\ttimestamp: %d\n", hdrptr->info.timestamp); + printf("\t fwdate: %s\n", hdrptr->info.fwdate); printf("\tfwversion: %s\n", hdrptr->info.fwversion); printf("\t fwlength: %d\n", hdrptr->info.fwlength); printf("\t key: %d\n", hdrptr->signature.pubkey1); @@ -616,6 +630,22 @@ static void dump_firmware_signature( for (int i = 0; i < SIGNATURE_LEN; ++i) printf("%02x", hdrptr->signature.signature2[i]); printf("\n"); + + // Print hashes + hash_fw_user(fwbuf, fwlen, fw_hash, HASH_LEN, true); + + printf("\nFW Build Hash: "); + for (int i = 0; i < HASH_LEN; ++i) + printf("%02x", fw_hash[i]); + printf("\n"); + + hash_fw_user(fwbuf, fwlen, fw_hash, HASH_LEN, false); + + printf("FW Download Hash: "); + for (int i = 0; i < HASH_LEN; ++i) + printf("%02x", fw_hash[i]); + printf("\n"); + } else printf("No firmware header found in file %s\n", fw); diff --git a/ports/stm32/boards/Passport/tools/provisioning/provision.py b/ports/stm32/boards/Passport/tools/provisioning/provision.py new file mode 100644 index 0000000..fd58703 --- /dev/null +++ b/ports/stm32/boards/Passport/tools/provisioning/provision.py @@ -0,0 +1,376 @@ +# SPDX-FileCopyrightText: 2021 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +import telnetlib +import sys +import os +import serial +import re +import time +from binascii import hexlify + +from simple_term_menu import TerminalMenu + +# - User must run ocd before this app and ensure it connected to the device + +HOST = 'localhost' + +tn = None + +BL_NVROM_BASE = 0x0801FF00 +SUPPLY_CHAIN_SECRET_ADDRESS = 0x81E0000 +SERIAL_PORT_NAME = '/dev/ttyACM0' + +FIRMWARE_PATH = os.path.expanduser('~/provisioning/passport-fw.bin') +BOOTLOADER_PATH = os.path.expanduser('~/provisioning/passport-bl.bin') +SECRETS_PATH = os.path.expanduser('~/provisioning/secrets.bin') +SCV_KEY_PATH = os.path.expanduser('~/provisioning/scv-key.bin') + +OCD_CMD_LINE = ['sudo', '/usr/local/bin/openocd', '-f', 'stlink.cfg', '-c', 'adapter speed 1000; transport select hla_swd', '-f', 'stm32h7x.cfg'] +ocd_proc = None + +TELNET_CMD_LINE = ['telnet', 'localhost', '4444'] +telnet_proc = None + + +# FACTORY SETTINGS +DIAGNOSTIC_MODE = False # Set to True to get more menu options + + +def connect_to_telnet(): + # Connect + global tn + tn = telnetlib.Telnet(HOST, 4444) + # Turn off echo so expect doesn't get confused by the commands we send + # We still see the commands we send echoed from the remote side, but they are not also echoed locally now. + tn.write(b'' + telnetlib.IAC + telnetlib.DONT + telnetlib.ECHO) + + +# Numato 32 channel GPIO board over USB serial: +# +# - https://numato.com/product/32-channel-usb-gpio-module-with-analog-inputs/ +# - https://github.com/numato/samplecode/blob/master/RelayAndGPIOModules/USBRelayAndGPIOModules/python/usbgpio16_32/gpioread.py +# - https://github.com/numato/samplecode/blob/master/RelayAndGPIOModules/USBRelayAndGPIOModules/python/usbgpio16_32/gpiowrite.py +# - https://github.com/numato/samplecode/blob/master/RelayAndGPIOModules/USBRelayAndGPIOModules/python/usbgpio16_32/analogread.py +def is_set(): + serial_port = serial.Serial(SERIAL_PORT_NAME, 19200, timeout=1) + print('serial_port={}'.format(serial_port)) + serial_port.write(b"gpio read 0\r") + response = serial_port.read(25).decode('utf-8') + print('response = {}'.format(response)) + value = response[-4:-3] + + print('GPIO status = {}'.format(value)) + serial_port.close() + return value == '1' + +def set_gpio(serial_port, gpio_num, set): + cmd = 'gpio {} {}\r'.format('set' if set else 'clear', gpio_num) + serial_port.write(bytes(cmd, 'utf-8')) + +def power_device(turn_on): + serial_port = serial.Serial(SERIAL_PORT_NAME, 19200, timeout=1) + + if turn_on: + # Hold low for at least 0.5s to turn on + print('Powering on!') + set_gpio(serial_port, 0, False) # Low means "active" + time.sleep(1) + set_gpio(serial_port, 0, True) # Back to normal + else: + # Hold low for at least 5s to turn off + set_gpio(serial_port, 0, False) # Low means "active" + for i in range(5,-1, -1): + print('Powering down in {}'.format(i)) + time.sleep(1) + + # One extra sleep to make sure we held it low long enough + time.sleep(1) + set_gpio(serial_port, 0, True) # Back to normal + + serial_port.close() + +def wait_for_prompt(timeout=None): + if timeout == None: + result = tn.expect([b'>',b'Error']) + else: + result = tn.expect([b'>',b'Error'], timeout) + + r = tn.read_very_eager() + + if result[0] == -1 or len(result[2]) == 0 or result[0] == 1: + return False + else: + return True + +# Put device into halt state, and discard all unread data to get ready for a new command +def init_device(timeout=None): + r = tn.read_very_eager() + # print('Halting device...') + tn.write(b'reset halt\r') + return wait_for_prompt(timeout) + +def random_fn_ext(l): + import os, binascii + return binascii.b2a_hex(os.urandom(l)).decode('utf-8') + +def provision_device(flash_bootloader=False, flash_firmware=False, with_secrets=False): + init_device() + + # Check to see if the device was already provisioned - it will have data in its secrets + if flash_bootloader and not with_secrets: + secrets_text = get_secrets() + secrets = parse_secrets(secrets_text) + if is_already_provisioned(secrets): + print('This device is already provisioned! Provisioning it again will erase the secrets and render the device inoperable.\n\nProvisioning canceled.') + return + + write_supply_chain_secret() + + if flash_firmware: + # Program the Firmware + print('Programming the Firmware...') + cmd = 'flash write_image erase {} 0x8020000\r'.format(FIRMWARE_PATH) + tn.write(bytes(cmd, 'utf-8')) + result = wait_for_prompt() + + if flash_bootloader: + # Program the Bootloader + if with_secrets: + from shutil import copyfile + # Create a temporary file and write the firmware to it with + dst_path = os.path.expanduser('~/provisioning/tmp-passport-bl-{}.bin'.format(random_fn_ext(4))) + copyfile(BOOTLOADER_PATH, dst_path) + + # Read in secrets and append to file + if os.path.isfile(SECRETS_PATH): + src_fd = open(SECRETS_PATH, 'rb') + dst_fd = open(dst_path, 'ab') + dst_fd.write(src_fd.read()) + dst_fd.close() # Have to close this immediately or the additional secrets bytes won't be flashed! + print('Creating temporary file for writing bootloader with secrets: {}'.format(dst_path)) + else: + print('Error: No secrets.bin file exists') + return + else: + dst_path = BOOTLOADER_PATH + + print('Programming the Bootloader...') + cmd = 'flash write_image erase {} 0x8000000\r'.format(dst_path) + print('cmd: {}'.format(cmd)) + tn.write(bytes(cmd, 'utf-8')) + wait_for_prompt() + + if with_secrets: + # Delete the temporary file + os.remove(dst_path) + pass + + # Reset device for first boot + print('Resetting device and waiting for initial provisioning to complete...') + tn.write(b'reset\r') + wait_for_prompt() + + # Provisioning should only take about 5 seconds, but boot takes 3-4 seconds + if not flash_bootloader or with_secrets: + print('Waiting for device to restart...') + else: + print('Device provisioning in progress...') + for i in range(10, -1, -1): + print(' {}...'.format(i)) + time.sleep(1) + + print('Complete!') + +def write_supply_chain_secret(): + init_device() + + # Write the supply chain secret + print('Setting Supply Chain Validation Secret...') + size = os.path.getsize(SCV_KEY_PATH) + if size != 32: + print('ERROR: scv-key.bin must be exactly 32 bytes long') + sys.exit(1) + + cmd = 'flash write_image erase {} {}\r'.format(SCV_KEY_PATH, hex(SUPPLY_CHAIN_SECRET_ADDRESS)) + tn.write(bytes(cmd, 'utf-8')) + wait_for_prompt() + +def test_device_connection(): + tn.read_very_eager() + device_found = init_device(timeout=5) + if device_found: + print('Passport is connected and responding to commands.') + else: + print('===================================================================') + print('Unable to connect to device (Error or timeout connecting to device)') + print('===================================================================') + +def read_supply_chain_secret(do_init=True): + if do_init: + init_device() + + # Read the supply chain secret to make sure the device is ready for provisioning + tn.write(bytes('mdb {} 32\r'.format(hex(SUPPLY_CHAIN_SECRET_ADDRESS)), 'utf-8')) + result = tn.expect([b'>'])[2].decode('utf-8') + lines = result.split('\r\n')[1:] + lines = lines[:-2] + lines = '\n'.join(lines) + print('\nSupply Chain Secret at {}:'.format(hex(SUPPLY_CHAIN_SECRET_ADDRESS))) + print(lines) + +def parse_secrets(lines): + buf = bytearray() + for line in lines: + line = line.strip() + parts = line.split(': ') + hex_bytes = parts[1].split(' ') + for h in hex_bytes: + i = int(h, 16) + buf.append(i) + return buf + +# If any byte is not 0xFF, then this has been provisioned already +def is_already_provisioned(secrets): + return any(map(lambda b: b != 0xFF, secrets)) + +def get_secrets(): + init_device() + + cmd = bytes('mdb {} 256\r'.format(hex(BL_NVROM_BASE)), 'utf-8') + tn.write(cmd) + be = bytes('{}: (.*)\r'.format(hex(BL_NVROM_BASE)), 'utf-8') + result = tn.expect([b'>'])[2].decode('utf-8') + lines = result.split('\r\n')[1:] + lines = lines[:-2] + return lines + +def print_secrets(): + lines = get_secrets() + lines = '\n'.join(lines) + print('\nPassport Secrets Memory:') + print(lines) + +def save_secrets(): + secrets_text = get_secrets() + secrets = parse_secrets(secrets_text) + if secrets and len(secrets) == 256: + fn = 'secrets.bin' + try: + with open(fn, 'wb') as fd: + fd.write(secrets) + print('\nSecrets saved to: {}'.format(fn)) + except Exception as err: + print('Error when saving secrets: {}'.format(err)) + else: + print('\nUnable to read secrets from device!') + +def reset_device(): + init_device() + + print('Resetting Device...') + tn.write(b'reset\r') + wait_for_prompt() + print('Done.') + +def erase_all_flash(): + init_device() + + print('Erasing all internal flash (bootloader, secrets, firmware, user settings)...') + tn.write(b'flash erase_address 0x8000000 0x200000\r') + wait_for_prompt() + print('Done.') + + +# In order to readout the secret key generated for supply chain validation: +# +# - The MPU should NOT be configured on initial boot (if SE is blank) +# - The Python script should issue a command like 'mdb 0x01234567 32' + + +# At a high level, this script will: +# +# - reset halt +# - Flash the firmware to the device +# - Flash the bootloader to the device +# - Reset +# - Wait x seconds for the basic config to complete +# + +def main(): + if DIAGNOSTIC_MODE: + options = [ + '[1] Test Device Connection', + '[2] Provision Device', + '[3] Update Bootloader Only (with secrets.bin)', + '[4] Update Firmware Only', + '[5] Print Secrets', + '[6] Save Secrets (to secrets.bin)', + '[7] Reset Device', + '[8] Power Device On', + '[9] Power Device Off', + '[E] Erase Internal Flash', + '[Q] Quit' + ] + else: + options = [ + '[1] Test Device Connection', + '[2] Provision Device', + '[3] Print Secrets', + '[4] Reset Device', + '[5] Power Device On', + '[6] Power Device Off', + '[Q] Quit' + ] + + menu = TerminalMenu(options, title='\nPassport Provisioning Tool\n Make a selection:') + exit = False + + while not exit: + selection = menu.show() + + if DIAGNOSTIC_MODE: + if selection == 0: + connect_to_telnet() + test_device_connection() + elif selection == 1: + provision_device(flash_bootloader=True, flash_firmware=True) + elif selection == 2: + provision_device(flash_bootloader=True, with_secrets=True) + elif selection == 3: + provision_device(flash_firmware=True) + elif selection == 4: + print_secrets() + elif selection == 5: + save_secrets() + elif selection == 6: + reset_device() + elif selection == 7: + power_device(True) + elif selection == 8: + power_device(False) + elif selection == 9: + erase_all_flash() + elif selection == 10 or selection == None: # Quit + exit = True + else: + if selection == 0: + connect_to_telnet() + test_device_connection() + elif selection == 1: + provision_device(flash_bootloader=True, flash_firmware=True) + elif selection == 2: + print_secrets() + elif selection == 3: + reset_device() + elif selection == 4: + power_device(True) + elif selection == 5: + power_device(False) + elif selection == 6 or selection == None: # Quit + exit = True + +if __name__ == '__main__': + main() +else: + print('File one executed when imported') diff --git a/ports/stm32/boards/Passport/tools/pubkey-to-c/pubkey-to-c b/ports/stm32/boards/Passport/tools/pubkey-to-c/pubkey-to-c new file mode 100755 index 0000000..07c1d15 --- /dev/null +++ b/ports/stm32/boards/Passport/tools/pubkey-to-c/pubkey-to-c @@ -0,0 +1,37 @@ +#! /usr/bin/python +# Dump binary pubkey as C text to insert into firmware-keys.h + +import os +import sys +from binascii import hexlify + +def main(): + if len(sys.argv) != 2: + print('Usage: pubkey-to-c ') + return + + # Read the file + filename = sys.argv[1] + with open(filename, mode='rb') as file: # b is important -> binary + content = file.read() + + # We don't need the first 24 bytes so get rid of them + content = content[24:] + + lines = [] + line = [] + print(' { // Key: ' + filename) + for b in content: + line.append('0x' + hexlify(b)) + if len(line) == 16: + lines.append(' ' + ', '.join(line)) + line = [] + + if len(line) > 0: + lines.append(' ' + ', '.join(line)) + + print(',\n'.join(lines)) + print(' },\n') + +if __name__ == "__main__": + main() diff --git a/ports/stm32/boards/Passport/tools/se_config_gen/se_config_gen.py b/ports/stm32/boards/Passport/tools/se_config_gen/se_config_gen.py new file mode 100644 index 0000000..268e1bb --- /dev/null +++ b/ports/stm32/boards/Passport/tools/se_config_gen/se_config_gen.py @@ -0,0 +1,255 @@ +#!/usr/bin/env python3 +# +# SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# SPDX-FileCopyrightText: 2018 Coinkite, Inc. +# SPDX-License-Identifier: GPL-3.0-only +# +# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard +# and is covered by GPLv3 license found in COPYING. +# +# Determine bits needed to configure ATECC608A for Passport. +# +# Some secrets are configured at factory initialization time, and are then used by +# the secure code in the main firmware. +# + +import sys +from secel_config import * +from textwrap import TextWrapper +from contextlib import contextmanager +from binascii import unhexlify as a2b_hex + +# Specific slots (aka key numbers) are reserved for specific purposes. +class KEYNUM: + # reserve 0: it's weird + pairing_secret = 1 # pairing hash key (picked by bootloader) + pin_stretch = 2 # secret used to stretch pin (random, forgotten) + pin_hash = 3 # user-defined PIN to protect the cryptocoins (primary) + pin_attempt = 4 # secret mixed into pin generation (rate limited, random, forgotten) + lastgood = 5 # publically readable, PIN required to update: last successful PIN entry (1) + match_count = 6 # match counter, updated if they get the PIN right + supply_chain = 7 # Supply chain validation secret value + seed = 9 # 72 arbitrary bytes protected by main pin (normal case) + user_fw_pubkey = 10 # Users can load a pubkey so they can load their own firmware which the bootloader will allow + firmware_hash = 14 # hash of flash areas, stored as an unreadable secret, controls GPIO+light + # reserve 15: non-special, but some fields have all ones and so point to it. + +class SEConfig: + def __init__(self): + # typical data from a specific virgin chip; serial number and hardware rev will vary! + self.data = bytearray(a2b_hex('01233b7e00005000e9f5342beec05400c0005500832087208f20c48f8f8f8f8f9f8faf8f0000000000000000000000000000af8fffffffff00000000ffffffff00000000ffffffffffffffffffffffffffffffff00005555ffff0000000000003300330033001c001c001c001c001c003c003c003c003c003c003c003c001c00')) + assert len(self.data) == 4*32 == 128 + self.d_slot = [None]*16 + + def set_slot(self, n, slot_conf, key_conf): + assert 0 <= n <= 15, n + assert isinstance(slot_conf, SlotConfig) + assert 'KeyConfig' in str(type(key_conf)) + + self.data[20+(n*2) : 22+(n*2)] = slot_conf.pack() + self.data[96+(n*2) : 98+(n*2)] = key_conf.pack() + + def set_combo(self, n, combo): + self.set_slot(n, combo.sc, combo.kc) + + def get_combo(self, n): + rv = ComboConfig() + blk = self.data + rv.kc = KeyConfig.unpack(blk[96+(2*n):2+96+(2*n)]) + rv.sc = SlotConfig.unpack(blk[20+(2*n):2+20+(2*n)]) + return rv + + def set_otp_mode(self, read_only): + # set OTPmode for consumption or read only + # default is consumption. + self.data[18] = 0xAA if read_only else 0x55 + + def dump(self): + secel_dump(self.data) + + def set_gpio_config(self, kn): + # GPIO is active-high output, controlled by indicated key number + assert 0 <= kn <= 15 + assert self.data[14] & 1 == 0, "can only work on chip w/ SWI not I2C" + self.data[16] = 0x1 | (kn << 4) # "Auth0" mode in table 7-1 + + def disable_KdfIvLoc(self): + # prevent use of weird AES KDF init vector junk + self.data[72] = 0xf0 + + def checks(self): + # reserved areas / known values + c = self.data + assert c[17] == 0 # reserved + if self.partno == 5: + assert c[18] in (0xaa, 0x55) # OTPmode + assert c[86] in (0x00, 0x55) # LockValue + if self.partno == 5: + assert set(c[90:96]) == set([0]) # RFU, X509Format + if self.partno == 6: + assert set(c[92:96]) == set([0]) # RFU, X509Format + + +class SEConfig608(SEConfig): + def __init__(self): + # typical data from a specific virgin chip; serial number and hardware rev will vary! + self.data = bytearray(a2b_hex('01236c4100006002bbe66928ee015400c0000000832087208f20c48f8f8f8f8f9f8faf8f0000000000000000000000000000af8fffffffff00000000ffffffff000000000000000000000000000000000000000000005555ffff0000000000003300330033001c001c001c001c001c003c003c003c003c003c003c003c001c00')) + assert len(self.data) == 4*32 == 128 + self.d_slot = [None]*16 + self.partno = 6 + + def counter_match(self, kn): + assert 0 <= kn <= 15 + self.data[18] = (kn << 4) | 0x1 + + @contextmanager + def chip_options(self): + co = ChipOptions.unpack(self.data[90:92]) + yield co + self.data[90:92] = co.pack() + + +def cpp_dump_hex(buf): + # format for CPP macro + txt = ', '.join('0x%02x' %i for i in buf) + tw = TextWrapper(width=60) + return '\n'.join('\t%s \\' % i for i in tw.wrap(txt)) + + + +def main(): + with open('../../include/se-config.h', 'wt') as fp: + print('// Autogenerated; see tools/se_config_gen\n', file=fp) + + doit(SEConfig608(), fp) + +def doit(se, fp): + # default all slots to storage + cfg = [ComboConfig() for i in range(16)] + for j in range(16): + cfg[j].for_storage() + + # unique keys per-device + # - pairing key for linking SE and main micro together + # - critical! + cfg[KEYNUM.pairing_secret].hash_key().lockable(False) + + secure_map = [ + (KEYNUM.pin_hash, KEYNUM.seed, KEYNUM.lastgood) + ] + + unused_slots = [0, 8, 11, 12, 13, 15] + + # new slots related to pin attempt- and rate-limiting + # - both hold random, unknown contents, can't be changed + # - use of the first one will cost a counter incr + # - actual PIN to be used is rv=HMAC(pin_stretch, rv) many times + cfg[KEYNUM.pin_attempt].hash_key().require_auth(KEYNUM.pairing_secret).deterministic().limited_use() + + # to rate-limit PIN attempts (also used for prefix words) we require + # many HMAC cycles using this random+unknown value. + cfg[KEYNUM.pin_stretch].hash_key().require_auth(KEYNUM.pairing_secret).deterministic() + + # chip-enforced pin attempts: link keynum and enable "match count" feature + cfg[KEYNUM.match_count].writeable_storage(KEYNUM.pin_hash).require_auth(KEYNUM.pairing_secret) + se.counter_match(KEYNUM.match_count) + + # User firmware installation pubkey - updatable with PIN, readable with just the pairing secret + cfg[KEYNUM.user_fw_pubkey].writeable_storage(KEYNUM.pin_hash).require_auth(KEYNUM.pairing_secret) + + # Supply chain secret + cfg[KEYNUM.supply_chain].hash_key().require_auth(KEYNUM.pairing_secret).deterministic() + + + # turn off selftest feature (performance problem), and enforce encryption + # (io protection) for verify, etc. + with se.chip_options() as opt: + opt.POSTEnable = 0 + opt.IOProtKeyEnable = 1 + opt.ECDHProt = 0x1 # allow encrypted output + opt.KDFProt = 0x1 # allow encrypted output + opt.IOProtKey = KEYNUM.pairing_secret + + # don't want + se.disable_KdfIvLoc() + + # PIN and corresponding protected secrets + # - if you know old value of PIN, you can write it (to change to new PIN) + for kn, sec_num, lg_num in secure_map: + cfg[kn].hash_key(write_kn=kn).require_auth(KEYNUM.pairing_secret) + cfg[sec_num].secret_storage(kn).require_auth(kn) + if lg_num is not None: + # used to hold counter0 value when we last successfully got that PIN + cfg[lg_num].writeable_storage(kn).require_auth(KEYNUM.pairing_secret) + + # Hash based on a combination of hardware IDs and the firmware hash + # - Green light is controlled by setting this value at boot + cfg[KEYNUM.firmware_hash].secret_storage(KEYNUM.firmware_hash).no_read().require_auth(KEYNUM.pairing_secret) + + # Slot 8 is special because its data area is larger and could hold a + # certificate in DER format. All the others are 36/72 bytes only + assert cfg[8].kc.KeyType == 7 + + # Slot 0 has baggage because a zero value for ReadKey has special meaning, + # so avoid using it. But had to put something in ReadKey, so it's 15 sometimes. + assert cfg[0].sc.IsSecret == 0 + assert cfg[15].sc.IsSecret == 0 + + assert len(cfg) == 16 + for idx, x in enumerate(cfg): + # no EC keys on this project + assert cfg[idx].kc.KeyType in [6,7], idx + + print('Processing slot {}'.format(idx)) + + if idx == KEYNUM.pairing_secret: + assert cfg[idx].kc.KeyType == 7 + elif idx == KEYNUM.supply_chain: + # Unreadable/unwritable + pass + elif idx in unused_slots: + # check not used + assert cfg[idx].sc.as_int() == 0x0000, idx + assert cfg[idx].kc.as_int() == 0x003c, idx + else: + # Use of **any** key requires knowledge of pairing secret + # except PIN-protected slots, which require PIN (which requires pairing secret) + assert cfg[idx].kc.ReqAuth, idx + assert (cfg[idx].kc.AuthKey == KEYNUM.pairing_secret) or \ + (cfg[cfg[idx].kc.AuthKey].kc.AuthKey == KEYNUM.pairing_secret), idx + + se.set_combo(idx, cfg[idx]) + + # require CheckMac on indicated key to turn on GPIO + se.set_gpio_config(KEYNUM.firmware_hash) + + se.checks() + + #se.dump() + + # generate a single header file we will need + if fp: + print('// bytes [16..84) of chip config area', file=fp) + print('#define SE_CHIP_CONFIG_1 { \\', file=fp) + print(cpp_dump_hex(se.data[16:84]), file=fp) + print('}\n\n', file=fp) + + print('// bytes [90..128) of chip config area', file=fp) + print('#define SE_CHIP_CONFIG_2 { \\', file=fp) + print(cpp_dump_hex(se.data[90:]), file=fp) + print('}\n\n', file=fp) + + print('// key/slot usage and names', file=fp) + names = [nm for nm in dir(KEYNUM) if nm[0] != '_'] + for v,nm in sorted((getattr(KEYNUM, nm), nm) for nm in names): + print('#define KEYNUM_%-20s\t%d' % (nm.lower(), v), file=fp) + + print('\n/*\n', file=fp) + sys.stdout = fp + se.dump() + print('\n*/', file=fp) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/ports/stm32/boards/Passport/tools/se_config_gen/secel_config.py b/ports/stm32/boards/Passport/tools/se_config_gen/secel_config.py new file mode 100644 index 0000000..84a7e1a --- /dev/null +++ b/ports/stm32/boards/Passport/tools/se_config_gen/secel_config.py @@ -0,0 +1,420 @@ +# SPDX-FileCopyrightText: 2018 Coinkite, Inc. +# SPDX-License-Identifier: GPL-3.0-only +# +# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard +# and is covered by GPLv3 license found in COPYING. +# +# Secure Element Config Area. +# +# Bitwise details about the the ATECC608a and 508a "config" area, which determines what +# you can and (mostly) cannot do with each private key in device. +# +# - you must contemplate the full datasheet at length +# - as of Jul/2019 the 608a datasheet is under NDA, sorry. +# - this file can be useful both in Micropython and CPython3 +# +try: + MPY=True + from ubinascii import hexlify as b2a_hex + from uhashlib import sha256 + from ucollections import namedtuple + import ustruct +except ImportError: + MPY=False + from binascii import b2a_hex + from hashlib import sha256 + import struct + +def secel_dump(blk, rnd=None, which_nums=range(16)): + ASC = lambda b: str(b, 'ascii') + + def hexdump(label, x): + print(label + ASC(b2a_hex(x)) + (' len=%d'%len(x))) + + #hexdump('SN: ', blk[0:4]+blk[8:13]) + hexdump('RevNum: ', blk[4:8]) + + # guessing this nibble in RevNum corresponds to chip 508a vs 608a + print("Chip type: atecc%x08a" % ((blk[6]>>4)&0xf)) + partno = ((blk[6]>>4)&0xf) + assert partno in [5, 6] + + if partno == 6: + print('AES_Enable = 0x%x' % (blk[13] & 0x1)) + + print('I2C_Enable = 0x%x' % (blk[14] & 0x1)) + if blk[14] & 0x01 == 0x01: + print('I2C_Address = 0x%x' % (blk[16] >> 1)) + else: + print('GPIO Mode = 0x%x' % (blk[16] & 0x3)) + print('GPIO Default = 0x%x' % ((blk[16]>>2) & 0x1)) + print('GPIO Detect (vs authout) = 0x%x' % ((blk[16]>>3) & 0x1)) + print('GPIO SignalKey/KeyId = 0x%x' % ((blk[16]>>4) & 0xf)) + print('I2C_Address(sic) = 0x%x' % blk[16]) + + if partno == 5: + print('OTPmode = 0x%x' % blk[18]) + if partno == 6: + print('CountMatchKey = 0x%x' % ((blk[18] >> 4)&0xf)) + print('CounterMatch enable = %d' % (blk[18] &0x1)) + + print('ChipMode = 0x%x' % blk[19]) + + print() + + for i in which_nums: + slot_conf = blk[20+(2*i):22+(2*i)] + conf = SlotConfig.unpack(slot_conf) + print(' Slot[%d] = 0x%s = %r' % (i, ASC(b2a_hex(slot_conf)), conf)) + + key_conf = blk[96+(2*i):2+96+(2*i)] + + cls = KeyConfig_508 if partno == 5 else KeyConfig_608 + + print('KeyConfig[%d] = 0x%s = %r' % (i, ASC(b2a_hex(key_conf)), + cls.unpack(key_conf))) + + print() + + hexdump('Counter[0]: ', blk[52:60]) + hexdump('Counter[1]: ', blk[60:68]) + if partno == 5: + hexdump('LastKeyUse: ', blk[68:84]) + if partno == 6: + print('UseLock = 0x%x' % blk[68]) + print('VolatileKeyPermission = 0x%x' % blk[69]) + hexdump('SecureBoot: ', blk[70:72]) + + print('KldfvLoc = 0x%x' % blk[72]) + hexdump('KdflvStr: ', blk[73:75]) + + # 75->83 reserved + print('UserExtra = 0x%x' % blk[84]) + if partno == 5: + print('Selector = 0x%x' % blk[85]) + if partno == 6: + print('UserExtraAdd = 0x%x' % blk[85]) + + print('LockValue = 0x%x' % blk[86]) + print('LockConfig = 0x%x' % blk[87]) + hexdump('SlotLocked: ', blk[88:90]) + + if partno == 6: + hexdump('ChipOptions: ', blk[90:92]) + print('ChipOptions = %r' % ChipOptions.unpack(blk[90:92])) + + hexdump('X509format: ', blk[92:96]) + + if rnd is not None: + hexdump('Random: ', rnd) + + +if MPY: + # XXX readonly version for micropython + + def make_bitmask(name, defs): + ''' + Take a list of bit widths and field names, and convert into a useful class. + ''' + custom_t = namedtuple(name, [n for w,n in defs]) + + assert sum(w for w,n in defs) == 16 + + + class wrapper: + def __init__(self, *a, **kws): + if not a: + a = [0] * len(defs) + for idx, (_, nm) in enumerate(defs): + if nm in kws: + a[idx] = kws[nm] + self.x = custom_t(*a) + + @classmethod + def unpack(cls, ss): + v = ustruct.unpack('> pos) & ((1<> pos) & ((1<LSB order, but elsewhere LSB->MSB +# - bit numbers are right, and register isn't other endian, just the text backwards +# - section 2.2.10 has in right order, but skips various bits in the register +ChipOptions = make_bitmask('ChipOptions', [ + (1, 'POSTEnable'), + (1, 'IOProtKeyEnable'), + (1, 'KDFAESEnable'), + (5, 'mustbezero'), + (2, 'ECDHProt'), + (2, 'KDFProt'), + (4, 'IOProtKey'), ]) + +class ComboConfig(object): + __slots__ = ['kc', 'sc', 'partno'] # block spelling mistakes + + def __init__(self, partno=5): + self.partno = partno + self.kc = KeyConfig_508() if partno == 5 else KeyConfig_608() + self.sc = SlotConfig(WriteConfig=0x8) # most restrictive + + @property + def is_ec_key(self): + return (self.kc.KeyType == 4) # secp256r1 + + def ec_key(self, limited_sign=False): + # basics for an EC key + self.kc.KeyType = 4 # secp256r1 + self.kc.Private = 1 # is a EC private key + self.kc.Lockable = 0 # normally set in stone + self.sc.IsSecret = 1 # because is a private key + self.sc.ReadKey = 0x2 if limited_sign else 0xf # allow CheckMac only, or all usages + self.sc.WriteConfig = 0x2 # enable GenKey (not PrivWrite), no mac for key roll + return self + + def hash_key(self, write_kn=None, roll_kn=None): + # basics for a hashing key (32-bytes of noise) + self.kc.KeyType = 7 # not EC + self.kc.Private = 0 # not an EC private key + self.kc.ReqRandom = 1 + self.sc.NoMac = 0 + self.sc.IsSecret = 1 # because is a secret key + self.sc.EncryptRead = 0 # no readback supported at all, even encrypted + self.sc.ReadKey = 0xf # don't allow checkMac? Do allow other uses? IDK + if write_kn is not None: + assert 0 <= write_kn <= 15 + self.sc.WriteKey = write_kn + self.sc.WriteConfig = 0x4 # encrypted writes allowed + else: + # 8="Never" - value must be written before data locked + self.sc.WriteConfig = 0x8 + + if roll_kn is not None: + assert write_kn is None + self.sc.WriteKey = roll_kn + self.sc.WriteConfig = 0x2 # see Table 2-0: enable Roll w/o MAC, still never write + + return self + + def for_storage(self, lockable=True): + # public data storage, not secrets + self.kc.KeyType = 7 # not EC + self.kc.Private = 0 # not an EC private key + self.kc.Lockable = int(lockable) # can delay slot locking + self.sc.IsSecret = 0 # not a secret + self.sc.ReadKey = 0x0 # allow checkMac + self.sc.WriteConfig = 0 # permissive + return self + + def writeable_storage(self, write_kn, lockable=False): + # public data storage but require key to update + self.kc.KeyType = 7 # not EC + self.kc.Private = 0 # not an EC private key + self.kc.Lockable = int(lockable) # can delay slot locking + self.sc.IsSecret = 0 # not a secret + self.sc.ReadKey = 0x0 # allow checkMac + # allow authenticated updates + self.sc.WriteKey = write_kn + self.sc.WriteConfig = 0x4 # encrypted writes allowed + return self + + def no_read(self): + self.sc.IsSecret = 1 # because is a secret key + self.sc.ReadKey = 0xf + self.sc.EncryptRead = 0 # no readback supported at all, even encrypted + return self + + def no_write(self): + self.sc.IsSecret = 1 # because is a secret key + self.sc.WriteKey = 0xf + self.sc.WriteConfig = 0x08 # Can't write + return self + + def secret_storage(self, rw_kn): + # secret data storage, which can be updated repeatedly in the field + assert 0 <= rw_kn <= 15 + self.kc.KeyType = 7 # not EC + self.kc.Private = 0 # not an EC private key + self.kc.Lockable = 0 # cannot lock the slot (would be DoS attack) + self.kc.ReqRandom = 1 # rng must be part of nonce + self.sc.IsSecret = 1 # shh.. secret + self.sc.EncryptRead = 1 # always encrypted read required + self.sc.ReadKey = rw_kn + self.sc.WriteKey = rw_kn + self.sc.WriteConfig = 0x4 # encrypted write, no DeriveKey support + return self + + def deterministic(self): + # most keyslots should have ReqRandom=1 but if we're using it to hash up + # a known value, like a PIN, then it can't be based on a nonce. + self.kc.ReqRandom = 0 + return self + + def require_auth(self, kn): + # knowledge of another key will be required + assert 0 <= kn <= 15 + self.kc.ReqAuth = 1 + self.kc.AuthKey = kn + return self + + def lockable(self, lockable): + self.kc.Lockable = int(lockable) # can delay slot locking + return self + + def limited_use(self): + self.sc.LimitedUse = 1 # counter0 will inc by one each use + return self + + def read_encrypted(self, kn): + # readout allowed, but it's encrypted by key kn + # "Reads from this slot are encrypted using the encryption algorithm + # documented in Section 9.16, Read Command" + assert 0 <= kn <= 15 + self.kc.EncryptRead = 1 + self.kc.ReadKey = kn + self.kc.IsSecret = 1 + return self + + def persistent_disable(self): + assert self.partno == 6, '608a only' + self.kc.PersistentDisable = 1 + return self + + def is_aes_key(self): + assert self.partno == 6, '608a only' + self.kc.KeyType = 6 # for use with AES + return self + +# EOF \ No newline at end of file diff --git a/ports/stm32/boards/Passport/tools/se_config_gen/secel_debug.py b/ports/stm32/boards/Passport/tools/se_config_gen/secel_debug.py new file mode 100644 index 0000000..f2783cc --- /dev/null +++ b/ports/stm32/boards/Passport/tools/se_config_gen/secel_debug.py @@ -0,0 +1,64 @@ +# SPDX-FileCopyrightText: 2018 Coinkite, Inc. +# SPDX-License-Identifier: GPL-3.0-only +# +# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard +# and is covered by GPLv3 license found in COPYING. +# +# Secure Element Config debugging tools. +# + +from secel_config import secel_dump +from binascii import b2a_hex, a2b_hex + +# copy from sSe_config.h +# bytes [16..84) of chip config area +SE_CHIP_CONFIG_1 = bytes([ + 0xe1, 0x00, 0x61, 0x00, 0x00, 0x00, 0x87, 0x20, 0x8f, 0x80, \ + 0x8f, 0x43, 0xaf, 0x80, 0x00, 0x43, 0x00, 0x43, 0x8f, 0x47, \ + 0xc3, 0x43, 0xc3, 0x43, 0xc7, 0x47, 0x8f, 0x80, 0x00, 0x00, \ + 0x8f, 0x4d, 0x8f, 0x43, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, \ + 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 \ +]) + + +# bytes [90..128) of chip config area +SE_CHIP_CONFIG_2 = bytes([ \ + 0x03, 0x15, 0x00, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x33, 0x00, \ + 0xbc, 0x01, 0xfc, 0x01, 0xfc, 0x01, 0x9c, 0x01, 0x9c, 0x01, \ + 0xfc, 0x01, 0xdc, 0x03, 0xdc, 0x03, 0xdc, 0x07, 0xfc, 0x04, \ + 0x3c, 0x00, 0xfc, 0x01, 0xdc, 0x01, 0x3c, 0x00 \ +]) + + +if __name__ == '__main__': + + # observed values from unprogrammed devices + ex_608 = a2b_hex('01236c4100006002bbe66928ee015400c0000000832087208f20c48f8f8f8f8f9f8faf8f0000000000000000000000000000af8fffffffff00000000ffffffff000000000000000000000000000000000000000000005555ffff0000000000003300330033001c001c001c001c001c003c003c003c003c003c003c003c001c00') + ex_508 = a2b_hex('01233b7e00005000e9f5342beec05400c0005500832087208f20c48f8f8f8f8f9f8faf8f0000000000000000000000000000af8fffffffff00000000ffffffff00000000ffffffffffffffffffffffffffffffff00005555ffff0000000000003300330033001c001c001c001c001c003c003c003c003c003c003c003c001c00') + + prob = bytes([ + 0x1, 0x23, 0x97, 0x42, 0x0, 0x0, 0x60, 0x2, 0x78, 0x79, 0x3, 0x6c, 0xee, + 0x1, 0x54, 0x0, 0xe1, 0x0, 0x61, 0x0, 0x0, 0x0, 0x87, 0x20, 0x8f, 0x80, + 0x8f, 0x43, 0xaf, 0x80, 0x0, 0x43, 0x0, 0x43, 0x8f, 0x47, 0xc3, 0x43, 0xc3, + 0x43, 0xc7, 0x47, 0x8f, 0x80, 0x0, 0x0, 0x8f, 0x4d, 0x8f, 0x43, 0x0, 0x0, + 0xff, 0xff, 0xff, 0xff, 0x0, 0x0, 0x0, 0x0, 0xff, 0xff, 0xff, 0xff, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x55, 0x0, 0xfe, 0xff, 0x3, 0x15, 0x0, 0x0, + 0x0, 0x0, 0x3c, 0x0, 0x33, 0x0, 0xbc, 0x1, 0xfc, 0x1, 0xfc, 0x1, 0x9c, 0x1, + 0x9c, 0x1, 0xfc, 0x1, 0xdc, 0x3, 0xdc, 0x3, 0xdc, 0x7, 0xfc, 0x4, 0x3c, 0x0, + 0xfc, 0x1, 0xdc, 0x1, 0x3c, 0x0 + ]) + assert len(prob) == 128 + + dev = ex_508 + + secel_dump(dev) + + if 0: + assert dev[16:84] == SE_CHIP_CONFIG_1 + assert dev[90:128] == SE_CHIP_CONFIG_2 + print("Bits are where they need to be") + +# EOF \ No newline at end of file diff --git a/ports/stm32/boards/Passport/tools/version_info/version_info b/ports/stm32/boards/Passport/tools/version_info/version_info new file mode 100755 index 0000000..47b9103 --- /dev/null +++ b/ports/stm32/boards/Passport/tools/version_info/version_info @@ -0,0 +1,19 @@ +usage() +{ + echo "Usage: `basename $0` [-h]" + echo " -h: help" + exit 1 +} + +file=$1 +release=$2 + +[ -z "$file" ] && usage +[ -z "$release" ] && usage + +echo "// SPDX-FileCopyrightText: $(date +"%Y") Foundation Devices, Inc. " > $file +echo "// SPDX-License-Identifier: GPL-3.0-or-later" >> $file +echo "//" >> $file +echo "" >> $file +echo "char *build_date = \"$(date +"%b. %d, %Y")\";" >> $file +echo "char *build_version = \"$release\";" >> $file \ No newline at end of file diff --git a/ports/stm32/boards/Passport/utils/README.md b/ports/stm32/boards/Passport/tools/word_list_gen/README.md similarity index 86% rename from ports/stm32/boards/Passport/utils/README.md rename to ports/stm32/boards/Passport/tools/word_list_gen/README.md index 372f6e2..d42c216 100644 --- a/ports/stm32/boards/Passport/utils/README.md +++ b/ports/stm32/boards/Passport/tools/word_list_gen/README.md @@ -5,7 +5,7 @@ in a different language. To compile it: - gcc bip39_gen.c bip39_words.c -o bip39_gen + gcc word_list_gen.c bip39_words.c bytewords_words.c -o word_list_gen To generate the word_info_t array: diff --git a/ports/stm32/boards/Passport/utils/bip39_test.c b/ports/stm32/boards/Passport/tools/word_list_gen/bip39_test.c similarity index 96% rename from ports/stm32/boards/Passport/utils/bip39_test.c rename to ports/stm32/boards/Passport/tools/word_list_gen/bip39_test.c index 7389897..87b870c 100644 --- a/ports/stm32/boards/Passport/utils/bip39_test.c +++ b/ports/stm32/boards/Passport/tools/word_list_gen/bip39_test.c @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // diff --git a/ports/stm32/boards/Passport/utils/bip39_words.c b/ports/stm32/boards/Passport/tools/word_list_gen/bip39_words.c similarity index 99% rename from ports/stm32/boards/Passport/utils/bip39_words.c rename to ports/stm32/boards/Passport/tools/word_list_gen/bip39_words.c index 71e63d1..3cbaae6 100644 --- a/ports/stm32/boards/Passport/utils/bip39_words.c +++ b/ports/stm32/boards/Passport/tools/word_list_gen/bip39_words.c @@ -1,8 +1,8 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // -const char* words[] = { +const char* bip39_words[] = { "abandon", "ability", "able", @@ -2051,4 +2051,4 @@ const char* words[] = { "zero", "zone", "zoo" -}; \ No newline at end of file +}; diff --git a/ports/stm32/boards/Passport/tools/word_list_gen/bytewords_words.c b/ports/stm32/boards/Passport/tools/word_list_gen/bytewords_words.c new file mode 100644 index 0000000..6ddcd5d --- /dev/null +++ b/ports/stm32/boards/Passport/tools/word_list_gen/bytewords_words.c @@ -0,0 +1,262 @@ +// SPDX-FileCopyrightText: 2021 Foundation Devices, Inc. +// SPDX-License-Identifier: GPL-3.0-or-later +// + +const char* bytewords_words[] = { + "able", + "acid", + "also", + "apex", + "aqua", + "arch", + "atom", + "aunt", + "away", + "axis", + "back", + "bald", + "barn", + "belt", + "beta", + "bias", + "blue", + "body", + "brag", + "brew", + "bulb", + "buzz", + "calm", + "cash", + "cats", + "chef", + "city", + "claw", + "code", + "cola", + "cook", + "cost", + "crux", + "curl", + "cusp", + "cyan", + "dark", + "data", + "days", + "deli", + "dice", + "diet", + "door", + "down", + "draw", + "drop", + "drum", + "dull", + "duty", + "each", + "easy", + "echo", + "edge", + "epic", + "even", + "exam", + "exit", + "eyes", + "fact", + "fair", + "fern", + "figs", + "film", + "fish", + "fizz", + "flap", + "flew", + "flux", + "foxy", + "free", + "frog", + "fuel", + "fund", + "gala", + "game", + "gear", + "gems", + "gift", + "girl", + "glow", + "good", + "gray", + "grim", + "guru", + "gush", + "gyro", + "half", + "hang", + "hard", + "hawk", + "heat", + "help", + "high", + "hill", + "holy", + "hope", + "horn", + "huts", + "iced", + "idea", + "idle", + "inch", + "inky", + "into", + "iris", + "iron", + "item", + "jade", + "jazz", + "join", + "jolt", + "jowl", + "judo", + "jugs", + "jump", + "junk", + "jury", + "keep", + "keno", + "kept", + "keys", + "kick", + "kiln", + "king", + "kite", + "kiwi", + "knob", + "lamb", + "lava", + "lazy", + "leaf", + "legs", + "liar", + "limp", + "lion", + "list", + "logo", + "loud", + "love", + "luau", + "luck", + "lung", + "main", + "many", + "math", + "maze", + "memo", + "menu", + "meow", + "mild", + "mint", + "miss", + "monk", + "nail", + "navy", + "need", + "news", + "next", + "noon", + "note", + "numb", + "obey", + "oboe", + "omit", + "onyx", + "open", + "oval", + "owls", + "paid", + "part", + "peck", + "play", + "plus", + "poem", + "pool", + "pose", + "puff", + "puma", + "purr", + "quad", + "quiz", + "race", + "ramp", + "real", + "redo", + "rich", + "road", + "rock", + "roof", + "ruby", + "ruin", + "runs", + "rust", + "safe", + "saga", + "scar", + "sets", + "silk", + "skew", + "slot", + "soap", + "solo", + "song", + "stub", + "surf", + "swan", + "taco", + "task", + "taxi", + "tent", + "tied", + "time", + "tiny", + "toil", + "tomb", + "toys", + "trip", + "tuna", + "twin", + "ugly", + "undo", + "unit", + "urge", + "user", + "vast", + "very", + "veto", + "vial", + "vibe", + "view", + "visa", + "void", + "vows", + "wall", + "wand", + "warm", + "wasp", + "wave", + "waxy", + "webs", + "what", + "when", + "whiz", + "wolf", + "work", + "yank", + "yawn", + "yell", + "yoga", + "yurt", + "zaps", + "zero", + "zest", + "zinc", + "zone", + "zoom" +}; diff --git a/ports/stm32/boards/Passport/tools/word_list_gen/word_list_gen b/ports/stm32/boards/Passport/tools/word_list_gen/word_list_gen new file mode 100755 index 0000000000000000000000000000000000000000..152367e1bb9ecd493db28d327c3d56e57a1d1256 GIT binary patch literal 101384 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 +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // @@ -7,15 +7,22 @@ #include #include -#define NUM_WORDS 2048 +uint32_t NUM_WORDS = 0; #define MAX_WORD_LEN 8 -extern const char* words[]; +extern const char* bip39_words[]; +extern const char* bytewords_words[]; #include #include #include +typedef struct { + uint32_t keypad_digits; + uint16_t offsets; + char word[MAX_WORD_LEN + 1]; +} word_info_t; + uint32_t letter_to_number(char ch) { if (ch >= 'a' && ch <= 'c') return 2; if (ch >= 'd' && ch <= 'f') return 3; @@ -72,7 +79,17 @@ uint16_t word_to_bit_offsets(char* word) { return result; } -void make_num_pairs_array() { +int compare_word_info(const void * a, const void * b) { + word_info_t* wa = (word_info_t*)a; + word_info_t* wb = (word_info_t*)b; + + return wa->keypad_digits - wb->keypad_digits; +} + +void make_num_pairs_array(const char** words, char* prefix) { + printf("// SPDX-FileCopyrightText: 2021 Foundation Devices, Inc. \n"); + printf("// SPDX-License-Identifier: GPL-3.0-or-later\n"); + printf("//\n\n"); printf("#include \n\n"); printf("typedef struct {\n"); @@ -80,18 +97,47 @@ void make_num_pairs_array() { printf(" uint16_t offsets;\n"); printf("} word_info_t;\n\n"); - printf("word_info_t word_info[] = {\n"); + printf("word_info_t %s_word_info[] = {\n", prefix); + + // Allocate a parallel array + word_info_t* output = malloc(sizeof(word_info_t) * NUM_WORDS); + for (int i=0; i 1) { + if (strcmp(argv[1], "bip39") == 0) { + words = bip39_words; + NUM_WORDS = 2048; + } else if (strcmp(argv[1], "bytewords") == 0) { + words = bytewords_words; + NUM_WORDS = 256; + } + } + + if (words == NULL) { + printUsage(); + return -1; + } + + make_num_pairs_array(words, argv[1]); + return 0; +} diff --git a/ports/stm32/boards/Passport/trezor-firmware/core/embed/extmod/modtrezorcrypto/modtrezorcrypto-bip32.h b/ports/stm32/boards/Passport/trezor-firmware/core/embed/extmod/modtrezorcrypto/modtrezorcrypto-bip32.h index 132b167..60c571b 100644 --- a/ports/stm32/boards/Passport/trezor-firmware/core/embed/extmod/modtrezorcrypto/modtrezorcrypto-bip32.h +++ b/ports/stm32/boards/Passport/trezor-firmware/core/embed/extmod/modtrezorcrypto/modtrezorcrypto-bip32.h @@ -29,6 +29,8 @@ #include "nem.h" #endif +#define FOUNDATION_ADDITIONS + /// package: trezorcrypto.bip32 /// class HDNode: @@ -258,6 +260,29 @@ STATIC mp_obj_t mod_trezorcrypto_HDNode_derive_path(mp_obj_t self, STATIC MP_DEFINE_CONST_FUN_OBJ_2(mod_trezorcrypto_HDNode_derive_path_obj, mod_trezorcrypto_HDNode_derive_path); + +#ifdef FOUNDATION_ADDITIONS +/// def serialize_private(self, version: int) -> str: +/// """ +/// Serialize the public info from HD node to base58 string. +/// """ +STATIC mp_obj_t mod_trezorcrypto_HDNode_serialize_private(mp_obj_t self, + mp_obj_t version) { + uint32_t ver = trezor_obj_get_uint(version); + mp_obj_HDNode_t *o = MP_OBJ_TO_PTR(self); + char xpub[XPUB_MAXLEN] = {0}; + int written = hdnode_serialize_private(&o->hdnode, o->fingerprint, ver, xpub, + XPUB_MAXLEN); + if (written <= 0) { + mp_raise_ValueError("Failed to serialize"); + } + // written includes NULL at the end of the string + return mp_obj_new_str_copy(&mp_type_str, (const uint8_t *)xpub, written - 1); +} +STATIC MP_DEFINE_CONST_FUN_OBJ_2(mod_trezorcrypto_HDNode_serialize_private_obj, mod_trezorcrypto_HDNode_serialize_private); +#endif + + /// def serialize_public(self, version: int) -> str: /// """ /// Serialize the public info from HD node to base58 string. @@ -316,7 +341,6 @@ STATIC mp_obj_t mod_trezorcrypto_HDNode_fingerprint(mp_obj_t self) { STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorcrypto_HDNode_fingerprint_obj, mod_trezorcrypto_HDNode_fingerprint); -#define FOUNDATION_ADDITIONS #ifdef FOUNDATION_ADDITIONS /// def my_fingerprint(self) -> int: /// ''' @@ -407,6 +431,22 @@ STATIC mp_obj_t mod_trezorcrypto_HDNode_address(mp_obj_t self, STATIC MP_DEFINE_CONST_FUN_OBJ_2(mod_trezorcrypto_HDNode_address_obj, mod_trezorcrypto_HDNode_address); +#ifdef FOUNDATION_ADDITIONS +/// def address_raw(self) -> bytes[20]: +/// ''' +/// Compute a ripemd160-hash of hash(pubkey). Always 20 bytes of binary. +/// ''' +STATIC mp_obj_t mod_trezorcrypto_HDNode_address_raw(mp_obj_t self) { + mp_obj_HDNode_t *o = MP_OBJ_TO_PTR(self); + // API requires a version, but we'll use zero and remove it. + uint8_t raw[21]; + hdnode_get_address_raw(&o->hdnode, 0x0, raw); + return mp_obj_new_bytes(raw+1, 20); +} +STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorcrypto_HDNode_address_raw_obj, mod_trezorcrypto_HDNode_address_raw); + +#endif // FOUNDATION_ADDITIONS + #if !BITCOIN_ONLY /// def nem_address(self, network: int) -> str: @@ -515,6 +555,10 @@ STATIC const mp_rom_map_elem_t mod_trezorcrypto_HDNode_locals_dict_table[] = { #endif {MP_ROM_QSTR(MP_QSTR_derive_path), MP_ROM_PTR(&mod_trezorcrypto_HDNode_derive_path_obj)}, +#ifdef FOUNDATION_ADDITIONS + {MP_ROM_QSTR(MP_QSTR_serialize_private), + MP_ROM_PTR(&mod_trezorcrypto_HDNode_serialize_private_obj)}, +#endif {MP_ROM_QSTR(MP_QSTR_serialize_public), MP_ROM_PTR(&mod_trezorcrypto_HDNode_serialize_public_obj)}, {MP_ROM_QSTR(MP_QSTR_clone), @@ -527,6 +571,8 @@ STATIC const mp_rom_map_elem_t mod_trezorcrypto_HDNode_locals_dict_table[] = { #ifdef FOUNDATION_ADDITIONS {MP_ROM_QSTR(MP_QSTR_my_fingerprint), MP_ROM_PTR(&mod_trezorcrypto_HDNode_my_fingerprint_obj)}, + {MP_ROM_QSTR(MP_QSTR_address_raw), + MP_ROM_PTR(&mod_trezorcrypto_HDNode_address_raw_obj)}, #endif {MP_ROM_QSTR(MP_QSTR_child_num), @@ -562,6 +608,44 @@ STATIC const mp_obj_type_t mod_trezorcrypto_HDNode_type = { /// mock:global +#ifdef FOUNDATION_ADDITIONS + +/// def deserialize(self, value: str, version: int, is_public: boolean) -> HDNode: +/// ''' +/// Construct a BIP0032 HD node from a base58-serialized value. +/// ''' +STATIC mp_obj_t mod_trezorcrypto_bip32_deserialize(mp_obj_t value, mp_obj_t version, mp_obj_t is_public) { + mp_buffer_info_t valueb; + mp_get_buffer_raise(value, &valueb, MP_BUFFER_READ); + if (valueb.len == 0) { + mp_raise_ValueError("Invalid value"); + } + uint32_t _version = mp_obj_get_int_truncated(version); + bool _is_public = mp_obj_is_true(is_public); + HDNode hdnode; + uint32_t fingerprint; + if (_is_public) { + printf("Calling hdnode_deserialize_public()\n"); + if (hdnode_deserialize_public(valueb.buf, _version, SECP256K1_NAME, &hdnode, &fingerprint) < 0) { + mp_raise_ValueError("Failed to deserialize public"); + } + } else { + printf("Calling hdnode_deserialize_private()\n"); + if (hdnode_deserialize_private(valueb.buf, _version, SECP256K1_NAME, &hdnode, &fingerprint) < 0) { + mp_raise_ValueError("Failed to deserialize private"); + } + } + + mp_obj_HDNode_t *o = m_new_obj(mp_obj_HDNode_t); + o->base.type = &mod_trezorcrypto_HDNode_type; + o->hdnode = hdnode; + o->fingerprint = fingerprint; + + return MP_OBJ_FROM_PTR(o); +} +STATIC MP_DEFINE_CONST_FUN_OBJ_3(mod_trezorcrypto_bip32_deserialize_obj, mod_trezorcrypto_bip32_deserialize); +#endif + /// def from_seed(seed: bytes, curve_name: str) -> HDNode: /// """ /// Construct a BIP0032 HD node from a BIP0039 seed value. @@ -653,6 +737,9 @@ STATIC const mp_rom_map_elem_t mod_trezorcrypto_bip32_globals_table[] = { {MP_ROM_QSTR(MP_QSTR_HDNode), MP_ROM_PTR(&mod_trezorcrypto_HDNode_type)}, {MP_ROM_QSTR(MP_QSTR_from_seed), MP_ROM_PTR(&mod_trezorcrypto_bip32_from_seed_obj)}, +#ifdef FOUNDATION_ADDITIONS + { MP_ROM_QSTR(MP_QSTR_deserialize), MP_ROM_PTR(&mod_trezorcrypto_bip32_deserialize_obj) }, +#endif #if !BITCOIN_ONLY {MP_ROM_QSTR(MP_QSTR_from_mnemonic_cardano), MP_ROM_PTR(&mod_trezorcrypto_bip32_from_mnemonic_cardano_obj)}, diff --git a/ports/stm32/boards/Passport/version.c b/ports/stm32/boards/Passport/version.c index e51d5c3..7cd5c4d 100644 --- a/ports/stm32/boards/Passport/version.c +++ b/ports/stm32/boards/Passport/version.c @@ -1,7 +1,7 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // -// SPDX-FileCopyrightText: 2018 Coinkite, Inc. +// SPDX-FileCopyrightText: 2018 Coinkite, Inc. // SPDX-License-Identifier: GPL-3.0-only // // (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard diff --git a/ports/stm32/boards/Passport/version.h b/ports/stm32/boards/Passport/version.h index 3e4972b..8f9b13e 100644 --- a/ports/stm32/boards/Passport/version.h +++ b/ports/stm32/boards/Passport/version.h @@ -1,7 +1,7 @@ -// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. +// SPDX-FileCopyrightText: 2020 Foundation Devices, Inc. // SPDX-License-Identifier: GPL-3.0-or-later // -// SPDX-FileCopyrightText: 2018 Coinkite, Inc. +// SPDX-FileCopyrightText: 2018 Coinkite, Inc. // SPDX-License-Identifier: GPL-3.0-only // /* diff --git a/ports/stm32/img.raw b/ports/stm32/img.raw deleted file mode 100644 index 7cc88b6b621509483391b1a0ea1ae148f9a55038..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 290400 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�zF3^vX9k}T|rWK|z0gS-WP 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�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 diff --git a/ports/stm32/stm32_it.c b/ports/stm32/stm32_it.c index e77642b..6d77b1d 100644 --- a/ports/stm32/stm32_it.c +++ b/ports/stm32/stm32_it.c @@ -122,7 +122,7 @@ STATIC void print_reg(const char *label, uint32_t val) { mp_hal_stdout_tx_str(fmt_hex(val, hexStr)); mp_hal_stdout_tx_str("\r\n"); } - +#ifndef MICROPY_PASSPORT STATIC void print_hex_hex(const char *label, uint32_t val1, uint32_t val2) { char hex_str[9]; mp_hal_stdout_tx_str(label); @@ -131,7 +131,7 @@ STATIC void print_hex_hex(const char *label, uint32_t val1, uint32_t val2) { mp_hal_stdout_tx_str(fmt_hex(val2, hex_str)); mp_hal_stdout_tx_str("\r\n"); } - +#endif /* MICROPY_PASSPORT */ // The ARMv7M Architecture manual (section B.1.5.6) says that upon entry // to an exception, that the registers will be in the following order on the // // stack: R0, R1, R2, R3, R12, LR, PC, XPSR @@ -140,7 +140,11 @@ typedef struct { uint32_t r0, r1, r2, r3, r12, lr, pc, xpsr; } ExceptionRegisters_t; +#ifdef MICROPY_PASSPORT +int pyb_hard_fault_debug = 1; +#else int pyb_hard_fault_debug = 0; +#endif /* MICROPY_PASSPORT */ void HardFault_C_Handler(ExceptionRegisters_t *regs) { if (!pyb_hard_fault_debug) { @@ -153,6 +157,47 @@ void HardFault_C_Handler(ExceptionRegisters_t *regs) { pyb_usb_flags = 0; #endif +#ifdef MICROPY_PASSPORT + uint32_t cfsr = SCB->CFSR; + + mp_hal_stdout_tx_str("HardFault Occurred\r\n"); + print_reg("HFSR ", SCB->HFSR); + print_reg("CFSR ", cfsr); + if (cfsr & 0x8000) { + uint32_t faultaddr = (uint32_t)SCB->BFAR; + uint32_t settings_sector = 0x081e0000; + + mp_hal_stdout_tx_str("Bus Fault Occurred\r\n"); + print_reg("Offending Address ", SCB->BFAR); + + if (faultaddr & settings_sector) { + FLASH_EraseInitTypeDef EraseInitStruct = {0}; + + /* + * Settings flash is pulling a double ECC fault. In order to + * recover we must erase the contents and then reset so that + * the software startup is successful. + */ + mp_hal_stdout_tx_str("Recovery in progress...\r\n"); + + HAL_FLASH_Unlock(); + __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_ALL_ERRORS_BANK1 | FLASH_FLAG_ALL_ERRORS_BANK2); + + EraseInitStruct.TypeErase = TYPEERASE_SECTORS; + EraseInitStruct.VoltageRange = VOLTAGE_RANGE_3; // voltage range needs to be 2.7V to 3.6V + EraseInitStruct.Banks = FLASH_BANK_2; + EraseInitStruct.Sector = 7; + EraseInitStruct.NbSectors = 1; + + uint32_t SectorError = 0; + HAL_FLASHEx_Erase(&EraseInitStruct, &SectorError); + HAL_FLASH_Lock(); + } + } + + /* Reset the board */ + powerctrl_mcu_reset(); +#else mp_hal_stdout_tx_str("HardFault\r\n"); print_reg("R0 ", regs->r0); @@ -189,7 +234,7 @@ void HardFault_C_Handler(ExceptionRegisters_t *regs) { print_hex_hex(" ", (uint32_t)sp, *sp); } } - +#endif /* MICROPY_PASSPORT */ /* Go to infinite loop when Hard Fault exception occurs */ while (1) { __fatal_error("HardFault"); @@ -671,11 +716,13 @@ void TIM6_DAC_IRQHandler(void) { #endif #if defined(TIM7) // STM32F401 doesn't have TIM7 +#ifndef MICROPY_PASSPORT void TIM7_IRQHandler(void) { IRQ_ENTER(TIM7_IRQn); timer_irq_handler(7); IRQ_EXIT(TIM7_IRQn); } +#endif /* MICROPY_PASSPORT */ #endif #if defined(TIM8) // STM32F401 doesn't have TIM8 diff --git a/ports/stm32/system_stm32.c b/ports/stm32/system_stm32.c index 78990bd..2ebb08e 100644 --- a/ports/stm32/system_stm32.c +++ b/ports/stm32/system_stm32.c @@ -206,6 +206,7 @@ void SystemClock_Config(void) #if defined(STM32F4) || defined(STM32F7) || defined(STM32H7) #ifdef MICROPY_PASSPORT RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE | RCC_OSCILLATORTYPE_HSI48; + RCC_OscInitStruct.HSI48State = RCC_HSI48_ON; #else RCC_OscInitStruct.OscillatorType = MICROPY_HW_RCC_OSCILLATOR_TYPE; #endif /* MICROPY_PASSPORT*/ diff --git a/py/objstringio.c b/py/objstringio.c index cca4a81..6f018bd 100644 --- a/py/objstringio.c +++ b/py/objstringio.c @@ -228,6 +228,7 @@ STATIC const mp_rom_map_elem_t stringio_locals_dict_table[] = { { MP_ROM_QSTR(MP_QSTR_readline), MP_ROM_PTR(&mp_stream_unbuffered_readline_obj) }, { MP_ROM_QSTR(MP_QSTR_write), MP_ROM_PTR(&mp_stream_write_obj) }, { MP_ROM_QSTR(MP_QSTR_seek), MP_ROM_PTR(&mp_stream_seek_obj) }, + { MP_ROM_QSTR(MP_QSTR_tell), MP_ROM_PTR(&mp_stream_tell_obj) }, { MP_ROM_QSTR(MP_QSTR_flush), MP_ROM_PTR(&mp_stream_flush_obj) }, { MP_ROM_QSTR(MP_QSTR_close), MP_ROM_PTR(&mp_stream_close_obj) }, { MP_ROM_QSTR(MP_QSTR_getvalue), MP_ROM_PTR(&stringio_getvalue_obj) },