From 428a1f0229cd7d56d925a6f20c41b513ff3afd06 Mon Sep 17 00:00:00 2001 From: Ken Carpenter <62639971+FoundationKen@users.noreply.github.com> Date: Mon, 20 Sep 2021 13:33:21 -0700 Subject: [PATCH] Dev v1.0.7 (#57) * PASS1-133: Modify cosign file naming for fully signed binaries. (#34) * PASS1-133: Modify cosign file naming for fully signed binaries. * Remove all USE_CRYPTO, it is not used anymore Also remove function that is not needed. * Added use of firmware version from header, also fixed a seg fault * PASS1-135: Fix sticky up or down key (#33) * PASS1-135: Fix sticky up or down key * Add comment for input.reset function * PASS1-128: Add support back for Bitcoin testnet (#29) * Fixes part of PASS-91 Show address and index of the address being verified * Second half of fix for ENV1-91 Add better messaging for address range searching Fix a bug when saving next_addrs (was comparing dicts by ref) * Fixes PASS1-122 Check change addresses in addition to receive address in "Verify Address" * Fix comment punctuation * Show backup filename to user after successful backup (#18) Fix PASS1-92 * Auto-truncate multisig config names (#19) Fix PASS1-101 * PASS1-101: Auto-truncate multisig config names (#19) Fix PASS1-101 * Remove unnecessary comments * PASS1-92 (#20) * Show backup filename to user after successful backup Fix PASS1-92 * Add missing 'card' parameter to `get_backups_folder_path()` calls * Revert path function changes since 'card' is not available * PASS1-102: Fix backwards microSD issue Found that `ErrorCode` in `SD_HandleTypeDef` was not reset after a failure. Updated `HAL_SD_Init()` to reset it before attempting initialization. * PASS1-102: Fix backwards microSD issue (#21) Found that `ErrorCode` in `SD_HandleTypeDef` was not reset after a failure. Updated `HAL_SD_Init()` to reset it before attempting initialization. * PASS1-102_b (#22) * PASS1-102: Fix backwards microSD issue Found that `ErrorCode` in `SD_HandleTypeDef` was not reset after a failure. Updated `HAL_SD_Init()` to reset it before attempting initialization. * Switch back to hard-coded path for now * PASS1-122_b (#23) * PASS1-102: Fix backwards microSD issue Found that `ErrorCode` in `SD_HandleTypeDef` was not reset after a failure. Updated `HAL_SD_Init()` to reset it before attempting initialization. * Update user messaging for found/not found case of Verify Address Fix bug with trailing space at end of line in `word_wrap()` * Strip ever time through the loop * PASS1-125: Add Git commit-msg hook to check for Linear ID (#24) * PASS1-125: Add Git commit-msg hook to check for Linear ID * Update .githooks/commit-msg Co-authored-by: Jean Pierre Dudey Co-authored-by: Jean Pierre Dudey * PASS1-122: Minor updates to text (#27) * PASS1-127: Fix `reuse lint` issues in the repo (#26) * PASS1-113: Give the user a way to clear the developer pubkey slot (#25) * PASS1-122: Added "Address Verified" text to new wallet pairing (#28) * PASS1-122: Minor updates to text * PASS1-122: Added "Address Verified" text to new wallet pairing * PASS1-128: Add support back for Bitcoin testnet Co-authored-by: Ken Carpenter Co-authored-by: Ken Carpenter <62639971+FoundationKen@users.noreply.github.com> Co-authored-by: Jean Pierre Dudey * PASS1-56: Use XFP in backups filename and don't save `backup_num` (#32) * PASS1-34: Refactor find address code so there is only one copy (#37) * PASS1-94: Prevent installing user-signed firmware if no user-key installed (#38) * PASS1-94: Prevent installing user-signed firmware if no user signing key installed * Fixed case where user pubkey was removed manually * Fixed text to match other areas where text is used * Update text message for developer pubkey * Hard coded user signed field to false Co-authored-by: Ken Carpenter <62639971+FoundationKen@users.noreply.github.com> * PASS1-55: Add menu to switch to a different Passphrase without rebooting (#35) * PASS1-55: Add menu to switch to a different Passphrase without rebooting * Changed order of menu items in Passphrase menu * Modified menu titles and removed "a" from inconsistent text * PASS1-137: Add Justfile support to Gen 1 repo (#36) * PASS1-137: Add Justfile support to Gen 1 repo First pass - not all expected commands are added yet * Update Justfile with fmt command Add py and c/h formatting Need to finalize .clang-format file before doing a full reformatting PR * Refactor Justfiles to separate them out Also add graphics build commands * Update Justfiles a bit Fix formatting of graphics header files in preparation for automatic code formatting * PASS1-139: Implement code to allow OCD to capture a screenshot over JTAG (#42) * PASS1-139: Implement code to allow OCD to capture a screenshot over JTAG * Update sram4.py * PASS1-132: Remove duplicate file compilation (#39) * PASS1-78: In display.text_input, split lines based on pixel widths (#41) * PASS1-78: In display.text_input, split lines based on pixel widths * Check for StringIO object before calling split_by_char_size * PASS1-89: Show exported filename when exporting wallet to microSD (#43) * PASS1-89: Show exported filename when exporting wallet to microSD * Deleted/commented unnecessary lines * PASS1-136: Add Specter wallet back once they fix UR issues (#44) * PASS1-136: Add Specter wallet back once they fix UR issues * Rebase onto dev-v1.0.7 * Remove passport from export filename * Remove flag from all wallets besides Specter wallet * Removed flag from unnecessary field and renamed flag to import * Renamed multisig_import function * PASS1-112: Passphrase input dialog improvements (#48) * PASS1-112: Passphrase input dialog improvements The passphrase is limited to 64 characters. The line spacing was reduced to make room for 7 lines. 63 capital W's will fill all 7 lines (+1 over), otherwise 64 characters usually takes about 4 lines. * Add constant for max message length * TOOL-3: Setup Docker infra for Gen 1 Development (#45) * Add Dockerfile for building the firmware Setting up a local environment for building the firmware can be a painful process. This wraps that process up in a Dockerfile containing all the deps needed which is then used in the justfile to build the firmware. * Add just targets for signing and cleaning * Change sha target to take a sha and verify it directly * Add docs for verifying the firmware SHA sum * Add version param to sign just target * Update verify-sha output to be more explicit * PASS1-67: Change unit to sats in settings (#46) * PASS1-67: Change unit to sats in settings * Added warnings for Testnet and made the setting volatile * Added 'chain' removal to schema_evolution and moved Units menu to top * Moved Units below Change Pin in menu * TOOL-4: Implement CI for Passport Gen 1 build (#49) * TOOL-4: Create CI for firmware build * TOOL-4: Improve handling of git describe output * TOOL-4: Rename Justfile to match others in repo * TOOL-4: Add caching and separated Docker building in CI * TOOL-4: Update CI to push image to local registry service * TOOL-4: Update CI to allow customizing of D_BASE * TOOL-4: Change clang format action * TOOL-4: User correct clang format version * TOOL-4: YAML :( * TOOL-4: Update to clang-format-10.0 * TOOL-4: Updaet to 10 * TOOL-4: Build and export the bootloader * TOOL-4: Add D_BASE to bootload build step * TOOL-4: Correctly pass D_BASE to bootloader job * TOOL-4: Update bootloader make path in Justfile * TOOL-4: Update CI to output tools * PASS1-140: Add Justfile commands to DEVELOPMENT.md (#51) * PASS1-140: Add Justfile commands to DEVELOPMENT.md * Update DEVELOPMENT.md * Update DEVELOPMENT.md Co-authored-by: Ken Carpenter <62639971+FoundationKen@users.noreply.github.com> * PASS1-148: Fix missing address prefixes for testnet (#53) * PASS1-148: Fix missing address prefixes for testnet * Add comma separations to sats values * Casa support added * Added testnet prefix check to Verify Address process * PASS1-150: Fixed missing argument in `import_from_psbt()` call (#55) * PASS1-150: Fixed missing argument in `import_from_psbt()` call Also fixed typo in function description. * Added a space between value and label of BTC/sats * Disable Casa Support Casa has not approved the support for Passport yet, until then Casa is disabled temporarily. Co-authored-by: Corey Lakey Co-authored-by: Jean Pierre Dudey Co-authored-by: Alex Sears --- .dockerignore | 1 + .githooks/commit-msg | 7 +- .github/workflows/validate_and_build.yaml | 126 ++++++++++++ .gitignore | 4 + DEVELOPMENT.md | 31 +++ Dockerfile | 33 ++++ Justfile | 100 ++++++++++ README.md | 8 + ports/stm32/Justfile | 180 ++++++++++++++++++ ports/stm32/Makefile | 2 +- ports/stm32/boards/Passport/.clang-format | 112 ++++++++++- ports/stm32/boards/Passport/.reuse/dep5 | 4 + .../stm32/boards/Passport/bootloader/Justfile | 39 ++++ .../stm32/boards/Passport/bootloader/Makefile | 2 +- .../Passport/bootloader/bootloader_graphics.h | 6 +- .../stm32/boards/Passport/bootloader/secrets | Bin 256 -> 0 bytes .../stm32/boards/Passport/firmware_graphics.h | 6 +- .../stm32/boards/Passport/graphics/c/Justfile | 9 + .../boards/Passport/graphics/c/cbuild.py | 10 +- .../boards/Passport/graphics/py/Justfile | 8 + ports/stm32/boards/Passport/modfoundation.c | 31 ++- .../stm32/boards/Passport/modules/actions.py | 103 +++++----- ports/stm32/boards/Passport/modules/auth.py | 8 +- ports/stm32/boards/Passport/modules/chains.py | 35 ++-- .../stm32/boards/Passport/modules/choosers.py | 52 +++++ .../boards/Passport/modules/constants.py | 7 + .../stm32/boards/Passport/modules/display.py | 44 +++-- ports/stm32/boards/Passport/modules/export.py | 12 +- ports/stm32/boards/Passport/modules/flow.py | 11 +- ports/stm32/boards/Passport/modules/menu.py | 1 + .../stm32/boards/Passport/modules/multisig.py | 4 +- .../boards/Passport/modules/new_wallet.py | 95 +++++---- .../Passport/modules/schema_evolution.py | 10 + ports/stm32/boards/Passport/modules/sram4.py | 2 + ports/stm32/boards/Passport/modules/stash.py | 2 +- ports/stm32/boards/Passport/modules/utils.py | 76 +++++++- ports/stm32/boards/Passport/modules/ux.py | 43 ++--- .../Passport/modules/wallets/bitcoin_core.py | 4 +- .../Passport/modules/wallets/bluewallet.py | 2 +- .../boards/Passport/modules/wallets/btcpay.py | 2 +- .../Passport/modules/wallets/caravan.py | 2 +- .../boards/Passport/modules/wallets/casa.py | 4 +- .../Passport/modules/wallets/dux_reserve.py | 2 +- .../Passport/modules/wallets/electrum.py | 2 +- .../Passport/modules/wallets/fullynoded.py | 2 +- .../modules/wallets/generic_json_wallet.py | 12 +- .../Passport/modules/wallets/gordian.py | 2 +- .../boards/Passport/modules/wallets/lily.py | 2 +- .../Passport/modules/wallets/multisig_json.py | 8 +- .../Passport/modules/wallets/sparrow.py | 2 +- .../Passport/modules/wallets/specter.py | 4 +- .../Passport/modules/wallets/sw_wallets.py | 2 +- .../boards/Passport/modules/wallets/utils.py | 34 ++-- .../boards/Passport/modules/wallets/vault.py | 5 +- .../boards/Passport/modules/wallets/wasabi.py | 9 +- .../boards/Passport/tools/cosign/Makefile | 1 - .../boards/Passport/tools/cosign/cosign.c | 83 +++----- .../Passport/tools/version_info/version_info | 9 +- .../tools/word_list_gen/word_list_gen.c | 5 +- py/dynruntime.mk | 2 +- py/mkenv.mk | 2 +- py/mkrules.mk | 2 +- requirements.txt | 2 + tools/makemanifest.py | 2 +- 64 files changed, 1119 insertions(+), 313 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/workflows/validate_and_build.yaml create mode 100644 Dockerfile create mode 100644 Justfile create mode 100644 ports/stm32/Justfile create mode 100644 ports/stm32/boards/Passport/bootloader/Justfile delete mode 100644 ports/stm32/boards/Passport/bootloader/secrets create mode 100644 ports/stm32/boards/Passport/graphics/c/Justfile create mode 100644 ports/stm32/boards/Passport/graphics/py/Justfile create mode 100644 requirements.txt diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..d09bd5e0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +cosign diff --git a/.githooks/commit-msg b/.githooks/commit-msg index 663188cf..fd88e3d8 100755 --- a/.githooks/commit-msg +++ b/.githooks/commit-msg @@ -2,13 +2,12 @@ # SPDX-FileCopyrightText: 2021 Foundation Devices, Inc. # SPDX-License-Identifier: GPL-3.0-or-later -TEAM_PREFIX='PASS1' -commit_regex="^($TEAM_PREFIX-[0-9]+:\ )" +commit_regex="^([A-Z]+[0-9]*-[0-9]+:\ )" -if ! `grep -iqE "$commit_regex" "$1"`; then +if ! grep -iqE "$commit_regex" "$1"; then echo "=========================================================================================" >&2 echo "Aborting commit. Your commit message must start with a Linear issue ID, colon then space." >&2 - echo "Example: '$TEAM_PREFIX-123: ' (To commit anyway, use the --no-verify option)" >&2 + echo "Example: 'PASS1-123: ' (To commit anyway, use the --no-verify option)" >&2 echo "=========================================================================================" >&2 exit 1 fi diff --git a/.github/workflows/validate_and_build.yaml b/.github/workflows/validate_and_build.yaml new file mode 100644 index 00000000..7c6a3b88 --- /dev/null +++ b/.github/workflows/validate_and_build.yaml @@ -0,0 +1,126 @@ +name: Validate and Build +on: [push] +jobs: + lint-py: + runs-on: ubuntu-18.04 + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.9 + uses: actions/setup-python@v2 + with: + python-version: 3.9 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pycodestyle + - name: Setup just + uses: extractions/setup-just@aa5d15c144db4585980a44ebfdd2cf337c4f14cb + - name: Analysing the code + run: just ports/stm32/lint-py + continue-on-error: true + + lint-c: + runs-on: ubuntu-18.04 + steps: + - uses: actions/checkout@v2 + - name: Analysing the code + uses: jidicula/clang-format-action@7f6b4bf5a7eb211c0872364ccd8072ff8a77ac44 + with: + clang-format-version: '10' + check-path: ./ports/stm32 + exclude-regex: trezor-firmware + continue-on-error: true + + build-firmware: + runs-on: ubuntu-18.04 + needs: [lint-py, lint-c] + services: + registry: + image: registry:2 + ports: + - 5000:5000 + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + with: + driver-opts: network=host + + - name: Cache Docker layers + uses: actions/cache@v2 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx- + + - name: Build the dependency Docker image + uses: docker/build-push-action@v2 + with: + push: true + tags: localhost:5000/foundation-devices/firmware-builder:${{ github.sha }} + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache + + - name: Setup just + uses: extractions/setup-just@aa5d15c144db4585980a44ebfdd2cf337c4f14cb + + - name: Build the firmware + run: | + echo "$SIGNING_KEY" > signing_key.pem + version=$(git describe --all --match *dev* | awk '{print $NF}' | cut -d '-' -f 2) + + just DOCKER_REGISTRY_BASE="$D_BASE" sign signing_key.pem "${version#?}" + env: + SIGNING_KEY: ${{ secrets.UserSigningKey }} + D_BASE: localhost:5000/ + + - name: Build the bootloader + run: just DOCKER_REGISTRY_BASE="$D_BASE" bootloader-build + env: + D_BASE: localhost:5000/ + + - name: Build and make tools available + run: just DOCKER_REGISTRY_BASE="$D_BASE" tools + env: + D_BASE: localhost:5000/ + + - name: Upload built firmware file + uses: actions/upload-artifact@v2 + with: + name: firmware.bin + path: ports/stm32/build-Passport/firmware.bin + + - name: Upload signed firmware file + uses: actions/upload-artifact@v2 + with: + name: firmware-key-user.bin + path: ports/stm32/build-Passport/firmware-key-user.bin + + - name: Upload bootloader + uses: actions/upload-artifact@v2 + with: + name: bootloader.bin + path: ports/stm32/boards/Passport/bootloader/arm/release/bootloader.bin + + - name: Upload cosign + uses: actions/upload-artifact@v2 + with: + name: cosign + path: cosign + + - name: Upload add-secrets + uses: actions/upload-artifact@v2 + with: + name: add-secrets + path: ports/stm32/boards/Passport/tools/add-secrets/x86/release/add-secrets + + - name: Upload word_list_gen + uses: actions/upload-artifact@v2 + with: + name: word_list_gen + path: ports/stm32/boards/Passport/tools/word_list_gen/word_list_gen diff --git a/.gitignore b/.gitignore index 72bdd190..3670649b 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,7 @@ ports/stm32/secrets* ports/stm32/boards/Passport/bootloader/version_info.c ports/stm32/boards/Passport/bootloader/secrets* + +*.pem +.vscode +cosign diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index cbeb0227..b046df90 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -44,6 +44,21 @@ Several tools are required for building and debugging Passport. sudo apt install python3-pip sudo pip3 install rshell # (this should install rshell in /usr/local/) +### Using Justfile commands +To use Just for running commands, first follow the instructions here: https://github.com/casey/just#installation to install Just. Note that `Pillow` must be updated to `8.3.1` for all commands to work properly. + +Once Just has been installed, the developer can use `just` commands to perform actions such as building, flashing, resetting and even taking screenshots of the displays screen. + +Note that all `just` commands must be run from `ports/stm32/` directory. + +Here are some of the most common `just` commands and their usages: + + just flash {version} - Builds if necessary, signs with a user key and then flashes the device with the firmware binary created under `build-Passport/` + just reset - Resets the device + just screenshot {filename} - Screenshots the device and saves to the desired filename + +See the `Justfile` included in our source for the full list of `just` commands. + ## Building ### Open Shell Windows/Tabs You will need several shell windows or tabs open to interact with the various tools. @@ -67,6 +82,10 @@ You should see it building various `.c` files and freezing `.py` files. Once co GEN build-Passport/firmware.dfu GEN build-Passport/firmware.hex +If you are using `just` commands, then building the firmware can be done by running the following command: + + just build + #### Code Signing In order to load the files onto the device, they need to first be signed by two separate keys. The `cosign` program performs this task, and it needs to be called twice with two separate @@ -91,6 +110,12 @@ You can also dump the contents of the firmware header with the following command cosign -f build-Passport/firmware-signed-signed.bin -x +If you are using `just` commands, then signing the firmware can be done by running the following command with the desired version: + + just sign 1.0.7 + +It will build the firmware first if necessary. + #### Building the Bootloader To build the bootloader do the following: @@ -148,6 +173,12 @@ These commands do the following: - Write the firmware to flash at address 0x8000000 - Reset the MCU and start executing code at address 0x8000000 +If you are using `just` commands, ocd and telnet steps are not required and instead, flashing the firmware can be done using the following command with the desired version number: + + just flash 1.0.7 + +It will build and sign the firmware first if necessary. + ### RShell Window We use `rshell` to connect to the MicroPython device over USB serial. Open another shell and run: diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..7f81af76 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,33 @@ +FROM ubuntu:18.04 AS cross_build +RUN apt-get update && \ + apt-get install -y git make gcc-arm-none-eabi python3 gcc && \ + rm -rf /var/lib/apt/lists/* +COPY drivers /workspace/passport-firmware/drivers +COPY docs /workspace/passport-firmware/docs +COPY extmod /workspace/passport-firmware/extmod +COPY lib /workspace/passport-firmware/lib +COPY mpy-cross /workspace/passport-firmware/mpy-cross +COPY py /workspace/passport-firmware/py +WORKDIR /workspace/passport-firmware/mpy-cross +RUN make + +FROM ubuntu:18.04 AS cosign_build +WORKDIR /workspace +RUN apt-get update && \ + apt-get install -y git make libssl-dev gcc && \ + rm -rf /var/lib/apt/lists/* +COPY ports/stm32/boards/Passport/tools/cosign /workspace/passport-firmware/ports/stm32/boards/Passport/tools/cosign +COPY ports/stm32/boards/Passport/include /workspace/passport-firmware/ports/stm32/boards/Passport/include +COPY lib /workspace/passport-firmware/lib +COPY ports/stm32/boards/Passport/common /workspace/passport-firmware/ports/stm32/boards/Passport/common +WORKDIR /workspace/passport-firmware/ports/stm32/boards/Passport/tools/cosign +RUN make + +FROM ubuntu:18.04 AS firmware_builder +COPY --from=cosign_build \ + /workspace/passport-firmware/ports/stm32/boards/Passport/tools/cosign/x86/release/cosign /usr/bin/cosign +COPY --from=cross_build \ + /workspace/passport-firmware/mpy-cross/mpy-cross /usr/bin/mpy-cross +RUN apt-get update && \ + apt-get install -y make gcc-arm-none-eabi autotools-dev automake libtool python3 && \ + rm -rf /var/lib/apt/lists/* diff --git a/Justfile b/Justfile new file mode 100644 index 00000000..4fdd0f70 --- /dev/null +++ b/Justfile @@ -0,0 +1,100 @@ +export DOCKER_REGISTRY_BASE := '' + +commit_sha := `git rev-parse HEAD` +docker_image := 'foundation-devices/firmware-builder:' + commit_sha +base_path := 'ports/stm32' +firmware_path := base_path + '/build-Passport/firmware.bin' + +# build the docker image and then the firmware and bootloader +build: docker-build firmware-build bootloader-build + +# build the dependency docker image +docker-build: + #!/usr/bin/env bash + set -exo pipefail + docker build -t ${DOCKER_REGISTRY_BASE}{{ docker_image }} . + +# build the firmware inside docker +firmware-build: + #!/usr/bin/env bash + set -exo pipefail + docker run --rm -v "$PWD":/workspace \ + -w /workspace/{{ base_path }} \ + --entrypoint bash \ + ${DOCKER_REGISTRY_BASE}{{ docker_image }} \ + -c 'make BOARD=Passport MPY_CROSS=/usr/bin/mpy-cross' + +# build the bootloader inside docker +bootloader-build: + #!/usr/bin/env bash + set -exo pipefail + docker run --rm -v "$PWD":/workspace \ + -w /workspace/{{ base_path }} \ + --entrypoint bash \ + ${DOCKER_REGISTRY_BASE}{{ docker_image }} \ + -c 'make -C boards/Passport/bootloader' + +# build the docker image and get the tools from it +tools: docker-build cosign-tool add-secrets-tool word-list-gen-tool + +# get cosign tool from built docker image +cosign-tool: + #!/usr/bin/env bash + set -exo pipefail + docker run --rm -v "$PWD":/workspace \ + -w /workspace \ + --entrypoint bash \ + ${DOCKER_REGISTRY_BASE}{{ docker_image }} \ + -c 'cp /usr/bin/cosign cosign' + +# get add-secrets tool from built docker image +add-secrets-tool: + #!/usr/bin/env bash + set -exo pipefail + docker run --rm -v "$PWD":/workspace \ + -w /workspace \ + --entrypoint bash \ + ${DOCKER_REGISTRY_BASE}{{ docker_image }} \ + -c 'make -C ports/stm32/boards/Passport/tools/add-secrets' + +# get word_list_gen tool from built docker image +word-list-gen-tool: + #!/usr/bin/env bash + set -exo pipefail + docker run --rm -v "$PWD":/workspace \ + -w /workspace/ports/stm32/boards/Passport/tools/word_list_gen \ + --entrypoint bash \ + ${DOCKER_REGISTRY_BASE}{{ docker_image }} \ + -c 'gcc word_list_gen.c bip39_words.c bytewords_words.c -o word_list_gen' + +# run the built firmware through SHA256 +verify-sha sha: build + #!/usr/bin/env bash + sha=$(shasum -a 256 {{ firmware_path }} | awk '{print $1}') + + echo -e "Expected SHA:\t{{ sha }}" + echo -e "Actual SHA:\t${sha}" + if [ "$sha" = "{{ sha }}" ]; then + echo "Hashes match!" + else + echo "ERROR: Hashes DO NOT match!" + fi + +# sign the built firmware using a private key and the cosign tool +sign keypath version filepath=firmware_path: firmware-build + #!/usr/bin/env bash + set -exo pipefail + + docker run --rm -v "$PWD":/workspace \ + -w /workspace \ + --entrypoint bash \ + ${DOCKER_REGISTRY_BASE}{{ docker_image }} \ + -c "cosign -f {{ filepath }} -k {{ keypath }} -v {{ version }}" + +# clean firmware build +clean: + docker run --rm -v "$PWD":/workspace \ + -w /workspace/{{ base_path }} \ + --entrypoint bash \ + ${DOCKER_REGISTRY_BASE}{{ docker_image }} \ + -c "make clean BOARD=Passport" diff --git a/README.md b/README.md index d0667944..02666406 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,14 @@ Code specific to Passport is included in the following folders: Please see [`DEVELOPMENT.md`](https://github.com/Foundation-Devices/passport/blob/main/DEVELOPMENT.md) for information on developing for Passport. +## Verifying Firmware SHA Sums + +To make building and verifying the firmware a simple process, there is a Dockerfile in the project that builds an image to be used to build the firmware. Using [`just`](https://github.com/casey/just), the following command can be used to verify the reproducability of the firmware. Make sure to substitute `` for the SHA string to verify. + +```shell +just verify-sha +``` + ## Open Source Components Passport's firmware incorporates open-source software from several third-party projects, as well as other first-party work we open-sourced. diff --git a/ports/stm32/Justfile b/ports/stm32/Justfile new file mode 100644 index 00000000..2f7d7423 --- /dev/null +++ b/ports/stm32/Justfile @@ -0,0 +1,180 @@ +# SPDX-FileCopyrightText: 2021 Foundation Devices, Inc. +# +# SPDX-License-Identifier: GPL-3.0-or-later + +# Install dependencies. +deps: + @echo "Not sure we will need this if all deps are setup via Dockerfile" + +# Initialize development environment +init: deps + git config core.hooksPath .githooks + +# Lint only the python code of the project +lint-py: + pycodestyle --exclude trezor-firmware --statistics . + +# Lint only the C code of the project +lint-c: + @echo "TBD" + +# Lint only the code of the project +lint-code: lint-py lint-c + +# Lint the licensing +lint-license: + reuse lint + +# Lint all of the project +lint: lint-code lint-license + +# +# Firmware Commands +# + +build: + make BOARD=Passport + +# Sign current firmware build with the user.pem key and set specified version +sign version="1.0.0": build + @echo "\nAdding user signature...\n" + @cosign -f build-Passport/firmware.bin -k ~/bin/keys/user.pem -v {{version}} > /dev/null + + @cosign -f build-Passport/firmware-key-user.bin -x + @echo "\nSigning Complete!" + +# Build, sign and flash the firmware with the specified version +flash version="1.0.0": (sign version) + just run-ocd-command "flash write_image erase build-Passport/firmware-key-user.bin 0x8020000" + just reset + +# Install a recent Foundation-signed build +flash-foundation version="1.0.0": + just run-ocd-command "flash write_image erase ../../releases/passport-fw-{{version}}.bin 0x8020000" + just reset + +# Clean the firmware build +clean: + make BOARD=Passport clean + +# +# Misc. Commands +# + +# Launch OCD, run a command and then exit +run-ocd-command command: + sudo /usr/local/bin/openocd -f stlink.cfg -c "adapter speed 1000; transport select hla_swd" -f stm32h7x.cfg -c "init; reset halt; {{command}}" -c "exit" + +run-ocd-command-no-halt command: + sudo /usr/local/bin/openocd -f stlink.cfg -c "adapter speed 1000; transport select hla_swd" -f stm32h7x.cfg -c "init; {{command}}" -c "exit" + +# Build all Python graphics +graphics-py: + just -f boards/Passport/graphics/py/Justfile build + +# Build all C graphics (firmware & bootloader) +graphics-c: + just -f boards/Passport/graphics/c/Justfile build + +graphics: graphics-py graphics-c + +# Reset the Passport +reset: + just run-ocd-command "reset" + +# Get the username for use below +user := `whoami` + +# Read the "ROM Secrets" from Passport and save them to a file +save-secrets filename="boards/Passport/bootloader/secrets": + just run-ocd-command "dump_image {{filename}} 0x0801FF00 256" + # Running OCD as sudo makes the output file be owned by root, so switch it back to the user + sudo chown {{user}}:{{user}} {{filename}} + +secrets: + #!/usr/bin/env bash + # The last bit below redirects stderr to stdout, which the backticks capture into the variable `secrets` + secrets=`just run-ocd-command "mdb 0x0801FF00 256" 2>&1` + secrets=`echo "$secrets" | tail -n 8` + echo -e "Passport ROM Secrets:\n$secrets" + +# Calculate all hashes and format it all for GitHub release notes +hash filepath: + #!/usr/bin/env bash + filename=`basename {{filepath}}` + + # SHA256 + sha=`shasum -b -a 256 {{filepath}} | sed -rn 's/^(.*) .*$/\1/p'` + echo -e "\n\`SHA256: $sha\`" + echo -e "\`(shasum -b -a 256 $filename)\`\n" + + # MD5 + md5=`mdsum {{filepath}} | sed -rn 's/^(.*) .*$/\1/p'` + echo "\`MD5: $md5\`" + echo -e "\`(md5 $filename or mdsum $filename)\`\n" + + # Build Hash + build_hash=`cosign -f {{filepath}} -x | sed -rn 's/^FW Build Hash: (.*)$/\1/p'` + echo -e "\`Build Hash: $build_hash\`" + echo -e "\`(Developers Only)\`\n" + +# Run all tests +test: + @echo "TBD" + +# Format the project's .py files under boards/Passport/modules +fmt-py: + #!/usr/bin/env bash + pushd boards/Passport/modules + files_to_fmt=`find . -path ./trezor-firmware -prune -false -o -name '*.py'` + autopep8 --max-line-length=120 --in-place $files_to_fmt + popd + +# Format the project's .c and .h files under boards/Passport/ +fmt-c: + #!/usr/bin/env bash + pushd boards/Passport + files_to_fmt=`find . -path ./trezor-firmware -prune -false -o -name '*.[c|h]'` + clang-format-5.0 -i --style=file $files_to_fmt + popd + +# Format the project's source code under boards/Passport +fmt: fmt-py fmt-c + +# Convert a raw pixel map to a PNG +convert-screenshot from_file to_file: + #!/usr/bin/python3 + from PIL import Image, ImageOps + raw_bits = open('{{from_file}}', 'rb').read() + WIDTH = 230 + HEIGHT = 303 + SCAN_WIDTH = 240 + + # Convert + img = Image.frombuffer('1', (SCAN_WIDTH, HEIGHT), raw_bits) + + # Crop to actual width (framebuffer is 240 vs 230 for actual display) + img = img.crop((0, 0, WIDTH, HEIGHT)) + + # Invert since raw image is actually white on black - have to convert to grayscale first since invert() doesn't work + # for 1-bit per pixel black/white images. + img = ImageOps.grayscale(img) + img = ImageOps.invert(img) + + # Apply a color shift to make it look nicer + img = ImageOps.colorize(img, (0,0,0,0), '#E0E0E0') + + img.save('{{to_file}}') + +# Capture a screenshot from Passport via OCD +screenshot filename: + #!/usr/bin/env bash + ADDR_FILE=screenshot-addr.tmp + TMP_FILE=screenshot.tmp + just run-ocd-command-no-halt "dump_image $ADDR_FILE 0x38006920 4" + N=`head -c 4 $ADDR_FILE | od -An --endian=little -t u4` + FRAMEBUFFER_ADDR=`printf '%x\n' $N` + echo FRAMEBUFFER_ADDR=$FRAMEBUFFER_ADDR + just run-ocd-command-no-halt "dump_image screenshot.tmp 0x$FRAMEBUFFER_ADDR 9090" + just convert-screenshot $TMP_FILE {{filename}} + rm -f $TMP_FILE $ADDR_FILE diff --git a/ports/stm32/Makefile b/ports/stm32/Makefile index 7c03b854..5a410db1 100644 --- a/ports/stm32/Makefile +++ b/ports/stm32/Makefile @@ -599,7 +599,7 @@ GEN_CDCINF_FILE = $(HEADER_BUILD)/pybcdc.inf GEN_CDCINF_HEADER = $(HEADER_BUILD)/pybcdc_inf.h # List of sources for qstr extraction -SRC_QSTR += $(SRC_C) $(SRC_MOD) $(SRC_LIB) $(EXTMOD_SRC_C) +SRC_QSTR += $(SRC_C) $(SRC_LIB) $(EXTMOD_SRC_C) # Append any auto-generated sources that are needed by sources listed in # SRC_QSTR SRC_QSTR_AUTO_DEPS += $(GEN_CDCINF_HEADER) diff --git a/ports/stm32/boards/Passport/.clang-format b/ports/stm32/boards/Passport/.clang-format index 2c3a5254..c20d6f4a 100644 --- a/ports/stm32/boards/Passport/.clang-format +++ b/ports/stm32/boards/Passport/.clang-format @@ -1,7 +1,107 @@ --- -# We'll use defaults from the LLVM style, but with 4 columns indentation. -BasedOnStyle: Mozilla -IndentWidth: 4 ---- -Language: Cpp -ColumnLimit: 120 +Language: Cpp +# BasedOnStyle: Chromium +AccessModifierOffset: -1 +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: true +AlignConsecutiveDeclarations: true +AlignEscapedNewlines: Left +AlignOperands: true +AlignTrailingComments: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: true +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: true +AlwaysBreakTemplateDeclarations: true +BinPackArguments: true +BinPackParameters: false +BraceWrapping: + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Attach +BreakBeforeInheritanceComma: false +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: BeforeColon +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: false +ColumnLimit: 120 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: true +ForEachMacros: + - foreach + - Q_FOREACH + - BOOST_FOREACH +IncludeCategories: + - Regex: '^<.*\.h>' + Priority: 1 + - Regex: '^<.*' + Priority: 2 + - Regex: '.*' + Priority: 3 +IncludeIsMainRegex: '([-_](test|unittest))?$' +IndentCaseLabels: true +IndentWidth: 4 +IndentWrappedFunctionNames: false +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: false +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBlockIndentWidth: 2 +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: false +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 1 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 200 +PointerAlignment: Left +ReflowComments: false +SortIncludes: true +SortUsingDeclarations: true +SpaceAfterCStyleCast: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeParens: ControlStatements +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 2 +SpacesInAngles: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Auto +TabWidth: 8 +UseTab: Never +... diff --git a/ports/stm32/boards/Passport/.reuse/dep5 b/ports/stm32/boards/Passport/.reuse/dep5 index 7cf701c7..ecabfb2e 100644 --- a/ports/stm32/boards/Passport/.reuse/dep5 +++ b/ports/stm32/boards/Passport/.reuse/dep5 @@ -18,3 +18,7 @@ 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. License: GPL-3.0-or-later + +Files: tools/version_info/version_info +Copyright: 2021 Foundation Devices, Inc. +License: GPL-3.0-or-later diff --git a/ports/stm32/boards/Passport/bootloader/Justfile b/ports/stm32/boards/Passport/bootloader/Justfile new file mode 100644 index 00000000..3d32aca0 --- /dev/null +++ b/ports/stm32/boards/Passport/bootloader/Justfile @@ -0,0 +1,39 @@ +# SPDX-FileCopyrightText: 2021 Foundation Devices, Inc. +# +# SPDX-License-Identifier: GPL-3.0-or-later + +# Launch OCD, run a command and then exit +run-ocd-command command: + cd ../../../; sudo /usr/local/bin/openocd -f stlink.cfg -c "adapter speed 1000; transport select hla_swd" -f stm32h7x.cfg -c "init; reset halt; {{command}}" -c "exit" + + +# Build the bootloader (debug, release, locked or production) +# TODO: Need to handle {{rel}} for locked and production, which should look in release folder for binary +build rel="release": + @echo "\nBuilding Bootloader..." + make {{rel}} + + @echo "\nAppending secrets to the end..." + add-secrets -b arm/{{rel}}/bootloader.bin -s secrets + + @echo "\nBootloader Build Complete" + +# Clean the bootloader build +clean: + @echo "Cleaning Bootloader..." + make clean + @echo "Bootloader Clean Complete" + +# Build and flash the bootloader with the secrets appended to the end +flash rel="release": (build rel) + just run-ocd-command "flash write_image erase boards/Passport/bootloader/arm/{{rel}}/bootloader-secrets.bin 0x8000000" + just reset + +# Build and flash the bootloader with no secrets (use to setup a new Secure Element) +flash-raw rel="release": (build rel) + just run-ocd-command "flash write_image erase boards/Passport/bootloader/arm/{{rel}}/bootloader.bin 0x8000000" + just reset + +# Reset the Passport +reset: + just run-ocd-command "reset" diff --git a/ports/stm32/boards/Passport/bootloader/Makefile b/ports/stm32/boards/Passport/bootloader/Makefile index 8d55e122..be8bad18 100644 --- a/ports/stm32/boards/Passport/bootloader/Makefile +++ b/ports/stm32/boards/Passport/bootloader/Makefile @@ -207,7 +207,7 @@ ifneq ($(MAKECMDGOALS),clean) endif # make a 'release' build -release: code-committed check-fontawesome clean all capture +release: clean all release: CFLAGS += -DRELEASE=1 -Werror clean: diff --git a/ports/stm32/boards/Passport/bootloader/bootloader_graphics.h b/ports/stm32/boards/Passport/bootloader/bootloader_graphics.h index f6810cd6..c97e460c 100644 --- a/ports/stm32/boards/Passport/bootloader/bootloader_graphics.h +++ b/ports/stm32/boards/Passport/bootloader/bootloader_graphics.h @@ -8,9 +8,9 @@ #include typedef struct _Image { - int16_t width; - int16_t height; - int16_t byte_width; + int16_t width; + int16_t height; + int16_t byte_width; uint8_t* data; } Image; diff --git a/ports/stm32/boards/Passport/bootloader/secrets b/ports/stm32/boards/Passport/bootloader/secrets deleted file mode 100644 index 6de6bab6a3920e0ec9833c4a9fb9fd53bee71817..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 256 zcmcES^$fS&BH|=mE4fwS>*we8$M3S1 typedef struct _Image { - int16_t width; - int16_t height; - int16_t byte_width; + int16_t width; + int16_t height; + int16_t byte_width; uint8_t* data; } Image; diff --git a/ports/stm32/boards/Passport/graphics/c/Justfile b/ports/stm32/boards/Passport/graphics/c/Justfile new file mode 100644 index 00000000..f4452c1e --- /dev/null +++ b/ports/stm32/boards/Passport/graphics/c/Justfile @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2021 Foundation Devices, Inc. +# +# SPDX-License-Identifier: GPL-3.0-or-later + +# Build all C graphics and copy files to main source folders +build: + make + cp firmware_graphics.* ../../ + cp bootloader_graphics.* ../../bootloader/ diff --git a/ports/stm32/boards/Passport/graphics/c/cbuild.py b/ports/stm32/boards/Passport/graphics/c/cbuild.py index fee825b4..bff39a7b 100755 --- a/ports/stm32/boards/Passport/graphics/c/cbuild.py +++ b/ports/stm32/boards/Passport/graphics/c/cbuild.py @@ -10,7 +10,7 @@ def read_text(fname): w = 0 - rows = [] + rows = [] Z = b'\0' F = b'\xff' @@ -59,7 +59,7 @@ def crunch(n): #print(' / '.join("%d => %d" % (wb,len(d)) for wb,d in a)) return a[0] - + def gen_header(outfile, fnames): @@ -78,9 +78,9 @@ def gen_header(outfile, fnames): #include typedef struct _Image { - int16_t width; - int16_t height; - int16_t byte_width; + int16_t width; + int16_t height; + int16_t byte_width; uint8_t* data; } Image; diff --git a/ports/stm32/boards/Passport/graphics/py/Justfile b/ports/stm32/boards/Passport/graphics/py/Justfile new file mode 100644 index 00000000..fd8a2715 --- /dev/null +++ b/ports/stm32/boards/Passport/graphics/py/Justfile @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: 2021 Foundation Devices, Inc. +# +# SPDX-License-Identifier: GPL-3.0-or-later + +# Build all Python graphics and copy files to main source folder +build: + make + cp graphics.py ../../modules/ diff --git a/ports/stm32/boards/Passport/modfoundation.c b/ports/stm32/boards/Passport/modfoundation.c index 7fab79df..caf8089a 100644 --- a/ports/stm32/boards/Passport/modfoundation.c +++ b/ports/stm32/boards/Passport/modfoundation.c @@ -205,6 +205,8 @@ const mp_obj_type_t keypad_type = { /*============================================================================= * Start of LCD class *=============================================================================*/ +#define FRAMEBUFFER_ADDR_SRAM4 0x38006920 // NOTE: If SRAM4 layout changes, this must change too! + void lcd_obj_print(const mp_print_t* print, mp_obj_t self_in, mp_print_kind_t kind) { @@ -224,6 +226,18 @@ lcd_obj_make_new(const mp_obj_type_t* type, size_t n_args, size_t n_kw, const mp lcd->base.type = &lcd_type; lcd->spi = &spi_obj[0]; // lcd_init(false); + + // If a framebuffer address was passed, save it to a known location + if (n_args == 1) { + // Get the buffer info from the passed in object + mp_buffer_info_t framebuffer_info; + mp_get_buffer_raise(args[0], &framebuffer_info, MP_BUFFER_READ); + + // Save the actual address + uint32_t* framebuffer_addr = (uint32_t*)FRAMEBUFFER_ADDR_SRAM4; + *framebuffer_addr = (uint32_t)(framebuffer_info.buf); + } + return MP_OBJ_FROM_PTR(lcd); } @@ -1276,7 +1290,7 @@ System_validate_firmware_header(mp_obj_t self, mp_obj_t header) // New header passport_firmware_header_t* new_fwhdr = (passport_firmware_header_t*)header_info.buf; - mp_obj_t tuple[3]; + mp_obj_t tuple[4]; bool is_valid = verify_header(header_info.buf); @@ -1303,7 +1317,10 @@ System_validate_firmware_header(mp_obj_t self, mp_obj_t header) 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); + // Is this user-signed firmware? + tuple[3] = mp_const_false; + + return mp_obj_new_tuple(4, tuple); } } else { // Invalid header @@ -1317,7 +1334,10 @@ System_validate_firmware_header(mp_obj_t self, mp_obj_t header) 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); + // No header = no user signed firmware + tuple[3] = mp_const_false; + + return mp_obj_new_tuple(4, tuple); } // is_valid @@ -1329,7 +1349,10 @@ System_validate_firmware_header(mp_obj_t self, mp_obj_t header) // No error message tuple[2] = mp_const_none; - return mp_obj_new_tuple(3, tuple); + // Is this user-signed firmware? + tuple[3] = (new_fwhdr->signature.pubkey1 == FW_USER_KEY) ? mp_const_true : mp_const_false; + + return mp_obj_new_tuple(4, tuple); } /// def System_set_user_firmware_pubkey(self, pubkey) -> None diff --git a/ports/stm32/boards/Passport/modules/actions.py b/ports/stm32/boards/Passport/modules/actions.py index b2965264..5f060952 100644 --- a/ports/stm32/boards/Passport/modules/actions.py +++ b/ports/stm32/boards/Passport/modules/actions.py @@ -23,7 +23,7 @@ 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, needs_microsd, format_btc_address, - is_all_zero, bytes_to_hex_str, split_to_lines) + is_all_zero, bytes_to_hex_str, split_to_lines, is_valid_btc_address, do_address_verify, run_chooser) 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, @@ -268,21 +268,15 @@ async def show(self): # Scan the address to be verified - should be a normal QR code system.turbo(True); address = await ux_scan_qr_code('Verify Address') + system.turbo(False) if address == None: return - # Strip prefix if present - if address[0:8].lower() == '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(): - # Nothing to return back to, so we must have skipped one or more steps...we're done - return - continue + address, is_valid_btc = await is_valid_btc_address(address) + if is_valid_btc == False: + if not self.goto_prev(): + return + continue # Get the address type from the address is_multisig = self.sig_type == 'multisig' @@ -290,29 +284,10 @@ async def show(self): 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) - # Scan addresses to see if it's valid - addr_idx, is_change = 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, is_change) - address = format_btc_address(address, addr_type) - - result = await ux_show_story('''Address Verified! - -{} - -This is a {} address at index {}.'''.format(address, 'change' if is_change == 1 else 'receive', addr_idx), - title='Verify', - left_btn='BACK', - right_btn='CONTINUE', - 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...we're done - return - - return + result = await do_address_verify(self.acct_num, address, addr_type, deriv_path, self.multisig_wallet) + if result == 'x': + if not self.goto_prev(): + return else: # User asked to stop searching return @@ -371,12 +346,19 @@ def no_pubkeys(filename): return # Validate the header - is_valid, version, error_msg = system.validate_firmware_header(header) + is_valid, version, error_msg, is_user_signed = 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 + if is_user_signed: + pubkey_result, pubkey = read_user_firmware_pubkey() + if not pubkey_result or is_all_zero(pubkey): + system.turbo(False) + await ux_show_story('Install a Developer PubKey before loading non-Foundation firmware.\n\n', title='Error', left_btn='BACK', right_btn='OK', center=True, center_vertically=True) + return + system.turbo(False) # Give the user a chance to confirm/back out @@ -1146,6 +1128,9 @@ async def sign_tx_from_sd(*a): import stash + # Let the user know that using Testnet is potentially dangerous + await show_testnet_warning() + if stash.bip39_passphrase: title = '[%s]' % xfp2str(settings.get('xfp')) else: @@ -1341,6 +1326,9 @@ async def magic_scan(menu, label, item): title = item.arg + # Let the user know that using Testnet is potentially dangerous + await show_testnet_warning() + while True: system.turbo(True) data = await ux_scan_qr_code(title) @@ -1412,13 +1400,20 @@ async def enter_passphrase(menu, label, item): from constants import MAX_PASSPHRASE_LENGTH title = item.arg - passphrase = await ux_enter_text(title, label="Enter a Passphrase", max_length=MAX_PASSPHRASE_LENGTH) - - # print("Chosen passphrase = {}".format(passphrase)) + passphrase = await ux_enter_text(title, label="Enter Passphrase", max_length=MAX_PASSPHRASE_LENGTH) - if not await ux_confirm('Are you sure you want to apply the passphrase:\n\n{}'.format(passphrase)): + # None is passed back when user chose "back" + if passphrase == None: return + # print("Chosen passphrase = {}".format(passphrase)) + if passphrase == '': + if not await ux_confirm('Are you sure you want to clear the passphrase?'): + return + else: + 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 dis.fullscreen("Applying Passphrase...") @@ -1855,13 +1850,13 @@ async def test_derive_addresses(*a): chain = chains.current_chain() addrs = [] - path = "m/84'/0'/{account}'/{change}/{idx}" + path = "m/84'/{coin_type}'/{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) + subpath = path.format(coin_type=chain.b44_cointype, account=0, change=0, idx=idx) node = sv.derive_path(subpath, register=False) addr = chain.address(node, AF_P2WPKH) addrs.append(addr) @@ -2082,4 +2077,24 @@ async def remove_user_firmware_pubkey(*a): title='Remove', center=True, center_vertically=True) - clear_cached_pubkey() \ No newline at end of file + clear_cached_pubkey() + +async def show_testnet_warning(): + chain = settings.get('chain', 'BTC') + if chain == 'TBTC': + await ux_show_story('Passport is in Testnet mode. Use a separate seed to avoid issues with malicious software wallets.', + title='Warning', + center=True, + center_vertically=True) + +async def testnet_chooser(*a): + from choosers import chain_chooser + + old_chain = settings.get('chain', 'BTC') + await run_chooser(chain_chooser, 'Passport', show_checks=True) + new_chain = settings.get('chain', 'BTC') + + # Only display the warning if the chain changed + if new_chain != old_chain: + # Let the user know that using Testnet is potentially dangerous + await show_testnet_warning() \ No newline at end of file diff --git a/ports/stm32/boards/Passport/modules/auth.py b/ports/stm32/boards/Passport/modules/auth.py index 42e3f27d..f21d7d33 100644 --- a/ports/stm32/boards/Passport/modules/auth.py +++ b/ports/stm32/boards/Passport/modules/auth.py @@ -356,10 +356,10 @@ def render_output(self, o): # - expects CTxOut object # - gives user-visible string # - val = ''.join(self.chain.render_value(o.nValue)) + val, label = self.chain.render_value(o.nValue) dest = self.chain.render_address(o.scriptPubKey) - return '\n%s\n\nDestination:\n%s' % (val, dest) + return '\n%s %s\n\nDestination:\n%s' % (val, label, dest) def render_warnings(self): with uio.StringIO() as msg: @@ -584,9 +584,7 @@ def render_change_text(self): msg.write('\nNo change') return msg.getvalue() - total_val = ' '.join(self.chain.render_value(total)) - - msg.write("\n%s\n" % total_val) + msg.write("\n%s %s\n" % self.chain.render_value(total)) if len(addrs) == 1: msg.write('\nChange Address:\n%s\n' % addrs[0]) diff --git a/ports/stm32/boards/Passport/modules/chains.py b/ports/stm32/boards/Passport/modules/chains.py index 57bc1254..61ff889c 100644 --- a/ports/stm32/boards/Passport/modules/chains.py +++ b/ports/stm32/boards/Passport/modules/chains.py @@ -150,20 +150,30 @@ def hash_message(cls, msg=None, msg_len=0): @classmethod def render_value(cls, val, unpad=False): - # convert nValue from a transaction into human form. + # convert nValue from a transaction into either BTC or sats # - always be precise # - return (string, units label) - if unpad: - if (val % 1E8): - # precise but unpadded - txt = ('%d.%08d' % (val // 1E8, val % 1E8)).rstrip('0') + from common import settings + from constants import UNIT_TYPE_BTC, UNIT_TYPE_SATS + + # BTC is the default if not set yet + units = settings.get('units', UNIT_TYPE_BTC) + if units == UNIT_TYPE_BTC: + label = cls.ctype + if unpad: + if (val % 1E8): + # precise but unpadded + txt = ('%d.%08d' % (val // 1E8, val % 1E8)).rstrip('0') + else: + # round BTC amount, show no decimal + txt = '%d' % (val // 1E8) else: - # round BTC amount, show no decimal - txt = '%d' % (val // 1E8) + # all the zeros + txt = '%d.%08d' % (val // 1E8, val % 1E8) else: - # all the zeros - txt = '%d.%08d' % (val // 1E8, val % 1E8) - return txt, cls.ctype + label = cls.ctype_sats + txt = ('{:,}'.format(val)) + return txt, label @classmethod def render_address(cls, script): @@ -193,8 +203,10 @@ def render_address(cls, script): class BitcoinMain(ChainsBase): # see ctype = 'BTC' + ctype_sats = 'sats' name = 'Bitcoin' core_name = 'Bitcoin Core' + menu_name = 'Bitcoin Mainnet' slip132 = { AF_CLASSIC: Slip132Version(0x0488B21E, 0x0488ADE4, 'x'), @@ -214,8 +226,9 @@ class BitcoinMain(ChainsBase): class BitcoinTestnet(BitcoinMain): ctype = 'TBTC' + ctype_sats = 'tsats' name = 'Bitcoin Testnet' - menu_name = 'Testnet: BTC' + menu_name = 'Bitcoin Testnet' slip132 = { AF_CLASSIC: Slip132Version(0x043587cf, 0x04358394, 't'), diff --git a/ports/stm32/boards/Passport/modules/choosers.py b/ports/stm32/boards/Passport/modules/choosers.py index 2b1613d6..b1ba5d9a 100644 --- a/ports/stm32/boards/Passport/modules/choosers.py +++ b/ports/stm32/boards/Passport/modules/choosers.py @@ -72,4 +72,56 @@ def set_enable_passphrase(idx, text): settings.set('enable_passphrase', va[idx]) return which, ch, set_enable_passphrase + +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_volatile('chain', val) + + 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 + + return which, [t for _,t in ch], set_chain + +def units_chooser(): + import chains + from constants import UNIT_TYPE_BTC, UNIT_TYPE_SATS + + chain = chains.current_chain() + units = settings.get('units', UNIT_TYPE_BTC) + + ch = [chain.ctype, + chain.ctype_sats] + val = [UNIT_TYPE_BTC, + UNIT_TYPE_SATS] + + try: + which = val.index(units) + except ValueError: + which = UNIT_TYPE_BTC + + def set_units(idx, text): + settings.set('units', val[idx]) + + return which, ch, set_units + # EOF diff --git a/ports/stm32/boards/Passport/modules/constants.py b/ports/stm32/boards/Passport/modules/constants.py index 8f22c3f9..bfa04bbe 100644 --- a/ports/stm32/boards/Passport/modules/constants.py +++ b/ports/stm32/boards/Passport/modules/constants.py @@ -39,3 +39,10 @@ MAX_MULTISIG_NAME_LEN = 20 DEFAULT_ACCOUNT_ENTRY = {'name': 'Primary', 'acct_num': 0} + +# Unit types for labeling conversions +UNIT_TYPE_BTC = 0 +UNIT_TYPE_SATS = 1 + +# Maximum amount of characters in a text entry screen +MAX_MESSAGE_LEN = 64 \ No newline at end of file diff --git a/ports/stm32/boards/Passport/modules/display.py b/ports/stm32/boards/Passport/modules/display.py index 3b49a24c..ca7e6140 100644 --- a/ports/stm32/boards/Passport/modules/display.py +++ b/ports/stm32/boards/Passport/modules/display.py @@ -42,11 +42,11 @@ class Display: def __init__(self): # Setup frame buffer, in show we will call scrn.update(self.dis) to show the buffer - self.scrn = LCD() - self.dis = framebuf.FrameBuffer(bytearray( self.LINE_SIZE_BYTES * self.HEIGHT), self.FB_WIDTH, self.HEIGHT, framebuf.MONO_HLSB) + self.scrn = LCD(self.dis) + self.backlight = Backlight() self.clear() @@ -192,27 +192,31 @@ def char_width(self, ch, font=FontSmall): fn = lookup(font, ord(ch)) return fn.advance - 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)] - - # 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) - 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 + def text_input(self, x, y, msg, font=FontSmall, invert=0, cursor_pos=None, visible_spaces=False, fixed_spacing=None, cursor_shape='line'): + from ux import word_wrap + from utils import split_by_char_size + from constants import MAX_MESSAGE_LEN + # Maximum message size is MAX_MESSAGE_LEN (64) characters + if len(msg) >= MAX_MESSAGE_LEN: + msg = msg[:MAX_MESSAGE_LEN] + + if hasattr(msg, 'readline'): + lines = split_by_char_size(msg.getvalue(), font) else: - self.text(x, y, msg, font, invert, cursor_pos, + lines = split_by_char_size(msg, font) + + # Special case to draw cursor by itself when no text is entered yet + if len(msg) == 0: + self.text(x, y, '', font, invert, cursor_pos, visible_spaces, fixed_spacing, cursor_shape) + else: + for line in lines: + self.text(x, y, line, font, invert, cursor_pos, + visible_spaces, fixed_spacing, cursor_shape, True) + # move the y down enough to make room for 7 lines of text (hence the -2) + y += font.leading - 2 + cursor_pos -= len(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) diff --git a/ports/stm32/boards/Passport/modules/export.py b/ports/stm32/boards/Passport/modules/export.py index 8d9bb89d..074ff216 100644 --- a/ports/stm32/boards/Passport/modules/export.py +++ b/ports/stm32/boards/Passport/modules/export.py @@ -484,8 +484,9 @@ async def write_complete_backup(words, auto_backup=False, is_first_backup=False) body = render_backup_contents().encode() - backup_num = settings.get('backup_num', 1) - # print('backup_num={}'.format(backup_num)) + backup_num = 1 + xfp = xfp2str(settings.get('xfp')).lower() + # print('XFP: {}'.format(xfp)) gc.collect() @@ -520,7 +521,7 @@ async def write_complete_backup(words, auto_backup=False, is_first_backup=False) # Make a unique filename while True: - base_filename = 'passport-backup-{}.7z'.format(backup_num) + base_filename = '{}-backup-{}.7z'.format(xfp, backup_num) fname = '{}/{}'.format(backups_path, base_filename) # Ensure filename doesn't already exist @@ -529,7 +530,6 @@ async def write_complete_backup(words, auto_backup=False, is_first_backup=False) # Ooops...that exists, so increment and try again backup_num += 1 - # print('backup_num={}'.format(backup_num)) # print('Saving to fname={}'.format(fname)) @@ -558,10 +558,6 @@ async def write_complete_backup(words, auto_backup=False, is_first_backup=False) else: return - # Update backup counter - backup_num += 1 - settings.set('backup_num', backup_num) - if not auto_backup: await ux_show_story('Saved backup to\n\n{}\n\nin /backups folder.'.format(base_filename), title='Success', left_btn='NEXT', center=True, center_vertically=True) diff --git a/ports/stm32/boards/Passport/modules/flow.py b/ports/stm32/boards/Passport/modules/flow.py index 74e4b3a9..3f445b87 100644 --- a/ports/stm32/boards/Passport/modules/flow.py +++ b/ports/stm32/boards/Passport/modules/flow.py @@ -19,7 +19,7 @@ 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, is_screenshot_mode_enabled +from utils import is_new_wallet_in_progress, get_accounts, is_screenshot_mode_enabled, run_chooser from new_wallet import pair_new_wallet from ie import show_browser @@ -52,6 +52,11 @@ def has_pubkey(): return False return not is_all_zero(common.cached_pubkey) +PassphraseMenu = [ + MenuItem('Set Passphrase', f=enter_passphrase, arg='Passphrase'), + MenuItem('Enter at Startup', menu_title='Passphrase', chooser=enable_passphrase_chooser) +] + DeveloperPubkeyMenu = [ MenuItem('Install PubKey', predicate=lambda: not has_pubkey(), f=install_user_firmware_pubkey), MenuItem('View PubKey', predicate=has_pubkey, f=view_user_firmware_pubkey), @@ -60,11 +65,13 @@ def has_pubkey(): AdvancedMenu = [ MenuItem('Change PIN', f=change_pin), - MenuItem('Passphrase', menu_title='Passphrase', chooser=enable_passphrase_chooser), + MenuItem('Units', chooser=units_chooser), + MenuItem('Passphrase', menu_title='Passphrase', menu=PassphraseMenu), 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', menu=DeveloperPubkeyMenu, menu_title='Developer'), + MenuItem('Testnet', f=testnet_chooser), MenuItem('Erase Passport', f=erase_wallet, arg=True) ] diff --git a/ports/stm32/boards/Passport/modules/menu.py b/ports/stm32/boards/Passport/modules/menu.py index 0baa2914..e5c769d7 100644 --- a/ports/stm32/boards/Passport/modules/menu.py +++ b/ports/stm32/boards/Passport/modules/menu.py @@ -277,6 +277,7 @@ async def activate(self, idx): ch = self.items[idx] await ch.activate(self, idx) + self.input.reset() async def interact(self): # Only public entry point: do stuff. diff --git a/ports/stm32/boards/Passport/modules/multisig.py b/ports/stm32/boards/Passport/modules/multisig.py index 6917fc3b..b79dbe72 100644 --- a/ports/stm32/boards/Passport/modules/multisig.py +++ b/ports/stm32/boards/Passport/modules/multisig.py @@ -981,7 +981,7 @@ def guess_addr_fmt(cls, npath): @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 for 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. @@ -1017,7 +1017,7 @@ def import_from_psbt(cls, M, N, xpubs_list): assert has_mine == 1 # 'my key not included' name = 'PSBT-%d-of-%d' % (M, N) - ms = cls(name, (M, N), xpubs, chain_type=expect_chain, addr_fmt=addr_fmt or AF_P2SH) + ms = cls(name, (M, N), xpubs, name, 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 diff --git a/ports/stm32/boards/Passport/modules/new_wallet.py b/ports/stm32/boards/Passport/modules/new_wallet.py index 31076ad2..54297eaa 100644 --- a/ports/stm32/boards/Passport/modules/new_wallet.py +++ b/ports/stm32/boards/Passport/modules/new_wallet.py @@ -25,7 +25,9 @@ save_next_addr, make_account_name_num, get_accounts, - format_btc_address) + format_btc_address, + is_valid_btc_address, + do_address_verify) from wallets.constants import * from uasyncio import sleep_ms from constants import DEFAULT_ACCOUNT_ENTRY @@ -389,6 +391,17 @@ def goto_address_verification_method(self, save_curr=True): elif method == 'show_addresses': self.goto(self.SHOW_RX_ADDRESSES_VERIFICATION_INTRO, save_curr=save_curr) + def choose_multisig_import_mode(self): + if 'mulitsig_import_mode' in self.export_mode: + if self.export_mode['mulitsig_import_mode'] == EXPORT_MODE_QR: + self.goto(self.IMPORT_MULTISIG_CONFIG_FROM_QR, save_curr=False) + else: + self.goto(self.IMPORT_MULTISIG_CONFIG_FROM_MICROSD, save_curr=False) + elif self.export_mode['id'] == EXPORT_MODE_QR: + self.goto(self.IMPORT_MULTISIG_CONFIG_FROM_QR, save_curr=False) + else: + self.goto(self.IMPORT_MULTISIG_CONFIG_FROM_MICROSD, save_curr=False) + async def show(self): while True: # print('show: state={}'.format(self.state)) @@ -539,23 +552,29 @@ async def show(self): # 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) + self.choose_multisig_import_mode() else: self.goto_address_verification_method(save_curr=False) elif self.state == self.EXPORT_TO_MICROSD: from files import CardSlot + from utils import xfp2str data = self.prepare_to_export() data_hash = bytearray(32) system.sha256(data, data_hash) + fname = '' # 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)) + fname = filename_pattern.format(sd=card.get_sd_root(), + hash=data_hash, + acct=self.acct_num, + random=random_hex(8), + xfp=xfp2str(settings.get('xfp')).lower()) # print('Saving to fname={}'.format(fname)) # Write the data @@ -579,17 +598,21 @@ async def show(self): self.exported = True self.save_new_wallet_progress() - dis.fullscreen('Saved to microSD') + base_filename = fname.split(card.get_sd_root() + '/', 1)[1] + result = await ux_show_story('Saved file to your microSD card.\n{}'.format(base_filename), + title='Success', + left_btn='NEXT', + center=True, + center_vertically=True) 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) + self.choose_multisig_import_mode() 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'])) @@ -682,8 +705,9 @@ async def show(self): elif self.state == self.SCAN_RX_ADDRESS: # Scan the address to be verified - should be a normal QR code - system.turbo(True); + system.turbo(True) address = await ux_scan_qr_code('Verify Address') + system.turbo(False) if address == None: # User backed out without scanning an address @@ -699,56 +723,30 @@ async def show(self): self.goto_prev() continue - # Strip prefix if present - if address[0:8].lower() == '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 + address, is_valid_btc = await is_valid_btc_address(address) + if is_valid_btc == False: + 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, is_change = 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, is_change) - address = format_btc_address(address, self.addr_type) - result = await ux_show_story('''Address Verified! - -{} - -This is a {} address at index {}.'''.format(address, 'change' if is_change == 1 else 'receive', addr_idx), - title='Verify', - left_btn='BACK', - right_btn='CONTINUE', - 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 - - self.goto(self.CONFIRMATION) - continue - else: + result = do_address_verify(self.acct_num, address, self.addr_type, self.deriv_path, self.multisig_wallet) + if result == False: 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 + else: + # Address was found! + self.verified = True + self.goto(self.CONFIRMATION) + continue + + # 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. @@ -843,10 +841,7 @@ async def show(self): 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) + self.choose_multisig_import_mode() continue if not self.verified: diff --git a/ports/stm32/boards/Passport/modules/schema_evolution.py b/ports/stm32/boards/Passport/modules/schema_evolution.py index e69f54b7..760defab 100644 --- a/ports/stm32/boards/Passport/modules/schema_evolution.py +++ b/ports/stm32/boards/Passport/modules/schema_evolution.py @@ -29,6 +29,16 @@ async def handle_schema_evolutions(update_from_to): from_version = to_version continue + elif from_version == '1.0.6' and to_version == '1.0.7': + # Handle evolutions + + # This no longer used, so clean it out + settings.remove('backup_num') + # This is now volatile, so clean it out + settings.remove('chain') + 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 diff --git a/ports/stm32/boards/Passport/modules/sram4.py b/ports/stm32/boards/Passport/modules/sram4.py index 5aca4c9d..5574af3e 100644 --- a/ports/stm32/boards/Passport/modules/sram4.py +++ b/ports/stm32/boards/Passport/modules/sram4.py @@ -36,5 +36,7 @@ def _alloc(ln): tmp_buf = _alloc(1024) psbt_tmp256 = _alloc(256) viewfinder_buf = _alloc((VIEWFINDER_WIDTH*VIEWFINDER_HEIGHT) // 8) +framebuffer_addr = _alloc(4) # Address of the framebuffer memory so we can read it from OCD + assert _start <= SRAM4_END diff --git a/ports/stm32/boards/Passport/modules/stash.py b/ports/stm32/boards/Passport/modules/stash.py index 3a404322..d2d217fd 100644 --- a/ports/stm32/boards/Passport/modules/stash.py +++ b/ports/stm32/boards/Passport/modules/stash.py @@ -221,7 +221,7 @@ def capture_xpub(self): settings.set_volatile('xfp', xfp) settings.set_volatile('xpub', xpub) - settings.set('chain', self.chain.ctype) + settings.set_volatile('chain', self.chain.ctype) settings.set('words', (self.mode == 'words')) def register(self, item): diff --git a/ports/stm32/boards/Passport/modules/utils.py b/ports/stm32/boards/Passport/modules/utils.py index 43c88be5..52d6a8dd 100644 --- a/ports/stm32/boards/Passport/modules/utils.py +++ b/ports/stm32/boards/Passport/modules/utils.py @@ -446,7 +446,15 @@ async def show_top_menu(): # 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')) + # Valid addresses: 1 , 3 , bc1, tb1, m, n, 2 + return (len(address) > 3) and \ + ((address[0] == '1') or \ + (address[0] == '2') or \ + (address[0] == '3') or \ + (address[0] == 'm') or \ + (address[0] == 'n') or \ + (address[0] == 'b' and address[1] == 'c' and address[2] == '1') or \ + (address[0] == 't' and address[1] == 'b' and address[2] == '1')) # Return array of bytewords where each byte in buf maps to a word @@ -491,10 +499,14 @@ def ensure_folder_exists(path): return def file_exists(path): + import os + from stat import S_ISREG + try: - with open(fname, 'wb') as fd: - return True - except: + s = os.stat(path) + mode = s[0] + return S_ISREG(mode) + except OSError as e: return False def folder_exists(path): @@ -691,6 +703,48 @@ async def scan_for_address(acct_num, address, addr_type, deriv_path, ms_wallet): if result == 'x': return -1, False +async def is_valid_btc_address(address): + from ux import ux_show_story + + # Strip prefix if present + if address[0:8].lower() == 'bitcoin:': + address = address[8:] + + if not is_valid_address(address): + await ux_show_story('That is not a valid Bitcoin address.', title='Error', left_btn='BACK', + right_btn='SCAN', center=True, center_vertically=True) + return address, False + else: + return address, True + +async def do_address_verify(acct_num, address, addr_type, deriv_path, multisig_wallet): + from common import system + from ux import ux_show_story + + system.turbo(True) + # Scan addresses to see if it's valid + addr_idx, is_change = await scan_for_address(acct_num, address, addr_type, deriv_path, multisig_wallet) + if addr_idx >= 0: + # Remember where to start from next time + save_next_addr(acct_num, addr_type, addr_idx, is_change) + address = format_btc_address(address, addr_type) + result = await ux_show_story('''Address Verified! + +{} + +This is a {} address at index {}.'''.format(address, 'change' if is_change == 1 else 'receive', addr_idx), + title='Verify', + left_btn='BACK', + right_btn='CONTINUE', + center=True, + center_vertically=True) + system.turbo(False) + return True + else: + system.turbo(False) + return + + def is_new_wallet_in_progress(): from common import settings ap = settings.get('wallet_prog', None) @@ -835,4 +889,18 @@ def is_all_zero(buf): def split_to_lines(s, width): return '\n'.join([s[i:i+width] for i in range(0, len(s), width)]) +def split_by_char_size(msg, font): + from display import Display + from ux import MAX_WIDTH, word_wrap + from common import dis + + lines = [] + for ln in msg.split('\n'): + if dis.width(ln, font) > MAX_WIDTH: + lines.extend(word_wrap(ln, font)) + else: + # ok if empty string, just a blank line + lines.append(ln) + return lines + # EOF diff --git a/ports/stm32/boards/Passport/modules/ux.py b/ports/stm32/boards/Passport/modules/ux.py index cb8334c6..f7c5f031 100644 --- a/ports/stm32/boards/Passport/modules/ux.py +++ b/ports/stm32/boards/Passport/modules/ux.py @@ -26,6 +26,8 @@ RIGHT_MARGIN = 6 TOP_MARGIN = 12 VERT_SPACING = 10 +MAX_WIDTH = Display.WIDTH - LEFT_MARGIN - \ + RIGHT_MARGIN - Display.SCROLLBAR_WIDTH TEXTBOX_MARGIN = 6 @@ -168,6 +170,13 @@ def clear(self): from common import keypad keypad.clear_keys() + # Reset internal state so that all pending kcodes and repeats are forgotten. + def reset(self): + self.time_pressed = {} + self.kcode_state = 0 + self.kcode_last_time_pressed = 0 + self.repeat_active = False + # 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 @@ -455,7 +464,7 @@ async def ux_enter_text(title="Enter Text", label="Text", initial_text='', left_ # 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=14) + cursor_pos=text_handler.cursor_pos, font=font) dis.draw_footer(left_btn, right_btn, input.is_pressed( 'x'), input.is_pressed('y')) @@ -587,7 +596,6 @@ async def ux_show_symbols_popup(title="Enter Passphrase"): def chars_per_line(font): return (Display.WIDTH - LEFT_MARGIN - Display.SCROLLBAR_WIDTH) // font.advance - def word_wrap(ln, font): from common import dis max_width = Display.WIDTH - LEFT_MARGIN - \ @@ -626,6 +634,7 @@ async def ux_show_story(msg, title='Passport', sensitive=False, font=FontSmall, 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 + from utils import split_by_char_size system.turbo(True) @@ -633,35 +642,11 @@ async def ux_show_story(msg, title='Passport', sensitive=False, font=FontSmall, if clear_keys: keypad.clear_keys() - ch_per_line = chars_per_line(font) - - lines = [] - - # First case is used with StringIO objects if hasattr(msg, 'readline'): - msg.seek(0) - for ln in msg: - if ln[-1] == '\n': - ln = ln[:-1] - - if len(ln) > ch_per_line: - lines.extend(word_wrap(ln, font)) - else: - # ok if empty string, just a blank line - lines.append(ln) - - # no longer needed & rude to our caller, but let's save the memory - msg.close() - del msg - gc.collect() + lines = split_by_char_size(msg.getvalue(), font) else: - for ln in msg.split('\n'): - if len(ln) > ch_per_line: - lines.extend(word_wrap(ln, font)) - else: - # ok if empty string, just a blank line - lines.append(ln) - + lines = split_by_char_size(msg, font) + # trim blank lines at end while not lines[-1]: lines = lines[:-1] diff --git a/ports/stm32/boards/Passport/modules/wallets/bitcoin_core.py b/ports/stm32/boards/Passport/modules/wallets/bitcoin_core.py index 1065e341..9c2a2fc2 100644 --- a/ports/stm32/boards/Passport/modules/wallets/bitcoin_core.py +++ b/ports/stm32/boards/Passport/modules/wallets/bitcoin_core.py @@ -102,7 +102,7 @@ def generate_bitcoin_core_wallet(example_addrs, acct_num): {'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'} + {'id': 'microsd', 'label': 'microSD', 'filename_pattern': '{sd}/{xfp}-bitcoin-core.txt', 'ext': '.txt', + 'filename_pattern_multisig': '{sd}/{xfp}-bitcoin-core-multisig.json'} ] } diff --git a/ports/stm32/boards/Passport/modules/wallets/bluewallet.py b/ports/stm32/boards/Passport/modules/wallets/bluewallet.py index dcfecf03..90a12e4c 100644 --- a/ports/stm32/boards/Passport/modules/wallets/bluewallet.py +++ b/ports/stm32/boards/Passport/modules/wallets/bluewallet.py @@ -19,6 +19,6 @@ ], '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'} + {'id': 'microsd', 'label': 'microSD', 'filename_pattern': '{sd}/{xfp}-bluewallet.json', 'filename_pattern_multisig': '{sd}/{xfp}-bluewallet-multisig.json'} ] } diff --git a/ports/stm32/boards/Passport/modules/wallets/btcpay.py b/ports/stm32/boards/Passport/modules/wallets/btcpay.py index 2c053bf7..92130278 100644 --- a/ports/stm32/boards/Passport/modules/wallets/btcpay.py +++ b/ports/stm32/boards/Passport/modules/wallets/btcpay.py @@ -18,6 +18,6 @@ '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'} + {'id': 'microsd', 'label': 'microSD', 'filename_pattern': '{sd}/{xfp}-btcpay.json', 'filename_pattern_multisig': '{sd}/{xfp}-btcpay-multisig.json'} ] } diff --git a/ports/stm32/boards/Passport/modules/wallets/caravan.py b/ports/stm32/boards/Passport/modules/wallets/caravan.py index 5443fa9b..4890c7e9 100644 --- a/ports/stm32/boards/Passport/modules/wallets/caravan.py +++ b/ports/stm32/boards/Passport/modules/wallets/caravan.py @@ -14,6 +14,6 @@ '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'} + {'id': 'microsd', 'label': 'microSD', 'filename_pattern': '{sd}/{xfp}-caravan.json', 'filename_pattern_multisig': '{sd}/{xfp}-caravan-multisig.json'} ] } diff --git a/ports/stm32/boards/Passport/modules/wallets/casa.py b/ports/stm32/boards/Passport/modules/wallets/casa.py index 73b18355..8230e418 100644 --- a/ports/stm32/boards/Passport/modules/wallets/casa.py +++ b/ports/stm32/boards/Passport/modules/wallets/casa.py @@ -54,7 +54,7 @@ def create_casa_export(sw_wallet=None, addr_type=None, acct_num=0, multisig=Fals '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',} + {'id': 'microsd', 'label': 'microSD', 'filename_pattern': '{sd}/{xfp}-casa.txt', 'ext': '.txt', + 'filename_pattern_multisig': '{sd}/{xfp}-casa-multisig.txt', 'ext_multisig': '.txt'} ] } diff --git a/ports/stm32/boards/Passport/modules/wallets/dux_reserve.py b/ports/stm32/boards/Passport/modules/wallets/dux_reserve.py index cb8cf8b1..a9d79623 100644 --- a/ports/stm32/boards/Passport/modules/wallets/dux_reserve.py +++ b/ports/stm32/boards/Passport/modules/wallets/dux_reserve.py @@ -18,6 +18,6 @@ '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'} + {'id': 'microsd', 'label': 'microSD', 'filename_pattern': '{sd}/{xfp}-dux.json', 'filename_pattern_multisig': '{sd}/{xfp}-dux-multisig.json'} ] } diff --git a/ports/stm32/boards/Passport/modules/wallets/electrum.py b/ports/stm32/boards/Passport/modules/wallets/electrum.py index 155f21eb..892d655f 100644 --- a/ports/stm32/boards/Passport/modules/wallets/electrum.py +++ b/ports/stm32/boards/Passport/modules/wallets/electrum.py @@ -136,6 +136,6 @@ def create_electrum_watch_only_export(sw_wallet=None, addr_type=None, acct_num=0 # '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'} + {'id': 'microsd', 'label': 'microSD', 'filename_pattern': '{sd}/{xfp}-electrum.json', 'filename_pattern_multisig': '{sd}/{xfp}-electrum-multisig.json'} ] } diff --git a/ports/stm32/boards/Passport/modules/wallets/fullynoded.py b/ports/stm32/boards/Passport/modules/wallets/fullynoded.py index af0285c2..7cac3028 100644 --- a/ports/stm32/boards/Passport/modules/wallets/fullynoded.py +++ b/ports/stm32/boards/Passport/modules/wallets/fullynoded.py @@ -19,6 +19,6 @@ ], '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'} + {'id': 'microsd', 'label': 'microSD', 'filename_pattern': '{sd}/{xfp}-fullynoded.json', 'filename_pattern_multisig': '{sd}/{xfp}-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 index 3dec8cc1..ef75f958 100644 --- a/ports/stm32/boards/Passport/modules/wallets/generic_json_wallet.py +++ b/ports/stm32/boards/Passport/modules/wallets/generic_json_wallet.py @@ -32,13 +32,13 @@ def create_generic_json_wallet(sw_wallet=None, addr_type=None, acct_num=0, multi 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 ), + ( 'bip44', "m/44'/{coin_type}'/{acct}'", AF_CLASSIC, 'p2pkh', False ), + ( 'bip49', "m/49'/{coin_type}'/{acct}'", AF_P2WPKH_P2SH, 'p2sh-p2wpkh', False ), # was "p2wpkh-p2sh" + ( 'bip84', "m/84'/{coin_type}'/{acct}'", AF_P2WPKH, 'p2wpkh', False ), + ( 'bip48_1', "m/48'/{coin_type}'/{acct}'/1'", AF_P2WSH_P2SH, 'p2sh-p2wsh', True ), + ( 'bip48_2', "m/48'/{coin_type}'/{acct}'/2'", AF_P2WSH, 'p2wsh', True ), ]: - dd = deriv.format(acct=acct_num) + dd = deriv.format(coin_type=chain.b44_cointype,acct=acct_num) node = sv.derive_path(dd) xfp = xfp2str(node.my_fingerprint()) xp = chain.serialize_public(node, AF_CLASSIC) diff --git a/ports/stm32/boards/Passport/modules/wallets/gordian.py b/ports/stm32/boards/Passport/modules/wallets/gordian.py index 76b218fd..bb46eee9 100644 --- a/ports/stm32/boards/Passport/modules/wallets/gordian.py +++ b/ports/stm32/boards/Passport/modules/wallets/gordian.py @@ -19,6 +19,6 @@ ], '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'} + {'id': 'microsd', 'label': 'microSD', 'filename_pattern': '{sd}/{xfp}-gordian.json', 'filename_pattern_multisig': '{sd}/{xfp}-gordian-multisig.json'} ] } diff --git a/ports/stm32/boards/Passport/modules/wallets/lily.py b/ports/stm32/boards/Passport/modules/wallets/lily.py index 8dcf77b6..278aa638 100644 --- a/ports/stm32/boards/Passport/modules/wallets/lily.py +++ b/ports/stm32/boards/Passport/modules/wallets/lily.py @@ -20,6 +20,6 @@ ], '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'} + {'id': 'microsd', 'label': 'microSD', 'filename_pattern': '{sd}/{xfp}-lily.json', 'filename_pattern_multisig': '{sd}/{xfp}-lily-multisig.json'} ] } diff --git a/ports/stm32/boards/Passport/modules/wallets/multisig_json.py b/ports/stm32/boards/Passport/modules/wallets/multisig_json.py index 9eb157df..45536090 100644 --- a/ports/stm32/boards/Passport/modules/wallets/multisig_json.py +++ b/ports/stm32/boards/Passport/modules/wallets/multisig_json.py @@ -10,6 +10,7 @@ # multisig_json.py - Multisig export format # +import chains import stash import uio from utils import xfp2str @@ -20,6 +21,7 @@ def create_multisig_json_wallet(sw_wallet=None, addr_type=None, acct_num=0, multisig=False, legacy=False): fp = uio.StringIO() + chain = chains.current_chain() fp.write('{\n') accts = [] @@ -27,11 +29,11 @@ def create_multisig_json_wallet(sw_wallet=None, addr_type=None, acct_num=0, mult 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) + ("m/48'/{coin_type}'/{acct}'/1'", 'p2wsh_p2sh', AF_P2WSH_P2SH), + ("m/48'/{coin_type}'/{acct}'/2'", 'p2wsh', AF_P2WSH) ]: # Fill in the acct number - dd = deriv.format(acct=acct_num) + dd = deriv.format(coin_type=chain.b44_cointype,acct=acct_num) node = sv.derive_path(dd) xfp = xfp2str(node.my_fingerprint()) xpub = sv.chain.serialize_public(node, fmt) diff --git a/ports/stm32/boards/Passport/modules/wallets/sparrow.py b/ports/stm32/boards/Passport/modules/wallets/sparrow.py index f5c035e7..6a7f3f5f 100644 --- a/ports/stm32/boards/Passport/modules/wallets/sparrow.py +++ b/ports/stm32/boards/Passport/modules/wallets/sparrow.py @@ -18,6 +18,6 @@ ], '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'} + {'id': 'microsd', 'label': 'microSD', 'filename_pattern': '{sd}/{xfp}-sparrow.json', 'filename_pattern_multisig': '{sd}/{xfp}-sparrow-multisig.json'} ] } diff --git a/ports/stm32/boards/Passport/modules/wallets/specter.py b/ports/stm32/boards/Passport/modules/wallets/specter.py index 4c56f61e..da67e8d9 100644 --- a/ports/stm32/boards/Passport/modules/wallets/specter.py +++ b/ports/stm32/boards/Passport/modules/wallets/specter.py @@ -18,7 +18,7 @@ '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'} + {'id': 'qr', 'label': 'QR Code', 'qr_type': QRType.UR2}, + {'id': 'microsd', 'label': 'microSD', 'filename_pattern': '{sd}/{xfp}-specter.json', 'filename_pattern_multisig': '{sd}/{xfp}-specter-multisig.json', 'mulitsig_import_mode': 'qr'} ] } diff --git a/ports/stm32/boards/Passport/modules/wallets/sw_wallets.py b/ports/stm32/boards/Passport/modules/wallets/sw_wallets.py index fb8e1c55..b75a3b37 100644 --- a/ports/stm32/boards/Passport/modules/wallets/sw_wallets.py +++ b/ports/stm32/boards/Passport/modules/wallets/sw_wallets.py @@ -32,6 +32,6 @@ # GordianWallet, # LilyWallet, SparrowWallet, - # SpecterWallet, + SpecterWallet, WasabiWallet, ] diff --git a/ports/stm32/boards/Passport/modules/wallets/utils.py b/ports/stm32/boards/Passport/modules/wallets/utils.py index c99317ed..01ba692d 100644 --- a/ports/stm32/boards/Passport/modules/wallets/utils.py +++ b/ports/stm32/boards/Passport/modules/wallets/utils.py @@ -4,6 +4,7 @@ # utils.py - Wallet utils # +import chains import common from common import settings from public_constants import AF_CLASSIC, AF_P2SH, AF_P2WPKH_P2SH, AF_P2WSH_P2SH, AF_P2WPKH, AF_P2WSH @@ -43,11 +44,12 @@ def get_addr_type_from_address(address, is_multisig): if len(address) < 26: return None - if address[0] == '1': + if address[0] == '1' or address[0] == 'm' or address[0] == 'n' : return AF_P2SH if is_multisig else AF_CLASSIC - elif address[0] == '3': + elif address[0] == '3' or address[0] == '2' : return AF_P2WSH_P2SH if is_multisig else AF_P2WPKH_P2SH - elif address[0] == 'b' and address[1] == 'c' and address[2] == '1': + elif (address[0] == 'b' and address[1] == 'c' and address[2] == '1') or \ + (address[0] == 't' and address[1] == 'b' and address[2] == '1'): return AF_P2WSH if is_multisig else AF_P2WPKH return None @@ -94,16 +96,16 @@ def get_deriv_fmt_from_address(address, is_multisig): # 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'" + return "m/48'/{coin_type}'/{acct}'/1'" elif address[0] == 'b' and address[1] == 'c' and address[2] == '1': - return "m/48'/0'/{acct}'/2'" + return "m/48'/{coin_type}'/{acct}'/2'" else: if address[0] == '1': - return "m/44'/0'/{acct}'" + return "m/44'/{coin_type}'/{acct}'" elif address[0] == '3': - return "m/49'/0'/{acct}'" + return "m/49'/{coin_type}'/{acct}'" elif address[0] == 'b' and address[1] == 'c' and address[2] == '1': - return "m/84'/0'/{acct}'" + return "m/84'/{coin_type}'/{acct}'" return None @@ -113,33 +115,35 @@ def get_deriv_fmt_from_addr_type(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'" + return "m/48'/{coin_type}'/{acct}'/1'" elif addr_type == AF_P2WSH: - return "m/48'/0'/{acct}'/2'" + return "m/48'/{coin_type}'/{acct}'/2'" else: if addr_type == AF_CLASSIC: - return "m/44'/0'/{acct}'" + return "m/44'/{coin_type}'/{acct}'" elif addr_type == AF_P2WPKH_P2SH: - return "m/49'/0'/{acct}'" + return "m/49'/{coin_type}'/{acct}'" elif addr_type == AF_P2WPKH: - return "m/84'/0'/{acct}'" + return "m/84'/{coin_type}'/{acct}'" return None def get_deriv_path_from_addr_type_and_acct(addr_type, acct_num, is_multisig): + chain = chains.current_chain() # 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 fmt.format(coin_type=chain.b44_cointype,acct=acct_num) return None # For single sig only def get_deriv_path_from_address_and_acct(address, acct, is_multisig): + chain = chains.current_chain() # 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 fmt.format(coin_type=chain.b44_cointype,acct=acct) return None diff --git a/ports/stm32/boards/Passport/modules/wallets/vault.py b/ports/stm32/boards/Passport/modules/wallets/vault.py index 86100121..614ddecc 100644 --- a/ports/stm32/boards/Passport/modules/wallets/vault.py +++ b/ports/stm32/boards/Passport/modules/wallets/vault.py @@ -4,6 +4,7 @@ # vault.py - Export format used by some wallets # +import chains import stash import ujson from utils import xfp2str, to_str @@ -14,8 +15,10 @@ def create_vault_export(sw_wallet=None, addr_type=None, acct_num=0, multisig=False, legacy=False): from common import settings, system + chain = chains.current_chain() + (fw_version, _, _, _) = system.get_software_info() - acct_path = "84'/0'/{acct}'".format(acct=acct_num) + acct_path = "84'/{coin_type}'/{acct}'".format(coin_type=chain.b44_cointype,acct=acct_num) master_xfp = xfp2str(settings.get('xfp')) with stash.SensitiveValues() as sv: diff --git a/ports/stm32/boards/Passport/modules/wallets/wasabi.py b/ports/stm32/boards/Passport/modules/wallets/wasabi.py index db4c8759..6a0a1ba2 100644 --- a/ports/stm32/boards/Passport/modules/wallets/wasabi.py +++ b/ports/stm32/boards/Passport/modules/wallets/wasabi.py @@ -20,15 +20,14 @@ 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 + chain = chains.current_chain() with stash.SensitiveValues() as sv: - acct_path = "m/84'/0'/{acct}'".format(acct=acct_num) + acct_path = "m/84'/{coin_type}'/{acct}'".format(coin_type=chain.b44_cointype,acct=acct_num) node = sv.derive_path(acct_path) xfp = xfp2str(settings.get('xfp')) - xpub = btc.serialize_public(node, AF_CLASSIC) + xpub = chain.serialize_public(node, AF_CLASSIC) - chain = chains.current_chain() assert chain.ctype in {'BTC', 'TBTC'}, "Only Bitcoin supported" (fw_version, _, _, _) = system.get_software_info() @@ -49,6 +48,6 @@ def create_wasabi_export(sw_wallet=None, addr_type=None, acct_num=0, multisig=Fa {'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'} + {'id': 'microsd', 'label': 'microSD', 'filename_pattern': '{sd}/{xfp}-wasabi.json', 'filename_pattern_multisig': '{sd}/{xfp}-wasabi-multisig.json'} ] } diff --git a/ports/stm32/boards/Passport/tools/cosign/Makefile b/ports/stm32/boards/Passport/tools/cosign/Makefile index 683eb7a0..98c27640 100644 --- a/ports/stm32/boards/Passport/tools/cosign/Makefile +++ b/ports/stm32/boards/Passport/tools/cosign/Makefile @@ -20,7 +20,6 @@ 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 += -L/usr/local/lib LDFLAGS = -Wl,-Map,$(MAP) diff --git a/ports/stm32/boards/Passport/tools/cosign/cosign.c b/ports/stm32/boards/Passport/tools/cosign/cosign.c index 688808be..88e42bd3 100644 --- a/ports/stm32/boards/Passport/tools/cosign/cosign.c +++ b/ports/stm32/boards/Passport/tools/cosign/cosign.c @@ -11,18 +11,13 @@ #include #include #include - -#ifdef USE_CRYPTO #include #include -#endif /* USE_CRYPTO */ #include "fwheader.h" #include "hash.h" -#ifdef USE_CRYPTO #include "firmware-keys.h" #include "uECC.h" -#endif /* USE_CRYPTO */ // This is the maximum length of "-key" + "-user", "00", "01", "02", or "03" // Also, + 1 for the folder "/" @@ -34,11 +29,10 @@ static bool help; static bool debug_log_level; static bool extract_signature; static uint8_t header[FW_HEADER_SIZE]; -#ifdef USE_CRYPTO static char *key; extern EC_KEY *PEM_read_bio_ECPrivateKey(BIO *bp, EC_KEY **key, void *cb, void *u); -#endif /* USE_CRYPTO */ + static void usage( char *name ) @@ -47,9 +41,7 @@ static void usage( printf("\t-d: debug logging\n" "\t-f : full path to firmware file to sign\n" "\t-h: this message\n" -#ifdef USE_CRYPTO "\t-k \n" -#endif /* USE_CRYPTO */ "\t-v : firmware version\n" ); exit(1); @@ -62,11 +54,7 @@ static void process_args( { int c = 0; -#ifdef USE_CRYPTO while ((c = getopt(argc, argv, "dhf:v:k:x")) != -1) -#else - while ((c = getopt(argc, argv, "dhf:v:x")) != -1) -#endif /* USE_CRYPTO */ { switch (c) { @@ -76,11 +64,9 @@ static void process_args( case 'v': version = optarg; break; -#ifdef USE_CRYPTO case 'k': key = optarg; break; -#endif /* USE_CRYPTO */ case 'd': debug_log_level = true; break; @@ -131,7 +117,7 @@ static size_t read_file( fclose(fp); return ret; } -#ifdef USE_CRYPTO + static uint8_t *read_private_key( char *key ) @@ -408,12 +394,10 @@ int find_public_key( } return -1; } -#endif /* USE_CRYPTO */ + static void sign_firmware( char *fw, -#ifdef USE_CRYPTO char *key, -#endif /* USE_CRYPTO */ char *version ) { @@ -431,18 +415,16 @@ static void sign_firmware( uint8_t *fwptr; 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; -#endif /* USE_CRYPTO */ + if (fw == NULL) { printf("firmware not specified\n"); return; } -#ifdef USE_CRYPTO if (key == NULL) { printf("private key not specified\n"); @@ -471,7 +453,7 @@ static void sign_firmware( } else working_key = rc; -#endif /* USE_CRYPTO */ + tmp = strdup(fw); filename = basename(tmp); @@ -511,16 +493,6 @@ static void sign_firmware( return; } - if (working_key == FW_USER_KEY) - { - sprintf(output, "%s/%s-key-user.bin", path, final_file); - } - else - { - sprintf(output, "%s/%s-key%02d.bin", path, final_file, working_key); - } - free(final_file); - if (debug_log_level) printf("Reading %s...", fw); fwlen = read_file(fw, &fwbuf); @@ -532,13 +504,6 @@ static void sign_firmware( if (debug_log_level) printf("done\n"); - fp = fopen(output, "wb"); - if (fp == NULL) - { - printf("failed to open %s\n", output); - goto out; - } - /* * Test for an existing header in the firwmare. If one exists that * means that it has been signed at least once already. @@ -562,7 +527,6 @@ static void sign_firmware( printf("Existing header found but FW length invalid\n"); goto out; } -#ifdef USE_CRYPTO else if (hdrptr->signature.pubkey1 == FW_USER_KEY) { printf("This firmware was already signed by a user private key.\n"); @@ -580,9 +544,11 @@ static void sign_firmware( } hdrptr->signature.pubkey2 = working_key; -#endif /* USE_CRYPTO */ working_signature = hdrptr->signature.signature2; fwptr = fwbuf + FW_HEADER_SIZE; + + // Generate output filename + sprintf(output, "%s/passport-fw-%s.bin", path, hdrptr->info.fwversion); } else { @@ -598,6 +564,16 @@ static void sign_firmware( goto out; } + // Generate output filename + if (working_key == FW_USER_KEY) + { + sprintf(output, "%s/%s-key-user.bin", path, final_file); + } + else + { + sprintf(output, "%s/%s-key%02d.bin", path, final_file, working_key); + } + hdrptr = (passport_firmware_header_t *)header; /* Create a new header...this is the first signature. */ @@ -612,12 +588,18 @@ static void sign_firmware( strcpy((char *)hdrptr->info.fwversion, version); hdrptr->info.fwlength = fwlen; -#ifdef USE_CRYPTO hdrptr->signature.pubkey1 = working_key; -#endif /* USE_CRYPTO */ working_signature = hdrptr->signature.signature1; fwptr = fwbuf; } + + free(final_file); + fp = fopen(output, "wb"); + if (fp == NULL) + { + printf("failed to open %s\n", output); + goto out; + } if (debug_log_level) { @@ -638,7 +620,6 @@ static void sign_firmware( printf("\n"); } -#ifdef USE_CRYPTO /* Encrypt the hash here... */ rc = uECC_sign(private_key, fw_hash, sizeof(fw_hash), @@ -660,10 +641,7 @@ static void sign_firmware( goto out; } } -#else - memset(working_signature, 0, SIGNATURE_LEN); - memcpy(working_signature, fw_hash, HASH_LEN); -#endif /* USE_CRYPTO */ + if (debug_log_level) { printf("signature: "); @@ -701,7 +679,8 @@ static void sign_firmware( free(fwbuf); free(output); free(tmp); - fclose(fp); + if (fp != NULL) + fclose(fp); } static void dump_firmware_signature( @@ -779,11 +758,7 @@ int main(int argc, char *argv[]) if (extract_signature) dump_firmware_signature(firmware); else -#ifdef USE_CRYPTO sign_firmware(firmware, key, version); -#else - sign_firmware(firmware, version); -#endif /* USE_CRYPTO */ exit(0); } diff --git a/ports/stm32/boards/Passport/tools/version_info/version_info b/ports/stm32/boards/Passport/tools/version_info/version_info index 47b91038..f7af5f78 100755 --- a/ports/stm32/boards/Passport/tools/version_info/version_info +++ b/ports/stm32/boards/Passport/tools/version_info/version_info @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2021 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later + usage() { echo "Usage: `basename $0` [-h]" @@ -11,9 +14,9 @@ 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 "// SPDX\x2dFileCopyrightText: $(date +"%Y") Foundation Devices, Inc. \n" > $file +echo "// SPDX\x2dLicense-Identifier: GPL-3.0-or-later\n" >> $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 +echo "char *build_version = \"$release\";" >> $file diff --git a/ports/stm32/boards/Passport/tools/word_list_gen/word_list_gen.c b/ports/stm32/boards/Passport/tools/word_list_gen/word_list_gen.c index b78cabbf..6096665d 100644 --- a/ports/stm32/boards/Passport/tools/word_list_gen/word_list_gen.c +++ b/ports/stm32/boards/Passport/tools/word_list_gen/word_list_gen.c @@ -87,8 +87,9 @@ int compare_word_info(const void * a, const void * b) { } 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"); + // Insert the hyphen all weird like this so that `reuse lint` doesn't complain about parsing this + printf("// SPDX%cFileCopyrightText: 2021 Foundation Devices, Inc. \n", '-'); + printf("// SPDX%cLicense-Identifier: GPL-3.0-or-later\n", '-'); printf("//\n\n"); printf("#include \n\n"); diff --git a/py/dynruntime.mk b/py/dynruntime.mk index 8b65745a..55971702 100644 --- a/py/dynruntime.mk +++ b/py/dynruntime.mk @@ -7,7 +7,7 @@ ECHO = @echo RM = /bin/rm MKDIR = /bin/mkdir PYTHON = python3 -MPY_CROSS = $(MPY_DIR)/mpy-cross/mpy-cross +MPY_CROSS ?= $(MPY_DIR)/mpy-cross/mpy-cross MPY_TOOL = $(PYTHON) $(MPY_DIR)/tools/mpy-tool.py MPY_LD = $(PYTHON) $(MPY_DIR)/tools/mpy_ld.py diff --git a/py/mkenv.mk b/py/mkenv.mk index 3efeb181..8db6344d 100644 --- a/py/mkenv.mk +++ b/py/mkenv.mk @@ -63,7 +63,7 @@ endif MAKE_MANIFEST = $(PYTHON) $(TOP)/tools/makemanifest.py MAKE_FROZEN = $(PYTHON) $(TOP)/tools/make-frozen.py -MPY_CROSS = $(TOP)/mpy-cross/mpy-cross +MPY_CROSS ?= $(TOP)/mpy-cross/mpy-cross MPY_TOOL = $(PYTHON) $(TOP)/tools/mpy-tool.py MPY_LIB_DIR = $(TOP)/../micropython-lib diff --git a/py/mkrules.mk b/py/mkrules.mk index 68da3e79..0a16a156 100644 --- a/py/mkrules.mk +++ b/py/mkrules.mk @@ -100,7 +100,7 @@ $(HEADER_BUILD): ifneq ($(FROZEN_MANIFEST),) # to build frozen_content.c from a manifest $(BUILD)/frozen_content.c: FORCE $(BUILD)/genhdr/qstrdefs.generated.h - $(Q)$(MAKE_MANIFEST) -o $@ -v "MPY_DIR=$(TOP)" -v "MPY_LIB_DIR=$(MPY_LIB_DIR)" -v "PORT_DIR=$(shell pwd)" -v "BOARD_DIR=$(BOARD_DIR)" -b "$(BUILD)" $(if $(MPY_CROSS_FLAGS),-f"$(MPY_CROSS_FLAGS)",) $(FROZEN_MANIFEST) + $(Q)$(MAKE_MANIFEST) -o $@ -v "MPY_DIR=$(TOP)" -v "MPY_LIB_DIR=$(MPY_LIB_DIR)" -v "MPY_CROSS=$(MPY_CROSS)" -v "PORT_DIR=$(shell pwd)" -v "BOARD_DIR=$(BOARD_DIR)" -b "$(BUILD)" $(if $(MPY_CROSS_FLAGS),-f"$(MPY_CROSS_FLAGS)",) $(FROZEN_MANIFEST) ifneq ($(FROZEN_DIR),) $(error FROZEN_DIR cannot be used in conjunction with FROZEN_MANIFEST) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..ed5b52b1 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +autopep8==1.5.7 +pycodestyle==2.7.0 diff --git a/tools/makemanifest.py b/tools/makemanifest.py index 90cec2bb..b634856c 100644 --- a/tools/makemanifest.py +++ b/tools/makemanifest.py @@ -216,7 +216,7 @@ def main(): # Get paths to tools MAKE_FROZEN = VARS['MPY_DIR'] + '/tools/make-frozen.py' - MPY_CROSS = VARS['MPY_DIR'] + '/mpy-cross/mpy-cross' + MPY_CROSS = VARS['MPY_CROSS'] MPY_TOOL = VARS['MPY_DIR'] + '/tools/mpy-tool.py' # Ensure mpy-cross is built